ASP.NET Coreの使い方をひととおり勉強したのでスコアランキングを偽装できないゲームとしてクラッシュローラーをつくりなおします。要はランキング機能を追加する JavaScriptでクラッシュローラーをつくる(6)の作り直しです。
まず通路がどのようになっているかを管理するためのテキストファイルを作成します。これはJavaScript版クラッシュローラーを作成するときにつくったjsファイルを元につくります。内容は数値とカンマだけテキストです。ここからダウンロード可能です。
Contents
Road列挙体とDirect列挙体
まず通路と移動方向を管理するための列挙体を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
namespace CrashRoller { public enum Road { NONE = -1, // 通路ではない ROAD = 1, // 普通の通路 BRIDGE_NS = 2, // 南北の橋 BRIDGE_WE = 3, // 東西の橋 BRIDGE_NS_CROSS = 4, // 東西の橋と通路が交差している点 BRIDGE_WE_CROSS = 5, // 東西の橋と通路が交差している点 BRIDGE_EDGE = 6, // 橋の開始地点 BRIDGE_NEAR_EDGE = 7, // 橋の開始地点の周辺 } public enum Direct { None, Up, Right, Down, Left, } } |
Gameクラスの作成
次にゲームを管理するGameクラスを作成します。CrashRollerという名前空間を定義し、そのなかにGameというクラスを定義します。
1 2 3 4 5 6 7 8 |
using System.Timers; namespace CrashRoller { public partial class Game { } } |
名前空間を書いてしまうとインデントが深くなるので以降は省略します。
またGameクラス内ではEnemyクラス、Rollerクラスのインスタンスが生成されていますが、これは次回示します。実はC# WindowsFormsでクラッシュローラーを作成したときの敵に追尾させ挟み撃ちさせる とローラーで敵を踏みつぶすに書かれているものとほとんど同じです。
定数とフィールド変数
最初に定数とフィールド変数を示します。
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 |
public partial class Game { // 各キャラクタの大きさ const int CHARACTOR_SIZE = 10; // プレイヤーを移動させるためのキーが押されているか public bool IsUpKeyDown = false; public bool IsDownKeyDown = false; public bool IsLeftKeyDown = false; public bool IsRightKeyDown = false; // マップ(ひとつ作れば使い回せるのでstaticにしてある) public static int[,]? Map = null; // マップの通路部分は巡回されたか? bool[,]? IsVisits = null; // 残機の初期値 const int MAX_REST = 6; // プレイヤーの初期座標 const int PLAYER_START_X = 90; const int PLAYER_START_Y = 198; // 敵オブジェクト List<Enemy> Enemies = new List<Enemy>(); // 敵の初期座標 const int INIT_ENEMY1_X = 71; const int INIT_ENEMY1_Y = 52; const int INIT_ENEMY2_X = 109; const int INIT_ENEMY2_Y = 52; // 敵を倒したときにの初期座標 const int POINT_CRASH_ENEMY_MIN = 100; // プレイヤーと敵を移動させるためのタイマー // ステージが進行すると各キャラクタが高速で移動するようにしたいので複数セットする readonly static System.Timers.Timer TimerNomal1 = new System.Timers.Timer(); readonly static System.Timers.Timer TimerNomal2 = new System.Timers.Timer(); readonly static System.Timers.Timer TimerNomal3 = new System.Timers.Timer(); readonly static System.Timers.Timer TimerNomal4 = new System.Timers.Timer(); readonly static System.Timers.Timer TimerNomal5 = new System.Timers.Timer(); // プレイヤーがローラーを使用しているときに動作速度を速めるためのタイマー readonly static System.Timers.Timer TimerSpeedUp = new System.Timers.Timer(); } |
プロパティ
次にプロパティを示します。外部からは変更されることなく取得することのみを可能にしています。
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 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 |
public partial class Game { // マップの幅と高さ public static int MapWidth { private set; get; } public static int MapHeight { private set; get; } // ゲーム中かどうか? public bool IsGaming { private set; get; } // スコア public int Score { private set; get; } // 残機数 public int Rest { private set; get; } // プレイヤーの座標と移動方向 public int PlayerX { private set; get; } public int PlayerY { private set; get; } public Direct PlayerDirect { private set; get; } // 2体の敵の座標と移動方向 public int Enemy1X { private set; get; } public int Enemy1Y { private set; get; } public Direct Enemy1Direct { private set; get; } public int Enemy2X { private set; get; } public int Enemy2Y { private set; get; } public Direct Enemy2Direct { private set; get; } // ローラーオブジェクト public RollerWE? RollerWE { private set; get; } public RollerNS? RollerNS { private set; get; } // 最後にプレイヤーが移動した通路の座標 public int EatX { private set; get; } public int EatY { private set; get; } // プレイヤーはローラーをこの方向(東西南北)に押しているかどうか? public bool IsHoldRollerE { private set; get; } public bool IsHoldRollerW { private set; get; } public bool IsHoldRollerN { private set; get; } public bool IsHoldRollerS { private set; get; } // 現在のステージ public int StageNumber { private set; get; } // ゲーム進行上動きを止めたいとき(ミス時、敵を倒したとき、ステージクリア時)にtrueにする public bool IsStop { private set; get; } // 次に敵を倒したときに追加される点数 public int AddPointCrashEnemy { private set; get; } // 敵を倒したときの座標 public int PositionHitEmemyX { private set; get; } public int PositionHitEmemyY { private set; get; } } |
初期化
次にコンストラクタを示します。
マップは作成したらそれを使い回せるので1回だけ作成します。MapHeightが0以外であればすでに作成されていることになります。そのあとプレイヤーと敵とローラーを初期化し、タイマーをセットします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public partial class Game { public Game() { if (MapHeight == 0) InitMap(); IsVisits = new bool[MapHeight, MapWidth]; InitPlayerPosition(); InitEnemies(); InitRollers(); IsStop = true; IsGaming = false; InitTimer(); } } |
マップを初期化する処理を示します。リソースに最初に示したテキストファイルを追加しておいてそれを読み込んで処理をします。
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 |
public partial class Game { void InitMap() { // テキストファイルからテキストを読み込む string str = Zero.Properties.Resources.crash_roller_map; // テキストを読み込んでマップに相当する二次元配列を生成する string[] rows = str.Split('\n', StringSplitOptions.RemoveEmptyEntries); int colMax = rows[0].Split(',', StringSplitOptions.RemoveEmptyEntries).Length; Map = new int[rows.Length, colMax - 1]; for (int y = 0; y < rows.Length; y++) { string[] vs = rows[y].Split(',', StringSplitOptions.RemoveEmptyEntries); for (int x = 0; x < colMax - 1; x++) Map[y, x] = int.Parse(vs[x]); } // マップの幅と高さを記憶させる MapHeight = Map.GetLength(0); MapWidth = Map.GetLength(1); } } |
プレイヤーと敵を初期化する処理を示します。外部から座標を取得できるようにするために初期化したら座標をプロパティにセットします。
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 |
public partial class Game { void InitPlayerPosition() { PlayerX = PLAYER_START_X; PlayerY = PLAYER_START_Y; PlayerDirect = Direct.None; } public void InitEnemies() { Enemies.Clear(); Enemy enemy1 = new Enemy(this, INIT_ENEMY1_X, INIT_ENEMY1_Y, 1); Enemies.Add(enemy1); enemy1.Reset(); Enemy1X = enemy1.X; Enemy1Y = enemy1.Y; Enemy enemy2 = new Enemy(this, INIT_ENEMY2_X, INIT_ENEMY2_Y, 2); Enemies.Add(enemy2); enemy2.Reset(); Enemy2X = enemy2.X; Enemy2Y = enemy2.Y; } } |
ローラーを初期化する処理を示します。これも外部から座標を取得できるように初期化したら座標をプロパティにセットしておきます。
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 |
public partial class Game { void InitRollers() { RollerWE = CreateRollerWE(); RollerNS = CreateRollerNS(); } RollerWE? CreateRollerWE() { for (int y = 0; y < MapHeight; y++) { int left = -1; int right = -1; for (int x = 0; x < MapWidth; x++) { if (Map != null && Map[y, x] == (int)Road.BRIDGE_EDGE && Map[y, x + 1] == (int)Road.BRIDGE_WE) left = x; if (Map != null && Map[y, x] == (int)Road.BRIDGE_EDGE && Map[y, x - 1] == (int)Road.BRIDGE_WE) right = x; } if (left != -1 && right != -1) return new RollerWE(left, y, left, right - CHARACTOR_SIZE); } return null; } RollerNS? CreateRollerNS() { for (int x = 0; x < MapWidth; x++) { int top = -1; int bottom = -1; for (int y = 0; y < MapHeight; y++) { if (Map != null && Map[y, x] == (int)Road.BRIDGE_EDGE && Map[y + 1, x] == (int)Road.BRIDGE_NS) top = y; if (Map != null && Map[y, x] == (int)Road.BRIDGE_EDGE && Map[y - 1, x] == (int)Road.BRIDGE_NS) bottom = y; } if (top != -1 && bottom != -1) return new RollerNS(x, bottom - CHARACTOR_SIZE, top, bottom); } return null; } } |
タイマーを初期化する処理を示します。タイマーは同じものを使いますが、イベントハンドラは別に設定します。ユーザーがページから離脱したらイベントハンドラを取り除くことができるようにフィールド変数に保存しておきます。
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 |
public partial class Game { ElapsedEventHandler ElapsedEventHandlerNomal; ElapsedEventHandler ElapsedEventHandlerSpeedUp; void InitTimer() { TimerNomal1.Elapsed += TimerNomal_Elapsed; TimerNomal1.Interval = 30; TimerNomal1.Start(); TimerNomal2.Elapsed += TimerNomal_Elapsed; TimerNomal2.Interval = 25; TimerNomal2.Start(); TimerNomal3.Elapsed += TimerNomal_Elapsed; TimerNomal3.Interval = 20; TimerNomal3.Start(); TimerNomal4.Elapsed += TimerNomal_Elapsed; TimerNomal4.Interval = 15; TimerNomal4.Start(); TimerNomal5.Elapsed += TimerNomal_Elapsed; TimerNomal5.Interval = 10; TimerNomal5.Start(); TimerSpeedUp.Elapsed += TimerSpeedUp_Elapsed; TimerSpeedUp.Interval = 30; TimerSpeedUp.Start(); ElapsedEventHandlerNomal = TimerNomal_Elapsed; ElapsedEventHandlerSpeedUp = TimerSpeedUp_Elapsed; } } |
ゲームオブジェクトが不要になったときの処理を示します。バックアップしておいたタイマーのイベントハンドラを取り除きます。
1 2 3 4 5 6 7 8 9 10 11 12 |
public partial class Game { public void DestroyGame() { TimerNomal1.Elapsed -= ElapsedEventHandlerNomal; TimerNomal2.Elapsed -= ElapsedEventHandlerNomal; TimerNomal3.Elapsed -= ElapsedEventHandlerNomal; TimerNomal4.Elapsed -= ElapsedEventHandlerNomal; TimerNomal5.Elapsed -= ElapsedEventHandlerNomal; TimerSpeedUp.Elapsed -= ElapsedEventHandlerSpeedUp; } } |
ゲーム開始の処理
ゲームを開始するための処理を示します。
ゲームオーバーになってからサイドゲーム開始の処理がおこなわれるかもしれないのでプレイヤーと敵、スコアなどを初期化しています。そしてクライアントサイドでも処理をおこなえるようにイベントを発生させて通知できるようにしています。
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 partial class Game { public void GameStart() { // ゲームの最中である場合は処理をしない if (!IsGaming) { // ゲームの状態を初期化する InitPlayerPosition(); InitEnemies(); IsVisits = new bool[MapHeight, MapWidth]; IsStop = false; IsGaming = true; Score = 0; Rest = MAX_REST; StageNumber = 1; AddPointCrashEnemy = POINT_CRASH_ENEMY_MIN; EatX = 0; EatY = 0; GameStarted?.Invoke(); } } } |
プレイヤーの移動処理
以下は各キャラクタの移動をおこなううえでそれが二次元配列の範囲内にあるかどうかを調べるためのメソッドです。
1 2 3 4 5 6 7 8 9 10 11 12 |
public partial class Game { public bool IsInMap(int y, int x) { if (x < 0 || y < 0) return false; if (x >= MapWidth || y >= MapHeight) return false; return true; } } |
プレイヤーを移動する処理を示します。
後述するSetPlayerDirectメソッドでキーが押されているかどうかで移動方向をセットします。そのあとPlayerDirectプロパティに応じてプレイヤーの座標を移動させます。
このときローラーを使用しているときはローラーも移動させます。そしてこれまで通路のうち通過していない部分を通過したらスコアを追加します。
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 56 57 |
public partial class Game { public void MovePlayer() { SetPlayerDirect(); if (Map == null || IsVisits == null) return; // 移動処理 if (PlayerDirect == Direct.Up) { if (PlayerY - 1 >= 0 && Map[PlayerY - 1, PlayerX] > 0) PlayerY--; // 画面端に来たらワープ処理 if (PlayerY - 1 == -1) { IsVisits[PlayerY, PlayerX] = true; PlayerY = MapHeight - 1; } } if (PlayerDirect == Direct.Down) { if (Map[PlayerY + 1, PlayerX] > 0) PlayerY++; if (PlayerY + 1 == MapHeight) { IsVisits[PlayerY, PlayerX] = true; PlayerY = 0; } } if (PlayerDirect == Direct.Left) { if (Map[PlayerY, PlayerX - 1] > 0) PlayerX--; if (PlayerX - 1 == -1) { IsVisits[PlayerY, PlayerX] = true; PlayerX = MapWidth - 1; } } if (PlayerDirect == Direct.Right) { if (Map[PlayerY, PlayerX + 1] > 0) PlayerX++; if (PlayerX + 1 == MapWidth) { IsVisits[PlayerY, PlayerX] = true; PlayerX = 0; } } MoveRollerIfPlayerHold(); AddScoreOnMoveIfNeed(); } } |
キーが押されているかどうかで移動方向をPlayerDirectプロパティにセットする処理を示します。
ローラー使用時は方向転換不可なのでIsHoldRollerXプロパティを調べて状況によってはそのままreturnしています。通路と橋が交差する部分では通路から橋に乗り入れることができるバグを避けるため、このような場合は方向転換できないようにしています。
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 |
public partial class Game { void SetPlayerDirect() { // ローラー使用時は方向転換不可とする if (IsHoldRollerN || IsHoldRollerE || IsHoldRollerS || IsHoldRollerW) return; // 橋の上または下では直進しかできない if (Map != null && IsInMap(PlayerY, PlayerX) && Map[PlayerY, PlayerX] != (int)Road.BRIDGE_NS_CROSS && Map[PlayerY, PlayerX] != (int)Road.BRIDGE_WE_CROSS) { if (IsUpKeyDown) { if (Map[PlayerY - 1, PlayerX] != (int)Road.NONE) PlayerDirect = Direct.Up; } if (IsDownKeyDown) { if (Map[PlayerY + 1, PlayerX] != (int)Road.NONE) PlayerDirect = Direct.Down; } if (IsLeftKeyDown) { if (Map[PlayerY, PlayerX - 1] != (int)Road.NONE) PlayerDirect = Direct.Left; } if (IsRightKeyDown) { if (Map[PlayerY, PlayerX + 1] != (int)Road.NONE) PlayerDirect = Direct.Right; } } } } |
ローラーを移動させる処理を示します。
ローラーの可動範囲でローラーを押すことができる位置にプレイヤーが存在する場合はローラーを移動させています。またどの方向にローラーが移動しているのかがクライアントサイドでもわかるようにIsHoldRollerXプロパティをセットしています。
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 56 57 58 59 60 61 62 63 64 65 |
public partial class Game { public void MoveRollerIfPlayerHold() { if ( RollerWE != null && PlayerDirect == Direct.Right && RollerWE.Y == PlayerY && RollerWE.X - PlayerX <= CHARACTOR_SIZE && RollerWE.X - PlayerX > 0 && RollerWE.X + 1 < RollerWE.RightEndX ) { RollerWE.X = PlayerX + CHARACTOR_SIZE; IsHoldRollerE = true; } else IsHoldRollerE = false; if ( RollerWE != null && PlayerDirect == Direct.Left && RollerWE.Y == PlayerY && PlayerX - RollerWE.X <= CHARACTOR_SIZE && PlayerX - RollerWE.X > 0 && RollerWE.X - 1 > RollerWE.LeftEndX ) { RollerWE.X = PlayerX - CHARACTOR_SIZE; IsHoldRollerW = true; } else IsHoldRollerW = false; if ( RollerNS != null && PlayerDirect == Direct.Up && RollerNS.X == PlayerX && PlayerY - RollerNS.Y <= CHARACTOR_SIZE && PlayerY - RollerNS.Y > 0 && RollerNS.Y - 1 > RollerNS.TopEndY + CHARACTOR_SIZE ) { RollerNS.Y = PlayerY - CHARACTOR_SIZE; IsHoldRollerN = true; } else IsHoldRollerN = false; if ( RollerNS != null && PlayerDirect == Direct.Down && RollerNS.X == PlayerX && RollerNS.Y - PlayerY <= CHARACTOR_SIZE && RollerNS.Y - PlayerY > 0 && RollerNS.Y - 1 < RollerNS.BottomEndY - CHARACTOR_SIZE ) { RollerNS.Y = PlayerY + CHARACTOR_SIZE; IsHoldRollerS = true; } else IsHoldRollerS = false; } } |
当たり判定
当たり判定の処理を示します。
プレイヤーと敵が重なっている場合、ローラーを使用している場合は敵を倒したときの処理をおこない、そうでない場合はミスの処理をおこないます。このとき近い距離にいても一方が橋のうえで他方がそうではない場所にいる場合は当たり判定はfalseになるように注意します。
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 |
public partial class Game { void CheckHit() { if (Map == null) return; Enemy? hitEnemy = null; foreach (Enemy enemy in Enemies) { // 敵とプレイヤーの距離が近い場合は当たり判定とする // ただし一方が橋のうえで他方がそうではない場所にいる場合は除外する if (Math.Abs(enemy.X - PlayerX) + Math.Abs(enemy.Y - PlayerY) < CHARACTOR_SIZE) { // 両者が同じ橋のうえにいるなら両者は接触している if ( (Map[PlayerY, PlayerX] == (int)Road.BRIDGE_NS && Map[enemy.Y, enemy.X] == (int)Road.BRIDGE_NS) || (Map[PlayerY, PlayerX] == (int)Road.BRIDGE_WE && Map[enemy.Y, enemy.X] == (int)Road.BRIDGE_WE) ) { hitEnemy = enemy; break; } // 両者が橋ではない場所にいるなら両者は接触している if ( Map[PlayerY, PlayerX] != (int)Road.BRIDGE_NS && Map[PlayerY, PlayerX] != (int)Road.BRIDGE_NS_CROSS && Map[PlayerY, PlayerX] != (int)Road.BRIDGE_WE && Map[PlayerY, PlayerX] != (int)Road.BRIDGE_WE_CROSS && Map[enemy.Y, enemy.X] != (int)Road.BRIDGE_NS && Map[enemy.Y, enemy.X] != (int)Road.BRIDGE_NS_CROSS && Map[enemy.Y, enemy.X] != (int)Road.BRIDGE_WE && Map[enemy.Y, enemy.X] != (int)Road.BRIDGE_WE_CROSS ) { hitEnemy = enemy; break; } } } if (hitEnemy != null) { if (IsHoldRollerN || IsHoldRollerS || IsHoldRollerW || IsHoldRollerE) OnHitEmemy(hitEnemy); else OnDeadPlayer(); } } } |
敵を倒したときとミス時の処理
敵を倒したときの処理を示します。
敵を倒したら2秒間ゲームの動作を停止させます。そしてクライアントサイドで倒した敵の近くに加算される点数を表示するため、その座標をPositionHitEmemyプロパティにセットしています。そのあとイベントを発生させてクライアントサイドで適切な処理ができるようにしています。
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 |
public partial class Game { public delegate void HitEmemyHandler(); public event HitEmemyHandler? HitEmemy; void OnHitEmemy(Enemy enemy) { IsStop = true; Score += AddPointCrashEnemy; // 倒した敵の座標をPositionHitEmemyプロパティにセットする PositionHitEmemyX = enemy.X; PositionHitEmemyY = enemy.Y; HitEmemy?.Invoke(); // 2秒間停止する System.Timers.Timer timer = new System.Timers.Timer(); timer.Interval = 2000; timer.Elapsed += (sender, e) => { System.Timers.Timer? t = (System.Timers.Timer?)sender; t?.Stop(); t?.Dispose(); // PositionHitEmemyX,Y == 0 になったら加算される点数は表示されなくなる PositionHitEmemyX = 0; PositionHitEmemyY = 0; HitEmemy?.Invoke(); enemy.Reset(); // 得点追加の処理 次回の得点は倍(ただし上限は9900点)になる // Scoreプロパティは後述 AddPointCrashEnemy *= 2; if (AddPointCrashEnemy > 9900) AddPointCrashEnemy = 9900; // 停止状態から復帰させる IsStop = false; }; timer.Start(); } } |
ミス時の処理を示します。
ミス時は2秒間ゲームの動作を停止させます。残機を1減らし敵を倒したときに加算される点数をリセットします。そしてクライアントサイドで残機の表示数を減らすためにイベントを発生させています。
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 |
public partial class Game { public delegate void DeadPlayerHandler(); public event DeadPlayerHandler? DeadPlayer; void OnDeadPlayer() { IsStop = true; // 敵を倒したときに加算される点数をリセット DeadPlayer?.Invoke(); AddPointCrashEnemy = POINT_CRASH_ENEMY_MIN; Rest--; System.Timers.Timer timer = new System.Timers.Timer(); timer.Interval = 2000; timer.Elapsed += (sender, e) => { System.Timers.Timer? t = (System.Timers.Timer?)sender; t?.Stop(); t?.Dispose(); // ミスをした結果、ゲームオーバーになっているかもしれないのでチェック if (Rest <= 0) { GameOver(); return; } // プレイヤーと敵の座標をもとに戻す InitPlayerPosition(); foreach (Enemy enemy in Enemies) enemy.Reset(); InitRollers(); // 停止状態から復帰させる IsStop = false; }; timer.Start(); } } |
ゲームオーバー時の処理を示します。IsGamingプロパティをfalseにして動作を停止させます。これによってゲームスタートの処理ができるようになります。
クライアントサイドでゲームオーバー時の効果音を鳴らす処理をおこないたいので、この場合もイベントを発生させます。
1 2 3 4 5 6 7 8 |
public partial class Game { public void GameOver() { IsGaming = false; GameOvered?.Invoke(); } } |
ステージクリアの処理
これまで通過していない座標を通過したら加点する処理を示します。
10点を加算しますが、加点の頻度を下げるため4回に1回の割合とします。またこれまで通過していない座標を通過した場合はその座標をEatXプロパティとEatYプロパティにセットします。クライアントサイドでこの座標を取得して通路の表示色を変更できるようにしています。
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 partial class Game { int _eatCount = 0; void AddScoreOnMoveIfNeed() { EatX = 0; EatY = 0; if (!IsVisits[PlayerY, PlayerX]) { IsVisits[PlayerY, PlayerX] = true; EatX = PlayerX; EatY = PlayerY; _eatCount++; if (_eatCount % 4 == 0) { Score += 10; _eatCount = 0; } // ステージクリアしているかもしれないのでチェックする if (CheckStageClear()) OnStageClear(); } } } |
ステージクリアしているかどうか調べる処理を示します。
通過した部分は色を変更しますが、これだと通過していない点が存在してもすでに色が変わっているため未通過であることにユーザーは気づくことができません。そこで通路のすべての部分の色が変更されている場合はステージクリアと判定できるように、未通過の座標が存在しても近くにすでに通過している点が存在する場合は通過済みとして扱います。
これで未通過の点が存在しない場合はステージクリアです。
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 |
public partial class Game { bool CheckStageClear() { if (IsVisits == null || Map == null) return false; for (int y = 0; y < MapHeight; y++) { for (int x = 0; x < MapWidth; x++) { if (!IsVisits[y, x] && Map[y, x] != (int)Road.NONE && !IsVisitSurroundingPosittions(x, y)) return false; } } return true; } // 未通過の座標が存在しても近くにすでに通過している点が存在する場合は通過済みとして扱う bool IsVisitSurroundingPosittions(int x, int y) { if(IsVisits == null) return false; for (int i = 0; i <= 16; i++) { for (int j = 0; j <= 16; j++) { if (y + i < 0) continue; if (y + i >= MapHeight) continue; if (x + j < 0) continue; if (x + j >= MapWidth) continue; if (IsVisits[y + i, x + j]) return true; } } return false; } } |
ステージクリアしていた場合の処理を示します。
前述のとおり通過していない点も通過していることにするため、目立つ塗り残しがあるかもしれません。そこでステージクリアと判定されたときはIsVisitsをすべてtrueにします。そのあと2秒間動作を停止させて次のステージに移行する処理をおこないます。またこのときにイベントを発生させてクライアントサイドで効果音を鳴らす処理を可能にします。
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 |
public partial class Game { public delegate void StageClearHandler(); public event StageClearHandler? StageClear; void OnStageClear() { if (Map == null || IsVisits == null) return; IsStop = true; // 目立つ塗り残しがあるかもしれないのでIsVisitsをすべてtrueにする StageClear?.Invoke(); // 敵を倒したときに加算される点数をリセット(100点)する AddPointCrashEnemy = POINT_CRASH_ENEMY_MIN; // 2秒後に次のステージに移動 System.Timers.Timer timer = new System.Timers.Timer(); timer.Interval = 2000; timer.Elapsed += (sender, e) => { System.Timers.Timer? t = (System.Timers.Timer?)sender; t?.Stop(); t?.Dispose(); ToNextStage(); }; timer.Start(); } } |
次のステージに移動する処理を示します。
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 partial class Game { public delegate void NextStageHandler(); public event NextStageHandler? NextStage; void ToNextStage() { if (Map == null || IsVisits == null) return; // IsVisitsをすべてfalseに for (int y = 0; y < MapHeight; y++) { for (int x = 0; x < MapWidth; x++) IsVisits[y, x] = false; } // プレイヤーと敵を初期の座標へ戻す foreach (Enemy enemy in Enemies) enemy.Reset(); InitPlayerPosition(); EatX = 0; EatY = 0; StageNumber++; NextStage?.Invoke(); IsStop = false; } } |
更新処理
タイマーイベントが発生したときの処理を示します。
IsStopプロパティがtrueのときはキャラクタを移動させてはならないので無視します。またステージに応じて必要のないイベントも無視します。
それから永久パターン対策として敵を倒し続けた場合は動作速度を速めます。そのために処理すべきタイマーを変更しています。
必要なイベントが発生した場合はプレイヤーと敵の移動処理をおこない、当たり判定をおこないます。そのあとChangeStatusイベントを発生させてクライアントサイドで描画処理を行なうための情報を取得させます。
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 |
public partial class Game { private void TimerNomal_Elapsed(object? sender, ElapsedEventArgs e) { if (IsStop) return; System.Timers.Timer? t = (System.Timers.Timer?)sender; // 永久パターン対策 int stage = StageNumber; if (AddPointCrashEnemy >= 6400) stage++; if (AddPointCrashEnemy == 9900) stage++; // ステージに応じて必要ないイベントは無視 if (stage == 1 && t != TimerNomal1) return; if (stage == 2 && t != TimerNomal2) return; if (stage == 3 && t != TimerNomal3) return; if (stage == 4 && t != TimerNomal4) return; if (stage >= 5 && t != TimerNomal5) return; MovePlayer(); Enemies[0].Move(PlayerX, PlayerY); Enemies[1].Move(PlayerX, PlayerY); // 敵の座標と移動方向をプロパティにセットする // プレイヤーの座標はMovePlayerメソッド内でセットされるようになっている Enemy1X = Enemies[0].X; Enemy1Y = Enemies[0].Y; Enemy1Direct = Enemies[0].EnemyDirect; Enemy2X = Enemies[1].X; Enemy2Y = Enemies[1].Y; Enemy2Direct = Enemies[1].EnemyDirect; CheckHit(); ChangeStatus?.Invoke(); } } |
プレイヤーがローラーを使用しているときはプレイヤーの動作速度を速めます。別のタイマーでもMovePlayerメソッドを呼び出すことで、これを実現します。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public partial class Game { private void TimerSpeedUp_Elapsed(object? sender, System.Timers.ElapsedEventArgs e) { if (IsStop) return; if (IsHoldRollerE || IsHoldRollerW || IsHoldRollerN || IsHoldRollerS) { MovePlayer(); } } } |
キー操作への対応
以下はキー操作がおこなわれたときの処理です。ここではIsXKeyDownプロパティを設定しているだけです。
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 |
public partial class Game { public void OnKeyDown(string key) { if (key == "ArrowUp") IsUpKeyDown = true; if (key == "ArrowDown") IsDownKeyDown = true; if (key == "ArrowLeft") IsLeftKeyDown = true; if (key == "ArrowRight") IsRightKeyDown = true; if (key == "s") GameStart(); } public void OnKeyUp(string key) { if (key == "ArrowUp") IsUpKeyDown = false; if (key == "ArrowDown") IsDownKeyDown = false; if (key == "ArrowLeft") IsLeftKeyDown = false; if (key == "ArrowRight") IsRightKeyDown = false; } } |