今回はASP.NET Core版 デジタルインベーダーをつくります。

デジタルインベーダーとは

「デジタルインベーダーゲーム電卓 MG880」は1980年にカシオ社から発売された電卓です。普通の電卓ではなくゲームができます。右側から迫り来る数字のインベーダーに対して照準をあわせてインベーダーを撃破していきます。原作では小数点キーと+キーの2つを使い、「照準」をあわせるキーが小数点キーで「発射」が+キーで発す。

照準は一番左側に表示されている数字であらわされています。発射されると一番左側の数字と同じインベーダーが消えていきます。そしてインベーダーが左側に達するとミスとなります。3回ミスをするとゲームオーバーです。16個のインベーダーを撃ち落とすとステージクリアとなり、ミスもリセットされます。ステージが進むにつれてインベーダーが侵略してくる速度も速くなります。

これが本物のデジタルインベーダーゲーム電卓 MG880の動画です。

主な仕様

PCで操作する場合は照準をあわせるキーとしてZキー、発射するキーとしてXキーを使います。またスマホでアクセスしたユーザーでも遊べるようにZとXのボタンを設置します。

ではさっそく作成することにします。名前空間はDigitalInvaderとします。C#で書かれたソースはPages\DigitalInvaderに置きます。それからNET 6.0をエックスサーバーにインストールするで示している手順が完了していることを前提としています。

DigitalInvaderGameクラスをつくる

これまで作成してきたゲームはプレイヤーと敵がいて異なる動作をするため別々のクラスを定義してきましたが、今回は単純なのでDigitalInvaderGameクラスだけ作ります。AspNetCore.SignalRでクライアントサイドとサーバーサイドの通信をする処理はDigitalInvaderHubクラスを定義してそこで行ないます。

以降は名前空間を省略して以下のように書きます。

定数部分

残機制で最大3、残機が0になったらゲームオーバーです。VISIBLE_ENEMIES_MAXは表示されているインベーダーの最大数で、原作とあわせて6にしています。ALL_ENEMIES_COUNT_MAXはステージごとのインベーダーの総数で、これも原作にあわせて16にしています。

ステージが進むにつれてインベーダーが前進する速度が上がります。そこで複数のタイマーを使用します。IntervalsはそのタイマーのIntervalプロパティにセットする値の配列です。実際に自分でプレイしてみましたが、前進速度800ミリ秒くらいになると難しくなり、700ミリ秒になると手が出せません。ただなかには優秀な人(?)もいると思うので、最速で470ミリ秒のものも作ってみました。これをクリアすると470ミリ秒のものが繰り返されます。

コンストラクタ

ConnectionIdはAspNetCore.SignalRでサーバーサイドに接続するときに付与されるIDです。またStageは後述するステージの番号で0はゲーム開始前です。ゲーム開始時に1をセットします。Nameはプレイヤー名です。

ミス時やステージクリア時はインベーダーの前進を止めますが、_ignoreTimerはそのためのものです。ミス時やステージクリア時はキー入力も無効にしたいので、この場合もこのフラグを使います。

各プロパティ

各プロパティを示します。

ConnectionIdはAspNetCore.SignalRでサーバーサイドに接続するときに付与されるIDです。Nameはプレイヤー名です。プレイヤー名で空文字列を指定したときは自動的に”名無しのゴンベ”となります。

Stageプロパティは現在のステージです。ステージクリア時にインクリメントされるので、そのときにサーバーサイドに通知できるようにChangeStageEventというイベントを定義しています。

Lifeプロパティは残機数です。最初は3でミスするとデクリメントされます。0になったらゲームオーバーですが、ステージクリア時に3にリセットされます。これも変更時にサーバーサイドに通知できるようにChangeLifeEventイベントを定義しています。

Scoreプロパティはスコアを管理するためのものです。これも値が変更されたらクライアントサイドに通知できるようにChangeScoreEventイベントを定義しています。

ユーザーがZキーを押下すると照準を変更することができます。Targetプロパティはどの数字に照準が合わされているかを管理するためのものです。これも変更時にクライアントサイドに通知できるようにChangeTargetEventイベントを定義しています。

ゲーム開始時の処理

ゲーム開始時の処理を示します。

