ASP.NET Core版 マッピー(MAPPY)をつくる(2)の続きです。今回は敵の動きを実装します。
Contents
Enemyクラスの定義
Enemyクラスを定義します。
1 2 3 4 5 6 |
namespace Mappy { public class Enemy { } } |
以降は名前空間を省略して以下のように書きます。
1 2 3 |
public class Enemy { } |
コンストラクタ
コンストラクタを示します。引数はMappyGameオブジェクトと初期座標です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class Enemy { MappyGame _game; int _initX = 0; int _initY = 0; public Enemy(MappyGame game, Position initPosition) { _game = game; X = _initX = initPosition.X; Y = _initY = initPosition.Y; VerticalSpeed = 7; HorizontalSpeed = 4; } } |
プロパティ
各プロパティを示します。
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 |
public class Enemy { // 現在の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; } // ドアの開閉に巻き込まれて飛ばされた敵が着地する部分のX座標 int _landingPositionX = 0; public int LandingPositionX { set { if (value < 0) _landingPositionX = 0; else if (value > MappyGame.CHARACTER_SIZE * _game.ColMax) _landingPositionX = MappyGame.CHARACTER_SIZE * _game.ColMax; else _landingPositionX = value; } get { return _landingPositionX; } } // CantMoveCountが0より大きいときは気絶状態で動けない public int CantMoveCount { get; set; } // 死亡フラグ public bool IsDead { private set; get; } } |
落下する処理
敵が穴からトランポリンに向けて落下する処理を示します。
落下した結果、敵のXY座標がともにCHARACTER_SIZEで割り切れるとき、そこにトランポリンがあるか調べます。あるときは移動方向を上方向に変更します。上昇するとき自機と同じ階に移動するのですが、自機もトランポリンで上昇または下降している場合はいつまでたっても上下運動以外できないと困るので、移動方向が上昇に転じたときにどの階でフロアに移動するのかを決めておきます。そしてそれをフィールド変数_rowLandingFloorに格納しておきます。
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 class Enemy { static Random _random = new Random(); int _rowLandingFloor = 0; void Fall() { // VerticalSpeedだけ下降するが、 // CHARACTER_SIZEがVerticalSpeedの倍数でなくても不具合がおきないように調整をする 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; // トランポリンで上昇するときにどの階でフロアに移動するのかを決めておく List<int> landingFloors = _game.GetLandingFloor(row, col); int r = _random.Next(landingFloors.Count); _rowLandingFloor = landingFloors[r]; } } } } |
Game.GetLandingFloorメソッドは現在使用しているトランポリンの座標から移動できる階のリストを取得します。これは以下のように定義されています。
トランポリンのcolから上にあるセルの両脇にあるセルがCell.Aisleであるか、開いているドアである場合はそのときのrowをリストに格納して返しています。
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 |
public class MappyGame { public List<int> GetLandingFloor(int row, int col) { List<int> ret = new List<int>(); for (int i = 0; i <= row; i++) { if (col + 1 < ColMax) { if (MapArray[i, col + 1] == Cell.Aisle) { ret.Add(i); continue; } Door? door = GetDoor(i, col + 1); if (door != null && (door.IsOpen || door.OpenDirect == Direct.Left)) { ret.Add(i); continue; } } if (col - 1 >= 0) { if (MapArray[i, col - 1] == Cell.Aisle) { ret.Add(i); continue; } Door? door = GetDoor(i, col - 1); if (door != null && (door.IsOpen || door.OpenDirect == Direct.Right)) { ret.Add(i); continue; } } } return ret; } } |
上昇時の処理
上昇処理をするときの処理を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
public class Enemy { public void Jump() { // CHARACTER_SIZEがVerticalSpeedの倍数でない場合の調整 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; // row, colを調べてフロアに移動できる場合はフロアに移動する EnemyLandingFloor(row, col); } } } |
トランポリンで上昇しているときにフロアに移動できる場合はフロアに移動する処理を示します。
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 |
public class Enemy { public void EnemyLandingFloor(int row, int col) { bool canLeft = false; bool canRight = false; if (col - 1 >= 0 && _game.MapArray[row, col - 1] == Cell.Aisle) canLeft = true; if (col + 1 < _game.ColMax && _game.MapArray[row, col + 1] == Cell.Aisle) canRight = true; if (_game.Player.MoveDirect != Direct.Up && _game.Player.MoveDirect != Direct.Down) { // 自機がトランポリンを使用していないときは同じフロアに移動し、接近できる方向に移動する if (_game.Player.Y == Y) { // 自機が左にいて左に移動できる場合は移動方向を左に設定 // 自機が右にいて右に移動できる場合は移動方向を右に設定 if (canLeft && X - _game.Player.X > 0) MoveDirect = Direct.Left; else if (canRight && X - _game.Player.X < 0) MoveDirect = Direct.Right; } } else { // 自機がトランポリンを使用しているときは上記の乱数で決めたフロアに移動する if (_rowLandingFloor == row) { // 左にしか移動できない場合は移動方向を左に設定 if (canLeft && !canRight) MoveDirect = Direct.Left; // 右にしか移動できない場合は移動方向を右に設定 if (!canLeft && canRight) MoveDirect = Direct.Right; // 両方移動できる場合は自機に接近できる方向に移動する // X座標が同じ場合は乱数で決める if (canLeft && canRight) { if (X - _game.Player.X > 0) MoveDirect = Direct.Left; else if (X - _game.Player.X < 0) MoveDirect = Direct.Right; MoveDirect = _random.Next(2) == 0 ? 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 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 |
public class Enemy { void CheckHitDoor(int row, int col) { if (MoveDirect == Direct.Left) { Door? door1 = _game.GetDoor(row, col - 1); if (door1 != null && !door1.IsOpen && door1.OpenDirect == Direct.Left) { if (!door1.IsPowerDoor) DoorOpen(door1); else { MoveDirect = Direct.Right; LastDirect = MoveDirect; } } Door? door2 = _game.GetDoor(row, col); if (door2 != null && !door2.IsOpen && door2.OpenDirect == Direct.Right) { if (!door2.IsPowerDoor) DoorOpen(door2); else { MoveDirect = Direct.Right; LastDirect = MoveDirect; } } if (door1 == null && door2 == null) LastDirect = MoveDirect; } if (MoveDirect == Direct.Right) { Door? door1 = _game.GetDoor(row, col + 1); if (door1 != null && !door1.IsOpen && door1.OpenDirect == Direct.Right) { if (!door1.IsPowerDoor) DoorOpen(door1); else { MoveDirect = Direct.Left; LastDirect = MoveDirect; } } Door? door2 = _game.GetDoor(row, col); if (door2 != null && !door2.IsOpen && door2.OpenDirect == Direct.Left) { if (!door2.IsPowerDoor) DoorOpen(door2); else { MoveDirect = Direct.Left; LastDirect = MoveDirect; } } if (door1 == null && door2 == null) LastDirect = MoveDirect; } } } |
DoorOpenメソッドではドアを開けることで影響をうける敵を取得し、敵が飛ばされる先のX座標を取得、設定します。またこの場合、PlayerのEnemyDownメソッドを呼び出します。するとPlayer側でイベントが発生します。EnemyクラスではなくPlayer側にイベント発生の処理をさせるのはイベント処理をするにはイベントハンドラを追加しないといけないので、その処理はPlayer側にまとめたい、ただそれだけの理由です。
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 Enemy { void DoorOpen(Door door) { door.IsOpen = true; // ドアの開閉に巻き込まれて飛ばされる先のX座標 int afterX = 0; // ドアを開けることで影響をうける敵を取得する 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座標を取得する if (door.OpenDirect == Direct.Left) afterX = door.X - MappyGame.CHARACTER_SIZE; if (door.OpenDirect == Direct.Right) afterX = door.X + MappyGame.CHARACTER_SIZE; foreach (Enemy enemy in enemies) { // ドアが開閉に伴って敵が飛ばされる先のX座標を設定する enemy.LandingPositionX = afterX; // enemy.X < afterXなら敵の向きは右向き。そうでないなら左向き if (enemy.X < afterX) enemy.LastDirect = Direct.Right; if (enemy.X > afterX) enemy.LastDirect = Direct.Left; // CANT_MOVE_COUNT_MAX回だけ更新されても自分で動くことはできない enemy.CantMoveCount = MappyGame.CANT_MOVE_COUNT_MAX; } // 敵がドアに巻き込まれて自滅したらPlayerに通知する(Player側でイベントを発生させる) if (enemies.Count > 0) _game.Player.EnemyDown(); } } |
ドアの開閉に巻き込まれて気絶したあとの処理を示します。CantMoveCountが0より大きいときは敵は動きませんが、飛ばされて倒れる感を出すために少し上方向に移動させたあとLandingPositionXに向けて移動させます。LandingPositionXの近くにきたらそのまましばらくのあいだ倒れたままにします。
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 class Enemy { void DownByDoor() { // LandingPositionXに向けて移動させる if (LandingPositionX - X >= 12) { X += 12; if (Y % MappyGame.CHARACTER_SIZE == 0) Y -= 4; } else if (X - LandingPositionX >= 12) { X -= 12; if (Y % MappyGame.CHARACTER_SIZE == 0) Y -= 4; } else { // LandingPositionXの近くにきたらXにLandingPositionXを代入する X = LandingPositionX; if (Y % MappyGame.CHARACTER_SIZE != 0) Y += 4; // ただしこのときのX座標が穴と一致していたら穴からトランポリンに向けて落下させる int col = X / MappyGame.CHARACTER_SIZE; int row = Y / MappyGame.CHARACTER_SIZE; if (_game.MapArray[row, col] == Cell.Hole) { MoveDirect = Direct.Down; CantMoveCount = 0; } } } } |
敵を死亡させる処理
パワードアから放たれた弾丸にあたったとき敵を死亡させる処理を示します。_timeToReviveは復活するまでに必要な更新回数です。まとめて敵を倒したとき復活する時間をずらしたいのでTIME_TO_ENEMY_REVIVE_MAXに(引数 * 16)を加えています。更新処理がおこなわれたときに_timeToReviveが0より大きい場合はなにもしません。
1 2 3 4 5 6 7 8 9 10 |
public class Enemy { int _timeToRevive = 0; public void Dead(int number) { IsDead = true; _timeToRevive = MappyGame.TIME_TO_ENEMY_REVIVE_MAX + number * 16; } } |
更新処理がおこなわれたときに_timeToReviveが0より大きい場合の処理を示します。_timeToReviveをデクリメントして0になったら復活の処理をおこないます。Game.EnemyRevivePositionの場所に敵を移動させて下方向に移動させます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public class Enemy { void Revive() { _timeToRevive--; if (_timeToRevive <= 0) { IsDead = false; X = _game.EnemyRevivePosition.X; Y = _game.EnemyRevivePosition.Y; MoveDirect = Direct.Down; CantMoveCount = 0; LandingPositionX = X; } } } |
リセット
自機死亡時は敵を初期位置に戻します。そのための処理を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class Enemy { public void Reset() { X = _initX; Y = _initY; _landingPositionX = X; MoveDirect = Direct.None; LastDirect = Direct.None; CantMoveCount = 0; IsDead = false; } } |
更新処理
更新時の処理を示します。IsDeadのときはReviveメソッドを呼び出して復活にむけての処理をおこない、それ以外のときは移動処理をおこないます。
1 2 3 4 5 6 7 8 9 10 |
public class Enemy { public void Update() { if (IsDead) Revive(); else Move(); } } |
移動処理は以下のようになっています。
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 Enemy { public void Move() { CantMoveCount--; if (CantMoveCount > 0) { DownByDoor(); return; } if (MoveDirect == Direct.Down || MoveDirect == Direct.Up) { if (MoveDirect == Direct.Down) Fall(); else if (MoveDirect == Direct.Up) Jump(); LandingPositionX = X; return; } if (MoveDirect == Direct.None) { if (X - _game.Player.X > 0) MoveDirect = Direct.Left; else MoveDirect = Direct.Right; } 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; LandingPositionX = X; 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; // 閉まっているドアがあるなら開ける CheckHitDoor(row, col); } } } |