これまではモンスターは同じフロアで左右に移動するだけでした。今回はプレイヤーを追わせます。
プレイヤーを追うアルゴリズム
プレイヤーを追うアルゴリズムですが、この動画をみると最後の敵は左に縦一列に掘った穴に吸い寄せられるように移動しているのがわかります。そして穴にハマって5段落ちでステージクリアとなっています。
ただそれ以外の場合は敵が途中でUターンをしたりよくわからない動きもしています。このあたりのアルゴリズムがさっぱりわからないので、
敵はUターンしなければならない場合を除きUターンはしない
プレイヤーと違うフロアにいる敵はまず同じフロアに移動しようとする
というアルゴリズムで敵を動かします(実際のスペースパニックの敵はこんな動きはしません)。
モンスターが方向転換できる場所とは
モンスターが方向転換するためには敵がプレイヤーがいる位置を知る必要があります。GameManagerクラスからプレイヤーの座標を取得できますね。この座標と敵の座標から敵に適切な方向転換をさせます。
敵が方向転換できるのはハシゴがある部分とフロアの端に来たとき、目の前に他の敵がハマっている穴があるときだけです。
ハシゴの下端の座標はGameManager.LadderPositionsからわかります。ではハシゴの上端の座標はどうなるでしょうか? LadderPositionsのY座標からCharctorSize * 3を引けばいいですね。
1 2 3 4 5 6 7 |
List<Point> GetLadderUpperPositions(List<Point> ladderPositions) { List<Point> ret = new List<Point>(); foreach (Point pt in ladderPositions) ret.Add(new Point(pt.X, pt.Y - CharctorSize * 3)); return ret; } |
まず Enemyクラスのコンストラクタ内で必要な情報は取得してしまいましょう。
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 Enemy { public int Life = 1; MoveDirect MoveDirect = MoveDirect.Stop; int X = 0; int Y = 0; int InitX = 0; int InitY = 0; // ハシゴで上り下り可能な座標が格納される List<Point> LadderPositions; List<Point> LadderUpperPositions; public Enemy(int x, int y) { X = x; Y = y; InitX = 0; InitY = 0; // 最初は右に移動するか左に移動するか? if(Random.Next(2) == 0) MoveDirect = MoveDirect.Left; else MoveDirect = MoveDirect.Right; Timer.Tick += GetoutHole; // ハシゴの上端と下端の座標を取得する LadderPositions = GameManager.GetLadderPositions(); LadderUpperPositions = GetLadderUpperPositions(LadderPositions); } } |
モンスターに進行方向を変更させる
次に敵を移動させる処理を考えます。移動方向が決まればその方向に移動させているだけです。水平移動をしているときにハシゴがあったらどうするのか、ハシゴの上り下りが完了したら左右どちらに移動するのかを決めるのが自作メソッドのChangeDirectionです。
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 Enemy { public void Move() { // 穴にハマっている敵は動けない if (Hole != null) return; ChangeDirection(); if (MoveDirect == MoveDirect.Left) X -= 2; if (MoveDirect == MoveDirect.Right) X += 2; if (MoveDirect == MoveDirect.Down) Y += 2; if (MoveDirect == MoveDirect.Up) Y -= 2; // フロアの端に来たら折り返す if (X > 14 * GameManager.CharctorSize) MoveDirect = MoveDirect.Left; if (X < 0) MoveDirect = MoveDirect.Right; // 他の敵がハマっている穴の前では引き返す if (TurnBackIfEnemyFallingHole()) return; // 自分の位置を確認、穴の位置であれば穴にハマる EnterHoleIfInHole(); } } |
ChangeDirectionメソッドを示します。
ハシゴがあったらプレイヤーと同じ階に移動することを追求します。ハシゴの上り下りが完了したらプレイヤーのX座標に合わせて水平移動させます。ハシゴのなかには複数回の移動ができるものがありますが、この場合はプレイヤーがいる階数を目指してそのまま上りまたは下りを継続させます。
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 |
public class Enemy { void ChangeDirection() { // ここは水平移動しているモンスターにとってハシゴの上端 bool b1 = MoveDirect == MoveDirect.Left || MoveDirect == MoveDirect.Right; bool b2 = LadderUpperPositions.Any(x => x.X == X && x.Y == Y); bool b3 = this.Y < GameManager.PlayerY; if (b1 && b2 && b3) { MoveDirect = MoveDirect.Down; return; } // ここは水平移動しているモンスターにとってハシゴの下端 b2 = LadderPositions.Any(x => x.X == X && x.Y == Y); b3 = this.Y > GameManager.PlayerY; if (b1 && b2 && b3) { MoveDirect = MoveDirect.Up; return; } // ここはハシゴを下りてきた敵にとってハシゴの下端 b1 = MoveDirect == MoveDirect.Down; b2 = LadderPositions.Any(x => x.X == X && x.Y == Y); b3 = this.X < GameManager.PlayerX; if (b1 && b2) { // ハシゴが繋がっていてプレイヤーがさらに下の階にいるのであればそのまま下に移動する if (LadderUpperPositions.Any(x => x.X == X && x.Y == Y) && Y < GameManager.PlayerY) return; if (b3) MoveDirect = MoveDirect.Right; else MoveDirect = MoveDirect.Left; return; } // ここはハシゴを上ってきた敵にとってハシゴの上端 b1 = MoveDirect == MoveDirect.Up; b2 = LadderUpperPositions.Any(x => x.X == X && x.Y == Y); b3 = this.X < GameManager.PlayerX; if (b1 && b2) { // ハシゴが繋がっていてプレイヤーがさらに上の階にいるのであればそのまま上に移動する if (LadderPositions.Any(x => x.X == X && x.Y == Y) && Y > GameManager.PlayerY) return; if (b3) MoveDirect = MoveDirect.Right; else MoveDirect = MoveDirect.Left; return; } // 敵同士が重なっていたら別方向に移動させる List<Enemy> enemies = GameManager.Enemies.Where(x => x != this).ToList(); Enemy enemy = enemies.FirstOrDefault(x => (x.X < this.X && this.X < x.X + GameManager.CharctorSize) && (x.Y == this.Y)); if (enemy != null && this.MoveDirect == enemy.MoveDirect) { if(this.MoveDirect == MoveDirect.Left) enemy.MoveDirect = MoveDirect.Right; if (this.MoveDirect == MoveDirect.Right) enemy.MoveDirect = MoveDirect.Left; } } } |
プレイヤーとの当たり判定
敵とプレイヤーが接触したらミスとなります。ミス時は各キャラクターの動作を止めます。GameManager.Updateメソッドをしばらく止めればいいですね。
EnemyクラスのIsPlayerDeadメソッドはその敵とプレイヤーが接触しているかどうかを調べています。
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 Enemy { public bool IsPlayerDead() { // 敵とプレイヤーの座標同士の距離がCharctorSize以下なら接触している double a = Math.Pow(GameManager.PlayerX - X, 2) + Math.Pow(GameManager.PlayerY - Y, 2); double b = Math.Pow(GameManager.CharctorSize, 2); if (a < b) return true; else return false; } // 敵を最初の位置・状態に戻す public void MoveInitPosition() { X = InitX; Y = InitY; // 穴にハマった状態であれば元に戻す this.Hole = null; // 最初に移動する方向を決める if (Random.Next(2) == 0) MoveDirect = MoveDirect.Left; else MoveDirect = MoveDirect.Right; } } |
当たり判定後の処理
GameManagerクラスのUpdateメソッド内で自作メソッド CheckPlayerDeadを呼び出してプレイヤーが敵と接触しているか調べます。プレイヤーが敵と接触している場合は残機を1減らして3秒後に敵とプレイヤーの位置を初期状態に戻してゲームを再開します。残機0のときはゲームオーバーです。
プレイヤー死亡のときとゲームオーバーのときはForm1クラスで処理ができるようにイベントを発生させます。
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 |
class GameManager { public int Rest = 3; // 最初は残機 3 bool IsGameStop = false; WMPLib.WindowsMediaPlayer PlayerMiss = new WMPLib.WindowsMediaPlayer(); public void Update() { if (IsGameStop) return; MovePlayer(); MoveEnemies(); // プレイヤーが敵と接触しているか調べる CheckPlayerDead(); } async void CheckPlayerDead() { // プレイヤーが敵と接触している if (Enemies.Any(x => x.IsPlayerDead())) { // 効果音を鳴らして残機を1減らす PlayerMiss.URL = System.Windows.Forms.Application.StartupPath + "\\miss.mp3"; Rest--; // 3秒間ゲームを中断 IsGameStop = true; await Task.Delay(3000); if (Rest > 0) { // 残機が残っている場合はプレイヤーと敵の位置を最初の位置に戻してゲーム再開 foreach (Enemy enemy in Enemies) { enemy.MoveInitPosition(); } PlayerX = 7 * CharctorSize; PlayerY = 15 * CharctorSize; Holes.Clear(); // 穴は埋めてしまう IsGameStop = false; } else { OnGameOver(); } } } void OnGameOver() { // ゲームオーバー処理 } } |