1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【HTML & JavaScript & CSS】夏休みの自由研究的な感じでシューティングゲーム作ってみた。

Last updated at Posted at 2022-08-21

はじめに

シューティングゲームと書いていますが、そんな大層なものではないです。
プレイヤーがいて、敵がいて、少し動いて、弾が出る、その程度です。
プレイヤーは〇インビーを模しています。

5日ほど夏季休暇があったので、何かを作ってみたいなという気持ちがあり挑戦してみました。

シューティングゲームの作り方は調べるとゴロゴロ出てくると思うのですが、折角?なので直接作り方を調べることはせずに手探り状態で自分なりに実装しました。

作業としては(本記事作成含め)1日2~3時間程度です。

滅茶苦茶なコードですが、すべて晒します。
汚いものを見ても耐えられる方のみ気が向いたときにご覧ください。
https://github.com/tacchan5424/CSSPrac

アウトプット

Live Serverを使うと遊ぶことができます。
k1d4y-xjxj9.gif

仕組み

移動

全て即時関数のループ処理にて制御するようになっており、移動可能な範囲を設けているためその外側へ移動できなくする制御も行っています。
プレイヤー移動処理の一部を抜粋します。

コード
// ### キー押下処理 ###
document.addEventListener("keydown", (e) => {
  // 左右キー押下時処理
  if (key.isMoveX(e.key)) {
   twinbee.setDirectionX(key.whichMoveX(e.key));
  }
  // 上下キー押下時処理
  if (key.isMoveY(e.key)) {
    twinbee.setDirectionY(key.whichMoveY(e.key));
  }
});

// ### キー離した処理 ###
document.addEventListener("keyup", (e) => {
  // 左右キー離したとき処理
  if (twinbee.getDirectionX() === key.whichMoveX(e.key)) {
    twinbee.setDirectionX(0);
  }
  // 上下キー離したとき処理
  if (twinbee.getDirectionY() === key.whichMoveY(e.key)) {
    twinbee.setDirectionY(0);
  }
});

// ### 移動の処理 ###
(function twinbeeMoveAnimation() {
  // スクロールしない範囲での移動に限定させる
  // 後々は移動できる範囲をボックス内に限定させる
  if (
    canMove(
      twinbee.getPositionX(),
      twinbee.getPositionY(),
      twinbee.getDirectionX(),
      twinbee.getDirectionY()
    )
  ) {
    // 左右移動
    twinbee.setPositionX(
      twinbee.getPositionX() + twinbee.getDirectionX() * twinbee.getMoveSpeed()
    );
    // 上下移動
    twinbee.setPositionY(
      twinbee.getPositionY() + twinbee.getDirectionY() * twinbee.getMoveSpeed()
    );
  }

  Object.assign(charElement.style, {
    left: `${twinbee.getPositionX()}px`,
    top: `${twinbee.getPositionY()}px`,
  });
  requestAnimationFrame(twinbeeMoveAnimation);
})();

// ### フィールド内のみの移動判定 ###
function canMove(positionX, positionY, directionX, directionY) {
  const fieldCoordinate = fieldElement.getBoundingClientRect();

  // 画面下に来た時に、下方向以外には動ける
  if (
    positionY + bottomAndRightMargin > fieldCoordinate.bottom &&
    directionY === 1
  )
    return false;
  // 画面上に来た時に、上方向以外には動ける
  if (positionY - topAndLeftMargin < fieldCoordinate.top && directionY === -1)
    return false;
  // 画面右に来た時に、右方向以外には動ける
  if (
    positionX + bottomAndRightMargin > fieldCoordinate.right &&
    directionX === 1
  )
    return false;
  // 画面左に来た時に、左方向以外には動ける
  if (positionX - topAndLeftMargin < fieldCoordinate.left && directionX === -1)
    return false;

  return true;
}

攻撃

移動と同様に即時関数のループにて制御をおこなっています。
少し特殊な点としまして、描画と位置のリセット処理を行っています。
弾が外れて一定以上進んだ場合と弾が当たった場合に位置のリセット、攻撃キーを離したときに非表示にしています。
処理の一部を抜粋します。

コード
// ### キー押下処理 ###
document.addEventListener("keydown", (e) => {
  // a(攻撃)キー押下時処理
  if (key.isAttack(e.key)) {
    Object.assign(bulletElement.style, {
      display: "",
    });
    isAttack = 1;
  }
});

// ### キー離した処理 ###
document.addEventListener("keyup", (e) => {
  // a(攻撃)キー離したとき処理
  if (key.isAttack(e.key)) {
    Object.assign(bulletElement.style, {
      display: "none",
    });
    isAttack = 0;
  }
});

