時間経過を伴う、一連の動きの定義方法。Reactive X凄ない？

最近(遅い)、リアクティブプログラミングという概念を知り、これが前々から気になっていた課題に関連していると感じたので、ちょっと考えをまとめておきます。

## 課題 / 時間経過を伴う処理のまとまりを、扱いやすい形で定義したい

たとえば、アクションゲームにおいて、「プレイヤーがボタンをおした時に開始される、キャラクターの一連の動き」であるとか、「AIが選択した一連の行動」を定義したいケースがあると思います。

架空の日本語風言語で実装するとこういう感じです。


1. 開始直後に、歩行アニメーションを開始する。
2. 同時に、前方に向かって速度0.5で前方に移動を開始する。
3. 0.5秒後に、移動をやめ、左右どちらかランダムな方向へ、回転開始する。
4. 1秒後に、回転を止め、攻撃判定を発生させる。
5. 同時に、攻撃アニメーションを開始する。


この「一連の行動」を簡単に定義したい。加えて、

- 任意に実行可能
- 他の行動と簡単に置換可能
- コレクションで扱ったりしたい
- 異なるキャラクターにも適用可能
- 各タイミングで行う処理は、一度だけ行えばいいもの(アニメーションの開始とか)、と、一定期間、毎フレーム継続して行うもの(キャラクターの移動とか)の両方に対応が必要
- その行動(だけ)に関わる、簡単な状態(今、右に行ってる or 左に行ってる、等)は自前で保持したい

などという条件が加わると、この「一連の動き」を表現できるクラスを作り、具体的なキャラクターのアクションはそのクラスのインスタンスとして実現する必要があるな、ということになると思います。

## ルール

4パターンの実装方法を考えたんですが、ルールとしては、それぞれキャラクターに定義された以下の行動を先述のタイムラインにそって実施できるものとします。多分そうなってると思います。上の3つのメソッドは毎フレーム実行する必要があり、下2つは一度だけ実行すればいいものです。

また、簡単な状態の保持がどのように可能かを確認するために、右に行くか、左に行くかはランダムに決定し、その後の行動でも一貫して同じ方向へ移動することにします。

```character.cs
using UnityEngine;

public class CharacterBehaviour : MonoBehaviour {
    // 毎フレーム呼ぶ
    public void Walking(float speed) { Debug.Log("."); }
    public void TurningRight()       { Debug.Log("右に曲がる"); }
    public void TurningLeft()        { Debug.Log("左に曲がる"); }

    // 一回呼べばいい
    public void StartAnim(Anime a)   { Debug.Log("anime:" + a.ToString()); }
    public void Attack()             { Debug.Log("攻撃"); }
}
```

## Updateで頑張る

一旦、追加の各種条件を外して一番単純に実装した場合、「一連の動き」をクラスのメソッドとして実装、その「一連の動き」が開始した時点でキャラクターの「現在の状態」に設定し、毎フレームのUpdate内で処理を委譲する感じになるかと思います。


```simple.cs
using UnityEngine;

namespace Simple {
    public class Character : CharacterBehaviour {
        ActBase CurrentAct;

        void Start() {
            ChangeAct(new AttackAfterRandomTurn(this));
        }

        void Update() {
            // Updateの処理は移譲する
            if (CurrentAct != null) CurrentAct.DoUpdate();
        }

        public void ChangeAct(ActBase act) {
            act.DoStart();
            CurrentAct = act;
        }
        public void StopAct() {
            CurrentAct = null;
        }
    }

    public abstract class ActBase {
        private  float      m_StartAt;
        protected Character m_Character;

        public ActBase(Character character) {
            m_Character = character;
        }

        public virtual void DoStart() {
            m_StartAt = Time.time;
        }

        public abstract void DoUpdate();

        protected float ElapsedTime {
            get { return Time.time - m_StartAt; }
        }
    }

    public class AttackAfterRandomTurn : ActBase {
        public AttackAfterRandomTurn(Character character) : base(character) { }

        private int  m_State = 0;
        private bool m_TurnRight;

        // 実際にUpdateで使用される一連の動き。Updateから毎フレーム呼ばれる
        public override void DoUpdate() {
            var time = ElapsedTime;
            switch (m_State) {
                case 0:
                    m_Character.StartAnim(Anime.Walk);
                    m_Character.Walking(0.5f);
                    m_State++;
                    break;
                case 1:
                    m_Character.Walking(0.5f);
                    if (time >= 0.5) m_State++;
                    break;
                case 2:
                    m_TurnRight = Random.Range(0, 2) == 0;
                    m_State++;
                    break;
                case 3:
                    if (m_TurnRight) {
                        m_Character.TurningRight();
                    } else {
                        m_Character.TurningLeft();
                    }
                    if (time >= 1f) m_State++;
                    break;
                case 4:
                    m_Character.Attack();
                    m_Character.StartAnim(Anime.Attack);
                    m_Character.StopAct();
                    break;
            }
        }
    }
}

```

