GameManagerクラスの作成
GameManagerクラスでゲーム全体の管理をします。
ゲーム開始
CoroutineStart関数で迷路、パックマン、ゴースト、アイテムを初期化後、一定時間"Ready"を表示しパックマンとゴーストを動作させます。
パックマンが食べられる
パックンが食べられたときの処理はPlaceGhost関数内で各ゴーストにイベントを登録します。死亡後、アニメーションが終わるのを待ってからパックマン、ゴーストを初期化します(エサ、パワーエサはそのまま)。アニメーションが終了後の処理はAnimation Eventを使って実装します。
Animation Eventの詳細は下記のサイトを参照してください。
ゴーストが食べられる
ゴーストが食べられたときの処理もPlaceGhost関数内で各ゴーストにイベントを登録します。
エサ、パワーエサを食べる
パックマンがエサ、パワーエサを食べたときの処理はPlaceDot、PlacePowerCokie関数内でイベントを登録します。
恐慌状態の解除
FixedUpdate関数で一定時間経過したらすべてのゴーストの恐慌状態を解除するようにします。
スコアの表示
ゴーストやフルーツを食べたとき、食べた座標にスコアを表示するScorerTextを作成します。TextオブジェクトにScorerクラスをアタッチします。
public class Scorer : MonoBehaviour { private Text scoreText = default; public void Set(int score, Vector2 pos, float span) { scoreText.text = score.ToString(); scoreText.transform.position = pos; scoreText.gameObject.SetActive(true); StartCoroutine(CoroutineShow(span)); } private void Awake() { scoreText = GetComponent<Text>(); scoreText.gameObject.SetActive(false); } private IEnumerator CoroutineShow(float span) { yield return new WaitForSeconds(span); scoreText.gameObject.SetActive(false); } }
public class GameManager : MonoBehaviour { [SerializeField] private Pacman pacmanPrefab = default; [SerializeField] private Maze[] mazePrefabs = default; [SerializeField] private Dot dotPrefab = default; [SerializeField] private PowerCokie powerCokiePrefab = default; [SerializeField] private Akabei akabeiPrefab = default; [SerializeField] private Pinky pinkyPrefab = default; [SerializeField] private Aosuke aosukePrefab = default; [SerializeField] private Guzuta guzutaPrefab = default; [SerializeField] private Scorer scorer = default; [SerializeField] private Text scoreText = default; //Readyの表示。 [SerializeField] private Text readyText = default; //ゲームオーバーの表示。 [SerializeField] private GameObject overPanel = default; [SerializeField] private GameObject allClearPanel = default; [SerializeField] private GameObject livesPanel = default; [SerializeField] private float scareSpan; [SerializeField] private float eatSpan; [SerializeField] private float eatenSpan; //"Ready"を表示する期間。 [SerializeField] private float readySpan; [SerializeField] private float nextSpan; private List<AGhost> ghosts; private Pacman pacman; private Maze maze; private Akabei akabei; private Pinky pinky; private Aosuke aosuke; private Guzuta guzuta; private FruitsCreator fruitsCreator; private Fruits fruits; private SoundManager soundManager; private bool isScare; private float scareEndTime; private int mazeIndex; private int score; private int lives; //一度の恐慌状態で食べたゴーストの数。 private int eatenGhost; //食べたエサの数。 private int eatenDot; private void Start() { lives = 2; score = 0; maze = Instantiate(mazePrefabs[mazeIndex]); StartCoroutine(CoroutineStart(SetGame)); } private void FixedUpdate() { if (isScare && scareEndTime < Time.fixedTime ) { isScare = false; eatenGhost = 0; ghosts.ForEach(g => g.Calm()); //BGMを再生。 soundManager.StopBGM(); soundManager.PlayBGM("GhostNormal", 0.6f); } } //Readyを一定時間表示した後、パックマン、ゴーストをActiveにする。 private IEnumerator CoroutineStart(Action callback) { readyText.gameObject.SetActive(true); callback(); yield return new WaitForSeconds(readySpan); readyText.gameObject.SetActive(false); pacman.Run(); ghosts.ForEach(g => g.Run()); //BGMを再生。 soundManager.PlayBGM("GhostNormal", 0.6f); } private void Awake() { SetAudio(); } //ステージ開始時の初期化。 private void SetGame() { //パックマンの作成。 pacman = Instantiate(pacmanPrefab, maze.transform); //パックマンの初期位置は必ず整数でないと移動中に引っかかる。 pacman.Initialize(maze.PacmanStartPosition); //パックマン死亡時のイベントを登録。 pacman.OnDead += (s, e) => CheckLives(); //ゴーストの作成。 PlaceGhost(); //エサの作成。 PlaceDot(); //パワーエサの作成。 PlacePowerCokie(); //残機の更新。 UpdateLives(); fruitsCreator = GetComponent<FruitsCreator>(); fruitsCreator.Initialize(); scareEndTime = 0; eatenGhost = 0; eatenDot = 0; } //パックマンが死亡後の初期化。 private void ResetGame() { pacman = Instantiate(pacmanPrefab, maze.transform); pacman.Initialize(maze.PacmanStartPosition); pacman.OnDead += (s, e) => CheckLives(); ghosts.ForEach(g => Destroy(g.gameObject)); PlaceGhost(); UpdateLives(); scareEndTime = 0; eatenGhost = 0; } 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; scoreText.text = score.ToString(); //得点を表示。 scorer.Set(scr, g.transform.position, eatSpan); eatenGhost++; soundManager.PlaySE("EatGhost", 0.6f); }; //ゴーストが食べたときの処理。 g.OnEat += (s, e) => { pacman.Dead(); //ゴーストを停止。 ghosts.ForEach(g2 => g2.Stop()); isScare = false; lives--; if (fruits != null) fruits.Destroy(); soundManager.PlaySE("Dead", 0.6f); soundManager.StopBGM(); }; }); } private void PlaceDot() { var poses = maze.DotPositions; int all = poses.Count; poses.ForEach(p => { var dot = Instantiate(dotPrefab, maze.transform, false); dot.transform.localPosition = p; //パックマンが食べたときの処理。 dot.OnEaten += (s, e) => { //得点の更新。 var d = (Dot)s; score += d.Score; scoreText.text = score.ToString(); eatenDot++; //フルーツの作成。 CreateFruits(); //SEの再生。 soundManager.PlaySE("EatDot", 0.4f); all--; //エサを全部食べた。 if (all == 0) { //パックマン、ゴーストの停止。 StopAll(); //BGM、SEの停止。 soundManager.StopBGM(); soundManager.StopSE(); //迷路を点滅。 maze.Blinking(); //次のステージを開始。 StartCoroutine(CoroutineNext()); } }; }); } private void PlacePowerCokie() { var poses = maze.PowerCokiePositions; poses.ForEach(p => { var pow = Instantiate(powerCokiePrefab, maze.transform, false); pow.transform.localPosition = p; //パックマンが食べたときの処理。 pow.OnEaten += (s, e) => { //ゴーストを恐慌状態にする。 isScare = true; scareEndTime = Time.fixedTime + scareSpan; ghosts.ForEach(g => g.Scare(scareSpan)); //得点の更新。 var pc = (PowerCokie)s; score += pc.Score; scoreText.text = score.ToString(); eatenDot++; //フルーツの作成。 CreateFruits(); //SE、BGMの再生。 soundManager.PlaySE("EatPowerCokie", 0.2f); soundManager.StopBGM(); soundManager.PlayBGM("GhostScare", 0.4f); }; }); } private void PauseAll(float span) { pacman.Pause(span); ghosts.ForEach(g => g.Pause(span)); } private void StopAll() { pacman.Stop(); ghosts.ForEach(g => g.Stop()); isScare = false; } private void CheckLives() { if (lives >= 0) { StartCoroutine(CoroutineStart(ResetGame)); } else { overPanel.SetActive(true); } } private void CreateFruits() { if (fruitsCreator.HasEatenDotEnough(eatenDot)) { fruits = fruitsCreator.Create(maze, mazeIndex); //パックマンが食べたときの処理。 fruits.OnEaten += (s, e) => { var f = (Fruits)s; //得点の更新。 score += f.Score; scoreText.text = score.ToString(); //得点を表示。 scorer.Set(f.Score, f.transform.position, eatSpan); //SEを再生。 soundManager.PlaySE("EatPowerCokie", 0.2f); }; } } private void UpdateLives() { var p = livesPanel.transform; for (int i = 0; i < p.childCount;i++) { if (i < lives) p.GetChild(i).gameObject.SetActive(true); else p.GetChild(i).gameObject.SetActive(false); } } private void SetAudio() { soundManager = SoundManager.Instance; soundManager.LoadSE("EatDot", "Byu"); soundManager.LoadSE("EatPowerCokie", "ByChance"); soundManager.LoadSE("EatGhost", "Byuu"); soundManager.LoadSE("Dead", "Qyurururu"); soundManager.LoadBGM("GhostNormal", "Panic"); soundManager.LoadBGM("GhostScare", "LFO"); soundManager.LoadSE("GhostDead", "Obake"); } }
public class Pacman : MonoBehaviour, IWarpable { //死亡時のイベントを登録。 public event EventHandler OnDead; private void Destroy() { Destroy(gameObject); OnDead(this, EventArgs.Empty); } }
完成
完成したものがこちら nullsuke.github.io
ソースコードはこちら github.com
まとめ
- ゴーストの状態管理をStateパターンを用いて実装したが、if文やSwtich文を使った方が簡単だったかもしれない。今後拡張するするつもりもないし。
- 今回、お絵描きソフトで矩形を描いてそこからBoxColliderやWaypointの座標を取得するという方法を使った。かかる時間自体は手作業とたいして変わらないけれど、マップを重ねて見ながら位置を指定できるのでそれなりに便利かなと思った。
- 今回使ったマップは巣の近辺のWaypointが小数になってしまい、移動の処理がちょっと複雑になってしまった。マップを調整するか、移動方法を根本的に見直した方がよいかもしれない。
- 追跡時に対象までの最短距離を直線距離でなくA*などで求めた方がいいような気もする(処理が重くなる?)。