ゴーストオブジェクトの作成
16px × 16pxの画像を用意し、Pixels Per Unitを8にします。画像からSpriteを作成し、Rigidbody2D、BoxCollider2Dをアタッチします。適当にColliderの大きさを設定します。
状態の管理
ゴーストは最初、巣に待機しており(アカベイは除く)、一定時間経過すると特定のエリアを巡回するようになります。一定時間巡回するとパックマンを追跡するようになります。一定時間追跡するとまた特定のエリアを巡回するようになります。パックマンがパワーエサを食べると恐慌状態になり、このときパックマンに食べられると、死亡状態となります。死亡状態では一旦巣に戻り、巣に入ると待機状態になります。
これら状態遷移と行動を実装します。今回はStateパターンを使いますが、if文やSwitch文の方が簡単かもしれません。
状態クラスについては下記を参照してください。
nullsuke.com
Stateパターンについては下記のサイトを参照してください。
qiita.com
抽象クラスAGhostの作成
状態クラスを使って移動や処理する抽象クラスです。ワープの為のインターフェイスIWarpableとパックマンに食べられる為のインターフェイスIEatableを継承します。
public abstract class AGhost : MonoBehaviour, IWarpable, IEatable
{
[SerializeField] protected float waitSpan;
[SerializeField] protected float scatterSpan;
[SerializeField] protected float chaseSpan;
[SerializeField] protected float scareTime;
public event EventHandler OnEaten;
public event EventHandler OnEat;
private readonly float warpSpan = 1f;
private readonly int score = 200;
private SpriteRenderer sprite;
private Animator animator;
private IState state;
private Wait wait;
private float pauseEndTime;
public int Score { get => score; }
public void Initialize(Mover mover, GhostWaypointsData gwd,
List<Vector2> nestWaypoints)
{
var states = new Dictionary<State, IState>();
pauseEndTime = 0;
wait = new Wait(mover, states, animator, ToQueue(gwd.Wait), waitSpan);
wait.OnTouch += (s, e) => OnEat(s, e);
var init = new Init(mover, states, animator, ToQueue(gwd.Init));
init.OnTouch += (s, e) => OnEat(s, e);
var scatter = new Scatter(mover, states, animator,
ToQueue(gwd.Scatter), scatterSpan);
scatter.OnTouch += (s, e) => OnEat(s, e);
var chase = new Chase(mover, states, animator, sprite, chaseSpan);
chase.OnTouch += (s, e) => OnEat(s, e);
var scare = new Scare(mover, states, animator, sprite);
scare.OnTouch += (s, e) => OnEaten(s, e);
var ret = new Return(mover, states, animator, sprite,
ToQueue(gwd.Scatter).Peek());
ret.OnTouch += (s, e) => OnEat(s, e);
var dead = new Dead(mover, states, animator,
ToQueue(nestWaypoints));
dead.OnTouch += (s, e) => { };
enabled = false;
animator.enabled = false;
}
public void Warp(Vector2 pos)
{
state.Warp(pos, warpSpan);
}
public void Eaten(Pacman pacman)
{
state = state.Eaten();
}
public void Destroy()
{
Destroy(gameObject);
}
public void Scare(float span)
{
state = state.Frighten(span);
}
public void Calm()
{
state = state.Calm();
}
public void Run()
{
wait.SetState();
state = wait;
enabled = true;
animator.enabled = true;
}
public void Pause(float span)
{
pauseEndTime = Time.fixedTime + span;
}
public void Stop()
{
enabled = false;
animator.enabled = false;
}
private void Awake()
{
sprite = GetComponent<SpriteRenderer>();
animator = GetComponent<Animator>();
}
private void FixedUpdate()
{
if (Time.fixedTime < pauseEndTime) return;
state = state.Excute();
state.Animate();
}
private Queue<Vector2> ToQueue(List<Vector2> waypoints)
{
var wps = new Queue<Vector2>();
waypoints.ForEach(w =>
{
wps.Enqueue(w);
});
return wps;
}
}
各ゴーストのクラスを作成
AGhostを継承して各ゴーストのクラスを作成します。各ゴーストのAIついては下記を参照してください。
アカベイ
public class Akabei : AGhost
{
public void Initialize(Maze maze, Pacman pacman)
{
transform.localPosition = maze.AkabeiWaypointsData.Start;
var ai = GetComponent<AkabeiAI>();
ai.Initialize(pacman, maze.TileUtility);
var mover = GetComponent<Mover>();
mover.Initialize(ai);
base.Initialize(mover, maze.AkabeiWaypointsData, maze.NestWaypoints);
}
}
ピンキー
public class Pinky : AGhost
{
public void Initialize(Maze maze, Pacman pacman)
{
transform.localPosition = maze.PinkyWaypointsData.Start;
var ai = GetComponent<PinkyAI>();
ai.Initialize(pacman, maze.TileUtility);
var mover = GetComponent<Mover>();
mover.Initialize(ai);
base.Initialize(mover, maze.PinkyWaypointsData, maze.NestWaypoints);
}
}
アオスケ
public class Aosuke : AGhost
{
public void Initialize(Maze maze, Pacman pacman, Akabei akabei)
{
transform.localPosition = maze.AosukeWaypointsData.Start;
var ai = GetComponent<AosukeAI>();
ai.Initialize(pacman, maze.TileUtility, akabei);
var mover = GetComponent<Mover>();
mover.Initialize(ai);
base.Initialize(mover, maze.AosukeWaypointsData, maze.NestWaypoints);
}
}
グズタ
public class Guzuta : AGhost
{
public void Initialize(Maze maze, Pacman pacman)
{
transform.localPosition = maze.GuzutaWaypointsData.Start;
var ai = GetComponent<GuzutaAI>();
ai.Initialize(pacman, maze.TileUtility);
var mover = GetComponent<Mover>();
mover.Initialize(ai);
base.Initialize(mover, maze.GuzutaWaypointsData, maze.NestWaypoints);
}
}
完成
ゴーストオブジェクトにゴーストクラス、ゴーストAI、Mover、Animatorをアタッチし、待機時間などパラメータを設定します。
迷路上にゴーストをセットすれば完成です。
public class GameManager : MonoBehaviour
{
[SerializeField] private Pacman pacmanPrefab = default;
[SerializeField] private Maze[] mazePrefabs = default;
[SerializeField] private Akabei akabeiPrefab = default;
[SerializeField] private Pinky pinkyPrefab = default;
[SerializeField] private Aosuke aosukePrefab = default;
[SerializeField] private Guzuta guzutaPrefab = default;
private List<AGhost> ghosts;
private Pacman pacman;
private Maze maze;
private Akabei akabei;
private Pinky pinky;
private Aosuke aosuke;
private Guzuta guzuta;
private bool isScare;
private int score;
private int eatenGhost;
private int eatenDot;
private void PlaceGhost()
{
akabei = Instantiate(akabeiPrefab, maze.transform);
akabei.Initialize(maze, pacman);
pinky = Instantiate(pinkyPrefab, maze.transform);
pinky.Initialize(maze, pacman);
aosuke = Instantiate(aosukePrefab, maze.transform);
aosuke.Initialize(maze, pacman, akabei);
guzuta = Instantiate(guzutaPrefab, maze.transform);
guzuta.Initialize(maze, pacman);
ghosts = new List<AGhost>
{ akabei, pinky, aosuke, guzuta };
ghosts.ForEach(g =>
{
g.OnEaten += (s, e) =>
{
PauseAll(eatSpan);
var scr = g.Score * (1 << eatenGhost);
score += scr;
eatenGhost++;
};
g.OnEat += (s, e) =>
{
pacman.Dead();
isScare = false;
lives--;
};
});
}
}