新たに「一連の動き」を追加する場合は、AttackAfterRandomTurnに相当するクラスを新規に作成することになります。

状態の保持は、適当にメンバー変数を増やせるので簡単。

この実装だと、各行動の違いがそのままメソッド定義なので、少し内容を変更するのにも別クラスの定義が必要になるのが問題かもしれない。

表現したい内容に比べて記述が冗長だし、stateにenumを使ったりしても、なお、読みにくい。これで各種の動きを量産するのはきつい。

## コルーチンを使う

Unityにはコルーチンが存在するので、これで実装してみます。

```coroutine.cs
using UnityEngine;
using System.Collections;

namespace Routine {
    public class Character : CharacterBehaviour {
        ActBase CurrentAct;

        void Start() {
            ChangeAct(new AttackAfterRandomTurn(this));
        }

        public void ChangeAct(ActBase act) {
            CurrentAct = act;
            // コルーチンを開始
            StartCoroutine(CurrentAct.Routine());
        }
    }

    public abstract class ActBase {
        protected Character m_Character;
        public ActBase(Character character) {
            m_Character = character;
        }
        public abstract IEnumerator Routine();
    }

    public class AttackAfterRandomTurn : ActBase {
        public AttackAfterRandomTurn(Character character) : base(character) { }

        // 処理実体
        public override IEnumerator Routine() {
            var start_at = Time.time;

            m_Character.StartAnim(Anime.Walk);
            // 継続して、繰り返し実行する必要がある者はwhileで時間が来るまで待機
            while (Time.time - start_at <= 0.5f) {
                m_Character.Walking(0.5f);
                yield return null;
            }

            var turn_right = Random.Range(0, 2) == 0;

            while (Time.time - start_at <= 1.0f) {
                if (turn_right) m_Character.TurningRight(); else m_Character.TurningLeft();
                yield return null;
            }

            m_Character.Attack();
            m_Character.StartAnim(Anime.Attack);

            yield break;
        }
    }
}
```

全体の作りは上とほぼ同じです。状態の保持はメンバー変数でもいいですが、メソッド内のローカル変数でも行けます。

なんでも実装可能だし、それほど見難くもないので、これはコレでありなのでは？ という気がしましたが、新しい動きを作るのにはやはりあたらしいクラス定義(か、無名メソッドでも可能か)が必要だし、やだなと。

## デリゲートのコレクションを使う

要は、現在の経過時間に対応したメソッドが呼び出せればいいだけなので、実行したいアクション(デリゲート)を、実行する時間とともにコレクションに突っ込んでおき、Update内では実行する必要があるものを探して実行する、というやりかた。実装上は、いつ実行するか、すでにしたか、などもデリゲートとともに保持しておく必要があるので、1個クラスを挟んでいます。

「一連の動き」を新たに追加する場合、必要なのは一番下のスタティックメソッド(というか、そこで生成されるActインスタンス)です。状態の保持には、クロージャーにキャプチャされるローカル変数を使っています。

