今回はASP.NET Core版 デジタルインベーダーをつくります。
Contents
デジタルインベーダーとは
「デジタルインベーダーゲーム電卓 MG880」は1980年にカシオ社から発売された電卓です。普通の電卓ではなくゲームができます。右側から迫り来る数字のインベーダーに対して照準をあわせてインベーダーを撃破していきます。原作では小数点キーと+キーの2つを使い、「照準」をあわせるキーが小数点キーで「発射」が+キーで発す。
照準は一番左側に表示されている数字であらわされています。発射されると一番左側の数字と同じインベーダーが消えていきます。そしてインベーダーが左側に達するとミスとなります。3回ミスをするとゲームオーバーです。16個のインベーダーを撃ち落とすとステージクリアとなり、ミスもリセットされます。ステージが進むにつれてインベーダーが侵略してくる速度も速くなります。
これが本物のデジタルインベーダーゲーム電卓 MG880の動画です。
主な仕様
PCで操作する場合は照準をあわせるキーとしてZキー、発射するキーとしてXキーを使います。またスマホでアクセスしたユーザーでも遊べるようにZとXのボタンを設置します。
ではさっそく作成することにします。名前空間はDigitalInvaderとします。C#で書かれたソースはPages\DigitalInvaderに置きます。それからNET 6.0をエックスサーバーにインストールするで示している手順が完了していることを前提としています。
DigitalInvaderGameクラスをつくる
これまで作成してきたゲームはプレイヤーと敵がいて異なる動作をするため別々のクラスを定義してきましたが、今回は単純なのでDigitalInvaderGameクラスだけ作ります。AspNetCore.SignalRでクライアントサイドとサーバーサイドの通信をする処理はDigitalInvaderHubクラスを定義してそこで行ないます。
1 2 3 4 5 6 |
namespace DigitalInvader { public class DigitalInvaderGame { } } |
以降は名前空間を省略して以下のように書きます。
1 2 3 |
public class DigitalInvaderGame { } |
定数部分
残機制で最大3、残機が0になったらゲームオーバーです。VISIBLE_ENEMIES_MAXは表示されているインベーダーの最大数で、原作とあわせて6にしています。ALL_ENEMIES_COUNT_MAXはステージごとのインベーダーの総数で、これも原作にあわせて16にしています。
ステージが進むにつれてインベーダーが前進する速度が上がります。そこで複数のタイマーを使用します。IntervalsはそのタイマーのIntervalプロパティにセットする値の配列です。実際に自分でプレイしてみましたが、前進速度800ミリ秒くらいになると難しくなり、700ミリ秒になると手が出せません。ただなかには優秀な人(?)もいると思うので、最速で470ミリ秒のものも作ってみました。これをクリアすると470ミリ秒のものが繰り返されます。
1 2 3 4 5 6 7 8 9 10 11 |
public class DigitalInvaderGame { public static readonly int[] Intervals = { 2000, 1750, 1500, 1250, 1100, 1000, 900, 800, 750, 700, 650, 600, 570, 550, 530, 510, 500, 490, 480, 470 }; const int LIFE_MAX = 3; const int VISIBLE_ENEMIES_MAX = 6; const int ALL_ENEMIES_COUNT_MAX = 16; } |
コンストラクタ
ConnectionIdはAspNetCore.SignalRでサーバーサイドに接続するときに付与されるIDです。またStageは後述するステージの番号で0はゲーム開始前です。ゲーム開始時に1をセットします。Nameはプレイヤー名です。
ミス時やステージクリア時はインベーダーの前進を止めますが、_ignoreTimerはそのためのものです。ミス時やステージクリア時はキー入力も無効にしたいので、この場合もこのフラグを使います。
1 2 3 4 5 6 7 8 9 10 |
public class DigitalInvaderGame { public DigitalInvaderGame(string connectionId) { ConnectionId = connectionId; Stage = 0; Name = ""; _ignoreTimer = true; } } |
各プロパティ
各プロパティを示します。
ConnectionIdはAspNetCore.SignalRでサーバーサイドに接続するときに付与されるIDです。Nameはプレイヤー名です。プレイヤー名で空文字列を指定したときは自動的に”名無しのゴンベ”となります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public class DigitalInvaderGame { public string ConnectionId { set; get; } string _name = ""; public string Name { set { _name = value; if (_name == "") _name = "名無しのゴンベ"; } get { return _name; } } } |
Stageプロパティは現在のステージです。ステージクリア時にインクリメントされるので、そのときにサーバーサイドに通知できるようにChangeStageEventというイベントを定義しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public class DigitalInvaderGame { public delegate void ChangeStageHandler(DigitalInvaderGame game, int stage); public event ChangeStageHandler? ChangeStageEvent; int _stage = 0; public int Stage { set { _stage = value; ChangeStageEvent?.Invoke(this, _stage); } get { return _stage; } } } |
Lifeプロパティは残機数です。最初は3でミスするとデクリメントされます。0になったらゲームオーバーですが、ステージクリア時に3にリセットされます。これも変更時にサーバーサイドに通知できるようにChangeLifeEventイベントを定義しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public class DigitalInvaderGame { public delegate void ChangeLifeHandler(DigitalInvaderGame game, int life); public event ChangeLifeHandler? ChangeLifeEvent; int _life = 0; int Life { set { _life = value; ChangeLifeEvent?.Invoke(this, _life); } get { return _life; } } } |
Scoreプロパティはスコアを管理するためのものです。これも値が変更されたらクライアントサイドに通知できるようにChangeScoreEventイベントを定義しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public class DigitalInvaderGame { public delegate void ChangeScoreHandler(DigitalInvaderGame game, int score); public event ChangeScoreHandler? ChangeScoreEvent; int _score = 0; public int Score { set { _score = value; ChangeScoreEvent?.Invoke(this, _score); } get { return _score; } } } |
ユーザーがZキーを押下すると照準を変更することができます。Targetプロパティはどの数字に照準が合わされているかを管理するためのものです。これも変更時にクライアントサイドに通知できるようにChangeTargetEventイベントを定義しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
public class DigitalInvaderGame { public delegate void ChangeTargetHandler(DigitalInvaderGame game, string target); public event ChangeTargetHandler? ChangeTargetEvent; int _target = 0; int Target { set { if (value > 10) value -= 11; _target = value; string s; if (_target != 10) { s = _target.ToString(); } else { s = "n"; } ChangeTargetEvent?.Invoke(this, s); } get { return _target; } } } |
ゲーム開始時の処理
ゲーム開始時の処理を示します。
_allEnemiesCountは残存するインベーダーの総数です。これが0になったらステージクリアです。_hitNumbersは撃ち落としたインベーダーに書かれている数の合計です。この総数が10で割り切れる数になったら_isShowUFOをtrueに変更します。すると次はUFOが出現します。
ここではこれらのフィールド変数の初期化をしています。またステージに1、スコアと照準に0を、LifeプロパティにLIFE_MAXを設定するとともに残存するインベーダーの総数をALL_ENEMIES_COUNT_MAXにしています。
またゲーム中に二重にGameStartの処理が始まらないように_isGamingがtrueのときはなにもしないようにしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
public class DigitalInvaderGame { int _allEnemiesCount = 0; int _hitNumbers = 0; bool _isShowUFO = false; bool _isGaming = false; public void GameStart() { if (_isGaming) return; _ignoreTimer = false; _allEnemiesCount = ALL_ENEMIES_COUNT_MAX; Stage = 1; Score = 0; Target = 0; _hitNumbers = 0; _isShowUFO = false; Life = LIFE_MAX; _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イベントを発生させます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
public class DigitalInvaderGame { string _enemiesText = ""; static Random _random = new Random(); public delegate void SendEnemiesHandler(DigitalInvaderGame game, string text); public event SendEnemiesHandler? SendEnemiesEvent; public void Update() { if(!_ignoreTimer) MoveEnemies(); } void MoveEnemies() { string enemies = _enemiesText.Replace("-", ""); if (enemies.Length == VISIBLE_ENEMIES_MAX) { Miss(); // 後述 return; } if (_allEnemiesCount > enemies.Length) { // 次にUFOを出現させるかどうか? if (!_isShowUFO) { int i = _random.Next(0, 10); enemies += i.ToString(); } else { enemies += "n"; _isShowUFO = false; } } else { // 残存するインベーダーが表示されているインベーダーと同じときは // 後ろの空白を意味する"+"を追加する enemies += "+"; } // インベーダーの前側を-で埋め、全体を6文字にする int space = VISIBLE_ENEMIES_MAX - enemies.Length; string s = ""; for (int i = 0; i < space; i++) s += "-"; _enemiesText = s + enemies; SendEnemiesEvent?.Invoke(this, _enemiesText); } } |
ターゲット変更の処理
ユーザーがZキーを押下したときはターゲットを変更します。Targetをインクリメントすると0から10まで増加し、10をインクリメントすると0に戻ります。ただし_ignoreTimerがtrueのときはなにもしません。
1 2 3 4 5 6 7 8 9 10 |
public class DigitalInvaderGame { public void ChangeTarget() { if (_ignoreTimer) return; Target++; } } |
インベーダーを攻撃する処理
インベーダーを攻撃するときの処理を示します。
_ignoreTimerがtrueの場合はなにもしません。Targetが10でないときはその数字、10のときはUFO(”n”)がターゲットです。_enemiesTextのなかから最初に見つけた文字を消します。
文字を消したときはクライアントサイドに通知するためにHitEventまたはHitUfoEventイベントを送信します。それと同時に得点を追加します。追加点は10点から60点で遠くで撃ち落としたほうが高くなります。UFOはどこであっても300点です。そのあと最初に前方の空白を意味する”-“を付加します。
それから撃墜によって_enemiesTextが変更された場合はSendEnemiesEventイベントでクライアントサイドにそれを通知するとともに_allEnemiesCountをデクリメントします。その結果、_allEnemiesCountが0になった場合はステージクリアです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
public class DigitalInvaderGame { public delegate void HitHandler(DigitalInvaderGame game); public event HitHandler? HitEvent; public event HitHandler? HitUfoEvent; public void Shot() { if (_ignoreTimer) return; string targetString; if (Target < 10) targetString = Target.ToString(); else targetString = "n"; int ret = _enemiesText.IndexOf(targetString); int plus; if (ret != -1) { if (Target != 10) { _hitNumbers += Target; if (Target != 0 && _hitNumbers % 10 == 0) _isShowUFO = true; else _isShowUFO = false; int index = _enemiesText.IndexOf(targetString); plus = (index + 1) * 10; HitEvent?.Invoke(this); } else { Score += 300; plus = 300; HitUfoEvent?.Invoke(this); } Score += plus; _enemiesText = _enemiesText.Remove(ret, 1); _enemiesText = "-" + _enemiesText; SendEnemiesEvent?.Invoke(this, _enemiesText); _allEnemiesCount--; if (_allEnemiesCount == 0) StageClear(); // 後述 } } } |
ステージクリア時の処理
ステージクリア時はStageプロパティをインクリメントして、それをクライアントサイドに通知するためにStageClearEventイベントを発生させます。_enemiesTextと_allEnemiesCountをリセットするために、それぞれ空文字列とALL_ENEMIES_COUNT_MAXを代入します。
UFOの出現に関する情報をリセットして、LifeプロパティをLIFE_MAXにセットします。そのあと3秒間インベーダーの前進処理を止めるために3秒間_ignoreTimerをtrueにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
public class DigitalInvaderGame { public delegate void StageClearHandler(DigitalInvaderGame game); public event StageClearHandler? StageClearEvent; void StageClear() { Task.Run(async () => { // 3秒後に再開 _ignoreTimer = true; await Task.Delay(3000); _ignoreTimer = false; }); Stage++; StageClearEvent?.Invoke(this); _enemiesText = ""; _allEnemiesCount = ALL_ENEMIES_COUNT_MAX; // UFOの出現に関する情報はクリアする _hitNumbers = 0; _isShowUFO = false; // ステージクリアをするとライフが回復する Life = LIFE_MAX; } } |
ミス時の処理
ミスをしたときはクライアントサイドにこれを通知するためにMissEventイベントを発生させます。
そして_enemiesTextをリセットするために空文字をセットします。LifeをデクリメントしてUFOの出現に関する情報をクリアします。このときにLifeが0になっていればゲームオーバーです。そうでない場合は3秒間だけインベーダーの前進を停止します。これで3秒後にゲームが再開されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
public class DigitalInvaderGame { public delegate void MissHandler(DigitalInvaderGame game); public event MissHandler? MissEvent; void Miss() { _enemiesText = ""; Life--; MissEvent?.Invoke(this); // UFOの出現に関する情報はクリアする _hitNumbers = 0; _isShowUFO = false; Task.Run(async () => { if (Life > 0) { // 3秒後に再開 _ignoreTimer = true; await Task.Delay(3000); _ignoreTimer = false; } else { // ゲームオーバーの通知 GameOver(); } }); } } |
ゲームオーバーの処理
ゲームオーバーになったらインベーダーの前進を止めるために_ignoreTimerをtrueに変更します。そしてゲームをもう一度始めることができるように_isGamingをfalseにします。またゲームオーバーをクライアントサイドに通知するためにGameOverEventイベントを発生させます。
1 2 3 4 5 6 7 8 9 10 11 12 |
public class DigitalInvaderGame { public delegate void GameOverHandler(DigitalInvaderGame game); public event GameOverHandler? GameOverEvent; void GameOver() { _ignoreTimer = true; _isGaming = false; GameOverEvent?.Invoke(this); } } |