予想できなかった問題発生
これまでのコード整理で、グローバル変数や関数はすべて消え、以下のようにGameMngのインスタンスを生成し、初期化、開始するだけになりました。
const Game = new GameMng();
// ゲーム状態の初期化
Game.InitSituation();
// ゲーム開始
Game.Start();
この中でStart()は、「ゲームを開始する」という意味合いが強いのでこのままでよいと思いますが、InitSituation()は、(各メンバのnewや、CanvasにBallを登録する処理と同様に)GameMngのコンストラクタの中で行ってしまっても良いように感じます。
そこで、以下のように修正しました。
/**
* ゲーム全体を管理するクラス
*/
class GameMng extends GameLoop {
/**
* コンストラクタ
*/
constructor() {
super();
this.FPS = GameParameter.FPS;
// 部品を登録
this.AddPart(this.Canvas);
this.AddPart(this.Message);
this.AddPart(this.Button1);
// CANVAS部品を登録
this._CanvasAddParts();
// ゲーム状態の初期化
this.InitSituation();
}
}
const Game = new GameMng();
// ゲーム開始
Game.Start();
InitSituation()がコールされる場所が移動しただけです。
しかし、これをトランスパイルしてできたjavascriptを使用すると、下記のエラーを出し、正常に動作しません。
index.ts:56 Uncaught ReferenceError: Cannot access 'Game' before initialization at Ball.Init (index.ts:56:22) at GameMng.InitSituation (index.ts:633:19) at new GameMng (index.ts:601:14) at index.ts:658:14 at index.ts:660:14 at index.ts:660:14
原因
エラーの出ているBall.Initのコードはこうなっています。
class Ball implements IGameCanvasPart {
/**
* ゲーム開始時の初期化処理
*/
public Init() {
// フレームカウンターをランダム値として使う。
let random = Game.Random(1, 18); // 1~18までのランダム値
// X座標:初期値はフレームカウンタに応じて、GameParameter.Areaの横幅の0.1~0.9の位置にランダムに変化する。
let random2 = 0.1 * (1 + (random % 9)); // randomから0.1~0.9まで0.1刻みの値を作る。
this._body.X = GameParameter.Area.Left + GameParameter.Area.Width * random2;
// Y座標:初期値はGameParameter.Areaの上辺から球の半径だけ下
this._body.Y = GameParameter.Area.Top + this._body.Radius;
// X軸速度:初期値はランダム値が奇数なら-5、偶数なら5に変化する。
this._VX = (random & 1) ? 5 : -5;
// Y軸速度:初期値は0
this._VY = 0;
}
エラーになっているのは、ハイライトされた7行目のGame.Random(1,18)でした。
Game.Random()が実行されるまでの呼び出しを、階層に分けて記載すると下のようになる。
GameMngのコンストラクタ +- GameMng.InitSituation +- Ball.Init() +- Game.Random()
この場合、Game.Random()が実行された時点ではGameMngのコンストラクタの途中です。
const Game = new GameMng(); が完了していないので、グローバル変数Gameもまだ値をセットされていません。それなのにGameを参照しようとしているので、'Game' before initialization が出て言いました。
変更前のコードでは、Gameがセットされた後に Game.InitSituation() をコールしているので、エラー無く動作していた。
根本的な問題
Game.InitSituation()の実行を戻せば、問題は回避できます。しかし、解決ではなく回避です。
この不具合の根本的な原因は、各クラスの中でグローバル変数Gameを直接参照していることです。
つまりGameを別の方法で参照できるようにすれば、解決できます。
必要なクラスにGameMngへの参照を持つ
つまり、以下のようなコードにします。
lass Ball implements IGameCanvasPart {
/**
* コンストラクタ
*/
constructor(private _game: GameMng) {
}
public Init() {
// フレームカウンターをランダム値として使う。
let random = this._game.Random(1, 18); // 1~18までのランダム値
}
}
class GameMng extends GameLoop {
/**
* 球
*/
public Ball = new Ball(this);
}
Ball.Init()時には、グローバル変数のGameではなく、new Ball(this)で渡されたGameMngへの参照_gameを使うことで、この問題を発生しなくなります。
今回問題が起こったのはBall.Init()でしたが、他のクラスでも同様の不具合が発生する可能性はあります。問題がおきたところだけ修正するのは、将来の不具合の温床となるので、他のクラスも同じように更新することにしました。
ボタン1クリックハンドラーの見直し
ボタン1がクリックされたときのハンドリングは下のようなコードになっています。
/**
* ボタン1(\<button id="button1">)を管理するクラス
*/
class Button1Mng extends ElementMng<HTMLButtonElement> {
/**
* コンストラクタ
*/
constructor(private _game: GameMng) {
super("button1", HTMLButtonElement);
this._AsyncElement.then(element => {
element.addEventListener("click", e => { this._ClickHandler(e) });
});
}
/**
* ボタンクリックイベントハンドラ
*/
protected _ClickHandler(e: MouseEvent) {
switch (this._game.Status) {
case "game":
this._game.Status = "stop";
break;
case "stop":
this._game.Status = "game";
break;
case "start":
case "gameover":
this._game.Status = "game";
this._game.InitSituation();
break;
}
}
}
ボタン1のクリックによりゲーム状態が、「停止」や「再開」などに変化するのだから、ここ(Button1Mngの中)でStatusが変化しているのは、それほど問題ではありません。
しかし、ここにInitSituation()があるのは少し問題を感じます。ゲームが開始または再開するときに、球やラケットや点数を初期化するのは、球や点数の都合であって、ボタン1が管理することではないはずです。
以上の理由ことから、GameMng.Button1_Clicked()メソッドを作成し、それをコールする形に変更します。
class Button1Mng extends ElementMng<HTMLButtonElement> {
/**
* コンストラクタ
*/
constructor(private _game: GameMng) {
super("button1", HTMLButtonElement);
this._AsyncElement.then(element => {
element.addEventListener("click", e => { this._game.Button1_Clicked() });
});
}
}
class GameMng extends GameLoop {
/**
* Button1がクリックされたときのイベントハンドラ
* ButtonMngの中で使用する。
*/
public Button1_Clicked() {
// ボタン1がクリックされたことで、ゲームの状態が変化する
switch (this.Status) {
case "game":
// 現在実行中 -> 停止に変化
this.Status = "stop";
break;
case "stop":
// 現在事項中 -> 実行に変化
this.Status = "game";
break;
case "start":
case "gameover":
// 現在開始 or ゲームオーバー -> 球やラケットなどの状態を初期化して、実行に変化
this.InitSituation();
this.Status = "game";
break;
}
}
}
ボタン1のUdpate()の見直し
同様に、Button1.Update()にはStatusを参照するコードがあります。
class Button1Mng extends ElementMng<HTMLButtonElement> {
/**
* メッセージボックスを更新する。
*/
public Update() {
switch (this._game.Status) {
case "start":
this._text = "スタート";
break;
case "game":
this._text = "ストップ";
break;
case "stop":
this._text = "スタート";
break;
case "gameover":
this._text = "NEXTゲーム";
break;
}
}
}
これも、GameMngにメソッドを作り、それをコールするべきでしょうか?
そうしても悪くはありませんが、現在のままでも良いと判断しました。
このコードはゲーム状態(Status)に応じて表示するテキストを決定しています。ボタンの動作を決定するコードなので、この場所にあるのは適切でしょう。
コメント