ASP.NET Core版 マッピー(MAPPY)をつくる(1)の続きです。
Contents
Playerクラスの定義
Playerクラスを定義します。
1 2 3 4 5 6 |
namespace Mappy { public class Player { } } |
以降は名前空間を省略して以下のように書きます。
1 2 3 |
public class Player { } |
それから移動方向はDirect列挙体を使って表現します。
1 2 3 4 5 6 7 8 9 10 11 |
namespace Mappy { public enum Direct { None, Up, Down, Left, Right, } } |
コンストラクタ
ここではスコア、移動している方向、最後に左右に移動した方向(キャラクタを右向き左向きのどちらで描画するかを決めるために必要)、プレイヤー名などを初期化します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public class Player { MappyGame _game; public Player(MappyGame game) { _game = game; Rest = MappyGame.PLAYER_MAX; Score = 0; MoveDirect = Direct.None; LastDirect = Direct.None; Name = ""; ConnectionId = ""; Bullets = new List<Bullet>(); IsGameOver = true; VerticalSpeed = 7; HorizontalSpeed = 4; } } |
Bulletクラスはパワードアを開けたときに発射される弾丸(本物のマッピーでは「衝撃破」)の座標と状態を格納するためのものです。KillCountプロパティはその弾丸に当たった敵の数が格納されます。
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 |
namespace Mappy { public class Bullet { public Bullet(int x, int y, bool isLeft) { X = x; Y = y; VelocityX = isLeft ? -16 : 16; KillCount = 0; } public int X { private set; get; } public int Y { get; } // 弾丸の速度(負数なら左向き) public int VelocityX { get; } public int KillCount { set; get; } public void Update() { X += VelocityX; } } } |
プロパティ
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 |
public class Player { // AspNetCore.SignalRで接続したときのID public string ConnectionId { private set; get; } // X座標 public int X { set; get; } // Y座標 public int Y { set; get; } // 垂直方向の移動速度 public int VerticalSpeed { set; get; } // 水平方向の移動速度 public int HorizontalSpeed { set; get; } // 現在の移動方向 public Direct MoveDirect { private set; get; } // キャラクタは右向き?左向き? public Direct LastDirect { private set; get; } // プレイヤー名(未指定の場合は"デフォルトの名無しさん") string _name = ""; public string Name { set { if (value == "") _name = "デフォルトの名無しさん"; else _name = value; } get { return _name; } } // スコア public int Score { private set; get; } // 残機 public int Rest { set; get; } // 死亡フラグ public bool IsDead { private set; get; } // 現在ゲームオーバー状態か? public bool IsGameOver { private set; get; } // プレイヤーが発射した弾丸 public List<Bullet> Bullets { set; get; } // ユーザーが↑キーを押しているか? bool _isUpKeyDown = false; public bool IsUpKeyDown { set { _isUpKeyDown = value; if (value) { _isDownKeyDown = false; _isLeftKeyDown = false; _isRightKeyDown = false; } } get { return _isUpKeyDown; } } // ユーザーが↓キーを押しているか? bool _isDownKeyDown = false; public bool IsDownKeyDown { set { _isDownKeyDown = value; if (value) { _isUpKeyDown = false; _isLeftKeyDown = false; _isRightKeyDown = false; } } get { return _isDownKeyDown; } } // ユーザーが←キーを押しているか? bool _isLeftKeyDown = false; public bool IsLeftKeyDown { set { _isLeftKeyDown = value; if (value) { _isUpKeyDown = false; _isDownKeyDown = false; _isRightKeyDown = false; } } get { return _isLeftKeyDown; } } // ユーザーが→キーを押しているか? bool _isRightKeyDown = false; public bool IsRightKeyDown { set { _isRightKeyDown = value; if (value) { _isUpKeyDown = false; _isDownKeyDown = false; _isLeftKeyDown = false; } } get { return _isRightKeyDown; } } } |
ゲーム開始の処理
AspNetCore.SignalRで接続するとMappyGameオブジェクトが生成され、そのなかにPlayerオブジェクトが生成されます。このときクライアントサイドからPlayerを操作できるようにPlayer.ConnectionIdプロパティにAspNetCore.SignalRで接続したときに与えられるIDを設定します。SetConnectionIdメソッドはその処理をおこないます。
1 2 3 4 5 6 7 |
public class Player { public void SetConnectionId(string connectionId) { ConnectionId = connectionId; } } |
ゲームが開始されるとMappyGame.GameStartメソッドが実行され、MappyGame.InitメソッドのなかでPlayerの初期座標がセットされます。そしてPlayerクラスのGameStartメソッドが実行されます。
1 2 3 4 5 6 7 8 9 |
public class MappyGame { public void GameStart() { StageNumber = 0; // ステージ番号を0にリセットする Init(); // 第一ステージのマップが生成される Player.GameStart(); } } |
PlayerクラスのSetInitPositionメソッドは以下のようになっています。自機が死亡したら初期位置に戻ってプレイを続行できるようにフィールド変数に値を格納しています。
1 2 3 4 5 6 7 8 9 10 11 |
public class Player { int _initX = 0; int _initY = 0; public void SetInitPosition(Position position) { X = _initX = position.X; Y = _initY = position.Y; } } |
PlayerクラスのGameStartメソッドではIsGameOverフラグのクリア、残機を最大値にセット、スコアのリセットなどの処理をおこなっています。またトランポリンの状態を初期化しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class Player { public void GameStart() { IsGameOver = false; Rest = MappyGame.PLAYER_MAX; IsDead = false; Score = 0; MoveDirect = Direct.None; Bullets = new List<Bullet>(); _game.ResetTrampolines(); } } |
トランポリンの状態を初期化する処理は以下のようになっています。
1 2 3 4 5 6 7 8 |
public class MappyGame { public void ResetTrampolines() { foreach (Trampoline trampoline in Trampolines) trampoline.Reset(); } } |
移動の処理
更新処理がおこなわれたらキーの押下状態によってプレイヤーを移動させ、弾丸が発射されているのであれば弾丸も移動させます。
1 2 3 4 5 6 7 8 9 10 |
public class Player { public void Update() { Move(); foreach (Bullet bullet in Bullets) bullet.Update(); } } |
プレイヤーを移動させる処理を示します。プレイヤーが上昇または下降している場合は後述するFallメソッドまたはJumpメソッドを呼び出し垂直方向の移動処理をおこないます。そうでない場合は水平方向の処理をおこないます。
移動したあとの座標のXY座標がともにCHARACTER_SIZEで割り切れる場合、そこには穴やドアがあるかもしれません。穴がある場合は落下させ、閉まっているドアがある場合は開けます。
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 |
public class Player { public void Move() { if (MoveDirect == Direct.Down || MoveDirect == Direct.Up) { if (MoveDirect == Direct.Down) Fall(); // 後述 else if (MoveDirect == Direct.Up) Jump(); // 後述 return; } PlayerMoveHorizontal(); // 後述 if (MoveDirect == Direct.Left) X -= HorizontalSpeed; if (MoveDirect == Direct.Right) X += HorizontalSpeed; int remainder = X % MappyGame.CHARACTER_SIZE; if (remainder != 0 && remainder < HorizontalSpeed) X -= remainder; if (X % MappyGame.CHARACTER_SIZE == 0 && Y % MappyGame.CHARACTER_SIZE == 0) { int col = X / MappyGame.CHARACTER_SIZE; int row = Y / MappyGame.CHARACTER_SIZE; // 穴があるなら落下させる if (_game.MapArray[row, col] == Cell.Hole) { MoveDirect = Direct.Down; return; } // 閉まっているドアがあるなら開ける OpenDoor(row, col); // 後述 } } } |
落下の処理
プレイヤーが落下しているときの処理を示します。
プレイヤーのY座標を増加させます。落下先にトランポリンがある場合は移動方向を反転させます。またクライアントサイドでトランポリンの効果音を鳴らす処理ができるようにイベントを発生させます。トランポリンを連続使用できるのは4回までです。そこでTrampoline.Lifeをデクリメントします。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 |
public class Player { public event EventHandler? JumpEvent; void Fall() { Y += VerticalSpeed; int remainder = Y % MappyGame.CHARACTER_SIZE; if (remainder != 0 && remainder < VerticalSpeed) Y -= remainder; if (X % MappyGame.CHARACTER_SIZE == 0 && Y % MappyGame.CHARACTER_SIZE == 0) { int col = X / MappyGame.CHARACTER_SIZE; int row = Y / MappyGame.CHARACTER_SIZE; if (_game.MapArray[row, col] == Cell.Trampoline) { MoveDirect = Direct.Up; JumpEvent?.Invoke(this, new EventArgs()); Trampoline? trampoline = _game.GetTrampoline(row, col); if (trampoline != null) { trampoline.Life--; if (trampoline.Life < 0) { // 更新処理が(MappyGame.TIME_TO_START_MAX)回おこなわれたらゲームを再開する _game.TimeToPlayerRevive = MappyGame.TIME_TO_START_MAX; Dead(); // 後述 _game.SetSparks(X, Y); // 後述 } } } } } } |
上昇中の処理
プレイヤーが上昇しているときの処理を示します。
Y座標を減らす処理をした後、Y座標がCHARACTER_SIZEで割り切れる場合はそこが天井かどうかを調べます。天井の場合は移動方向を下向きに変更します。またユーザーが左右いずれかのキーを押している場合はそこで床に着地することができる場合はその階に着地させます。この処理ができるかどうかは後述するPlayerLandingFloorメソッドでおこないます。
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 Player { public void Jump() { Y -= VerticalSpeed; int remainder = Y % MappyGame.CHARACTER_SIZE; if (remainder != 0 && remainder < VerticalSpeed) Y -= remainder; if (X % MappyGame.CHARACTER_SIZE == 0 && Y % MappyGame.CHARACTER_SIZE == 0) { int col = X / MappyGame.CHARACTER_SIZE; int row = Y / MappyGame.CHARACTER_SIZE; if (_game.MapArray[row, col] == Cell.Ceiling) MoveDirect = Direct.Down; } // 上に少しずれた場所で左右のキーを押しても着地できるようにしている if (X % MappyGame.CHARACTER_SIZE == 0 && (Y % MappyGame.CHARACTER_SIZE == 0 || Y % MappyGame.CHARACTER_SIZE >= MappyGame.CHARACTER_SIZE - VerticalSpeed * 3)) { int col = X / MappyGame.CHARACTER_SIZE; int row = Y / MappyGame.CHARACTER_SIZE; if (Y % MappyGame.CHARACTER_SIZE >= MappyGame.CHARACTER_SIZE - VerticalSpeed * 3) row++; PlayerLandingFloor(row, col); // 後述 } } } |
左右の移動
左右の移動をする処理を示します。PlayerMoveHorizontalメソッドでは左右のいずれかのキーが押されている場合はLastDirectプロパティとLastDirectプロパティにその方向にセットします。このあとMoveDirectプロパティの値によって左右の移動の処理がおこなわれます。どちらのキーも押されていない場合はプレイヤーは移動しません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public class Player { public void PlayerMoveHorizontal() { if (IsLeftKeyDown) { MoveDirect = Direct.Left; LastDirect = MoveDirect; } else if (IsRightKeyDown) { MoveDirect = Direct.Right; LastDirect = MoveDirect; } else MoveDirect = Direct.None; } } |
接触したドアを開ける
プレイヤーが閉まっているドアに接触したとき、これを開ける処理を示します。プレイヤーの現在位置にドアがあるか調べて、ある場合はそのドアを開けます。
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 |
public class Player { void OpenDoor(int row, int col) { List<Door> doors = new List<Door>(); if (MoveDirect == Direct.Left) { Door? door = _game.GetDoor(row, col - 1); if (door != null && !door.IsOpen && door.OpenDirect == Direct.Left) doors.Add(door); door = _game.GetDoor(row, col); if (door != null && !door.IsOpen && door.OpenDirect == Direct.Right) doors.Add(door); } if (MoveDirect == Direct.Right) { Door? door = _game.GetDoor(row, col + 1); if (door != null && !door.IsOpen && door.OpenDirect == Direct.Right) doors.Add(door); door = _game.GetDoor(row, col); if (door != null && !door.IsOpen && door.OpenDirect == Direct.Left) doors.Add(door); } foreach (Door door in doors) { if (door.IsPowerDoor) OpenPowerDoor(door); // 後述 else OpenDoor(door); // 後述 } } } |
プレイヤーが接触したドアを開ける処理を示します。
ドアを開けたときに敵がドアに巻き込まれる場合、敵を気絶させる処理をおこないます。この処理はThrowEnemiesメソッドでおこないます。敵を気絶させることができた場合は効果音を鳴らすためにEnemyDownEventを発生させます。また該当する敵がいなかった場合は通常のドアの開閉時の効果音を鳴らすためにOpenCloseDoorEventを発生させます。
開けたドアがパワードアの場合はOpenPowerDoorメソッドが呼び出されます。この場合はドアが開く方向に弾丸が発射されます。そのあとパワードアは普通のドアに変わります。またクライアントサイドで発射音を鳴らすことができるようにFireBulletイベントを発生させます。
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 class Player { public event EventHandler? EnemyDownEvent; public event EventHandler? OpenCloseDoorEvent; void OpenDoor(Door door) { door.IsOpen = true; if (ThrowEnemies(door)) EnemyDownEvent?.Invoke(this, new EventArgs()); else OpenCloseDoorEvent?.Invoke(this, new EventArgs()); } public event EventHandler? FireBullet; void OpenPowerDoor(Door door) { door.IsOpen = true; door.IsPowerDoor = false; if (door.OpenDirect == Direct.Left) Bullets.Add(new Bullet(door.X, door.Y, true)); else Bullets.Add(new Bullet(door.X, door.Y, false)); FireBullet?.Invoke(this, new EventArgs()); } } |
敵を気絶させる
ドアの開閉時に巻き込まれた敵を気絶させる処理を示します。ドアの近くに敵がいた場合はドアの移動方向に敵を飛ばして気絶させます。
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 |
public class Player { bool ThrowEnemies(Door door) { // ドアの開閉で影響をうける敵を取得する List<Enemy> enemies = _game.Enemies.Where(_ => _.CantMoveCount <= 0 && _.Y == door.Y && door.X - MappyGame.CHARACTER_SIZE <= _.X && _.X <= door.X + MappyGame.CHARACTER_SIZE ).ToList(); // ドアに巻き込まれた敵が飛ばされるX座標 int afterX = 0; if (door.OpenDirect == Direct.Left) { if (door.IsOpen) afterX = door.X - MappyGame.CHARACTER_SIZE * 2; else afterX = door.X + MappyGame.CHARACTER_SIZE * 2; } if (door.OpenDirect == Direct.Right) { if (door.IsOpen) afterX = door.X + MappyGame.CHARACTER_SIZE * 2; else afterX = door.X - MappyGame.CHARACTER_SIZE * 2; } foreach (Enemy enemy in enemies) { // 敵が飛ばされるX座標を設定する enemy.LandingPositionX = afterX; // 飛ばされた敵はしばらく動けない enemy.CantMoveCount = MappyGame.CANT_MOVE_COUNT_MAX; } // スコアを加算(敵一体につき50点) if (enemies.Count > 0) { AddScore1(50 * enemies.Count, enemies[0].X, enemies[0].Y); return true; } else return false; } } |
スコアの加算
スコアを加算する処理を示します。
スコアの加算がおこなわれたときはその点数を表示させます。そのため点数と座標をクライアントサイドに伝えることができるようにイベントを発生させます。第二引数が加算される点数、第三引数と第四引数が加算される点数が表示される座標です。
1 2 3 4 5 6 7 8 9 10 |
public class Player { public delegate void AddScoreHnaler1(Player sender, int value, int x, int y); public event AddScoreHnaler1? AddScoreEvent1; public void AddScore1(int value, int x, int y) { Score += value; AddScoreEvent1?.Invoke(this, value, x, y); } } |
パワードアを開けて敵を倒したときは横に流れるように加算される点数が表示されます。その処理をおこなうメソッドを示します。第二引数が加算される点数、第三引数が点数が表示されるY座標です。
1 2 3 4 5 6 7 8 9 10 |
public class Player { public delegate void AddScoreHnaler2(Player sender, int value, int y, bool isFromLeft); public event AddScoreHnaler2? AddScoreEvent2; public void AddScore2(int value, int y, bool isFromLeft) { Score += value; AddScoreEvent2?.Invoke(this, value, y, isFromLeft); } } |
遠隔操作によるドアの開閉
ドアは遠隔操作で開閉することができます。以下はプレイヤーにとって一番近いドアを開閉する処理をおこないます。
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 class Player { public void OpenCloseDoor() { // プレイヤー死亡時、上昇または下降時はドアの開閉はできない if (IsDead || MoveDirect == Direct.Up || MoveDirect == Direct.Down) return; if (Y % MappyGame.CHARACTER_SIZE == 0) { // プレイヤーとドアが重なっている場合はそのドアを開閉する // それ以外のときはプレイヤーが向いている方向で一番近いドアを取得して、それを開閉する Door? door = _game.Doors.FirstOrDefault(_ => _.Y == Y && X - MappyGame.CHARACTER_SIZE <= _.X && _.X <= X + MappyGame.CHARACTER_SIZE); if (door != null) OpenCloseDoor(door); else if (LastDirect == Direct.Left) { List<Door> doors = _game.Doors.Where(_ => _.Y == Y && _.X < X).OrderByDescending(_ => _.X).ToList(); if (doors.Count > 0) OpenCloseDoor(doors[0]); } else if (LastDirect == Direct.Right) { List<Door> doors = _game.Doors.Where(_ => _.Y == Y && _.X > X).OrderBy(_ => _.X).ToList(); if (doors.Count > 0) OpenCloseDoor(doors[0]); } } } void OpenCloseDoor(Door door) { // パワードアを開けたとき if (door.IsPowerDoor) { OpenPowerDoor(door); return; } // 普通のドアの場合は開いているなら閉じ、閉じているなら開ける if (door.IsOpen) door.IsOpen = false; else door.IsOpen = true; // ドアが開閉にともなうイベントを発生させる // ドアの開閉で敵が巻き込まれた場合で送信されるイベントを変える if (ThrowEnemies(door)) EnemyDownEvent?.Invoke(this, new EventArgs()); else OpenCloseDoorEvent?.Invoke(this, new EventArgs()); } } |
トランポリンから着地
トランポリンから着地する処理を示します。
キーが押されているときで移動することができる場所であればMoveDirectにDirect.LeftまたはDirect.Rightをセットします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public class Player { public void PlayerLandingFloor(int row, int col) { if (IsLeftKeyDown && col - 1 >= 0 && _game.MapArray[row, col - 1] == Cell.Aisle) MoveDirect = Direct.Left; if (IsRightKeyDown && col + 1 < _game.ColMax && _game.MapArray[row, col + 1] == Cell.Aisle) MoveDirect = Direct.Right; if (MoveDirect == Direct.Left || MoveDirect == Direct.Right) { // 上に少しずれた場所で左右のキーを押しても着地できるようにしている // この場合、Y座標の調整が必要 if (Y % MappyGame.CHARACTER_SIZE != 0) { Y = (Y / MappyGame.CHARACTER_SIZE + 1) * MappyGame.CHARACTER_SIZE; } _game.ResetTrampolines(); } } } |
アイテムを回収したとき
アイテムを回収したときの処理を示します。アイテムのNumberプロパティによって加算される点数を変えます。またイベントを発生させます。
1 2 3 4 5 6 7 8 9 |
public class Player { public event EventHandler? GetItemEvent; public void GetItem(Item item) { AddScore1(item.Number * 100, item.X, item.Y); GetItemEvent?.Invoke(this, new EventArgs()); } } |
敵を倒したとき
敵を気絶させたり弾丸で吹き飛ばしたときの処理を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public class Player { // 敵を気絶させた(敵が自滅したときも含む) public void EnemyDown() { EnemyDownEvent?.Invoke(this, new EventArgs()); } // 敵を弾丸で吹き飛ばした public event EventHandler? EnemyDeadEvent; public void EnemyDead() { EnemyDeadEvent?.Invoke(this, new EventArgs()); } } |
ステージクリア
ステージクリアのときの処理と新しいステージが開始されたときの処理を示します。いずれの場合もイベントを発生させます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public class Player { public event EventHandler? StageCleared; public void StageClear() { StageCleared?.Invoke(this, new EventArgs()); } public event EventHandler? NewStageEvent; public void NewStage() { NewStageEvent?.Invoke(this, new EventArgs()); } } |
プレイヤー死亡時
プレイヤー死亡時の処理を示します。残機を1減らしてイベントを発生させます。
1 2 3 4 5 6 7 8 9 10 11 |
public class Player { public event EventHandler? PlayerDeadEvent; public void Dead() { IsDead = true; Rest--; PlayerDeadEvent?.Invoke(this, new EventArgs()); } } |
死亡したプレイヤーを復活させる処理を示します。プレイヤーの座標に記憶しておいた初期座標をセットし、IsDeadフラグをクリアします。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class Player { public void Reset() { X = _initX; Y = _initY; IsDead = false; MoveDirect = Direct.None; LastDirect = Direct.None; _game.ResetTrampolines(); } } |
ゲームオーバー時
ゲームオーバー時の処理を示します。IsGameOverフラグをセットしてイベントを発生させます。
1 2 3 4 5 6 7 8 9 |
public class Player { public event EventHandler? GameOverEvent; public void GameOver() { IsGameOver = true; GameOverEvent?.Invoke(this, new EventArgs()); } } |