GameManagerクラスの作成
GameManagerクラスでゲーム全体の管理をします。
ゲーム開始
CoroutineStart関数で迷路、パックマン、ゴースト、アイテムを初期化後、一定時間"Ready"を表示しパックマンとゴーストを動作させます。
パックマンが食べられる
パックンが食べられたときの処理はPlaceGhost関数内で各ゴーストにイベントを登録します。死亡後、アニメーションが終わるのを待ってからパックマン、ゴーストを初期化します(エサ、パワーエサはそのまま)。アニメーションが終了後の処理はAnimation Eventを使って実装します。
Animation Eventの詳細は下記のサイトを参照してください。
qiita.com
ゴーストが食べられる
ゴーストが食べられたときの処理も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;
[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;
[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());
soundManager.StopBGM();
soundManager.PlayBGM("GhostNormal", 0.6f);
}
}
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());
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();
soundManager.PlaySE("EatDot", 0.4f);
all--;
if (all == 0)
{
StopAll();
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();
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);
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*などで求めた方がいいような気もする(処理が重くなる?)。