リモート開発メインのソフトウェア開発企業のエンジニアブログです

Vanilla JavaScriptでシューティングゲームを作成してみた。

すぐにプレイするには下記のURLからどうぞ

https://risarisato.github.io/public_shooter/public_shooter.html

ゲームの概要と目的

今回作成したシューティングゲームの主な目的は、Vanilla JavaScriptを使ってプログラミングの文法理解を深めることです。特に、クラスやコンストラクタ、継承といったオブジェクト指向の概念を学ぶことが目的です。動的な要素が多く、クラスを用いた設計で敵キャラクターに共通の機能を持たせるため、クラス継承を使用して実装しました。

クラスとコンストラクタ、newによるインスタンス化

クラスを使ってキャラクター、弾、敵などのオブジェクトを定義し、newキーワードを用いてインスタンス化しています。コンストラクタは、クラスのインスタンス生成時に自動的に呼び出されるメソッドで、オブジェクトの初期化を行います。また、コンストラクタは引数を受け取り、オブジェクトのプロパティを動的に設定することができます。

Moba Pro

Canvas APIの説明とシューティングゲームの仕組み

Canvasは、JavaScriptを使ってブラウザ上で2Dグラフィックスを動的に描画できるHTML要素です。Canvasを使ったアニメーションは、requestAnimationFrame()を使ってフレーム単位で動作させることができます。

詳細な情報は Canvasの公式ドキュメント を参照してください。

シューティングゲームでは、下記のような要素をCanvasで描画し、操作できます。

  • プレイヤー: キーボードの入力で移動し、弾を発射する。
  • 弾: プレイヤーが発射し、当たったオブジェクト(敵・パワーアップ)を破壊する。
  • 敵: ランダムで画面に出現し、プレイヤーに接近する。

クラス継承を使った開発の流れ

クラス継承とは、あるクラス(親クラス)の機能を別のクラス(子クラス)が引き継ぐ仕組みです。親クラスに定義されたプロパティやメソッドを子クラスが再利用して共通機能を共有して、コードの冗長性が減ります。今回は、敵キャラクターのクラスを継承しました。

クラス継承の利点

  • 親クラスで共通の動作を定義して、子クラスの重複コードを減らせます。
  • 子クラスで親クラスのメソッドを再利用、子クラスで新たなメソッドを追加できます。
  • super()を使って親クラスのコンストラクタを呼び出し、子クラスで追加の初期化処理を行います。

親クラス名の変更と適切な命名の重要性

当初、敵キャラクターの親クラスをEnemyとしていました。しかし、ゲーム内には敵だけでなく、パワーアップアイテムなどのプレイヤーにとって有益なオブジェクトも存在します。これらも同じように画面をスクロールして移動するため、共通の機能を持たせたいと考えました。

そのため、親クラスの名前をScrollingEntityに変更しました。これは「スクロールする存在」という意味で、敵だけでなくアイテムなども含む汎用的なクラスとなります。適切な名前を付けることは、プログラミングにおいて非常に重要です。クラス名や変数名がその役割を正確に表していると、コードの可読性が向上し、チーム開発やメンテナンスが容易になります。

ソースコードは GitHubリポジトリ で公開しています。

ソースコードの解説

基本構造について

各クラスは、オブジェクトの設計図classとして定義されます。

  • オブジェクトの動作はupdate()メソッドで更新され、draw()メソッドで描画されます。
  • context(描画コンテキスト)をdraw()に渡して、Canvas上にオブジェクトを描画します。
  • 各クラスのconstructorには、gameオブジェクトを引数として渡し、ゲーム全体の状態や他のクラスのプロパティにアクセスします。
  • 最後に、Mainクラスで各クラスをインタンス化して、animate()でアニメーションを描画します。

InputHandlerクラス

InputHandlerクラスは、キーボード入力を管理するクラスです。
main.jsでインスタンス化され、ゲーム全体のキーボード操作を処理します。
このクラスでは、this.keys = []の配列を使って、現在押されているキーを追跡します。

ProjectileクラスとPlayerクラス

Projectileクラスは、プレイヤーが発射する弾を管理するクラスです。

Playerクラスは、プレイヤーキャラクターを管理するクラスで、内部でProjectileクラスのインスタンスを作成し、プレイヤーが発射する弾を描画・更新します。ちなみに、Projectileクラスのconstructor(directionY)は、弾の進行方向を設定し、パワーアップ中に弾が斜め方向にも発射できるようにしています。

ScrollingEntityクラス

ScrollingEntityクラスは、敵キャラクターやアイテムなど、画面をスクロールして移動するオブジェクトを管理するクラスです。

  • このクラスを継承して、複数の敵キャラクター(Sub1Sub2RockBossPowerUpなど)を作成しています。
  • 親クラスの共通機能は、ScrollingEntityクラスの共通の動作(例: update()draw(context))はすべての敵キャラクターに適用され、個別の特徴は子クラスで追加しています。
  • super()を使って親クラスのコンストラクタを呼び出し、子クラスの初期化処理をします。

