玉転がしチュートリアルをUniRxで書きかえてみた

# やったこと
![demo_picture.png](https://qiita-image-store.s3.amazonaws.com/0/159680/39f4345b-cdd8-45c1-28e6-f8d057e19488.png)  
Unityのチュートリアル「[玉転がし(Roll-a-ball)](https://unity3d.com/jp/learn/tutorials/projects/hajiuni-jp)」を[UniRx](https://github.com/neuecc/UniRx)で書き改めてみた。
チュートリアル*終了後*のコードをUniRxで書きかえた、ということです。

# 結論
- UniRx、とても良さそう！ 積極的に使っていきたい。
- 「Subjectを積極的に使用する場面とは？」を理解しきれていない。 (今回使ってみたものの...)

# 実装
ディレクトリ構造はこんなかんじ。  
![directory_picture.png](https://qiita-image-store.s3.amazonaws.com/0/159680/f8ef5806-67c6-dc16-e90c-7025e8ca5f55.png)  
このチュートリアルのスクリプトは5つのみで、今回書き改めたのもその5つだけ。
設計改造やゲーム性変更をすることはせず、<font color="ORANGE">**UniRxで書き改めることのみ**</font>を行った。

なお、UniRxの理解を深めるために、小さい機能を1つだけを追加した。⬇︎

- 「ゲットしたItemの名前を保持しておく」
  - 0個になったら、ゲットしたそれら全てをログに出力
  - ゲットするたびに、ログにその名前を出力

## GameController.cs
``` GameController.cs
using UniRx;
using UniRx.Triggers;
using UnityEngine;
using UnityEngine.SceneManagement;

public class GameController : MonoBehaviour
{
    public UnityEngine.UI.Text scoreLabel;
    public GameObject winnerLabelObject;

    // ゲットしたItemの名前を保持するCollection
    public ReactiveCollection<string> getItemNames = new ReactiveCollection<string>();

    [SerializeField]
    private PlayerController playerController;

    private void Start()
    {
        PlayerDieObserve();         // Playerの死亡ストリームを購読
        ExistsItemCountObserve();   // 存在しているアイテムの数を購読
        ObtainedItemCountObserve(); // 獲得したアイテムの数を購読
    }

    private void PlayerDieObserve()
    {
        // 現在のシーンを再読込
        playerController.OnPlayerDie
            .Select(_ => SceneManager.GetActiveScene().buildIndex)
            .Subscribe(currentIndex => SceneManager.LoadScene(currentIndex));
    }

    private void ExistsItemCountObserve()
    {
        // アイテムの個数をGUIのテキストに反映
        this.UpdateAsObservable()
            .Select(_ => GameObject.FindGameObjectsWithTag("Item").Length)
            .Subscribe(itemCount => scoreLabel.text = itemCount.ToString());

        // アイテムの個数が0個になった時
        var ItemZeroObserve = GetItemZeroObserve();
        // winnerLabelを表示
        ItemZeroObserve
            .Subscribe(_ => winnerLabelObject.SetActive(true));
        // これまでにゲットしたItemの名前を、ログに出力
        ItemZeroObserve
            .Take(1).Subscribe(_ => LogCollection(getItemNames)); // これの実行は1回だけでよいのでTask(1)。
    }

    private IObservable<int> GetItemZeroObserve()
    {
        var ItemZeroObserve =
            this.UpdateAsObservable()
                .Select(_ => GameObject.FindGameObjectsWithTag("Item").Length)
                .Where(itemCount => itemCount == 0);
        return ItemZeroObserve;
    }

    private void ObtainedItemCountObserve()
    {
        getItemNames.ObserveAdd().Subscribe(x => Debug.Log(x + "をゲットしました"));
    }

    private void LogCollection(ReactiveCollection<string> colleciton)
    {
        for (int i = 0; i < getItemNames.Count; i++)
        {
            Debug.Log("getしたアイテム" + i.ToString() + "つ目: " + getItemNames[i]);
        }
    }
}
```

### ポイント
- Playerの死亡時にScene再読込をしたいので、Playerの死亡情報を購読している
- ReactiveCollectionであるgetItemNamesへのAddを、`ObserveAdd()`で購読している
- Task(n)で実行回数を指定している

なお、 PlayerControllerはSerializeFieldにして、Inspectorからのアタッチを前提とした。これはチュートリアルに無い点。

## PlayerController.cs
```PlayerController.cs
using UniRx;
using UniRx.Triggers;
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    public float speed = 10;
    public BoolReactiveProperty isDead = new BoolReactiveProperty(false); // 死亡フラグ

    private Subject<Unit> playerDieSubject = new Subject<Unit>(); // 死亡情報を配信

    // 死亡情報を購読側のみ公開
    public IObservable<Unit> OnPlayerDie
    {
        get { return playerDieSubject; }
    }

    private void Start()
    {
        // キー入力を元にPlayerを動かす
        this.FixedUpdateAsObservable()
            .Select(_ => new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical")))
            .Subscribe(v => Move(v));

        // 死亡フラグの変更を購読
        isDead
            .Where(x => x == true)
            .Subscribe(_ => Die());
    }

    private void Move(Vector3 v3)
    {
        Rigidbody rigidbody = GetComponent<Rigidbody>();
        rigidbody.AddForce(v3.x * speed, 0, v3.z * speed);
    }

    private void Die()
    {
        // 「OnNext」を使って、自身の死亡を購読者に通知する
        playerDieSubject.OnNext(Unit.Default);  // Unit.Defaultは、データ不要の通知で使うもの
    }
}
```

### ポイント
- `Subject`を使い、死亡情報を配信している
- ReactivePropertyの`isDead`がtrueに変わったタイミングで、`OnNext`を使って死亡通知を発行している
- 死亡情報は**死亡の通知タイミングのみ**が重要なので、`Unit.Default`を使っている
- `Select`内でInput情報を`Vector3`として扱っている

## Items.cs
```Items.cs
using UniRx;
using UniRx.Triggers;
using UnityEngine;

public class Item : MonoBehaviour
{
    private void Start()
    {
        // Playerと衝突した時
        var OnTriggerEnterPlayer = this.OnTriggerEnterAsObservable()
            .Select(collision => collision.tag)
            .Where(tag => tag == "Player");
        // GameControllerの獲得リストに自身を追加
        OnTriggerEnterPlayer
            .Select(_ => GameObject.FindObjectOfType<GameController>())
            .Subscribe(gameController => gameController.getItemNames.Add(gameObject.name));
        // 自身をDestroy
        OnTriggerEnterPlayer
            .Subscribe(_ => Destroy(gameObject));
    }
}
```

### ポイント
- `this.OnTriggerEnterAsObservable()`は、`void OnTriggerEnter()`を監視する
- Where句により、Playerとの衝突のみを購読している

## DangerWall.cs
```DangerWall.cs
using UnityEngine;
using UniRx;
using UniRx.Triggers;

public class DangerWall : MonoBehaviour
{
    void Start()
    {
        // Playerと衝突したら、Playerは死亡する
        this.OnCollisionEnterAsObservable()
            .Select(hit => hit.gameObject.tag)
            .Where(tag => tag == "Player")
            .Select(_ => GameObject.FindObjectOfType<PlayerController>())
            .Subscribe(player => player.isDead.Value = true);
    }
}
```

### ポイント
- `OnCollisionEnterAsObservable()`は、`void OnCollisionEnter`を監視する
- ReactivePropertyの値参照は`hogehoge.Value`

## FollowPlayer.cs
```FollowPlayer.cs
using UniRx;
using UniRx.Triggers;
using UnityEngine;

public class FollowPlayer : MonoBehaviour
{
    [SerializeField]
    private Transform target;

    private Vector3 offset;

    private void Start()
    {
        // 自分とターゲットとの相対距離を求める
        offset = GetComponent<Transform>().position - target.position;
        // Playerのpositionに追従する
        FollowPlayerObserve();
    }

    private void FollowPlayerObserve()
    {
        this.UpdateAsObservable()
            .Select(_ => target.position + offset)
            .Subscribe(destination_pos => GetComponent<Transform>().position = destination_pos);
    }
}
```

# 補足
## ReactivePropertyは、異なる値の代入で変更通知が発火する
UniRxのソースコード内の[ReactiveProperty.cs](https://github.com/neuecc/UniRx/blob/0f4659c201062e1da37e86f3cb23aeddcedd1e6c/Assets/Plugins/UniRx/Scripts/UnityEngineBridge/ReactiveProperty.cs)の60行目に以下の記述がある。

```ReactiveProperty.cs
public T Value
    {
        get
        {
            return value;
        }
        set
        {
            if (!canPublishValueOnSubscribe)
            {
                canPublishValueOnSubscribe = true;
                SetValue(value);

                if (isDisposed) return; // don't notify but set value
                var p = publisher;
                if (p != null)
                {
                    p.OnNext(this.value);
                }
                return;
            }

            if (!EqualityComparer.Equals(this.value, value))  // <-- ここ
            {
                SetValue(value);

                if (isDisposed) return;
                var p = publisher;
                if (p != null)
                {
                    p.OnNext(this.value);
                }
            }
        }
    }
```

おんなじ値を入れても、変更通知は発行されない(=`OnNext`は実行されない)

# 参考
[UniRx入門シリーズ](https://qiita.com/toRisouP/items/00b8a5bb8e7b68e0686c)