```delegate.cs
using System;
using System.Linq;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace Delegate {
    public class Character : CharacterBehaviour {
        Act CurrentAct;

        void Start() {
            ChangeAct(Act.AttackAfterRandomTurn(this));
        }

        void Update() {
            // 処理の移譲
            if (CurrentAct != null) CurrentAct.DoUpdate();
        }

        public void ChangeAct(Act act) {
            act.DoStart();
            CurrentAct = act;
        }
    }

    // 1個1個の動作
    public abstract class CommandBase {
        public    bool   Done = false;
        protected Action m_Action;
        public abstract void Invoke(float time);
        public CommandBase(Action action) {
            m_Action = action;
        }
    }

    // 一回しか実行しない
    public class OneShotCommand : CommandBase {
        private float m_InvokeTime;
        public OneShotCommand(float invoke_time, Action action) : base(action) {
            m_InvokeTime = invoke_time;
        }

        public override void Invoke(float time) {
            if (m_InvokeTime <= time) {
                Done = true;
                m_Action.Invoke();
            }
        }
    }

    // 一定期間内、毎フレーム実行する
    public class RangeCommand : CommandBase {
        private float m_InvokeStartTime;
        private float m_InvokeStopTime;

        public RangeCommand(float start_time, float stop_time, Action action) : base(action) {
            m_InvokeStartTime = start_time;
            m_InvokeStopTime  = stop_time;
        }

        public override void Invoke(float time) {
            if (m_InvokeStartTime <= time && time <= m_InvokeStopTime) {
                m_Action.Invoke();
            }
            if (m_InvokeStopTime < time) {
                Done = true;
            }
        }
    }

    public class Act {
        private List<CommandBase> m_CommandList = new List<CommandBase>();
        private float m_StartAt;

        public void DoStart() {
            m_StartAt = Time.time;
        }

        public void DoUpdate() {
            m_CommandList
                .Where  (c => !c.Done).ToList()
                .ForEach(c => c.Invoke(ElapsedTime));
        }

        protected float ElapsedTime {
            get { return Time.time - m_StartAt; }
        }

        public Act OneShot(float invoke_time, Action action) {
            m_CommandList.Add(new OneShotCommand(invoke_time, action));
            return this;
        }

        public Act Range(float start_time, float stop_time, Action action) {
            m_CommandList.Add(new RangeCommand(start_time, stop_time, action));
            return this;
        }

        public static Act AttackAfterRandomTurn(Character c) {
            var turn_right = false;

            return new Act()
                .OneShot(0.0f,       () => c.StartAnim(Anime.Walk))
                .Range  (0.0f, 0.5f, () => c.Walking(0.5f))
                .OneShot(0.5f,       () => turn_right = UnityEngine.Random.Range(0, 2) == 0)
                .Range  (0.5f, 1.0f, () => { if (turn_right) c.TurningRight(); else c.TurningLeft(); })
                .OneShot(1.0f,       () => {
                    c.Attack();
                    c.StartAnim(Anime.Attack);
                });
        }
    }
}
```

各々の「一連の動き」をメソッド定義ではなく、同じクラスのインスタンスで表現できるので、アプリのアップデート以外で調整しやすい(なんらかの方法でリソースファイルから生成するとか)のではないか、というのと、書き方が楽でいい、ということで結構満足していました。

## RX(UniRX)を使ってみる

やっと、本題のReactive X(Unityでの実装はUniRX)を使った実装です。

UpdateAsObservableで毎フレームのUpdateをオブザーブし？ フィルタリングしたものをデリゲートが購読(サブスクライブ)するということでなんだか意味の分からない説明だな、と思いますが、メソッド名に沿って説明するとそうなる。

```reactivex.cs
using UnityEngine;
using System;
using UniRx;
using UniRx.Triggers;

namespace RX {
    public class Character : CharacterBehaviour {
        void Start() {
            ActMaker.AttackAfterRandomTurn(this);
        }

        public IObservable<float> TimeConnector() {
            var p = this.UpdateAsObservable().Publish();
            var start_at = Time.time;
            p.Connect();
            return p.Select(_ => Time.time - start_at);
        }
    }

    public static class RXHelper {
        public static IObservable<float> OneShot(
            this IObservable<float> observable, float time, Action action) {
            observable.Where(t => t >= time).First().Subscribe(_ => action.Invoke());
            return observable;
        }

        public static IObservable<float> Range(
            this IObservable<float> observable,
            float start_time, float stop_time, Action action) {
            observable.Where(t => start_time <= t && t <= stop_time).Subscribe(_ => action.Invoke());
            return observable;
        }
    }

    public static class ActMaker {
        public static void AttackAfterRandomTurn(Character c) {
            var turn_right = false;

            c.TimeConnector()
                .OneShot(0.0f,       () => c.StartAnim(Anime.Walk))
                .Range  (0.0f, 0.5f, () => c.Walking(0.5f))
                .OneShot(0.5f,       () => turn_right = UnityEngine.Random.Range(0, 2) == 0)
                .Range  (0.5f, 1.0f, () => { if (turn_right) c.TurningRight(); else c.TurningLeft(); })
                .OneShot(1.0f,       () => {
                    c.Attack();
                    c.StartAnim(Anime.Attack);
                });
        }
    }
}
```

全然Reactive Xの全貌がつかめていない状態で作ってるので、変だったりするかもしれないんですが、とりあえず上のdelegate版と同じ構文で使えるようにしたのがこれです。状態の保持も同じく、キャプチャされたローカル変数を使います。

見た目を揃えるためにヘルパーの拡張メソッドを幾つか定義してますが、実際のところ、自分がやりたかったことはほぼRXにもとから実装されている機能で実現可能、ということで、驚きました。しかも、自分で実装したdelegate版では指定したタイミングで一度 or 期間中ずっとなにかを実行する、以外のことをしたければ、自分で機能を拡張していくしか無いわけですが、RXは他にも色々出来るわけで、素晴らしい。もうちょっとこの辺は勉強してみたい。