下記は親クラスを子クラスが引き継ぐ仕組みです。

// 親のクラス
class ScrollingEntity {
    constructor(game) {
        this.game = game;
        this.x = this.game.width;
        this.speedX = Math.random() * -1.5 - 5.5;
        this.markedForDeletion = false;
        this.color = '';
    }
    update() {
        this.x += this.speedX - this.game.speed;
        if (this.x + this.width < 0) this.markedForDeletion = true;
    }
    draw(context) {
        context.fillStyle = this.color;
        context.fillRect(this.x, this.y, this.width, this.height)
        if (this.game.debug) {
            context.font = '20px Helvetica';
            context.fillText(this.lives, this.x, this.y);// HP表示
        }
    }
}

下記が子クラスの継承関係の省略例です。

// 省略例:子のクラス
class Sub1 extends ScrollingEntity {
    constructor(game) {
        super(game);
        this.width = 70; // 好きな大きさ
        this.height = 200;
        this.lives = 10; // 好きなHP
        this.score = this.lives; 
        this.color = "#940202"; //好きな色
    }
}
/*
 * Sub2、Rock、Bossなどの下記が不要になる
 * update() { }
 * draw(context) { }
 */

//子クラスで新たなメソッドを追加
class PowerUp extends ScrollingEntity {
    constructor(game) {
        super(game);
    // 省略

  }

//子クラスで新たなメソッドを追加
draw(context) {
    context.beginPath();
    context.arc(this.x, this.y, this.width / 2, 0, 2 * Math.PI, false); // 形を丸にした
    this.y += Math.sin(this.x * 0.05) * 5; // 上下に揺れる
    // 省略
    }
}

ソースコードだとSub1Sub2rockBossSubBossupdate()draw(context)ScrollingEntityクラスから継承関係であることがわかります。敵キャラクターの大きさや色などのプロパティを共通化し、コードの冗長性を減らしています。PowerUp(黄色い丸)は、子クラスで新たなメソッドを追加しています。

UIクラス

UIクラスは、スコアや弾薬数やゲーム終了といった情報を表示するためのクラスです。
ゲーム進行状況をCanvas上に描画します。

Mainクラス

Mainクラスは、ゲーム全体を管理するクラスです。
オブジェクトのインスタンスを生成し、ゲームのロジックを制御します。

  • 時間の更新: プレイヤーや敵の出現頻度、弾薬数などを管理します。
  • 当たり判定: プレイヤー、弾、敵、パワーアップの当たり判定を設定し、衝突を処理します。
  • レンダリングの順番: オブジェクトの描画順序を管理し、Canvasに描画します。

最後にMainクラスをインスタンス化して、animate()を使ってCanvas上にゲームオブジェクトを描画し続けます。

感想

今回は、フレームワークを使わずに、Vanilla JavaScriptでクラス継承を活用したシューティングゲームを作成しました。クラス継承を使うことで、親クラスで共通の動作を定義し、子クラスで親クラスのメソッドやプロパティを再利用することができました。適切なクラス名を付けることで、コードの可読性や拡張性も向上しました。これにより、コードの冗長性を減らし、再利用性を高めることができました。今後は、クラス継承やオブジェクト指向の設計を深く理解して、他のパターンも手を動かして、理解に努めていきます。

今後の展開として

まだ、パワーアップのHP表示が消せないことなどバグがあります。
今後のアイデアとして、以下の機能追加を検討できます。

  • ファイルを分割する。
  • image画像やsounds音声を読み込む。
  • ローカルストレージにハイスコアを保存

下記は、アイデアの参考例です。

├── Player.js            // プレイヤーを管理するクラス
├── Projectile.js        // 弾薬(レーザー)を管理するクラス
├── ScrollingEntity.js   // スクロールするものを管理するクラス
├── UI.js                // スコアや弾薬数を表示するUIクラス
│     └── HighScore.JS   // ローカルストレージにハイスコアを保存する
│
├── InputHandler.js      // キーボード入力を管理するクラス
│
└── assets
│     ├── images         // ゲームで使用する画像
│     └── sounds         // ゲームで使用する音声
│
└── main.js              // ゲームクラス

アイデアを追加することで、ゲームの完成度を高め、自身のプログラムの理解とより良いユーザー体験を提供できると考えています。

アウトプットの重要性について

ブログに限らず、プロジェクト内のPRや見積り作成でも、貴重なフィードバックをいただいております。今回のブログでは、プログラム内で誤解されやすい変数名や、JSDocの未使用、簡潔なコメント表現などについて指摘を受けました。これらの指摘事項は、自分の視点では気づけなかったものであり、全体像を理解しているエンジニアだからこその的確なアドバイスだと思いました。アウトプットしない限りフィードバックを得ることはできませんが、エンジニアからの指摘は宝の山だと思ます。

← 前の投稿

APNGの構造を理解する

次の投稿 →

AWS CDK の個人的に良かったプラクティス

コメントを残す