CrossViewとは
こちらの記事をご覧ください。
はじめに
前回、ログファイルを読み込み、記述されたログ(ボールやプレーヤーの動きなど)を100msec間隔で順次実行するLogPlayerの仕組みを構築するところまで実装しました。
実際にログを再生した時、ボールの動きまでは動作を確認することができたので、今回はプレーヤー(選手)の番です。
プレーヤーは、左右のチームごとに11名ずつ、合計22名の3Dモデルを実行時にインスタンス化しなければなりません。
事前に3Dモデルを22個用意しておくというやり方もありますが、ゲームで使う3Dモデルをユーザーが設定画面から選択できるようにもしたかったので、Prefabによるインスタンス化を採用します。
Football_4
今回のテストプログラムでは、以下の項目を実装します。
- Prefabによるプレーヤー3Dモデルのインスタンス化
- プレーヤーごとの背番号を2DTextで描画する仕組み
- AnimatorControllerを実行時に切り替える
- インスタンス化したらアニメーションをデモ実行
- NightModeの仮実装
プレーヤーのインスタンス化
プレーヤーのGameObject階層とアタッチするScriptは、次のような形にしました。
プレーヤーをPrefab化するにあたり、クラス構成も見直しを行いました。
Prefabs
+-- Player_00 -> FieldPlayerBehaviour
+-- {3DModel} -> PlayerAnimBehaviour
+-- ViewAngle -> ViewAngleBehaviour
+-- Spot Light
{3DModel}の部分に、BananaManやSpaceRobotKyle、Unityちゃんといった3Dモデルを入れ替えて、それぞれ Player_xxとしてプレハブ化しています。
アタッチするScriptのクラス継承、および各クラスの機能は、次の通りです。
MonoBehaviour
+-- MovingBehaviour -> MoveTo()
+-- PlayerBehaviour -> TurnTo()
+-- FieldPlayerBehaviour -> TurnBodyNeckViewAngleTo(), Idle(), Dash(), Kick(), Tackle(), Catch()
MonoBehaviour
+-- PlayerNeckBehaviour -> TurnNeckTo()
+-- PlayerAnimBehaviour -> Idle(), Dash(), Kick(), Tackle(), Catch()
MonoBehaviour
+-- ViewAngleBehaviour -> TurnTo(), ChangeAngle()
Prefabデータが用意出来たら、あとはInstantiate()でインスタンス化するだけです。
今回は6種類のPrefabを用意しました。
テストプログラムでは、画面下部で使用するPrefabの種類を選んでサッカーフィールド上をマウスクリックすると、インスタンスが生成されるようになっています。
背番号を2DTextで描画
背番号は、Canvas上に2DTextを描画することで実現します。
- 3Dモデルの頭部中央の3D座標を求める。
- 3D座標をスクリーン座標に変換する。
- スクリーン座標に2DTextを生成して、配置する。背番号の番号文字列も実行時に決定する。
3Dモデルの頭部中央の3D座標は、次のように求めています。
private Vector3 getPlayerHeadPosition(GameObject obj)
{
Bounds bounds = new Bounds(obj.transform.position, Vector3.zero);
Renderer[] renderers = obj.GetComponentsInChildren<Renderer>();
foreach (Renderer renderer in renderers)
{
bounds.Encapsulate(renderer.bounds);
}
Vector3 head = new Vector3(bounds.center.x, bounds.max.y, bounds.center.z);
return head;
}
テストプログラムでは、背番号は「画面上にインスタンス化されたプレーヤーの人数+1」を新規に割り当てるようにしています。
プレーヤーは数秒経つと自動的に消滅するようにしているので、タイミングにより、背番号が重複することがありますが、まぁ、テストプログラムということで。。。汗)
AnimatorControllerの切り替え
FieldPlayerとGoalKeeperで、アニメーションを一部変えています。
- Idle状態にて、GoalKeeperはボールをキャッチする構えをするアニメーションを使います。
- CatchはGoalKeeperのみ実行でき、Catch直後のKickは、ボールを下投げするアニメーションを実行します。
テストプログラムでは、デフォルトのAnimatorControllerはFieldPlayer用とし、背番号が11カウントアップする毎に、GoalKeeper用のAnimatorControllerに変更しています。
実装方法は、次のような感じです。
// FieldPlayer or GoalKeeper
if (unum % 11 == 1)
{
obj.GetComponentInChildren<Animator>().runtimeAnimatorController = Resources.Load<RuntimeAnimatorController>("Animators/GoalKeeper");
}
ちなみに、Resources.Load()の利用はUnity的には【非推奨】のようですが、この場面でしか使わないので良しとしています。
アニメーションのデモ
プレーヤーをインスタンス化したら、アニメーションの動きも確認したいので、デモを実装してみることにしました。
- サッカーフィールド上の任意の位置をクリックする。
- センターサークル中央位置に、画面下のチェックボックスで選択したプレーヤーのインスタンスを1つ生成する。
- クリックした座標にプレーヤーの向きを変え、Dashで2秒かけて移動させる。この時、視界の向きも体の向きに対して右90度に変化させる。
- クリック位置に到着したら、センターサークルに向けて体の向きを変える。
- DashからIdle状態に変更し、Catchアニメーションを実行。
- 5秒後に、Kickアニメーションを実行。
- 2秒後に、Tackleアニメーションを実行。
- 再度、Dash状態を5秒間続けてからIdleに戻り、最後に、プレーヤーを自動消滅させる。
StartCoroutine()によって、次のデモシナリオを実行しています。
IEnumerator MovePlayer(FieldPlayerBehaviour player, Vector3 clickPos, float delta)
{
float body = Mathf.Atan2(clickPos.x, clickPos.z) * Mathf.Rad2Deg;
float neck = 90f;
float angle = 120f;
float range = 5f;
// Instantiate() and call it's method soon, it occured NullReferenceException.
yield return new WaitForSeconds(0.1f);
// Turn to clicked position
player.TurnBodyNeckViewAngleTo(body, 0f, 90f, range, 0f);
// Animation
player.Dash();
// Move to clicked position with delta seconds.
player.MoveTo(clickPos, delta);
player.TurnBodyNeckViewAngleTo(body, neck, angle, range, delta);
yield return new WaitForSeconds(delta);
// Turn to center circle
body = Mathf.Atan2(-clickPos.x, -clickPos.z) * Mathf.Rad2Deg;
angle = 60f;
range = 15f;
player.TurnBodyNeckViewAngleTo(body, 0f, angle, range, 0f);
player.Idle();
player.Catch();
yield return new WaitForSeconds(5f);
player.Kick();
yield return new WaitForSeconds(2f);
player.Tackle();
yield return new WaitForSeconds(1f);
player.Dash();
yield return new WaitForSeconds(5f);
player.Idle();
yield return new WaitForSeconds(1f);
OnDestroyPlayer(player.gameObject);
Destroy(player.gameObject);
}
画面上を一気にクリックしていって、90個くらいインスタンスを生成してみました。
NightMode
画面右下のNightModeチェックボックスをONにすると、画面を暗くして各プレーヤーの足元にあるSpotLightを点灯させます。
- 環境光(RenderSettings.ambientIntensity)とDirectionalLightを消す/暗くする。
- 各プレーヤーのSpotLightを点灯させる。
ソースコード
最新版のソースコードは、CrossViewのリポジトリにあります。
参考サイト
- https://qiita.com/2dgames_jp/items/8a28fd9cf625681faf87
- https://qiita.com/NNNiNiNNN/items/ab983db04bcb1a70dd8e
- https://nekojara.city/unity-object-ui-position
WebGL version
操作方法は、前述の通りです。
以上です。