// ### 弾の移動処理 ###
(function bulletMoveAnimation() {
  bullet.setPosition(
    bullet.getPosition() + bullet.getDirection() * bullet.getSpeed() * isAttack
  );
  attackFinish();

  Object.assign(bulletElement.style, {
    top: `${bullet.getPosition()}px`,
  });

  requestAnimationFrame(bulletMoveAnimation);
})();

// 弾のループ用処理
function attackFinish() {
  if (parseInt(bulletElement.style.top) < -1000) {
    bullet.setPosition(0);
  }
}

当たり判定

プレイヤーも敵も弾もすべて円で作成していますので、ユークリッド距離を用いて判定しています。(学生時代に画像処理の研究で使用していたので懐かしく感じました。)

2点間の距離<=対象の半径の和となった時に当たった、と判定しています。簡単に図示します。
左側は2点間の距離の方が大きいため当たっておらず、対して右側は半径の和の方が大きくなるため当たっていることになります。
image.png

敵の周りの玉

アニメーションで作成しています。簡単にですが後述していますので省略します。

勉強できたこと

即時関数

関数作成後に1度だけ実行される関数です。
以前作成した記事にてコメントいただいて初めて見て、調べて知りました。
requestAnimationFrameを組み合わせると、即時実行するループ処理が作れることも教えていただきました。

(function functionName(){
  // 何か処理 ← ループされる
  requestAnimationFrame(functionName)
})();

良いのか悪いのかわかりませんが、便利だったので多用してしまいました。

アニメーション

この存在を忘れていて敵の直線移動はJavaScriptで実装してしまったのですが、周囲の玉に対して使用しました。
以下に実装内容の一部を抜粋します。

.item1 {
  /* アニメーションに4秒かける、そのアニメーションの速度は全体を通して一定 */
  animation: circle-move-anim1 4s linear;
}

/* 距離150pxのところを1周する */
@keyframes circle-move-anim1 {
  0% {
    transform: rotate(0deg) translateX(-150px) rotate(0deg);
  }
  100% {
    transform: rotate(360deg) translateX(-150px) rotate(-360deg);
  }
}

keyframesを使うことで一連の動作をスムーズかつ簡単に実装することができます。
これをanimationタグに設定することで動作させることができます。
余談ですが、複数アニメーションを設定する場合はカンマ区切りにするみたいです。

クラス設計

こちらを少しずつ読み進めていたので、アウトプットするつもりで実装していました。

これまでクラス設計を行ったことがなく、かつ今回の開発は結構行き当たりばったりだったため難しかったのですが、できないなりに実装したつもりではいます。
なので当然レベル感としてはまだまだ低いです。さらに読み進めて勉強します。

反省点

他の誘惑に負けた

もっと時間を使って色々な機能を盛り込みたかったのですが、思うように時間が取れませんでした。。。
あれやこれやと誘惑に負けて遊んでしまいました。自宅PCでの開発の難しさを再認識しました。

知識・力不足

圧倒的にこれです。
簡単に実装できることを汚く難しく実装している気がしてなりません。
htmlファイルにJavaScript処理がまとめられていたり、即時関数を使ったループ処理がいくつもあったり、アニメーションを使わずに移動制御している処理があったり、などなど。

調べることに多く時間を使ってしまっていたため最低限実装したかった部分までも届かなかったです。
以下の辺りをやり切りたかったです。

〇被ダメージ時のディレイや点滅などの処理
→ ダメージ出してる感があると思ったので実装したかったのですが、間に合いませんでした。
〇効果音
→ 同上
〇敵の行動パターンdt>
→ 今のところ2パターンしかなく、現状とてもゲームと言えるようなものではありません。
〇リファクタリング
→ (楽しいので)動かすのを優先して、あとでリファクタリングしようと思っていたのですが時間がたりなくなってしまいました。

さいごに

最初はCSSだけでキャラクターを作ろうと思っていました。
これを見て、ゼニガメすげーとなったためです。
ですが、作っているうちに動かしてみたくなり今に至ります。

前述の通り方針としてとりあえず動かすことを優先して進めた結果、夏季休暇の間では時間的にも実力的にも片づけられないところまで来てしまいました。。。
ということで設計・コードが滅茶苦茶になっているので、後日リファクタリングをして遊ぼうと思います。

最後まで見ていただきありがとうございました。
どなたかの参考になりましたら幸いです。

エンジニア夏休み企画!~自由研究や読書感想文を発表しよう~というものが開催中でしたので、タグを修正して参加してみました。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?