_allEnemiesCountは残存するインベーダーの総数です。これが0になったらステージクリアです。_hitNumbersは撃ち落としたインベーダーに書かれている数の合計です。この総数が10で割り切れる数になったら_isShowUFOをtrueに変更します。すると次はUFOが出現します。

ここではこれらのフィールド変数の初期化をしています。またステージに1、スコアと照準に0を、LifeプロパティにLIFE_MAXを設定するとともに残存するインベーダーの総数をALL_ENEMIES_COUNT_MAXにしています。

またゲーム中に二重にGameStartの処理が始まらないように_isGamingがtrueのときはなにもしないようにしています。

インベーダーを前進させる処理

Updateが呼び出されたらインベーダーが前進させます。

_enemiesTextにはインベーダーが持つ数字またはUFOであることを示すnで構成される文字と前後の空白が格納されます。デバッグしやすくするためにインベーダーの前側のスペースには”-“を、後ろ側のスペースには”+”を追加して全部で6桁になるように調整しています。

最初に_enemiesTextから前側の空白であるを意味すする文字”-“を取り除きます。これでインベーダーとUFO、”+”で構成された文字列(ローカル変数 enemies)が得られます。この文字列の長さがVISIBLE_ENEMIES_MAXと同じならミスと判定します。

その次に残存するインベーダーの総数とenemiesの文字数を比較します。インベーダーの総数のほうが大きい場合は新しいインベーダーを作ります。ただし_isShowUFOがtrueの場合はUFOを作ります。UFOを作ったときは_isShowUFOをfalseに戻します。インベーダーの総数とenemiesの文字数が同じまたは後者のほうが大きい場合はenemiesの後方に文字数が6になるように”+”を追加します。そしてこれを_enemiesTextにセットします。

_enemiesTextに新しい文字列がセットされたらこれをクライアントサイドに通知するためにSendEnemiesHandlerイベントを発生させます。

ターゲット変更の処理

ユーザーがZキーを押下したときはターゲットを変更します。Targetをインクリメントすると0から10まで増加し、10をインクリメントすると0に戻ります。ただし_ignoreTimerがtrueのときはなにもしません。

インベーダーを攻撃する処理

インベーダーを攻撃するときの処理を示します。

_ignoreTimerがtrueの場合はなにもしません。Targetが10でないときはその数字、10のときはUFO(”n”)がターゲットです。_enemiesTextのなかから最初に見つけた文字を消します。

文字を消したときはクライアントサイドに通知するためにHitEventまたはHitUfoEventイベントを送信します。それと同時に得点を追加します。追加点は10点から60点で遠くで撃ち落としたほうが高くなります。UFOはどこであっても300点です。そのあと最初に前方の空白を意味する”-“を付加します。

それから撃墜によって_enemiesTextが変更された場合はSendEnemiesEventイベントでクライアントサイドにそれを通知するとともに_allEnemiesCountをデクリメントします。その結果、_allEnemiesCountが0になった場合はステージクリアです。

ステージクリア時の処理

ステージクリア時はStageプロパティをインクリメントして、それをクライアントサイドに通知するためにStageClearEventイベントを発生させます。_enemiesTextと_allEnemiesCountをリセットするために、それぞれ空文字列とALL_ENEMIES_COUNT_MAXを代入します。

UFOの出現に関する情報をリセットして、LifeプロパティをLIFE_MAXにセットします。そのあと3秒間インベーダーの前進処理を止めるために3秒間_ignoreTimerをtrueにします。

ミス時の処理

ミスをしたときはクライアントサイドにこれを通知するためにMissEventイベントを発生させます。

そして_enemiesTextをリセットするために空文字をセットします。LifeをデクリメントしてUFOの出現に関する情報をクリアします。このときにLifeが0になっていればゲームオーバーです。そうでない場合は3秒間だけインベーダーの前進を停止します。これで3秒後にゲームが再開されます。

ゲームオーバーの処理

ゲームオーバーになったらインベーダーの前進を止めるために_ignoreTimerをtrueに変更します。そしてゲームをもう一度始めることができるように_isGamingをfalseにします。またゲームオーバーをクライアントサイドに通知するためにGameOverEventイベントを発生させます。