Unityにおける疎結合な実装
はじめに
勉強したことをメモします
同シーン内の他オブジェクトにアタッチされているスクリプトの変数を取得
Unity Events / C# Events
Unity EventsやC#のイベントを使用して、イベントが発生したときにデータを渡すことができます
送信側スクリプト
using UnityEngine;
using UnityEngine.Events;
public class SenderScript : MonoBehaviour
{
public UnityEvent<int> onValueChanged;
private int value;
public void SetValue(int newValue)
{
value = newValue;
onValueChanged?.Invoke(value);
}
}
受信側スクリプト
using UnityEngine;
public class ReceiverScript : MonoBehaviour
{
public SenderScript sender;
void Start()
{
sender.onValueChanged.AddListener(OnValueChanged);
}
void OnValueChanged(int newValue)
{
// ここでnewValueを使用する
}
}
ScriptableObjects
ScriptableObjectを使用して、データを共有することができます
DataContainer(ScriptableObject)
using UnityEngine;
[CreateAssetMenu]
public class DataContainer : ScriptableObject
{
public int value;
}
送信側スクリプト
using UnityEngine;
public class SenderScript : MonoBehaviour
{
public DataContainer dataContainer;
public void SetValue(int newValue)
{
dataContainer.value = newValue;
}
}
受信側スクリプト
using UnityEngine;
public class ReceiverScript : MonoBehaviour
{
public DataContainer dataContainer;
void Update()
{
// dataContainer.valueを使用する
}
}
インターフェース
インターフェースを使用して、具体的なクラスに依存しない方法でデータを取得することができます
インターフェース
public interface IValueProvider
{
int GetValue();
}
送信側スクリプト
using UnityEngine;
public class SenderScript : MonoBehaviour, IValueProvider
{
private int value;
public int GetValue()
{
return value;
}
public void SetValue(int newValue)
{
value = newValue;
}
}
受信側スクリプト
using UnityEngine;
public class ReceiverScript : MonoBehaviour
{
public GameObject senderObject;
void Start()
{
IValueProvider valueProvider = senderObject.GetComponent<IValueProvider>();
if (valueProvider != null)
{
int value = valueProvider.GetValue();
// ここでvalueを使用する
}
}
}
UniRx
UniRx(Unity Reactive Extensions)は、リアクティブプログラミングをUnityに導入するライブラリです。データの変更を監視して、変更があった場合に自動的に処理を行うことができます。
送信側スクリプト
using UniRx;
public class SenderScript : MonoBehaviour
{
public ReactiveProperty<int> Value = new ReactiveProperty<int>(0);
public void SetValue(int newValue)
{
Value.Value = newValue;
}
}
受信側スクリプト
using UniRx;
public class ReceiverScript : MonoBehaviour
{
public SenderScript sender;
void Start()
{
sender.Value.Subscribe(newValue =>
{
// ここでnewValueを使用する
}).AddTo(this);
}
}
Extenject
Extenject(旧Zenject)は、依存性注入(DI)をUnityに導入するためのフレームワークです。これを使うと、コンポーネント間の依存関係を疎結合に保つことができます。
※この例では、MyInstallerスクリプトがSenderScriptの依存関係をReceiverScriptに注入しています。AsSingle()メソッドによって、SenderScriptはシングルトンとして扱われます。
送信側スクリプト
public class SenderScript : MonoBehaviour
{
private int value = 0;
public int GetValue()
{
return value;
}
public void SetValue(int newValue)
{
value = newValue;
}
}
受信側スクリプト
using Zenject;
public class ReceiverScript : MonoBehaviour
{
private SenderScript senderScript;
[Inject]
public void Construct(SenderScript senderScript)
{
this.senderScript = senderScript;
}
void Start()
{
int receivedValue = senderScript.GetValue();
// ここでreceivedValueを使用
}
}
Installer(依存関係を定義するスクリプト)
using Zenject;
public class MyInstaller : MonoInstaller
{
public override void InstallBindings()
{
Container.Bind<SenderScript>().FromComponentInHierarchy().AsSingle();
}
}
MVPパターン
MVP(Model-View-Presenter)は、UIロジックを整理し、テスト可能で再利用可能なコードを書くためのデザインパターンです。
Model
データとビジネスロジックを扱います
public class DataModel
{
public int Value { get; private set; }
public void SetValue(int value)
{
Value = value;
}
}
View
UIとユーザー入力を扱います
public interface IDataView
{
void UpdateValue(int value);
}
Presenter
ModelとViewを繋ぐ役割を果たします
public class DataPresenter
{
private DataModel model;
private IDataView view;
public DataPresenter(DataModel model, IDataView view)
{
this.model = model;
this.view = view;
}
public void OnValueChanged(int newValue)
{
model.SetValue(newValue);
view.UpdateValue(model.Value);
}
}
MVPパターンに基づくExtenjectとUniRxの利用
MVPの役割を担うコード
- Model: DataModel
- View: ReceiverScript(IDataViewインターフェースを実装)
- Presenter: DataPresenter
それ以外の役割を担うコード
- 外部からModelにデータを設定するアクター: SenderScript
Model(DataModel)
using UniRx;
public class DataModel
{
public ReactiveProperty<int> Value = new ReactiveProperty<int>(0);
public void SetValue(int value)
{
Value.Value = value;
}
}
View(IDataView Interface & ReceiverScript)
public interface IDataView
{
void UpdateValue(int value);
}
public class ReceiverScript : MonoBehaviour, IDataView
{
public void UpdateValue(int value)
{
// ここで取得した値を何らかの形で表示する
}
}
Presenter(DataPresenter)
using UniRx;
using Zenject;
public class DataPresenter
{
private DataModel model;
private IDataView view;
[Inject]
public void Construct(DataModel model, IDataView view)
{
this.model = model;
this.view = view;
this.model.Value.Subscribe(newValue =>
{
view.UpdateValue(newValue);
});
}
}
Installer(MyInstaller)
using Zenject;
public class MyInstaller : MonoInstaller
{
public ReceiverScript receiverScript;
public SenderScript senderScript;
public override void InstallBindings()
{
Container.Bind<DataModel>().AsSingle().NonLazy();
Container.Bind<IDataView>().FromInstance(receiverScript).AsSingle();
Container.Bind<SenderScript>().FromInstance(senderScript).AsSingle();
Container.Bind<DataPresenter>().AsSingle().NonLazy();
}
}
送信側スクリプト(SenderScript)
using Zenject;
public class SenderScript : MonoBehaviour
{
private DataModel dataModel;
[Inject]
public void Construct(DataModel dataModel)
{
this.dataModel = dataModel;
}
public void SomeMethod()
{
// 何らかの操作で値を変更する
dataModel.SetValue(5);
}
}
ReceiverScriptがViewの役割を果たし、DataModelがModelの役割を果たしています。SenderScriptはある種のアクターまたはコントローラとして、Model(DataModel)にデータを設定する役割を担っています。
SenderScriptはMVPのどのコンポーネントにも完全には当てはまりませんが、システム全体の動作には必要です。
※今回のシナリオでは、MVPパターンの各コンポーネントと「送信側」や「受信側」という概念が完全に一致するわけではありません。