#こんな経験ありませんか?
[SerializeField]~View;
[SerializeField]~Model;
そして起こる無限のNullReferenceException...
今回はこれを解決するように組んでいきます
#例えば
おそらくZenject未使用でMVP(UniRx)ありで組もうとするとこんな感じになると思います。
using System;
using UniRx;
using UnityEngine;
public class HPModel : MonoBehaviour
{
[SerializeField]float hp = 100f;
Subject<float> hPChanged = new Subject<float>();
public IObservable<float> HPChanged => hPChanged;
private void Start()
{
hPChanged.OnNext(hp);
}
public void Damage(float damage)
{
hp -= damage;
hPChanged.OnNext(hp);
}
}
using UnityEngine;
using UniRx;
public class HPPresenter : MonoBehaviour
{
[SerializeField] HPModel model;
[SerializeField] HPView view;
void Start()
{
model.HPChanged.Subscribe(view.DisplayHP);
}
}
using UnityEngine;
using UnityEngine.UI;
public class HPView : MonoBehaviour
{
[SerializeField] Text hp;
public void DisplayHP(float HP)
{
hp.text = HP.ToString();
}
}
のようにマウスでカチカチして設定して
using UnityEngine;
using UnityEngine.UI;
using UniRx;
public class DamageButton : MonoBehaviour
{
[SerializeField] HPModel model;
[SerializeField] float damageAmount;
void Start()
{
GetComponent<Button>().onClick.AsObservable().Subscribe(_ => model.Damage(damageAmount));
}
}
をButtonにつけて
またマウスでカチカチして...完成!
とこんな感じに
できます!
#問題点
##設定忘れ==死問題
はい...
こうなるだけで死にます、この例程度ならわかりますが規模がでかくなると死にます、つらい
さらに別の問題もあって
ModelにMonobehaviourが依存する形で実装することで[SerializeField]でModelをいじることが出来るようになっていますが、ほんとは依存させたくない(いろいろ理由はありますが今回は省略)し負荷的にもあまりよろしくないです。あと実行順序で死ぬ場合もあるし
少し前まではここでシングルトンの説明が出てきてたのですが今回は別のソリューションでLet'sGo!
#解決策
ここでZenject
##1.Zenjectのインストール
以下URLからダウンロードしてUnityに入れます。
https://github.com/modesttree/Zenject/releases
##2.SceneContextの用意
まずSceneContextを用意します
こんな感じで1つのシーンに1つSceneContextを用意してください
##3.スクリプトの書き換え
Zenjectっぽく書いていきますよー!
using System;
using UniRx;
using UnityEngine;
public class HPModel//MonoBehaviourが消えた!(どこからも参照されなくて大丈夫です)
{
float hp = 100f;
BehaviorSubject<float> hPChanged;
public IObservable<float> HPChanged => hPChanged;
public HPModel()
{
hPChanged = new BehaviorSubject<float>(hp);//初期化がコンストラクタでできる!(Monobehaviourがあるとできない)
}
public void Damage(float damage)
{
hp -= damage;
hPChanged.OnNext(hp);
}
public void Heal(float heal)
{
hp += heal;
hPChanged.OnNext(hp);
}
}
using UniRx;
using System;
public class HPPresenter //MonoBehaviourが消えた!(どこからも参照されなくて大丈夫です)
{
HPModel model;
HPView view;//SerializeFieldが消えた!
public HPPresenter(HPModel model, HPView view)//コンストラクターに移動した!
{
this.model = model ?? throw new ArgumentNullException(nameof(model));
this.view = view ?? throw new ArgumentNullException(nameof(view));
model.HPChanged.Subscribe(view.DisplayHP);
}
}
using UnityEngine;
using UnityEngine.UI;
using Zenject;
public class HPView : MonoBehaviour
{
[Inject] Text hp; //SerializeFieldがInjectに変わった!
public void DisplayHP(float HP)
{
hp.text = HP.ToString();
}
}
using UnityEngine.UI;
using UniRx;
using Zenject;
public class DamageButton : MonoBehaviour
{
[Inject] HPModel model;//どこからでも参照できる!
[SerializeField] float damageAmount;
void Start()
{
GetComponent<Button>().onClick.AsObservable().Subscribe(_ => model.Damage(damageAmount));
}
}
##4.新しいスクリプトの追加
これだけだとどこからもスクリプトが参照されないので動かないのでZenjectをしb...依存関係を書いていきます
ここにあるMonoInstallerを押して任意の名前を付けます(今回はHPInstallerで行きます)
using UnityEngine;
using Zenject;
public class HPInstaller : MonoInstaller
{
public override void InstallBindings()
{
Container.Bind<HPModel>().FromNew().AsCached();//HPModelをNewをして新しく作ってくださいの命令です
Container.Bind<HPPresenter>().AsCached().NonLazy();//FromNew()は省略可能、NonLazy()は即時に実行してくださいという意味(おまじないと思っていただければ)
}
}
##4.SceneContextに追加
このように書いて、さらに
それを適当な場所にアタッチしておきます
そして先ほどのSceneContextにマウスかちかちで設定
##5.ZenjectBindingを追加
以下の画像のように設定することでHPViewをZenjectに認識させてやります
またTextもZenjectに乗せてあげます
#完成!
同じものが動きます
#つまり何やってるの?
イメージとしてはこんな感じです
コンテナにInstallerとかがクラス(データ)を収納して
(依存性を解決して)データを配ってくれる
#で何がいいの?
[SerializeField]が消えてNullReferenceExceptionになりづらく!
依存関係が自動的に解消されるのでデータがないときにnullチェックをする必要性が少なくなるのもポイントですね
#気づいた方はいるだろうか...
実はこのままだとHPの設定が出来ません(ModelがUnityから見えなくなったため)
こういう時の対処法も書いておきます
このようなときはたいてい初期化用のデータの場合が多いのでデータを外から注入してやればいいことになりますのでこういう場合に便利なのがScriptableObjectInstallerになります
MonoInstallerと同様に下の部分から
で作成します今回はHPDataとしました
using UnityEngine;
using Zenject;
[CreateAssetMenu(fileName = "HPData", menuName = "Installers/HPData")]
public class HPData : ScriptableObjectInstaller<HPData>
{
[SerializeField] float hp;
public float HP { get => hp; }
public override void InstallBindings()
{
Container.Bind<HPData>().FromInstance(this).AsCached();
}
}
HPModelを書き換え
using System;
using UniRx;
using UnityEngine;
public class HPModel
{
[SerializeField] float hp = 100f;
BehaviorSubject<float> hPChanged;
public IObservable<float> HPChanged => hPChanged;
public HPModel(HPData data)//ここを書き換え!
{
hp = data.HP;
hPChanged = new BehaviorSubject<float>(hp);
}
public void Damage(float damage)
{
hp -= damage;
hPChanged.OnNext(hp);
}
public void Heal(float heal)
{
hp += heal;
hPChanged.OnNext(hp);
}
}
次に実体を作ります
そして設定して
SceneContextに登録します
完了!
後はさっきの設定のところを適当な値にすると実行時に反映されます
#最後に...
Zenjectはいいぞ!!
Zenject悪循環
— VRのたぐすキャット(@notargs ) 2019, 4月