Unityでオブジェクトの多段階の動きを実装する

# 概要
下のサンプルではプレイヤーが近づいたとき、敵キャラクターが「落下する→一定時間静止する→上昇する」という3段階からなる動作をしています。このようなオブジェクトの多段階の動きを簡単に実装するために作成したコンポーネントを紹介します。
![motionSample2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531051/d084bea1-4c60-0f3c-2031-9dd6cf5ad74d.gif)

# 従来の方法
初めは「落下する→一定時間静止する→上昇する」という3段階の動作を、敵キャラクターを操作する1つのコンポーネントで実装していました。この方法では`update()`関数で行う処理は次のよう流れでした。
なお、動作の開始状態を表すbool型の変数と、静止状態に入ってからの経過時間を表すfloat型のタイマー変数を使用しています。
```
if 敵キャラクターとプレイヤーの距離が一定以下で、かつ落下状態でない:
    動作の開始状態をtrueにする
end if

if 動作の開始状態がtrue:
    if 敵キャラクターが地面より上にあり、かつタイマー変数が0:
        下に移動する
    else:
        // 敵キャラクターが接地している、またはタイマー変数 > 0(→落下し終わった後)
        タイマー変数を前のフレームからの経過時間分加算する
    end if
    
    if 敵キャラクターが元の位置より下にあり、かつタイマー変数の値が一定以上:
        // 着地してしばらくは静止し、その後上昇し始める
        上に移動する
    else if 敵キャラクターが元の位置にある:
        // 一連の動作が終了したとき
        動作の開始状態をfalseにする // 再びプレイヤーが接近したときに同様の動作をするため
        タイマー変数を0にする
end if
```
しかしこの方法は処理がわかりづらく、今後動きにバリエーションをもたせる場合や「左右に往復する床」など別のオブジェクトを作る際に再利用するのも難しいと感じたため、次に紹介する別の方法をとることにしました。

# 改良した方法
多段階の動作を簡単に実装するには、各段階の動作と時間の組のリストを与えるとそれを順に実行するコンポーネントがあればよいと考えて、次の`MultiStageMotionController`コンポーネントを作成しました。
まず「ある段階の動作と時間の組」を表す`Motion`クラスのコードを示します。
``` c#:Motion.cs
using System.Collections;
using System.Collections.Generic;

/// <summary>
/// 多段階のモーションのうち（動作、時間）からなる一段階を表すクラス
/// </summary>
public class Motion
{
    public System.Action motion; // この段階の動作を表すメソッド
    public float duration; // この段階の持続時間
    
    // コンストラクタ
    public Motion(System.Action motion, float duration)
    {
        this.motion = motion;
        this.duration = duration;
    }
}
```
`motion`には「下に移動する」など、その段階の1フレームの処理を表すメソッドを指定し、`duration`にその動作を継続する時間を指定します。

次に`MultiStageMotionController`コンポーネントのコードを示します。先ほどの敵キャラクターの動作の最後に行っていた「開始状態をfalseにする」など、動作が終了したときに行う処理を`SetLastAction`メソッドで設定するようにしています。
``` c#:MultiStageMotionController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MultiStageMotionController : MonoBehaviour
{
    private bool isStart = false; // 一連の動作の実行状態
    private List<Motion> multiStageMotion; // 一連の動作と時間のの情報
    private int currentStageIndex; // 現在の段階のインデックス
    private System.Action currentStageMotion; //現在の段階の動作
    private float currentStageDuration; // 現在の段階の持続時間
    private System.Action lastAction = null; // 終了時の動作

    private int nsteps; // 動作の段階の数
    private float timer = 0; // 各段階を開始してからの経過時間

    /// <summary>
    /// 実行する動作をセットする
    /// </summary>
    /// <param name="list">実行する動作と時間(Motion)のリスト</param>
    public void SetMultiStageMotion(List<Motion> list)
    {
        multiStageMotion = new List<Motion>(list);
    }

    /// <summary>
    /// 一連の動作が終了したときに実行する動作をセットする 
    /// </summary>
    /// <param name="action">最後に実行する動作</param>
    public void SetLastAction(System.Action action)
    {
        lastAction = action;
    }

    /// <summary>
    /// 設定された動作を開始する
    /// </summary>
    public void StartMotion()
    {
        if (multiStageMotion.Count > 0)
        {
            isStart = true;
            nsteps = multiStageMotion.Count;
            currentStageIndex = -1;
        }
    }

    // Update is called once per frame
    void Update()
    {
        if (isStart)
        {
            // 前の段階が終わったあと次の段階の情報をセットする
            if (timer >= currentStageDuration)
            {
                // 現在が最後の段階であれば動作を終了する
                if (currentStageIndex == nsteps - 1)
                {
                    lastAction();

                    // 一連の動作終了時に状態をリセットする
                    isStart = false;
                    timer = 0;
                    currentStageIndex = -1;
                    currentStageDuration = 0;
                    currentStageMotion = null;
                } else
                {
                    // 続きの動作がある場合対象のインデックスを次に進める
                    currentStageIndex++;
                    currentStageMotion = multiStageMotion[currentStageIndex].motion;
                    currentStageDuration = multiStageMotion[currentStageIndex].duration;
                    // 次の動作のためにタイマーをリセットする
                    timer = 0;
                }
            } else
            {
                // 現在の段階の処理を実行し、タイマーを加算する
                currentStageMotion();
                timer += Time.deltaTime;
            }
        }
    }
}
```
## 使用例
`MultiStageMotionController`コンポーネントの使用例として、先程の敵キャラクターの「落下する→静止する→上昇する」という動作をこのコンポーネントを使って実装したコードを示します。
``` c#:EnemyController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class EnemyController : MonoBehaviour
{
    [SerializeField] private GameObject player;
    [SerializeField] private Rigidbody2D rb;
    [SerializeField] private MultiStageMotionController ms;

    private bool isFalling = false;

    // Start is called before the first frame update
    void Start()
    {
        // MultiStageMotionControllerに動作と時間のリストをセットする
        Motion motion_fall = new Motion(Fall, 0.65f);
        Motion motion_wait = new Motion(Wait, 1f);
        Motion motion_rise = new Motion(Rise, 1.3f);
        ms.SetMultiStageMotion(new List<Motion>() {motion_fall, motion_wait, motion_rise });
        ms.SetLastAction(motion_last);
    }

    // Update is called once per frame
    void Update()
    {
        if (Mathf.Abs(player.transform.position.x - gameObject.transform.position.x) < 2.0f && !isFalling)
        {
            // プレイヤーが接近してきて、かつ落下中でないとき動作を開始する
            isFalling = true;
            ms.StartMotion();
        }
    }

    // MultiStageMotionControllerに渡す用の動作
    public void Fall()
    {
        Vector3 pos = gameObject.transform.position;
        pos.y -= 4f * Time.deltaTime;
        gameObject.transform.position = pos;
    }

    public void Wait()
    {
        // その場で待機する
    }

    public void Rise()
    {
        Vector3 pos = gameObject.transform.position;
        pos.y += 2f * Time.deltaTime;
        gameObject.transform.position = pos;
    }


    // 一連の動作の最後に落下状態のフラグをfalseにする
    public void motion_last()
    {
        isFalling = false;
    }
}
```

# 参考資料、使用した素材
- [2D横スクロールアクションゲーム | Raiun Games](https://raiungames.com/archives/category/unity/development-introduction/2d-action-game)
- [Unity Asset Store：Free Platform Game Assets](https://assetstore.unity.com/packages/2d/environments/free-platform-game-assets-85838)