ぬるーむ

Unity初心者が誰もが知っているゲームの模倣をしています。個人的な備忘録ですが、入門書を読み終えたばかりの初心者の方は「こんなへなちょこでもいいのか!」「俺の方がうまく作れる」と作成意欲がわいたりするかもしれません。

Unityによるパックマンの作り方 ~完成~

GameManagerクラスの作成

GameManagerクラスでゲーム全体の管理をします。

ゲーム開始

CoroutineStart関数で迷路、パックマン、ゴースト、アイテムを初期化後、一定時間"Ready"を表示しパックマンとゴーストを動作させます。

パックマンが食べられる

パックンが食べられたときの処理はPlaceGhost関数内で各ゴーストにイベントを登録します。死亡後、アニメーションが終わるのを待ってからパックマン、ゴーストを初期化します(エサ、パワーエサはそのまま)。アニメーションが終了後の処理はAnimation Eventを使って実装します。

f:id:Nullsuke:20210523213913p:plain
Animation Event

Animation Eventの詳細は下記のサイトを参照してください。

qiita.com

ゴーストが食べられる

ゴーストが食べられたときの処理もPlaceGhost関数内で各ゴーストにイベントを登録します。

エサ、パワーエサを食べる

パックマンがエサ、パワーエサを食べたときの処理はPlaceDot、PlacePowerCokie関数内でイベントを登録します。

恐慌状態の解除

FixedUpdate関数で一定時間経過したらすべてのゴーストの恐慌状態を解除するようにします。

スコアの表示

ゴーストやフルーツを食べたとき、食べた座標にスコアを表示するScorerTextを作成します。TextオブジェクトにScorerクラスをアタッチします。

完成

完成したものがこちら nullsuke.github.io

ソースコードはこちら github.com

まとめ

  • ゴーストの状態管理をStateパターンを用いて実装したが、if文やSwtich文を使った方が簡単だったかもしれない。今後拡張するするつもりもないし。
  • 今回、お絵描きソフトで矩形を描いてそこからBoxColliderやWaypointの座標を取得するという方法を使った。かかる時間自体は手作業とたいして変わらないけれど、マップを重ねて見ながら位置を指定できるのでそれなりに便利かなと思った。
  • 今回使ったマップは巣の近辺のWaypointが小数になってしまい、移動の処理がちょっと複雑になってしまった。マップを調整するか、移動方法を根本的に見直した方がよいかもしれない。
  • 追跡時に対象までの最短距離を直線距離でなくA*などで求めた方がいいような気もする(処理が重くなる?)。

Unityによるパックマンの作り方 ~BGM・SEを再生する~

BGM・SEを管理する専用のクラス、SoundManagerを作成します。どこのクラスから使用されてもいいようにシングルトンにします。

GameManagerの起動時にAudioClipを読み込みます。

再生したい部分でPlayBGM、PlaySEを実行します。

Unityによるパックマンの作り方 ~アイテムの作成~

オブジェクトの作成

16px × 16pxの画像を用意し、Pixels Per Unitを8にします。画像からSpriteを作成し、CirlceCollider2Dをアタッチします。適当にColliderの大きさを設定します。また、パックマンの当たり判定は無視しなければならないので、Layerの設定をIgnore Laycastにします。

f:id:Nullsuke:20210504105158p:plain
Sprite画像
f:id:Nullsuke:20210518215701p:plainf:id:Nullsuke:20210518215704p:plainf:id:Nullsuke:20210518215707p:plain
アイテムのオブジェクト

クラスの作成

エサ

Dotクラスを作成します。パックマンに食べられるのでインターフェイスIEatableを継承します。

パワーエサ

PowerCokieクラスを作成します。パックマンに食べられるのでインターフェイスIEatableを継承します。

フルーツ

フルーツ毎にクラスを作成します。まずインターフェイスIEatableを継承したFruitsクラスを作成し、これを継承した各フルーツのクラスを作成します(まぁ、中身はスコアだけですが)。

迷路に設置

各アイテムを迷路に設置します。各アイテムのオブジェクトにそれぞれのクラスをアタッチします。また、ゴーストのWaypointsMapの作成と同じ要領でDotMap、PowerCokieMapを作成し、エサとパワーエサの座標を取得します。

フルーツはエサを70、170個食べた時点で出現する仕様なのでちょっと大げさですが専用のクラスFruitsCreatorを作成します。

