Vanilla JavaScriptでシューティングゲームを作成してみた。
目次
すぐにプレイするには下記のURLからどうぞ
https://risarisato.github.io/public_shooter/public_shooter.html
Table of Contents
ゲームの概要と目的
今回作成したシューティングゲームの主な目的は、Vanilla JavaScriptを使ってプログラミングの文法理解を深めることです。特に、クラスやコンストラクタ、継承といったオブジェクト指向の概念を学ぶことが目的です。動的な要素が多く、クラスを用いた設計で敵キャラクターに共通の機能を持たせるため、クラス継承を使用して実装しました。
クラスとコンストラクタ、newによるインスタンス化
クラスを使ってキャラクター、弾、敵などのオブジェクトを定義し、new
キーワードを用いてインスタンス化しています。コンストラクタは、クラスのインスタンス生成時に自動的に呼び出されるメソッドで、オブジェクトの初期化を行います。また、コンストラクタは引数を受け取り、オブジェクトのプロパティを動的に設定することができます。
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
クラスは、敵キャラクターやアイテムなど、画面をスクロールして移動するオブジェクトを管理するクラスです。
- このクラスを継承して、複数の敵キャラクター(
Sub1
、Sub2
、Rock
、Boss
、PowerUp
など)を作成しています。 - 親クラスの共通機能は、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; // 上下に揺れる
// 省略
}
}
ソースコードだとSub1
、Sub2
、rock
、Boss
、SubBoss
はupdate()
や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の未使用、簡潔なコメント表現などについて指摘を受けました。これらの指摘事項は、自分の視点では気づけなかったものであり、全体像を理解しているエンジニアだからこその的確なアドバイスだと思いました。アウトプットしない限りフィードバックを得ることはできませんが、エンジニアからの指摘は宝の山だと思ます。
コメントを残す