やったこと
Unityのチュートリアル「玉転がし(Roll-a-ball)」をUniRxで書き改めてみた。
チュートリアル終了後のコードをUniRxで書きかえた、ということです。
結論
- UniRx、とても良さそう! 積極的に使っていきたい。
- 「Subjectを積極的に使用する場面とは?」を理解しきれていない。 (今回使ってみたものの...)
実装
ディレクトリ構造はこんなかんじ。
このチュートリアルのスクリプトは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
は実行されない)