交互逻辑和表现逻辑
上一节已经引入了Model模块,这里多介绍一下MVC架构模式。
代码应该写在View、Controller还是Model里?区分交互逻辑和表现逻辑,就能更好判断
假设有一个计数器应用

它的交互逻辑和表现逻辑如下所示

用伪代码表示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public class Controller { void Start { View.CountText.text = Model.Count.ToString(); View.BtnAdd.onClick.AddListener(()=>{ Model.Count++; View.CountText.text = Model.Count.ToString(); }); View.BtnSub.onClick.AddListener(()=>{ Model.Count--; View.CountText.text = Model.Count.ToString(); }); } }
|
简单总结就是:
- 交互逻辑:View->Model
- 表现逻辑:Model->View
用户的输入和操作等等,叫做交互逻辑,一个App的展现数据的逻辑叫做表现逻辑。
在很多时候,并不会真的去用MVC架构,而是使用表现(View)和数据(Model)分离这样的思想,只要知道View和Model之间的表现逻辑和交互逻辑,就不用管中间到底是Controller、还是ViewModel、还是Presenter。
仅仅把逻辑分出来还不够,在中间层里写的表现逻辑和交互逻辑越来越多,那么这个层就会越来越臃肿,解决这个问题比较好的方法,就是引入Command这个概念,这个和设计模式中的“命令模式”是相同的含义,在深入了解之前,关于交互逻辑和表现逻辑还有一些事情需要学习
首先我们在Unity里面实现计数器,因为是多余的小项目,所以在Project中新建一个CounterApp文件夹
整体结构如下

将CounterViewController
脚本挂载在Panel上
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| using UnityEngine; using UnityEngine.UI;
namespace CounterApp { public class CounterViewController : MonoBehaviour { private void Start() { UpdateView(); transform.Find("Button_add").GetComponent<Button>().onClick.AddListener(() => { CounterModel.count++; UpdateView(); }); transform.Find("Button_sub").GetComponent<Button>().onClick.AddListener(() => { CounterModel.count--; UpdateView(); }); }
void UpdateView() { transform.Find("Text_Count").GetComponent<Text>().text = CounterModel.count.ToString(); } }
public class CounterModel { public static int count = 0; } }
|
为什么起名叫“ViewController”呢,在Unity中,View和Controller是比较难区分的,在别的开发领域里,View类引用了各种UI,但是Unity的UI是基于Component的,只要是继承了MonoBehaviour,都可以轻易获取到,单独创建一个View类会比较浪费(比如挂载两个脚本,一个View,一个Controller)。一个基于MonoBehaviour的脚本由于是挂载在GameObject上,所以既有View职责,又有Controller职责
引入BindableProperty
上面的计时器应用,调用表现逻辑是方法直接调用CounterModel.count
,这里我们使用对象之间的第二种交互方式来实现它,即使用委托
修改CounterViewController
脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| using UnityEngine; using UnityEngine.UI; using System;
namespace CounterApp { public class CounterViewController : MonoBehaviour { private void Start() { CounterModel.OnCountChanged += UpdateView; UpdateView(CounterModel.Count); transform.Find("Button_add").GetComponent<Button>().onClick.AddListener(() => { CounterModel.Count++; }); transform.Find("Button_sub").GetComponent<Button>().onClick.AddListener(() => { CounterModel.Count--; }); }
void UpdateView(int value) { transform.Find("Text_Count").GetComponent<Text>().text =value.ToString(); } private void OnDestroy() { CounterModel.OnCountChanged -= UpdateView; } }
public class CounterModel { private static int m_Count = 0; public static Action<int> OnCountChanged;
public static int Count { get => m_Count; set { if(value != m_Count) { m_Count = value; OnCountChanged?.Invoke(value); } } } } }
|
这样逻辑就改成了:
View——> 增删改数据 ——>Model:交互逻辑
View<—— View注册Model的OnCountChanged
委托 <——Model:表现逻辑
或者我们使用对象之间的第三种交互方式来实现它,即使用事件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| using UnityEngine; using UnityEngine.UI; using System; using FrameWorkDesign;
namespace CounterApp { public class CounterViewController : MonoBehaviour { private void Start() { OnCountChangedEvent.Register(UpdateView); UpdateView(); transform.Find("Button_add").GetComponent<Button>().onClick.AddListener(() => { CounterModel.Count++; }); transform.Find("Button_sub").GetComponent<Button>().onClick.AddListener(() => { CounterModel.Count--; }); }
void UpdateView() { transform.Find("Text_Count").GetComponent<Text>().text =CounterModel.Count.ToString(); } private void OnDestroy() { OnCountChangedEvent.Unregister(UpdateView); } }
public class CounterModel { private static int m_Count = 0;
public static int Count { get => m_Count; set { if(value != m_Count) { m_Count = value; OnCountChangedEvent.Trigger(); } } } }
public class OnCountChangedEvent : BaseEvent<OnCountChangedEvent> { } }
|
编写事件时要注意,不可以在同一个类内调用Event.Register
和Event.Trigger
,这很明显是逻辑混乱
这里使用的事件时不能传参的,后面会优化
这样逻辑就改成了:
View——> 增删改数据 ——>Model:交互逻辑
View<—— 发送变更事件 <——Model:表现逻辑
在CounterViewController
脚本中方法调用、委托&回调、事件&消息都实现一遍了,我们可以发现 委托&回调、事件&消息 的方式可以让Controller的代码量更少,具体来讲:
在使用方法调用时,每次按钮点击之后,都需要主动调用一下表现逻辑,而CounterApp的数据不多,只有一个Count,当有更多数据的时候,这种方式会造成很多的代码调用,而且还容易造成疏忽,等等,其实严重的问题有两个
- 表现逻辑需要交互逻辑之后主动调用,会增加开发者的负担
- 容易让Controller变得臃肿,增加维护成本
在使用委托&回调或事件的时候,每次应用交互逻辑会自动触发表现逻辑,我们不需要再调用表现逻辑的代码,这就是所谓的数据驱动。所以通过以上比较就会发现,表现逻辑的实现使用 委托&回调 或者 事件是比直接方法调用更合适的。
以经验来看,如果是单个数值变化,用委托的方式更合适,比如金币、分数、等级、经验值等等,如果是颗粒度较大的更新用事件比较合适,比如从服务器拉取了一个任务列表数据,然后任务列表数据存到了Model,此时Model的任务列表数据发生了变更,这个时候就向View触发这个事件比较合适。
我们继续演化,首先是CounterModel
这个类在实际应用中应该会有很多类似的数据,每一个都写一次里面包含的委托和属性很麻烦,用泛型解决,这也就是BindableProperty
将Framework文件夹拉取出来放在FrameworkDesign文件夹下,然后在Framework文件夹内新建BindableProperty文件夹,新建BindableProperty
脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| using System;
namespace FrameWorkDesign { public class BindableProperty<T> where T : IEquatable<T> { private T m_Value = default; public Action<T> OnValueChanged;
public T Value { get => m_Value; set { if (!value.Equals(m_Value)) { m_Value = value; OnValueChanged?.Invoke(value); } } } } }
|
然后再次修改CounterViewController
脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| using UnityEngine; using UnityEngine.UI; using System; using FrameWorkDesign;
namespace CounterApp { public class CounterViewController : MonoBehaviour { private void Start() { CounterModel.Count.OnValueChanged += UpdateView; UpdateView(CounterModel.Count.Value); transform.Find("Button_add").GetComponent<Button>().onClick.AddListener(() => { CounterModel.Count.Value++; }); transform.Find("Button_sub").GetComponent<Button>().onClick.AddListener(() => { CounterModel.Count.Value--; }); }
void UpdateView(int value) { transform.Find("Text_Count").GetComponent<Text>().text =value.ToString(); } private void OnDestroy() { CounterModel.Count.OnValueChanged -= UpdateView; } }
public class CounterModel { public static BindableProperty<int> Count = new BindableProperty<int>() { Value = 0 }; } }
|
这个BindableProperty属于很简化的版本,可以拓展出很多功能,其思想借鉴自UniRx的ReactiveProperty。一个BindableProperty既能完成数据读写的基础功能,又能在数据变更时及时发布委托,减少了大量的样板代码,样板代码就是经常需要重复编写的代码,很多框架使用代码生成或者泛型来减少此类工作。
之前说过,子节点调用父节点可以使用委托或事件(子节点声明Delegate并Trigger,父节点Register和Unregister),这里的Model和View也用了委托和事件(Model声明Delegate和Trigger,View来Register和Unregister),一般来讲数据Model是项目的底层,View是项目的上层,和子节点和父节点一样是一个自下而上的逻辑关系,只要是下层调用(也可以说通知,触发)上层就可以用委托和事件,上层调用下层直接方法调用。
总结
- 表现逻辑适合用事件或委托
- 表现逻辑用方法调用会造成很多问题,Controller臃肿难维护
- Model和View是自底向上的关系
- 自底向上用事件或委托
- 自顶向下用方法调用
- Event工具类不能传参
- BindableProperty是数据+数据变更事件,可以节省代码量
在《点点点》项目中引入BindableProperty
先看一下《点点点》项目目前的结构

