LoginSignup
14
15

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-05-18

やったこと

demo_picture.png

Unityのチュートリアル「玉転がし(Roll-a-ball)」をUniRxで書き改めてみた。
チュートリアル終了後のコードをUniRxで書きかえた、ということです。

結論

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

実装

ディレクトリ構造はこんなかんじ。

directory_picture.png

このチュートリアルのスクリプトは5つのみで、今回書き改めたのもその5つだけ。
設計改造やゲーム性変更をすることはせず、UniRxで書き改めることのみを行った。

なお、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の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入門シリーズ

14
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
15