[Unity][RoboCup] CrossView開発日記 #2

# CrossViewとは
こちらの記事をご覧ください。

https://qiita.com/suzuki_nene/items/cbdb701622a738f1b2ee

# はじめに
[前回](https://qiita.com/suzuki_nene/items/69af880ae3a3e979c782)、3Dモデルとアニメーション操作を実装したので、次は「3Dモデルの移動、方向転換、首振り動作」を実装しました。

![Football_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/843818/befc1830-71a3-ba8e-da9d-ead566c8f8f7.png)

# Football_2
CrossViewは、RoboCup Soccer Simulation(2D)のログファイルを再生するアプリですが、ログファイルにはボールや各プレーヤーの座標が「100msec」間隔で記録されています。

これをそのまま再現しただけだと、カクカクしたアニメーションになってしまいますので、間のフレーム描画を補間しなければなりません。

## 3Dモデルの移動
そこで、「T秒後に座標(x,y,z)に移動しろ」と指示できるクラスを用意しました。
プレーヤーだけでなくボールの移動でも利用しています。

```MovingBehaviour.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MovingBehaviour : MonoBehaviour
{
    public class Move
    {
        private Vector3 srcPos = Vector3.zero;
        private Vector3 dstPos = Vector3.zero;
        private float totalTime = 0f;
        private float pastTime = 0f;

        public void UpdatePosition(Transform tf, float delta)
        {
            float remainingTime = totalTime - pastTime;

            if (remainingTime > 0f)
            {
                delta = (delta > remainingTime) ? remainingTime : delta;
                pastTime += delta;

                Vector3 distance = (dstPos - srcPos) * delta / totalTime;
                if (pastTime == totalTime)
                {
                    distance = dstPos - tf.position;
                }
                tf.Translate(distance, Space.World);
            }
        }

        public void MoveTo(Transform tf, Vector3 destination, float seconds)
        {
            srcPos = tf.position;
            dstPos = destination;
            totalTime = seconds;
            pastTime = 0f;

            if (seconds == 0)
            {
                // Apply immediately.
                tf.position = dstPos;
            }
        }
    }
    private Move move = new Move();

    /*
    // Start is called before the first frame update
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {

    }
    */

    protected void UpdatePosition(float delta)
    {
        move.UpdatePosition(this.transform, delta);
    }

    public void MoveTo(Vector3 destination, float seconds)
    {
        move.MoveTo(this.transform, destination, seconds);
    }
}
```
MoveTo()メソッドで、移動先の座標と到達までにかける時間を指定します。
ここから派生するクラスを用意して、3Dモデルにアタッチします。

派生クラスでは、FixedUpdate()の中で親のUpdatePosition()を呼び出します。
引数にはTime.fixedDeltaTimeを渡します。

FixedUpdate()ではなくUpdate()から呼び出しても動作するとは思います。
その際は、Time.deltaTimeを渡してください。（ただし、未検証）
```Sample.cs
public class Sample : MovingBehaviour
{
    void Update()
    {
        // 例）マウスクリックされた画面上の位置へ3秒後に移動
        Vector3 dest = new Vector3(xxx, yyy, zzz);
        MoveTo(dest, 3f);
    }
    
    void FixedUpdate()
    {
        UpdatePosition(Time.fixedDeltaTime);
    }
}
```

## 方向転換
3Dモデルの方向転換も同様の考えで、TurnTo()というメソッドを持つクラスを実装しました。
```PlayerBehaviour.cs
public class PlayerBehaviour : MovingBehaviour
{
    protected void UpdateRotation(float delta)
    {
        // snip
    }
    
    public void TurnTo(float direction, float seconds)
    {
        // snip
    }
}
```

## 首振り動作
3Dモデルの首振り（顔の向き）も、TurnNeckTo()というメソッドを持つクラスを実装しました。

実際の首振り動作はAnimatorによって操作するものなので、UpdateNeckRotation()では顔を向かせる方向を表す座標値(Vector3)のみを内部でアップデートし、派生クラスのOnAnimatorIK()からUpdateNeckAnimator()を呼んでもらい、座標値をアニメーションに反映させるという形になります。
```PlayerNeckBehaviour.cs
public class PlayerNeckBehaviour : MonoBehaviour
{
    protected void UpdateNeckRotation(float delta)
    {
        // snip
    }

    protected void UpdateNeckAnimator(Animator animator)
    {
        // snip
    }

    public void TurnNeckTo(float direction, float seconds)
    {
        // snip
    }
}
```
```Sample.cs
public class Sample : PlayerNeckBehaviour
{
    void Start()
    {
        animator = GetComponent<Animator>();
    }

    void Update()
    {
        // 例）キー入力されたら、顔の向きを1秒かけて指定の角度に変える
        float direction = xxx;
        TurnNeckTo(direction, 1f);
    }

    void FixedUpdate()
    {
        UpdateNeckRotation(Time.fixedDeltaTime);
    }

    void OnAnimatorIK(int layerIndex)
    {
        UpdateNeckAnimator(animator);
    }
}
```

## 扇形オブジェクト
また、プレーヤーの足元に、プレーヤーの視界(向きと角度)を表す「扇形のオブジェクト」を描画しています。
さらに、スポットライトも持たせ、視界を光の範囲で表現できるようにもしています。

3Dモデルの子オブジェクトとして空のViewAngleオブジェクトを追加し、ViewAngleBehaviourクラスのスクリプトをアタッチします。
また、ViewAngleオブジェクトの子オブジェクトとしてスポットライトを追加しておきます。
```ViewAngleBehaviour.cs
public class ViewAngleBehaviour : MonoBehaviour
{
    void Start()
    {
        // 自分で扇形のMeshオブジェクトを作り、GameObjectとして生成します。
    }

    void OnDestroy()
    {
        // 扇形のGameObjectを破棄
    }

    void FixedUpdate()
    {
        // ViewAngle自身の向きを、Time.fixedDeltaTimeの分だけ更新
        // 扇形オブジェクトおよびスポットライトの角度と距離を、Time.fixedDeltaTimeの分だけ更新
    }

    public void TurnTo(float body, float neck, float seconds)
    {
        // snip
    }

    public void ChangeAngle(float angle, float range, float seconds)
    {
        // snip
    }
}
```

## ボールの回転
ボールの移動はMovingBehaviourで行いますが、進行方向に向けて回転するRollTo()を実装しています。
```BallBehaviour.cs
public class BallBehaviour : MovingBehaviour
{
    void FixedUpdate()
    {
        UpdatePosition(Time.fixedDeltaTime);
        UpdateRotation(5f);
    }

    protected void UpdateRotation(float angle)
    {
        // 設定された座標にまだ到達していなければ、X軸を中心に5度回転させる。
    }

    public void RollTo(Vector3 destination)
    {
        // MoveTo()と同じ座標を渡して、ボールがその座標に向くようにする。
    }
}
```

# ソースコード
最新版のソースコードは、CrossViewのリポジトリにあります。
- [MovingBehaviour.cs](https://github.com/SuzukiNene/CrossView3D/blob/master/Assets/Scripts/MovingBehaviour.cs)
- [PlayerBehaviour.cs](https://github.com/SuzukiNene/CrossView3D/blob/master/Assets/Scripts/PlayerBehaviour.cs)
- [PlayerNeckBehaviour.cs](https://github.com/SuzukiNene/CrossView3D/blob/master/Assets/Scripts/PlayerNeckBehaviour.cs)
- [ViewAngleBehaviour.cs](https://github.com/SuzukiNene/CrossView3D/blob/master/Assets/Scripts/ViewAngleBehaviour.cs)
- [BallBehaviour.cs](https://github.com/SuzukiNene/CrossView3D/blob/master/Assets/Scripts/BallBehaviour.cs)

# 参考サイト
- https://docs.unity3d.com/2020.3/Documentation/ScriptReference/Quaternion.AngleAxis.html
- https://docs.unity3d.com/ja/current/Manual/InverseKinematics.html
- https://www.urablog.xyz/entry/2017/10/22/183331
- https://kazupon.org/unity-memory-leak-list/
- https://edunity.hatenablog.com/entry/20220830/1661785557

# WebGL version

https://suzukinene.github.io/FootballWeb/Football_2/

操作方法は次の通り。
- マウスの右クリックで、ボールが1秒後に指定した座標に移動します。
- マウスの左クリックで、プレーヤーが3秒後に指定した座標に移動します。
- 上下左右キーで、プレーヤーの体の向き、顔の向き、視界の角度と距離が変わります。
- 画面左上のNightModeボタンで、スポットライトのON/OFFが切り替わります。
- 移動しながら向きの変更なども、同時並行に実行できます。
- 次のキー操作でアニメーションを切り替えることができます。
- Bool系
    - 8：Dash
    - 5：Idle
    - 2：Back
- Trigger系
    - 7：Tackle
    - 9：Kick

※Bool系は、一旦Idleに戻ってから別の状態に遷移させます。
※Trigger系は、Back状態では反応しないようにしています。

以上です。