사용자의 입력은 Controller를 통해 처리된다. Controller에서 Model에 데이터 처리를 명령하고, 그 결과를 View에 넘겨 UI에 출력한다.
View를 통해 들어온 입력에 따라 Controller가 Model을 갱신하고, 그 결과를 다시 View에 출력한다. View가 직접 Model을 가지고 업데이트 하기 때문에 의존성이 높다고한다.
조사 결과 Model의 변경을 View에 반영하는 방법이 갈리는 것 같다.
//Model
public class MVCModel
{
int _attack;
public int Attack
{
set
{
_attack = value;
OnAttackChanged?.Invoke(this);
}
get => _attack;
}
public event Action<MVCModel> OnAttackChanged;
}
//View
public class MVCView : MonoBehaviour
{
public Text attackText;
public void UpdateUI(MVCModel model)
{
attackText.text = $"Attack : {model.Attack}";
}
}
//Controller
public class MVCController : MonoBehaviour
{
public MVCView view;
public MVCModel model;
private void Start()
{
model = new MVCModel();
view = FindFirstObjectByType<MVCView>();
model.OnAttackChanged += (model) => view.UpdateUI(model);
model.Attack = 0;
}
public void OnPress_Upgrade()
{
model.Attack++;
view.UpdateUI(model);
}
}
View를 통해 들어온 입력은 Presenter에게 데이터를 요청한다. Presenter는 Model에서 데이터를 받아와서 가공한 뒤 View에 넘겨준다. MVC와 다른 점은 Presenter는 View와 1:1대응이고 보통 View의 인터페이스를 바라본다.
//Model
public class MVPModel
{
int _attack;
public int Attack
{
get => _attack;
}
public void Upgrade()
{
_attack++;
OnAttackChanged?.Invoke();
}
public event Action OnAttackChanged;
}
//View
public class MVPView : MonoBehaviour, IMVPView
{
Text text;
MVPPresenter presenter;
private void Awake()
{
TryGetComponent(out text);
}
public void UpdateUI(int attack)
{
text.text = $"Attack : {attack}";
}
public void Init(MVPPresenter presenter)
{
this.presenter = presenter;
}
public void OnPress_Upgrade()
{
presenter.Upgrade();
}
}
public interface IMVPView
{
public void Init(MVPPresenter presenter);
public void UpdateUI(int attack);
}
//Presenter
public class MVPPresenter : MonoBehaviour
{
MVPModel model;
IMVPView view;
private void Start()
{
model = new MVPModel();
view = FindFirstObjectByType<MVPView>();
view.Init(this);
model.OnAttackChanged += UpdateUI;
view.UpdateUI(model.Attack);
}
public void Upgrade()
{
model.Upgrade();
}
void UpdateUI()
{
view.UpdateUI(model.Attack);
}
}
View와 ViewModel간 데이터 바인딩을 통해 동기화되고(보통 1:1 연결), ViewModel과 Model은 서로 직접 연결되며, ViewModel이 Model을 갱신, Model이 ViewModel에 데이터를 전달한다.
//Model
public class MVVMModel : MonoBehaviour
{
public MVVMViewModel vm;
int _attack;
public int Attack
{
set
{
_attack = value;
vm.Attack = value;
}
get => _attack;
}
public void Upgrade()
{
Attack++;
}
}
//View
public class MVVMView : MonoBehaviour
{
public Text UIText;
public string Path;
public string Format;
public object Value;
public object Obj;
private void Awake()
{
TryGetComponent(out UIText);
}
private void Start()
{
FindViewModel();
OnChange();
}
void FindViewModel()
{
Transform parent = transform.parent;
MVVMViewModel vm = null;
while(parent != null && !parent.TryGetComponent(out vm))
{
parent = parent.parent;
}
if(vm != null)
{
vm.Bind(this, Path);
}
}
public object GetValue(object obj, string path)
{
var info = obj.GetType().GetProperty(path, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static);
return info.GetValue(obj);
}
public void OnChange()
{
Value = GetValue(Obj, Path);
UIText.text = string.Format(Format, Value);
}
}
//ViewModel
public class MVVMViewModel : MonoBehaviour
{
MVVMProperty<int> attack = new MVVMProperty<int>();
public int Attack
{
set=>attack.Value = value;
get=>attack.Value;
}
public void Bind(MVVMView view, string path)
{
string propertyName = path.ToLower();
var field = GetType().GetField(propertyName, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static);
var obj = (MVVMPropertyBase)field.GetValue(this);
obj.Bind(view);
view.Obj = this;
view.Value = view.GetValue(this, path);
}
}
public interface MVVMPropertyBase
{
public void Bind(MVVMView view);
}
public class MVVMProperty<T> : MVVMPropertyBase
{
T _value;
public T Value
{
set
{
_value = value;
Bindings?.Invoke();
}
get => _value;
}
event Action Bindings;
public void Bind(MVVMView view)
{
Bindings += view.OnChange;
}
}
엄밀하게는 MVVM이 아니다. Path 텍스트를 따라서 데이터를 찾는 것에 가깝다. 간단하게 만들었지만 자동 텍스트 업데이트는 가능하다. 리플렉션과 박싱이 불가피하기 때문에 성능측면에서 단점을 가질 수 있지만, 레퍼런스를 잃어버릴 걱정이 없다. 단순히 컴포넌트에 맞는 스크립트만 추가해주면 되기 때문이다. 대략 작동 순서는 아래와 같다.
위 코드에서는 업데이트할 때 다시 Path를 따라서 찾았지만, PropertyInfo나 FieldInfo를 캐싱해서 업데이트하는 편이 좋을 것이다.
더 자세히 알고싶은 사람은 아래 두 링크를 참고하는 것이 더 이해가 될 것이다.
Dev Weeks: 작업 효율을 높이기 위한 유니티 UI 제작 프로그래밍 패턴들
MVVM 4 uGUI (무료 에셋)