まえがき
こんにちは、プログラマのshizawaです。
前回の記事からだいぶ経ってしまいました。
あっという間に寒くなりましたね。
台風一過、天気も清々しい今日この頃。みなさんいかがお過ごしでしょうか。
導入
今回もUniRxについて書いていきたいと思いますが、一旦UniRxについては今回で一区切りにしようかと 思っています。
今回はHotとColdを超噛み砕いて、理解した後に、ちょっとしたゲームを作ってみたいと思います。
ここではUnityの設定などの説明はしていません。
前回までの記事URL
初めてのUniRx + MVP(Model View Presenter)(Unity 2017.1.0) Part1 - gracetory’s blog
初めてのUniRx(Unity 2017.1.0) Part2 - gracetory’s blog
HotとColdの説明
こちらのサイトがわかりやすいので引用させていただきます。
【Reactive Extensions】 Hot変換はどういう時に必要なのか? - Qiita
RxのHotとColdについて - Qiita
●Cold Observable
自発的に何もしない受動的なObservable
Observerが登録されて(Subscribeされて)初めて仕事を始める
ストリームの前後をただつなぐだけ。ストリームを枝分かれさせる機能は無い。●Hot Observable
自分から値を発行する能動的なObservable
後続のObserverの存在に関係なしにメッセージを発行する
自分より上流のCold Observableを起動し、値の発行を要求する機能を持つ
下流のObserverを全て束ね、まとめて同じ値を発行する(ストリームを枝分かれさせる)
つまり、一つのストリームに対して、複数のsubscribeをする場合は
Hot変換をする必要があるということです。
変換しない場合はColdなObservableです。
そして、複数回SubscribeしたColdなObservableは分岐はできず、
subscribeした時にストリームを内部で複製しているようです。
この場合、余計なストリームを複製しているので、無駄にメモリを消費していることになります。
Hot変換の例(上記URLから)
// 入力された文字列を監視する var keyBufferStream = Observable.FromEvent<KeyEventHandler, KeyEventArgs>( h => (sender, e) => h(e), h => KeyDown += h, h => KeyDown -= h) .Select(x => x.Key.ToString()) .Buffer(4, 1) .Select(x => x.Aggregate((p, c) => p + c)) .Publish() //PublishでHot変換(Publishが代表してSubscribeしてくれる) .RefCount(); //RefCountはObserverが追加された時に自動Connectしてくれるオペレータ // 特定の文字が入力されたら、コンソールにログ表示 keyBufferStream .Where(x => x == "HOGE") .Subscribe(_ => Console.WriteLine("Input HOGE")); keyBufferStream .Where(x => x == "FUGA") .Subscribe(_ => Console.WriteLine("Input FUGA")); // 2回Subscribeしている
.Publish() //PublishでHot変換(Publishが代表してSubscribeしてくれる)
.RefCount(); //RefCountはObserverが追加された時に自動Connectしてくれるオペレータ
の部分がHot変換している部分になります。
つまり
ストリームに流れるイベントを分岐させるのが、Hot変換です。
一つのイベントに対して、違うオペレータ(上記の例だとwhere句)を差し込みたい場合など、
Hot変換をして、別の処理としてsubscribeできます。
以上がストリームのHot変換の説明です。
ちょっとしたゲームを作成
今までのことを振り返りながら、ゲームチックなものを作りました。
ルールはシンプル。
制限時間までにコインを集めればGame Clear表示、集められなければGameOver表示。
ソースコード
GameManager.cs
using UnityEngine; public class GameManager : MonoBehaviour { public static GameManager Instance; // シングルトンインスタンス public UIPresenter uiPresenter; // UI全般 public GameInfoModel gameInfoModel; // ゲーム情報 public GameObject coinParent; // コインオブジェクト親 float TIME_LIMIT = 15.0f; // 制限時間 void Awake () { // シングルトン if (Instance == null) { Instance = this; DontDestroyOnLoad (gameObject); } else { Destroy (gameObject); } // ゲーム情報クラス this.gameInfoModel = new GameInfoModel(TIME_LIMIT); } // Use this for initialization void Start () { // イベントを設定 this.uiPresenter.Init(); // 初期値をセット、表示も更新 this.gameInfoModel.SetCoinNum(coinParent.transform.childCount); } // コイン取得イベント public void OnGetCoin() { this.gameInfoModel.SubCoinNum(); } // ゲームステートを取得 public GameInfoModel.GameState GetGameState() { return this.gameInfoModel.gameState.Value; } // ゲームステートをセット public void SetGameState(GameInfoModel.GameState state) { this.gameInfoModel.SetGameState(state); } }
GameManagerが基本的にゲームの中核を担っています。
UIをコントロールする public UIPresenter uiPresenter;
ゲーム内の情報をModelとして保存しておくのが public GameInfoModel gameInfoModel;
PlayerController.cs
using UnityEngine; // プレイヤー制御用スクリプト public class PlayerController : MonoBehaviour { [SerializeField] float MOVE_VELOCITY = 3.0f; // 移動速度m/s [SerializeField] float JUMP_FORCE = 8.0f; // ジャンプ力 Rigidbody rigidbody; [SerializeField] LayerMask groundLayer; // 接地レイヤー [SerializeField] bool isGround = false; // 接地判定 // Use this for initialization void Start() { // RigidBody取得 this.rigidbody = GetComponent<Rigidbody>(); } void Update() { // 接地判定 this.isGround = Physics.Linecast(transform.position, transform.position - transform.up * 2.0f, groundLayer); // ジャンプ処理 if (this.isGround && Input.GetKeyDown(KeyCode.W)) { // 上方向に力を加える this.rigidbody.AddForce(Vector3.up * JUMP_FORCE, ForceMode.Impulse); } } void FixedUpdate () { // 横移動処理 float xInput = Input.GetAxisRaw("Horizontal"); if (xInput != 0.0f) { this.rigidbody.velocity = new Vector3(xInput * MOVE_VELOCITY, rigidbody.velocity.y, 0); } else { this.rigidbody.velocity = new Vector3(0, rigidbody.velocity.y, 0); } } private void OnTriggerEnter(Collider col) { // コイン判定 if (col.gameObject.CompareTag("Coin")) { col.gameObject.GetComponent<CoinController>().OnGetCoin(); } } }
主にプレイヤーを移動させるスクリプトです。
コインとの当たり判定など
プレイヤーオブジェクトにアタッチ
GameInfoModel.cs
using System; using UnityEngine; using UniRx; public class GameInfoModel { // ゲームステート public enum GameState { GAME_PLAY, GAME_CLEAR, GAME_OVER } public ReactiveProperty<GameState> gameState { get; private set; } public IObservable<GameState> gameStateObservableHot { get; private set; } // 未使用 // コイン数 public ReactiveProperty<int> coinNum { get; private set; } public IObservable<int> coinObservableHot { get; private set; } // 制限時間 public ReactiveProperty<float> timeLimit { get; private set; } public IObservable<float> timerObservableHot { get; private set; } // コンストラクタ public GameInfoModel(float timeLimit) { // ステートのリアクティブプロパティ this.gameState = new ReactiveProperty<GameState>(); this.gameState.Value = GameState.GAME_PLAY; // コイン this.coinNum = new ReactiveProperty<int>(); coinNum.Value = 0; // 時間制限 this.timeLimit = new ReactiveProperty<float>(); this.timeLimit.Value = timeLimit; // 制限時間のストリーム作成(HOT) this.timerObservableHot = CreateTimerObservable(timeLimit); // コイン(HOT) this.coinObservableHot = this.coinNum.Publish().RefCount(); } // 時間をセット public void SetTimeLimit(float time) { this.timeLimit.Value = time; Debug.Log(this.timeLimit.Value); } // 時間を減算 public void SubTime(float subTime) { this.timeLimit.Value -= subTime; } // タイマーストリーム作成 public IObservable<float> CreateTimerObservable(float timeSecond) { return Observable .Timer(TimeSpan.FromSeconds(0), TimeSpan.FromSeconds(1)) // 0秒後に1秒間隔で実行する .TakeWhile(x => timeSecond - x >= 0) // 0以下になったらOnComplete .Select(x => timeSecond - x) // 残り時間に変換 .Publish() // ストリームをHot変換 .RefCount(); // Observerが追加された時に自動でConnectし、いなくなったらDispose } // コイン数をセット public void SetCoinNum(int coinNum) { this.coinNum.Value = coinNum; } // コイン数をデクリメント public void SubCoinNum() { this.coinNum.Value--; } // ゲームステートをセット public void SetGameState(GameState state) { this.gameState.Value = state; } }
public IObservable
.
.
.
ここででHot変換をしています。
おさらいですが、Hot変換は複数のSubscribeをするために必要な処理でした。
UIPresenter.cs
using UnityEngine; using UniRx; public class UIPresenter : MonoBehaviour { [SerializeField] private UIView uiView; public void Init () { // タイマーストリーム(Hot) var timerObserbavleHot = GameManager.Instance.gameInfoModel.timerObservableHot; // 表示変更 timerObserbavleHot .Subscribe(setTime => this.uiView.OnDisplaySetTime(setTime)); // 一秒ごとにイベントが発行されるので、残り時間をセット timerObserbavleHot .DistinctUntilChanged() // 重複した値は通さない .Subscribe(x => GameManager.Instance.gameInfoModel.SetTimeLimit(x)); /* タイマーイベントが1秒ごとに発火し、そのタイミングでテキスト更新と残り時間をModelに格納している。 同じtimerObserbavleから通知を受け取るので、Hot変換されているものを使う必要がある。 ちなみに.DistinctUntilChanged()を入れなければ、 timerObserbavleHot .Subscribe(setTime => { this.uiView.OnDisplaySetTime(setTime); GameManager.Instance.gameInfoModel.SetTimeLimit(x); }); のようにまとめて、ColdなObservableでも大丈夫です。 */ // 時間が0になったらゲームオーバー表示イベント登録(Cold) var coinNum = GameManager.Instance.gameInfoModel.coinNum; var timeLimit = GameManager.Instance.gameInfoModel.timeLimit; timeLimit .Where(x => x <= 0 && coinNum.Value > 0) .Subscribe(_ => { GameManager.Instance.SetGameState(GameInfoModel.GameState.GAME_OVER); this.uiView.SetGameStateText("Game Over"); } ); /* timerObserbavleHotでGameManager.Instance.gameInfoModel.timeLimitの値が変更 ↓ 検知したtimeLimit(Reactive Propaty)に、Subscribeしたゲームオーバー処理を実行するという流れ */ // コインストリーム(Hot) var coinObservable = GameManager.Instance.gameInfoModel.coinObservableHot; // 表示変更 coinObservable .Subscribe(setCoinNum => this.uiView.OnDisplayAddCoin(setCoinNum)); // ゲームオーバー中ではない かつ コインを全て取得したらゲームクリア表示 coinObservable .Where(x => x == 0 && GameManager.Instance.GetGameState() != GameInfoModel.GameState.GAME_OVER) .Subscribe(setCoinNum => { GameManager.Instance.SetGameState(GameInfoModel.GameState.GAME_CLEAR); this.uiView.SetGameStateText("Game Clear"); } ); } }
UIのViewとGameInfoModelを繋げる役目のPresenterスクリプトです。
ここで、諸々Observableに登録をしています。
UIView.cs
using UnityEngine; using UnityEngine.UI; public class UIView : MonoBehaviour { [SerializeField] private Text timeCountText; // 残り時間 [SerializeField] private Text coinNumText; // 取得コイン数 [SerializeField] private Text gameStateText; // ゲームステート // コインを取得した時に呼ばれる public void OnDisplayAddCoin(int coinNum) { this.coinNumText.text = "残りコイン数 : " + coinNum.ToString(); } // 時間をセットする public void OnDisplaySetTime(float timeLimit) { // テキストを更新 this.timeCountText.text = "Time : " + timeLimit.ToString(); } // ゲームステート表示用テキスト public void SetGameStateText(string text) { this.gameStateText.text = text; } }
MVCのViewの部分
CoinController.cs
using UnityEngine; public class CoinController : MonoBehaviour { // コイン取得時のイベント public void OnGetCoin() { GameManager.Instance.OnGetCoin(); Destroy(this.gameObject); } }
コインオブジェクトにアタッチ
あとがき
Model(値)を変更すると自動でViewが更新される。Model側に通知先を保持しない。
UniRxが活きるような設計にうまくできていないような気がしますが、こんな感じになりました。
特にObservableに処理をsubscribeするのをどこでどうやって管理していけば良いのか、考える余地がありますね・・・。
告知
弊社からゲームアプリがリリースされました
簡単操作で楽しめるジャンプアクションゲームなっています。
かわいいニワトリが沢山登場しますよ。
apps.apple.com