マップに登場するキャラを設計して、実装します。開発の流れは、プレイヤーと同じです。1つのゲームオブジェクトに統合したプレイヤーに対して、分割したクラスを組み合わせて、バリエーションが作れるようにします。
企画構想で挙げた各キャラの機能は次のとおりです。
これらから、キャラの機能を列挙することから始めます。
すべてのキャラで共通なのが、ゲームの進行によって、動いたり、停止することです。
プレイヤーで使ったIGameStateListenerが使えます。ゲームの通知を受け取って、移動の開始や停止させるクラスを用意します。
プレイヤーと接触したときの動作は、次のとおりです。
爆発を検知するのを、プレイヤーにするか、爆弾にするかの問題がありました。今回は、プレイヤーにやることがありません。爆弾側で完結させるのがよさそうです。この方法の懸案は、爆弾をすべて検索して、通知を渡すのが重い可能性です。この点を、実装時に確認します。
爆弾に、OnTriggerEnterを実装して、相手がプレイヤーかどうかを確認します。プレイヤーなら、爆発を発生させます。また、第4章で検討したゲームオーバーを要求するIGameOverEmitterを実装して、ゲームオーバーを要求します。接触相手がプレイヤー以外なら、何もしません。
爆弾が問題なければ、コインの方も、コインにまとめて実装する方法でよいでしょう。コインには、IGetCoinEmitterを実装して、プレイヤーと接触したら、コインの取得を報告させます。
動き方は、次のとおりです。
動き方は、プレイヤーに触れたときの動作とは別のグループでまとめられます。これらを別クラスに分けて実装することで、ゲームオブジェクトにアタッチするスクリプトの組み合わせで、上記のキャラが用意できます。
動かないものは、ゲームオブジェクトにコライダーと、必要に応じてタグ、レイヤーを設定すればよさそうです。岩などの衝突があるものは、staticを有効にしておくとよいでしょう。スクリプトは、不要です。
決まったルートを一定速度で巡回するのは、黄爆弾、銀のコイン、木箱です。通過する座標と、ループ方法をインスペクターで設定したいので、MonoBehaiourで実装するのが良さそうです。木箱は、プレイヤーで押しても動かせません。RigidbodyのIs Kinematicを有効にして、MovePositionで移動させることになります。プレイヤーとは、違う動きになる可能性があるので、CharacterMoverとは別のクラスを用意します。
等速直線運動をして、壁や障害物にぶつかると跳ね返るのは、赤爆弾と金のコインです。これらは、移動開始時にRigidbodyのvelocityに初速を設定して、あとは慣性移動に任せます。これも、CharacterMoverとは違うので、別のクラスを用意します。
巡回と跳ね返りは、どちらもゲームの開始と停止の通知を受け取って、動作をはじめたり、停止する機能を持ちます。IStartStopインターフェースを実装すればよいでしょう。通知を受け取ったときの処理は、両者で異なります。処理的な共通点はほとんどないので、独立したスクリプトで実装することにします。
状態の切り替えは、CharacterBehaviourクラスで管理することにします。IGameStateListenerを実装すれば、ゲーム進行の通知を受け取れます。
動かし方を検討した結果、プレイヤーのように、受け取った移動量に応じて動かすような処理はありませんでした。設定されているパラメータに基づいて、自動的に移動します。状態に応じて、移動の開始と停止を指示できるようにすればよいでしょう。
爆発やコインの取得も、ゲーム中のみ有効です。移動と同様に、開始と停止が受け取れれば制御できます。
以上から、リスト6.1のようなインターフェースを定義します。
リスト6.1: IStartStopインターフェース
1: /// <summary>
2: /// ゲームが開始したり、停止したときに呼び出されるメソッドを定義するインターフェース。
3: /// </summary>
4: public interface IStartStop
5: {
6: /// <summary>
7: /// ゲームが開始したときに呼び出されるメソッド。
8: /// </summary>
9: public void OnGameStarted();
10:
11: /// <summary>
12: /// ゲームが停止したときに呼び出されるメソッド。
13: /// </summary>
14: public void OnGameStopped();
15: }
CharacterBehaviourクラスの開始時に、IStartStopインターフェースのインスタンスをGetComponentsで取得して、変数に代入してキャッシュします。CharacterBehaviourクラスは実装済みです。リスト6.2が、該当するコードです。
リスト6.2: CharacterBehaviourのIStartStopのキャッシュ処理
IStartStop[] startStops;
void Awake()
{
startStops = GetComponents<IStartStop>();
}
ゲームの開始や停止の通知が届いたら、キャッシュしたインスタンスをループで取り出して、対応するメソッドを呼び出します。InitStateに、リスト6.3のとおり、実装しています。
リスト6.3: CharacterBehaviourのIStartStopの呼び出し処理
void InitState()
{
if (!state.ChangeState())
{
return;
}
switch(state.CurrentState)
{
case State.Play:
for (int i=0;i<startStops.Length;i++)
{
startStops[i].OnGameStarted();
}
break;
case State.End:
for (int i = 0; i < startStops.Length; i++)
{
startStops[i].OnGameStopped();
}
break;
}
}
GetComponentsと、ちょっとしたループで、オブジェクト内の通知システムを構築しました。
残りの作業は、次のとおりです。
プレイヤーの開発と同様に、状態管理も含めて、こららも別々に開発できます。
爆弾は、プレイヤーと接触したことを検知したら、爆発用のゲームオブジェクトをInstantiateして、自分をDestroyします。ゲームオーバーを要求するには、IGameOverEmitterで定義したGameOverRequestをInvokeします。
爆発処理は、Attackerクラスに実装します。スクリプトのひな形を、/Assets/Yoketoru/Scripts/Game/Bombフォルダーに用意してあります。指示を読んで、実装してください。
ここまでで、ゲームの開始と停止を状態管理から受け取って、isStartedで確認できる仕組みが実装できました。続いて、OnTriggerEnterに、次の処理を実装して、爆発の処理を実装します。
GameOverRequest.Invoke();を実行して、ゲームオーバーを要求するDestroy(gameObject);で、爆弾を消す以上で完了です。プレイヤーの操作が完成していたら、プレイヤーを爆弾にぶつけて、爆発して、ゲームオーバーへ遷移するのを確認してください。
できたら、コミットして、プッシュします。
コインを拾う処理を実装します。
Coinスクリプトの雛形は、実装済みです。UnityのProjectビューから、Assets/Yoketoru/Scripts/Game/Itemフォルダーを開いて、Coinスクリプトを開きます。
爆発の接触処理と同様の手順で、次の作業をしてください。
次に、OnTriggerEnterの内容を、次の手順で実装します。
CoinGot.Invoke(point);と書く。これで、ゲームの進行管理にコインを取ったことを知らせるDestroy(gameObject);で、コインを消す以上で完了です。プレイヤーの操作が完成していたら、プレイヤーでコインを取ってください。コインが消えて、得点が入ります。また、すべてのコインを取ったら、クリアします。
できたら、コミットして、プッシュします。
移動には、次の3種類がありました。
検討したことをもとにして、実装します。
masterブランチから、dev-static-objectブランチを作成して、作業するとよいでしょう。
動かないものには、スクリプトは不要でした。ProjectビューのAssets/Yoketoru/Prefabsフォルダーを開いて、該当するオブジェクトの設定を、インスペクターで確認してください。
以上で、動かないオブジェクトの設定は完了です。Playして、岩にプレイヤーで突撃して、動かせないことを確認してください。
完成したら、コミットとプッシュします。
登場する順番的には、ルートの巡回が先なのですが、実装が難しいため、こちらを先に解説します。作業順は、簡単なものを優先するのが効率的です。
masterブランチから、dev-patrolブランチを作成して、作業するとよいでしょう。
跳ね返りは、Stage1では出てこないので、このままでは確認しにくいです。Stage2からはじまるように設定します。
Playすると、ゲームがStage2からはじまるようになります。完成するまでのPlay回数が多くなると、タイトルから起動して、カウントダウンを待つのが面倒です。手間は増えますが、開発用のシーンを作成したり、テストランナーを利用する選択肢もあります。このあたりは、開発する内容に応じて、楽そうな方法を選びます。
跳ね返り移動は、MonoBehaviourを継承して、IStartStopインターフェースを実装します。ルートの巡回移動とは共通点が少ないので、新規スクリプトで作成します。ひな形を、Assets/Yoketoru/Scripts/Game/AutoMoverフォルダーに用意しています。Projectウィンドウから、該当フォルダーを開いて、ReflectionMoverスクリプトをダブルクリックして開いてください。リスト6.4が、コードです。
リスト6.4: ReflectionMoverのひな形
1: using UnityEngine;
2:
3: /// <summary>
4: /// 反射移動を制御するクラス。
5: /// </summary>
6: public class ReflectionMover : MonoBehaviour, IStartStop
7: {
8: [Tooltip("移動方向"), SerializeField]
9: Vector3 firstDirection = Vector3.right;
10: [Tooltip("移動速度"), SerializeField]
11: float speed = 2;
12:
13: Rigidbody rb;
14:
15: void Awake()
16: {
17: rb = GetComponent<Rigidbody>();
18: rb.velocity = Vector3.zero;
19: }
20:
21: private void FixedUpdate()
22: {
23: // TODO: 速度を維持する
24: }
25:
26: public void OnGameStarted()
27: {
28: Debug.Log($"{name} 移動開始");
29: }
30:
31: public void OnGameStopped()
32: {
33: Debug.Log($"{name} 移動停止");
34: }
35: }
ReflectionMoverクラスは、MonoBehaviourを継承して、IStartStopインターフェースを実装しています。
最初の移動方向をfirstDistance、速度をspeedで定義しています。これらは、Inspectorウィンドウで設定できます。方向と速度を分けているのは、設定を簡単にするためです。
Rigidbodyのキャッシュ用の変数としてrbを定義して、Awakeで取得と、停止する設定をしています。
あとは、実装予定のメソッドを定義しています。
跳ね返り移動は、とても簡単です。ゲームが開始したときに初速を設定して、ゲームが停止したときに移動を止めます。
理屈的にはこれだけでよいのですが、ゲーム用の物理エンジンは、精度を犠牲にして、処理速度を優先するものが多くあります。Unityの物理エンジンも同様です。誤差から、衝突するごとに徐々に速くなったり、逆に、遅くなったりします。これを防ぐために、方向はそのままで、速さをspeedに保つ処理を、FixedUpdateに実装します。
また、一定の速度より遅い場合、跳ね返らないようにする設定があります。この設定がないと、別のオブジェクトに載っているオブジェクトが、数値の誤差によって、いつまでも跳ね続ける不具合が起きます。それをなくすための工夫です。しかし、今回のように、跳ね返って欲しいゲームの場合、この設定が悪い方向に作用します。薄い角度で壁にぶつかると、跳ね返らずに、壁にくっついて動くようになります。ブロック崩しなどを作っていると、この現象に遭遇します。この設定を、無効にする必要があります。
ここまでの話しを踏まえて、実装と設定の手順を考えます。
OnGameStartedの実装は、次のとおりです。
firstDirectionは、長さが1とは限りません。normalizedを参照することで、単位ベクトルを求めて、それにspeedをかけます。
OnGameStoppedは、次のとおりです。
Playして、動作を確認してください。Inspectorで設定されているスピードと方向にしたがって、動き始めます。ただし、壁に衝突しても、期待する方向には跳ね返らず、動きが遅くなります。また、角に達すると、止まってしまいます。これを直していきます。
跳ね返りの設定は、Project SettingsのPhysicsで設定します。
Bounce Thresholdが、跳ね返るかを判定する閾値(しきいち)です。デフォルトの2だと、速度が2以下では跳ね返らなくなります。0にすると、遅くても跳ね返るようになります。
Playして、動作を確認してください。壁で跳ね返るようになります。ただ、角度が期待と違います。これは、衝突したときの摩擦と跳ね返り係数が未設定なためです。
摩擦と跳ね返り係数は、PhysicMaterialで設定します。Projectウィンドウの+から作成できますが、今回は用意してあります(図6.1)。
図6.1: 跳ね返り用のPhysicMaterial
Dynamic Friction(動摩擦係数)と、Static Friction(静摩擦係数)は、0に設定します。これは、接触したときに、オブジェクトが回転しないようにするためです。回転すると、回転に力を使ってしまい、跳ね返りが弱くなります。
Bounciness(跳ね返り係数)は、1に設定します。これは、完全に跳ね返すための設定です。0だと、跳ね返りません。
物理の世界では、衝突した相手のPhysic Materialとの組み合わせで、跳ね返り方が決まります。今回のようなゲームの世界では、こちらの設定を優先したい場合があります。そのために、Friction CombineをMinimumにして、抵抗は小さい方を採用するようにします。また、Bounce CombineをMaximumに設定して、跳ね返り係数は大きい方を採用するようにします。これで、抵抗を0、跳ね返りを1に固定します。
Reflectマテリアルを、BombReflectionプレハブと、CoinReflectionプレハブのColliderにアタッチします。
図6.2: Reflectマテリアルをアタッチする
Playして、動作を確認してください。納得のできる跳ね返り方になります。しばらく見ていると、速度が遅くなったり、速くなったりします。これを修正します。
速度が変わるのは、Physicsの誤差によるものです。物理エンジンにすべての制御を任せて作れるゲームは、意外とありません。多かれ少なかれ、スクリプトでの調整が必要になります。今回は、FixedUpdateで、速度を再設定するようにします。
Playして、様子を確認してください。速さが変わらなければ、実装完了です。コミットして、プッシュします。
決まったルートの巡回移動を実装します。まずは、機能を書き出します。
これらを実装する手順を検討します。
今回の処理は、やや複雑なので、段階を経て実装します。これまでと同じく、masterブランチから、dev-patrolブランチを作成してから作業をするとよいでしょう。
巡回移動は、黄色爆弾、銀のコイン、木箱です。これらのゲームオブジェクトの設定を確認します。
以上で、オブジェクトの設定は完了です。
ここで設定した爆弾とコインは、コライダーをIs Triggerにしたので、跳ね回る爆弾やコインと接触しません。接触させたい場合は、跳ね回るオブジェクトの設定を参考に、設定してください。
これは、すでにひな型として実装済みです。Projectウィンドウで、Assets/Yoketoru/Scenes/Stagesフォルダーを開いて、Stage2を、Hierarchyウィンドウにドラッグ&ドロップして開きます。
Stage2シーンにあるCrate (1)などをクリックして選択してください。Scenesビューを見ると、右の木箱の場所と、少し下に、赤い丸が表示されます(図6.3)。これが、ウェイポイントの場所を示すギズモです。
図6.3: ウェイポイントのギズモ
他のCrateや、ItemPatrolを選択すると、それぞれのウェイポイントが確認できます。また、Inspectorウィンドウで、Way Pointsを変更すると、ギズモの位置が変わります。
OnDrawGizmosSelectedメソッドが、このギズモを描画するコードです。便利なので、ご活用ください。
ゲームの開始と停止の処理を検討します。ルートの巡回は、現在の座標から、目的のウェイポイントへ移動します。FixedUpdateごとに、MovePositionで移動させるので、その処理をしなければ移動しません。
そこで、bool型の変数isStartedを定義します。ゲームが開始したら、trueにして、移動を開始するようにします。FixedUpdateの最初で、isStartedを確認して、移動するか判断します。ゲームが停止したら、isStartedをfalseにします。
移動中は、Debug.Logで何か表示するようにします。移動の実装は、手間がかかりそうなので、切り分けて開発する工夫をします。
できたら、Playして動作を確認してください。カウントダウンが終わってから「移動」が表示されて、ゲームオーバーやクリアしたら、ログ表示が停止すれば成功です。
移動の開始と停止ができたら、動きを実装します。これも、段階を踏んで進めましょう。
到着の確認と、目的地の切り替えは、次のステップで実装します。今回は、このまま移動させます。
Playして、カウントダウンが終わると、木箱や銀のコインが動きます。最初の目的地に到着して、停止したら成功です。
ルートの巡回は、やや難しい処理でした。このような処理は、いきなりすべてを実装しようとせずに、分割するのが開発のコツです。Debug.Logを活用すると、実際に処理を実装していなくても、プログラムが正しく動作していることが確認できます。