yumeの日記: メデューサ・ゲーム改・6 1
◉ターンシステムと兵士
このゲームのもうひとつのコア・メカニクスであり、最も実装が面倒くさい兵士を作る……の前に、ターンシステムを作る。
ターンは、次のような概念だと思う:
・プレイヤーがターンを消費する操作を行ったとき:
・全てのターンを持つオブジェクトのターンが経過する。
・ターンを持つオブジェクトは、自分が次に行う動作を知っている。
・ターンを持つオブジェクトは、ターン経過時にその動作を実行する。
ううむ、厄介そうな概念だ。でも多分、イベントをうまく使えば実装できると思う。やってみよう。
TurnManager
ステージごとにひとつ存在する。次の要素を持つ:
・TurnEvent
ターンが進むことを購読者に知らせる。
・AdvanceTurn()
プレイヤーの操作や、あるいは他の要素から呼び出されて、TurnEventを発行する。
PlayerControl
・入力制御の関数の、ターンを消費する動作が確定した時点でTurnManagerにコールする。
……ターン関連がちょっとゴチャってきたので、色々見直す。
・まずMovementやFaceDirectionはMonoBehaviourを継承していなかったが、やっぱり継承して、コンポーネントとして管理(かっこつけて無駄な手数を……)
・各Actorのターン状態を管理するTurnActクラスはActorに統合する。
・全てのActorはActorクラスを継承し、実際の動作命令(右に歩け!みたいなAI的な)は継承先で実装する。
Playerの場合は、これはPlayerControlが全部やっているけど、考え方としてはPlayerControlはActorを継承したPlayerクラスに展開すべきかも。でも名前がよくないので、一旦保留。
ん〜。ターンの概念があるけど歩いたりはしないもの(時間を数えるスイッチとか扉とか)があるとしても、それもやっぱりActorとして継承しなきゃならんのかなぁ。それか、Actorのさらに上位(下位?)の抽象概念を作るか。まぁそこは後回し。
で、新たなクラスSoldier : ActorはSoldierの挙動を設定する。
Soldierは、TurnManagerのターン経過イベントを購読し、イベント発行ごとにTurnAction関数を実行する。
TurnAction関数の中に、ターンごとに実行したいことをつめこむ。
これで、ターンごとにSoldierが好きな行動を実行できるようになった。
--
兵士であることを定義する最低限の要素は、次の要素だけだ。
A. プレイヤーに隣接したとき、プレイヤーを捕まえる。
最も基本的な兵士は、さらに以下の要素を持つ。
B1. プレイヤーを発見することができる。
B2. プレイヤーを発見したなら、プレイヤーに向かう経路を割り出す。
B3. 経路があるなら、その経路に従って毎ターン進む。
C. 邪眼で見られたとき、設定したターン数だけ耐えたあと、石化する。
うわー、山盛りだな。
いっぺんに作るとわけがわからなくなるから、まずはB1を作ろう。
DetectPlayerクラスは、Actorがプレイヤーを発見するためのコンポーネント で、ターン終了時にプレイヤーと自身の座標間に視界を遮るものがないかチェックし、ないなら視界がつながったとみなす。
見つけたときは、それを表現するためにびっくりマークを出す。
よさそうなので、SoldierクラスにDetectPlayerクラスへの参照を持たせておいて:
・プレイヤーを発見しているなら、左へ歩く(とりあえず)
・してないなら、単に待機する
という動作を設定した。
ん〜。気になるのは、SoldierクラスとDetectPlayerクラスが相互に参照を持っている点だ。
・DetectPlayerクラスはターンの開始と終了を検知するために、Actorクラスに依存する。
・Soldierクラスは、プレイヤーの発見状態を保持するDetectPlayerクラスに依存する。
ん〜〜〜。MovementとActorが相互参照してる、とかもそうなんだけど、すげえ結合度が高い感じになってるなぁ。
直感的には、Actorの設計がまずそうだ。ちょっといきあたりばったり気味だったし。
ん〜〜〜〜〜〜〜。この辺も考え直した方がよい気がしてきたぞ。
ターンの開始と終了を司るのは、レベルごとにひとつだけで良いはずで、それはTurnManagerでよいはず。
制御の流れをもう一度考えよう。次のようになるはず:
1. プレイヤーがターンを消費する行動を入力する
2. 全てのアクターが、ターンを消費する行動を実行する(隣のマスにステップするなど)
3. ターンが終了したら、全てのアクターはターン終了時処理を行う(視界チェックなど)
4. 次の操作を待機する
これだけだから、もっとシンプルに実装できる、はず。
ちょっとブランチを分けて作ってみよう。
ターンの開始時と終了時の判定もTurnManagerに足して、
ターンの制御がTurnManagerだけで完結するなら、各Actorのターン制御関連はごっそりいらないはずだ。そういうわけで削除して、依存していた部分を全部TurnManagerに向ける……。
あれ……それだけでもうできちゃったぞ。しかもちゃんと動く。
しかも、Actorは完全に空になっちゃって、それを継承したPlayerクラスも空になっちゃった。
Soldierクラスはかろうじて中身がいきてるけど、それはAIの動作の定義+TurnManagerの購読だ。
というか、そうか。そもそも、Actorを定義する最低限の要素というのは実は「ターンの流れを受けて行動する」ことであって、そういう意味ではPlayerはActorではない。なのになぜか共通する根底の概念だと考えてたから、共通する最低限の要素が定義できなかったんだ。言葉を先に考えて作っちゃったのがまずかった。
PlayerとSoldierや他のActorと繋がる根底の要素は、多分無い。
というわけで、PlayerはActorという概念から外して、Actorクラスはターンの流れを受けて行動するものと定義する。
SoldierはActorを継承し、捕縛能力に関しては別のクラスを作って、それを持つという形にしよう。
ともあれ、これで結合度がぐっと下がったと思う。
Movementはそれを必要とするクラスが参照する(SoldierやPlayerControl)が、MovementからSoldierを参照したりはしない。これらはただ命令を待つだけ。DetectPlayerも同様。
次からは、概念を意味するクラスを作る前に、しっかり定義をしておこう。名前だけで雰囲気で作るとおかしいことになる。
空のクラスは悪いことじゃない (スコア:0)
ターゲットクラスで振る舞い変更したり新たに処理追加したりするときに便利。
だから今回のは概念と処理のマッチングミスかな。
幾何学の補助線と同じで、ちょっとしたことを追加するだけで複雑なものが一気に単純になる。
複雑になりそうだと感じた時点で、何か追加することで単純にならないか検討するといいんじゃないかな。
たまにやり過ぎて余計に複雑になったりするけど。