先将项目中的GameModel
都变成BindableProperty
1 2 3 4 5 6 7 8 9 10
| namespace FrameWorkDesign.Example { public class GameModel { public static BindableProperty<int> KillCount = new BindableProperty<int>() { Value = 0 }; public static BindableProperty<int> Gold = new BindableProperty<int>() { Value = 0 }; public static BindableProperty<int> Score = new BindableProperty<int>() { Value = 0 }; public static BindableProperty<int> BestScore = new BindableProperty<int>() { Value = 0 }; } }
|
然后修改Enemy
脚本
1 2 3 4 5 6 7 8 9 10 11 12 13
| using UnityEngine;
namespace FrameWorkDesign.Example { public class Enemy : MonoBehaviour { private void OnMouseDown() { GameModel.KillCount.Value++; Destroy(gameObject); } } }
|
修改GameRoot
脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| using UnityEngine;
namespace FrameWorkDesign.Example { public class GameRoot : MonoBehaviour { private void Awake() { GameStartEvent.Register(OnGameStart); GameModel.KillCount.OnValueChanged += OnCountChanged; }
private void OnCountChanged(int count) { if (count == 9) GameEndEvent.Trigger(); }
private void OnGameStart() { transform.Find("Enemies").gameObject.SetActive(true); }
void OnDestroy() { GameStartEvent.Unregister(OnGameStart); GameModel.KillCount.OnValueChanged -= OnCountChanged; } } }
|
这样我们就不用之前创建的KilledOneEnemyEvent
,代码更加简洁方便

数据变更事件是事件的一种,除了数据变更事件以外,我们还接触到了跨模块事件,这个跨模块事件会和KilledOneEnemyEvent
一样,在未来会被其他的概念或工具替代。