ゲームループの管理クラス
「多くのゲームでは、一定時間ごとにゲームの状況が更新され、それをCANVASに再描画することを繰り返している。」という話はプログラムを作り始める一番最初に書きました。
そしてこれまで、window.requestAnimationFrame()とGameLoop()を使って状態更新とCANVASへの再描画を行うためのプログラムを作ってきました。
今回はこのゲームループをクラス化します。最終的には、
// ゲームループ管理
let gameLoop = new GameLoop();
// FPSをセット
gameLoop.FPS = 20;
/// その他の初期処理(球やラケットやメッセージボックスのインスタンスを作る)///
// ゲームループスタート
gameLoop.start();
のようなコードで、ゲームが始まるようにしたいと考えています。
最初のひな型として次のようになります。
/**
* ゲームループの管理クラス
*/
class GameLoop {
/**
* 1秒毎のゲームフレーム数 Frame Per Second
*/
public set FPS(value: number) {
if (value > 0) {
this._FPS = value;
}
else {
console.log("FPSは0より大きな数値にしてください。");
}
}
public get FPS() { return this._FPS; }
/**
* 1秒毎のゲームフレーム数 Frame Per Second
*/
private _FPS: number;
/**
* コンストラクタ
*/
constructor() {
this._FPS = 30;
}
}
Start()とループ処理
変更前のGameLoop()関数とほぼ処理を行う_Loop()メソッドを追加します。そして、Start()の中で、_Loop()をwindow.requestAnimationFrame()に引き渡します。
ただし、クラスメソッドをrequestAnimationFrame()の引数として直接渡すことができないので、アロー関数を仲介しています。
class GameLoop {
/**
* ゲームループ処理開始
*/
public Start() {
window.requestAnimationFrame(timestamp => this._Loop(timestamp));
}
/**
* フレームカウンタ(ゲーム内の時刻1カウンタ)
* GameLoop()が最初にコールされたときを0とし、
* 1/FPS秒を1フレームとしてカウントする。
*/
public get Frame() { return this._FrameCounter; }
/**
* フレームカウンタ(ゲーム内の時刻1カウンタ)
* GameLoop()が最初にコールされたときを0とし、
* 1/FPS秒を1フレームとしてカウントする。
*/
private _FrameCounter: number | undefined;
/**
* _Loop()が最初にコールされたときのtimestampを保持する
*/
private _LoopStartTime = 0;
/**
* ゲームループ処理本体
* window.requestAnimationFrame()のコールバック
* @param timestamp window.requestAnimationFrame()から渡されるタイムスタンプ
*/
private _Loop(timestamp: number) {
// 初めてコールされたときに、_LoopStartTimeと_FrameCounterを初期化
if (this._FrameCounter === undefined) {
this._FrameCounter = 0;
this._LoopStartTime = timestamp;
}
// ゲームループ開始時と現在のtimestampの差から、フレーム数を計算
let frame = Math.floor((timestamp - this._LoopStartTime) * FPS / 1000);
if (frame > this._FrameCounter) {
// フレームカウンタを更新
this._FrameCounter = frame;
// フレーム数が更新されたので、ゲームの状態を更新する。
this._Update();
// フレーム数が更新されたので、ゲームの状態を表示する。
this._Show();
}
// ゲームの状況更新、再描画のために、requestAnimationFrame()に再度実行する。
window.requestAnimationFrame(timestamp => this._Loop(timestamp));
}
/**
* ゲームの状態を更新する。
*/
private _Update() {
// ゲームの状況を更新する。
UpdateGameSituation();
// メッセージボックスを更新する。
Message.Update();
// ボタン1を更新する。
Button1.Update();
}
/**
* ゲームの状態を表示する。
*/
private _Show() {
// 描画を行う。
DrawGameCanvas();
// メッセージボックスを表示する。
Message.Show();
// ボタン1を表示する。
Button1.Show();
}
}
その他にも変更点はあります。
変更前は、フレームカウンタ―や最初にコールされたときの時間を保持するために変数FrameCounterとGameLoopStartTimeを使っていましたが、同等のクラスメンバに置き換えてました。これらは他の場所で誤って変更されるのは良くないので、クラス化したことでより安全なコードになりました。
また、「ゲーム状態を更新」と「ゲーム状態を表示」をそれぞれメソッド化しました。
関係ない処理を追い出す
しかし、Message.Update()やMessage.Show()などがGameLoopクラスの中で直接コールされているのは、コードの管理上は良くありません。何か新しいゲーム要素が増えるたびにGameLoopを更新し続けなければならないからです。
このような処理をクラスのコードから追い出すには、おおむね2つの方法が考えられます。
- イベントを発行し、イベントハンドラ(イベントリスナ)でMessage.Update()やMessage.Show()を実行する。
- Update()とShow()をメソッドに持つオブジェクトをGameLoopに登録し、GameLoop._Update()と_Show()で実行する。
ゲーム部品のインターフェースを追加
今回は2番目の方法を採用することにしました。
GameLoopクラスの中で_Update()や_Show()を行うオブジェクトを「ゲーム部品」と定義します。それを表すInterfaceを以下に示します。
/**
* ゲームの部品を表すインターフェイス
*/
interface IGamePart {
/**
* ゲームフレームが更新されたときに、部品の状態を更新するメソッド
*/
Update(): void;
/**
* Updateの後で、部品の状態をプレイヤーに見せるための表示するメソッド
*/
Show(): void;
}
MessageMngもButton1MngもすでにUpdate()/Show()メソッドを持っているので、IGamePartの実装にすることは簡単です。
class MessageMng implements IGamePart {
// 中身は変更なし
}
class Button1Mng implements IGamePart {
// 中身は変更なし。
}
そしてGameLoopがIGamePartを保持し、Update()やShow()を実行するように変更します。
class GameLoop {
/**
* ゲーム部品のリスト
*/
private _partlist: IGamePart[] = [];
/**
* ゲーム部品を登録する
* @param part ゲーム部品
*/
public AddPart(part: IGamePart) {
this._partlist.push(part);
}
/**
* ゲームの状態を更新する。
*/
private _Update() {
// ゲーム部品を更新する。
this._partlist.forEach(part=>part.Update());
}
/**
* ゲームの状態を表示する。
*/
private _Show() {
// ゲーム部品を表示する。
this._partlist.forEach(part=>part.Show());
}
}
最後に、gameLoop.Start()する前にMessageとButton1を登録します。UpdateGameSItuation()とDrawGameCanvasは、とりあえず無名クラスを使ってみました。
let gameLoop = new GameLoop();
gameLoop.FPS = 20;
let tmp = new class implements IGamePart {
Update(): void {
// ゲームの状況を更新する。
UpdateGameSituation();
}
Show(): void {
// 描画を行う。
DrawGameCanvas();
}
}();
gameLoop.AddPart(tmp);
gameLoop.AddPart(Message);
gameLoop.AddPart(Button1);
gameLoop.Start();
コメント