LoginSignup
3
5

More than 5 years have passed since last update.

UnityのScript構成を考えてみる その4

Last updated at Posted at 2016-12-29

UnityのScript構成を考えてみる その3の続き。

≫ まとめ記事はこちら。

5. UniRxを導入し、ViewをPresenterに。

さて前回まででかなりリファクタリングできたと思うが、まだ微妙な箇所が残っている。例えばViewクラスである。コードとして微妙な点がちらほら残っている。そもそも、Viewクラスという名前だが最早どう見てもViewではない

というわけで再び改修した。主な変更内容は下記の3点。

  • UniRxを導入
  • クラス名をPresenterに変更
  • 入力部品をUnityのインスペクタで指定。
Presenter.cs
using System;
using UnityEngine;
using UnityEngine.UI;
using UniRx;
using UniRx.Triggers;

public class Presenter : MonoBehaviour
{
    [SerializeField]
    private InputField InputForm;

    [SerializeField]
    private Text OutputText;

    [SerializeField]
    private Button PowButton;

    [SerializeField]
    private Button IncrementButton;

    [SerializeField]
    private Button DecrementButton;

    private NumberMediator NumberMediator;

    private void Start()
    {
        // Modelの値の保持 
        NumberMediator = new NumberMediator();

        // Modelの監視
        NumberMediator.ReactNum
                      .Skip(1)
                      .Subscribe(number => OutputText.text = "処理結果 " + number.ToString());

        // 値のチェック(とボタンの有効/無効登録3点。)
        var stream = InputForm.OnValueChangedAsObservable().Select(x => Validate(x)).Publish();
        stream.SubscribeToInteractable(PowButton);
        stream.SubscribeToInteractable(IncrementButton);
        stream.SubscribeToInteractable(DecrementButton);
        stream.Connect();


        // ユーザーアクションとロジックの紐づけ
        PowButton.OnClickAsObservable()
                 .Where(_ => Validate(InputForm.text))
                 .Subscribe(_ => NumberMediator.Pow(Int32.Parse(InputForm.text)));

        IncrementButton.OnClickAsObservable()
                 .Where(_ => Validate(InputForm.text))
                 .Subscribe(_ => NumberMediator.Increment(Int32.Parse(InputForm.text)));

        DecrementButton.OnClickAsObservable()
                 .Where(_ => Validate(InputForm.text))
                 .Subscribe(_ => NumberMediator.Decrement(Int32.Parse(InputForm.text)));
    }

    private bool Validate(string input)
    {
        int tmp;
        if (!Int32.TryParse(input, out tmp)) return false;

        return true;
    }
}

UniRxを導入して書き方を変更したため、ものすごく変わって見えるが、実行しようとしていることはほぼ変わらない。
前回、個別のメソッドで表現されていたバリデーションとモデルへの通信部分が

View.csの一部
    public void OnClickPow()
    {
        // check
        if (!Check(InputForm.text))
            return;

        // logic
        logic.Pow(Int32.Parse(InputForm.text));
    }

Start()でUniRxを用いてこう表現されるようになり、

Presenter.cs
    private void Start()
    {
        // check
        var stream = InputForm.OnValueChangedAsObservable().Select(x => Validate(x)).Publish();
        stream.SubscribeToInteractable(PowButton);
        stream.Connect();

        // logic
        PowButton.OnClickAsObservable()
                 .Where(_ => Validate(InputForm.text))
                 .Subscribe(_ => NumberMediator.Pow(Int32.Parse(InputForm.text)));
    }
}

また、Modelの監視を行う箇所が

View.csの一部
        // Modelの監視
        logic.Calculated += new Logic.NumberChangedEventHandler((int sender) => OutputText.text = "処理結果 " + sender.ToString());
    }

UniRx式の書き方に変わっただけである。

Presenter.csの一部
        // Modelの監視
        NumberMediator.ReactNum
                      .Skip(1)
                      .Subscribe(number => OutputText.text = "処理結果 " + number.ToString());

実際動作もほぼ同じままである。

なお、前回までの書き方ならStart()にこう書いていたところだが

    InputForm = GameObject.Find("InputForm").GetComponent<InputField>();
    OutputText = GameObject.Find("OutputText").GetComponent<Text>();
    PowButton = GameObject.Find("PowButton").GetComponent<Button>();
    IncrementButton = GameObject.Find("IncrementButton").GetComponent<Button>();
    DecrementButton = GameObject.Find("DecrementButton").GetComponent<Button>();

その代わりにPresenter.csをCanvasにアタッチし、各変数に[SerializeField]を加えた上で、それぞれフォーム部品を割り当てた。

Presenter.png

Start()で書いても動作自体は同じなのだが、部品名をFind()で指定するよりは、こちらのほうが管理が楽ではないかと思う。

窓口クラスもUniRx式に書き換える

View側のイベント登録方式を変更したため、メッセージを発行する側である窓口クラスも若干変更が必要となった。そこで修正を加えたのが以下である。窓口以外はやりとりしていないため、修正はここにしか発生しない。

NumberMediator.cs
using UniRx;

class NumberMediator
{
    public ReactiveProperty<int> ReactNum { get; private set; }

    public NumberMediator ()
    {
        ReactNum = new ReactiveProperty<int>();
    }

    public void Pow(int number)
    {
        var Number = new Number(number);
        Number.Pow();

        var dao = new NumberFile("pow");
        dao.Save(Number);

        ReactNum.Value = Number.Value;
    }

    public void Increment(int number)
    {
        var Number = new Number(number);
        Number.Increment();

        var dao = new NumberFile("increment");
        dao.Save(Number);

        ReactNum.Value = Number.Value;
    }

    public void Decrement(int number)
    {
        var Number = new Number(number);
        Number.Decrement();

        var dao = new NumberFile("decrement");
        dao.Save(Number);

        ReactNum.Value = Number.Value;
    }
}

eventとdelegateを自分で宣言する場合と比べて直観的に書けるようになったと思う。
ModelにUniRx依存が入り込むのは若干気になるが、Rxを導入するとはそういうことだろう。幸いにしてUnityEngineがなくともUniRxは使えるらしく、NuGetからUniRxを取得して空のソリューションをビルドしてみたところ普通に通ってくれた。UnityEngineがないと動かないModelは嫌だったのでありがたい。

余談

ものによっては上記よりもイベントの発行を意識しない形で書くことができそうだ。
例えば以下のように。

PlayerMediator.cs
class PlayerMediator
{
    public ReactiveProperty<Player> Player { get; private set; }

    public void GetMoney(int money)
    {
        Player.Value.Money += money;

        var dao = Factory.Create<PlayerTable>();
        dao.Save(Player.Value);
    }
}

今回のように本質的にステートレスなクラスよりも、値を保持し続けるステートフルなタイプのクラスのほうがおそらく向いているのだろう。

残る課題

UniRxの導入も無事成功し、かなり良くなったが、まだレイヤ間の呼び出しにnewを使っている点が気になる。
テスタビリティを確保しよりAdaptiveにするため、DiやFactoryの導入を検討してみたい。

参考

UniRx 4.8 - 軽量イベントフックとuGUI連携によるデータバインディング
Reactive Programming by UniRx for Asynchronous & Event Processing
UniRx とか Reactive Property とか
UniRx オペレータ逆引き

3
5
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
3
5