GameManagerクラスでゲーム開始時にアイテムを設置します。このときOnEatenイベントにパックマンが食べたときの処理を実装します。

Unityによるパックマンの作り方 ~ゴーストの仕様~

すべてのゴーストは待機状態から始まり、巡回→追跡→巡回を繰り返します。オリジナルでは巡回・追跡のループを重ねるごとに各状態の実行時間が変化するようですが、面倒なので今回は常に巡回を7秒、追跡を20秒としました。

f:id:Nullsuke:20210515005712p:plain
各ゴーストの巡回経路

アカベイ

  • 待機:なし。最初から巣の外にいます。
  • 巡回:巣の右上付近を巡回。
  • 追跡:パックマン自身を追跡対象とします。
    f:id:Nullsuke:20210515154411p:plain
    アカベイのWaypointsData

ピンキー

  • 待機:3秒待機します。待機中上・下の移動を繰り返す。
  • 巡回:巣の左上付近を巡回。
  • 追跡:パックマンの移動方向の4Unit先を追跡対象とします。
    f:id:Nullsuke:20210515154451p:plain
    ピンキーのWaypointsData

アオスケ

  • 待機:5秒待機します。待機中下・上の移動を繰り返す。
  • 巡回:巣の左下付近を巡回。
  • 追跡:パックマンの進行方向の2Unit先を点P、アカベイと点Pを結ぶベクトルをAPとしたとき、ベクトルAPの長さを2倍にしたときの点を追跡対象とします。

f:id:Nullsuke:20210515014205p:plain
アオスケの追跡対象

f:id:Nullsuke:20210515154545p:plain
アオスケのWaypointsData

グズタ

  • 待機:7秒待機します。待機中上・下の移動を繰り返す。
  • 巡回:巣の左下付近を巡回。
  • 追跡:自身とパックマンの距離が8Unit以上ならパックマンを、8Unit未満のときは迷路の原点(左下)を追跡対象とします。

f:id:Nullsuke:20210515154610p:plain
グズタのWaypointsData

Unityによるパックマンの作り方 ~ゴーストのアニメーション~

通常、恐慌、死亡時のアニメーションをサブステートマシンを使って作成します。すべてのアニメーションはAny Stateから遷移するようにします。

Animation ControllerのParametersに以下の変数を作成します。

  • float DirX
  • float DirY
  • bool isScare
  • bool isCalmSoom
  • bool isDead

f:id:Nullsuke:20210509221503p:plainf:id:Nullsuke:20210509221539p:plainf:id:Nullsuke:20210509221542p:plainf:id:Nullsuke:20210509221545p:plain
Animation Controller

遷移の条件は以下の通りです。また、Can Transition To Selfは必ずオフにしておきましょう。

通常

  • Up:DirY > 0.1 & isDead == false & isScare == false
  • Right:DirX > 0.1 & isDead == false & isScare == false
  • Down:DirY < -0.1 & isDead == false & isScare == false
  • Left:DirX < -0.1 & isDead == false & isScare == false

f:id:Nullsuke:20210509221803p:plain
通常状態のTransition

恐慌

  • Purple:isScare == true & isCalmSoon == false
  • PurpleWhite:isScare == true & isCalmSoon == true

f:id:Nullsuke:20210509221806p:plain
恐慌状態のTransition

死亡

  • Up:DirY > 0.1 & isDead == true
  • Right:DirX > 0.1 & isDead == true
  • Down:DirY < -0.1 & isDead == true
  • Left:DirX < -0.1 & isDead == true

f:id:Nullsuke:20210509221809p:plain
死亡状態のTransition

Unityによるパックマンの作り方 ~ゴーストのAI~

概要

Waypointに到達するごとに次のWaypointを取得するこで経路を決定します。以下のものが必要になります。

  • Waypoints:ゴーストが移動できる全座標のリスト。
  • Tile:Waypoint同士のつながりを表すクラス。
  • TileUtility:ゴーストの現在地からTileを取得するクラス。
  • AAI:TileUtilityを使ってゴーストの現在地からTileを取得し、移動方向を決定する抽象クラス。これを継承してゴーストごとのAIを作成します。
  • 各ゴーストのAI:追跡対象を取得するクラス。

処理の流れは以下の通りです。

  1. TileUtilityを使ってゴーストの現在地からTileを取得します。
  2. TileからAIを使って移動先のWaypointを決定します。

Waypointsの取得

ゴーストが移動できる全座標を手作業で座標を書き出してもよいのですが、迷路のColliderMapの作成と同じ要領でWaypointsMapを作成し、MazeUtilityを使いWaypointsを取得します(MazeUtilityについては下記参照)。

nullsuke.com

f:id:Nullsuke:20210504225518g:plain
WaypointsMapの描画

f:id:Nullsuke:20210506223417p:plain
WaypointsMap

MazeオブジェクトにWaypointsMapをセットし、ついでに迷路の幅と高さも設定します。

f:id:Nullsuke:20210505110831p:plain
Mazeオブジェクト(余計なものがいろいろついてますが気にしないでください)

TileUtilityの作成

Mazeオブジェクトが読み込まれたときにCreateTiles関数でTileのリスト作成し、それをもとにTileUtilityを作成します。

AAIの作成

処理の流れは以下の通りです。

  • GetNextTile関数で進行方向にあるTile(NextTile)を取得します。
  • NextTileが存在して、かつ交差点でない場合、そのTileの位置をWaypointとします。
  • NextTileが存在せず、かつ交差点でない場合、現在のタイルから進めるTileを探して、Waypointとします。
  • NextTileが存在せず、かつ交差点の場合ゴーストの状態によって方法が異なります。
    • 追跡状態:GetTarget関数で追跡対象のTileを取得し、そこまでの直線距離が最も短くなるTileの位置をWaypointとします。
    • 恐慌状態:現在のTileに隣接するTileからランダムでWaypointを決定します。
    • 帰還状態:巡回開始地点を目的地とし、そこまでの直線距離が最も短くなるTileの位置をWaypointとします。
    • 死亡状態:ゴーストの巣付近を目的地とし、そこまでの直線距離が最も短くなるTileの位置をWaypointとします。

パックマンなどの追跡対象への距離を単純な直線距離で計算しているため、場合によって遠回りしています(マヌケ)。気になる人は距離の求め方を修正するとよいと思います。

f:id:Nullsuke:20210515152246p:plain
マヌケなAI

各ゴーストのAIの作成

AAIを継承してゴーストごとのAIを作成します。追跡対象がいるTileを取得するGetTargetTileを実装します。

各ゴーストの追跡時の仕様については下記を参照してください。

nullsuke.com

Unityによるパックマンの作り方 ~ゴーストの移動~

概要

ゴーストが移動できる座標(Waypoint)のリスト(Waypoints)とWaypointに向かって移動させるMoverクラスが必要になります。MoverクラスでWaypointへの移動を繰り返すことでゴーストを移動させます。

Waypointsの設定・取得

ゴーストの状態によりWaypointsの取得の仕方が異なります。

待機・準備・巡回状態でのWaypoints

ゴーストの状態が待機・準備・巡回の場合、あらかじめWaypointsを設定しておきます。1匹のゴーストが複数のWaypointsを保持する必要があるので、ScriptableObjectを使って各状態のWaypointsを保持するGhostWaypointsDataを作成します。これらをMazeオブジェクトにセットします。ついでにスタート位置もここで設定します。

f:id:Nullsuke:20210505155201p:plain
アカベイのGhostWaypointsData

各ゴーストのGhostWaypointsDataをMazeオブジェクトにセットします。

f:id:Nullsuke:20210505161652p:plain
各ゴーストのGhostWaypointsData

死亡状態でのWaypoints

今回使用した迷路はゴーストの巣の入り口正面のWaypointが整数にならず、追跡状態のようにAIで目的地まで移動させることができません。よって、あらかじめNestWaypointsを設定して、Mazeオブジェクトにセットしておきます。迷路の形にこだわらければ、巣の位置を調整してWaypointが整数になるようにしてもよいと思います。

f:id:Nullsuke:20210508230819p:plain
NestWaypoints

追跡・恐慌・帰還状態でのWaypoints

追跡・恐慌・帰還状態ではAI(といってもかなりしょぼい)によって経路を決定します。ゴーストのAIについては 下記を参考にしてください。

nullsuke.com

Moverの作成

WaypointへVector2.MoveTowardsとrigid2D.MovePositionを使い移動させるクラスです。