无框架设计
(对象交互、模块化、表现和数据分离优化) 拖拽+少量脚本编写
知识点:
1.静态变量的使用,点击事件
2.Unity 中有 场景结构、项目结构,在一般开发的时候有代码结构,架构简单理解为结构等。
3.场景结构(树状结构):考虑场景可维护性,使用父子结构或者树状结构进一步整理。根节点尽可能少,层次分明,逻辑清晰。因为大部分的视图结构都是树状结构,在理想情况下,一个
App 或一个游戏最好是只有一颗树。
4.委托、?.Invoke()
5.事件
6.泛型、泛型约束
7.继承
8.MVC
遇到问题
·
编辑器操作btnstart按钮拖拽关闭gameStartPanel面板,打开Enemies游戏物体。–问题:父子间调用,跨模块交互
·Enemy.cs–点击10次打开GamePassPanel面板–问题:跨模块交互·
·10个怪物的游戏数据决定通关条件和计数逻辑–问题:游戏核心数据与表现没有分离。
·如果点击了10次就通关–问题:游戏判断是否结束逻辑写在enemy里
代码
public class Enemy : MonoBehaviour
{
public GameObject GamePassPanel;
private static int mKillCount = 0;
private void OnMouseDown()
{
Destroy(gameObject);
mKillCount++;
if (mKillCount == 10)
{
GamePassPanel.SetActive(true);
}
}
}
无框架一次性项目问题:
1.难维护(拖拽的时候,对象之间的引用关系是随机的没有规律可循的,所以当时间久了或者比人来接手次项目时要理清其中的逻辑所花费的时间会很多。)
2.难协作(很多的拖拽操作和逻辑是在场景中操作的,并且也会记录到场景里,所以在使用 git 或者 svn 或者官方的 plastics 时团队难以管理,容易造成冲突。)
3.难扩展(对象之间的引用关系是随机的没有规律可循的,这样会造成大量的耦合(牵一发动全身),后边当扩展功能的时候,花的时间越来越久。)
解决问题的途径?
1.低耦合(对象之间如何交互):降低对象、类的双向引用、循环引用。
2.高内聚(如何做到模块化):尽量把同样类型的代码放在一起。
在框架或者架构层面,我们只要关注两个问题就可以了,对象之间的交互 和 模块化,是整个框架搭建系列的终极问题。
对象之间的交互一般有三种:
·方法调用,例如:A 调用 B 的 SayHello 方法(新手先接触的方式,因为简单)
·委托或者回调,例如:界面监听子按钮的点击事件。
委托的话一般会造成单向引用,单向引用是弱耦合关系,所以危害比较少。 ·消息或事件,例如:服务器向客户端发送通知
对象交互前提(引用关系)
·方法调用 - A 需要持有 B 才能调用 B 的方法。 A 持有
B,那么方法调用必然会造成至少单向引用的关系。对象和对象之间的关系是随机的,越多无限制地使用方法调用,耦合的概率就会越高。 ·委托/回调 -
A 需要持有 B 才能注册 B 的委托 ·消息/事件 - A 不需要持有 B,中间用事件
问题一:
子节点引用父节点(物理关系)–UI逻辑的子节点开始按钮调用父节点(开始界面)关闭
耦合就是双向引用或循环引用,所以父子节点间的关系也不能是双向引用或循环引用。那么既然不能耦合想要引用,必然只能是单向的,共识是 父节点可以引用子节点,但是子节点不能引用父节点。
结论:
子节点要想引用父节点,不用方法调用,用委托或回调方式。
修改视图拖拽,改GameStartPanel .cs
//低耦合
//解决UI逻辑的开始按钮调用父节点(开始界面)关闭,调用游戏逻辑里Enemies游戏物体打开
//子调用父是双向引用,单向用委托或回调修改
//修改视图编辑器添加onclick事件操作,父物体查找子物体调用自己和enimies,更新引用关系(btnStart仅仅只是个UI组件了,没有承担业务逻辑)
public class GameStartPanel : MonoBehaviour
{
public GameObject enimies;
private void Start()
{
transform.Find("btnStart").GetComponent<Button>().onClick.AddListener(()=>
{
gameObject.SetActive(false);
enimies.SetActive(true);//问题:跨模块交互
});
}
}
问题二:
跨模块的对象交互,父节点不同业务对象用事件–GameStartPanel调用游戏逻辑里Enemies(gameobject)打开。
GameStartPanel 负责 UI 相关的逻辑,Enemies 则是负责游戏相关逻辑。分工的时候,往往会把 UI 的制作和游戏性(Gameplay) 逻辑分给不同的人来负责,那么这时候,直接跨模块调用方法是很容易造成问题。
结论:
委托不行因为委托注册的前提也是要持有对象,最好的方式就是没有任何引用关系。而使用对象之间的交互方式中的 消息&事件 能够满足要求。
GameStartEvent.cs
using System;
//解决UI跨模块打开Enemies的问题引入事件,代码不难,就是一个静态类,封装了一个委托而已。
public static class GameStartEvent
{
private static Action mOnEventTrigger;
//注册
public static void Register(Action onEvent)
{
mOnEventTrigger += onEvent;
}
//注销
public static void UnRegister(Action onEvent)
{
mOnEventTrigger -= onEvent;
}
//触发
public static void Trigger()
{
mOnEventTrigger?.Invoke();
}
}
修改GameStartPanel .cs
private void Start()
{
transform.Find("btnStart").GetComponent<Button>().onClick.AddListener(()=>
{
gameObject.SetActive(false);
GameStartEvent.Trigger();//触发事件//enimies.SetActive(true);//问题:跨模块交互
});
}
处理事件(注册事件在Awake 和 Start)Game.cs
public class Game : MonoBehaviour
{
void Start()
{
GameStartEvent.Register(OnGameStart);
}
private void OnDestroy()
{
GameStartEvent.UnRegister(OnGameStart);
}
void OnGameStart()
{
//点击开始按钮打开Enemies
transform.Find("enimies").gameObject.SetActive(true);
}
}
这样 UI 模块和 Game 模块通信的时候只要相关的负责人约定一下事件就可以了。
enemy逻辑点击10次打开UI(结束界面)
同样为跨模块交互,用事件
问题二点一:
GameStartEvent和GamePassEvent除了类名不一样,两个代码几乎是重复的。
作为框架搭建课程,这样的代码是肯定不允许存在的,需要做进一步优化。
两个类的实现代码完全一样,只有类名或者类型不一样且未来需要扩展时(未来会增加各种事件修改跨模块交互),这时候用 泛型+继承 来提取。
继承解决扩展问题;泛型解决实现代码一致,类不一致问题,这是一个重构技巧。
using System;
//重构两个重复的Event
public class Event<T>where T: Event<T>
{
private static Action mOnEventTrigger;
public static void Register(Action onEvent)
{
mOnEventTrigger += onEvent;
}
public static void UnRegister(Action onEvent)
{
mOnEventTrigger -= onEvent;
}
public static void Trigger()
{
mOnEventTrigger?.Invoke();
}
public class GameStartEvent:Event<GameStartEvent>
{
}
public class GamePassEvent :Event<GamePassEvent>
{
}
代码一下子少了很多,这样我们扩展一个事件的时候就非常容易了,主要创建个类,继承 Event 就可以。通过引入事件这个概念,让项目更容易维护、跟容易协作。
问题三:
游戏核心数据与表现的部分没有分离,用Model把游戏中数据抽了出来
未来可能不止是个敌人,加分数、最高分、金币、拥有道具等功能,功能所需数据大多数在多个场景、界面和游戏物体是共享的(在空间和时间上都需要共享,需要存储起来)
所以开发者共识就是:把数据部分抽离出来单独放在一个位置维护。常见开发架构就是使用MVC开发架构。这里先用MVC中其中一个概念Model。
Model就是管理数据、存储数据,管理数据可以通过Model对象或类对数据进行增删改查,有的时候可以进行存储。
public class GameModel
{
public static int KillCount = 0;
}
Enemy.cs
private void OnMouseDown()
{
GameModel.killCount++;
if (GameModel.killCount == 10)
{
GamePassEvent.Trigger();//事件
}
}
问题四:
游戏通关的判断,其实是游戏的规则,那么判断一个游戏是否要通关是一个敌人该负责的事情么?
棋子没有那么智能,棋子的职责仅仅是被移动或者吃掉或者是传达信息而已,那么是谁来判断象棋的胜负条件呢?答案是双方玩家根据规则去判断胜负条件,而这个规则是属于一场游戏的,规则、游戏这些概念理解起来比较抽象,但是在我们项目中,确确实实存在一个游戏这样的概念,写在Game类里
在 Enemy 里实现游戏结束的判断,很不符合逻辑,不是自洽的,不是面向对象的,不是对真实世界的抽象描述。如果我们写的代码不是符合逻辑的抽象,那么别人看这个代码的时候就会增加理解成本,也就会让项目更难以维护,记住这些代码以前笔者是怎么写的,以后要避免。
enemy点击告诉game类,game和enemy是1对10,子节点向父节点通信,用委托或事件,委托不现实,如果想用委托 Game 或者 Enemies 就需要维护一个 Enemy 数组,所以用事件是最佳选择。
又多了一个事件,即 KilledOneEnemyEvent,而现在整个项目做到了正确的代码放到了正确的位置这一个原则,这一个原则更严谨的叫法就是单一职责原则,实际上代码对业务抽象得合理自然就符合单一职责原则了。
public class KilledOneEnemyEvent : Event<KilledOneEnemyEvent>
{
}
public class Enemy : MonoBehaviour
{
private void OnMouseDown()
{
Destroy(gameObject);
KilledOneEnemyEvent.Trigger();
}
}
public class Game : MonoBehaviour
{
void Start()
{
GameStartEvent.Register(OnGameStart);
KilledOneEnemyEvent.Register(OnEnemyKilled);
}
private void OnDestroy()
{
GameStartEvent.UnRegister(OnGameStart);
KilledOneEnemyEvent.UnRegister(OnEnemyKilled);
}
void OnGameStart()
{
transform.Find("enimies").gameObject.SetActive(true);
}
void OnEnemyKilled()
{
GameModel.killCount++;
if (GameModel.killCount == 10)
{
GamePassEvent.Trigger();//事件
}
}
}
以上不良引用关系调整(什么时候该用什么,需要大家根据当时的条件和环境自己判断的,这也是一门艺术):
子节点通知父节点用委托或事件
父节点调用子节点可以直接方法调用
跨模块通信用事件
多事件重复时用泛型 + 继承 提取 Event 工具类
子节点通知父节点也可以用事件(根据情况)
数据时间空间共享,抽离维护,表现和数据分离
正确的代码要放在正确的位置,判断放在游戏规则里,数据放在model里,单一职责原则
模块化也一般有三种
·单例,例如:Manager Of Managers IOC,例如:Extenject、uFrame 的
·Container、StrangeIOC 的绑定等等 分层,例如:MVC、三层架构、领域驱动分层等等(当然可以做模块化的还有 Entity
·Component、门面模式 等等,先理解这三种就够)
MVC交互逻辑和表现逻辑
就是当用户按下 + 或者 - 时,程序会更改 Model 里的 Count 数据,这个就叫做交互数据。
当变更数据之后或者初始化时,从 Model 里查询数据显示到 View 上的,就是表现逻辑。
交互逻辑:View -> Model(一个 App 的交互,就是用户的输入和操作等等)
表现逻辑: Model -> View(一个 App 的展现,一般都是展现数据的,展现数据的逻辑)
伪代码表示如下:
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(); // 表现逻辑
});
}
}
那么为什么要介绍这两个概念呢?
因为在很多时候,我们不会真的去用 MVC 开发架构,而是使用表现(View)和数据(Model)分离这样的思想,而我们只要知道 View 和 Model 之间有两种逻辑,即交互逻辑 和 表现逻辑,我们就不用管中间到底是 Controller、还是 ViewModel、还是 Presenter。只需要想清楚交互逻辑 和 交互逻辑如何实现的就可以了。
换句话说,一般 Controller 会负责两种逻辑,即交互逻辑和表现逻辑。这也是为什么 Controller 写得越来越臃肿的原因,这是因为 Controller 负责了两种逻辑,里边拥有两种逻辑的代码,随着项目规模越来越大,代码越来越多了。
而解决 Controller 臃肿问题的比较好的解决方法,就是引入 Command 这个概念,也就是命令,这个命令和命令模式的命令是一回事
按加减实现:表现逻辑是用的是对象之间的交互方式中的 方法调用方式,即 Controller 获取 Model 对象,然后把 Model 对象的 Count 数据显示个 View。
//表现逻辑是在交互逻辑完成之后主动调用
public class CounterViewController : MonoBehaviour
{
private void Start()
{
transform.Find("btnAdd").GetComponent<Button>().onClick.AddListener(() =>
{
CountModel.count++;//交互逻辑
UpdateView();//表现逻辑
});
transform.Find("btnSub").GetComponent<Button>().onClick.AddListener(() =>
{
CountModel.count--;//交互逻辑
UpdateView();//表现逻辑
});
UpdateView();//表现逻辑
}
private void UpdateView()
{
transform.Find("CountText").GetComponent<Text>().text = CountModel.count.ToString();
}
}
public static class CountModel
{
public static int count = 0;
}
来看看 View 和 Model 对象怎样交互比较好,或者说 交互逻辑 和 表现逻辑 怎样实现会比较好。
用委托&回调 调用:实际上 View 对象只需要监听 CounterModel 的数据变化就可以
增加了一个 OnCountChanged 委托,然后将 Count 变量变成了属性,在 ViewController 里只需要监听 OnCountChanged 委托就行了。
private void Start()
{
CounterModel.OnCountChanged += OnCountChanged;
transform.Find("btnAdd").GetComponent<Button>().onClick.AddListener(() =>
{
CounterModel.Count++;
//UpdateView();
});
transform.Find("btnSub").GetComponent<Button>().onClick.AddListener(() =>
{
CounterModel.Count--;
//UpdateView();
});
OnCountChanged(CounterModel.Count);
}
//private void UpdateView()
//{
// transform.Find("CountText").GetComponent().text = CountModel.count.ToString();
//}
private void OnCountChanged(int newCount)
{
transform.Find("CountText").GetComponent<Text>().text = newCount.ToString();
}
private void OnDestroy()
{
// 注销
CounterModel.OnCountChanged -= OnCountChanged;
}
}
public static class CounterModel
{
private static int mCount = 0;
public static Action<int> OnCountChanged;//定义一个委托
public static int Count//count变量变成属性,只要监听OnCountChanged就行了
{
//get => mCount;优化
get
{
return mCount;
}
set
{
if (value != mCount)
{
mCount = value;
OnCountChanged?.Invoke(value);//每次变化的时候调用一下这个委托
}
}
}
}
第三种方式;事件&消息。
把之前的委托删掉了,然后改成了用事件,这里事件发现一个缺陷,就是之前笔者设计的 Event 工具类,不支持携带参数,这个要在后边完善下。
public class ViewController : MonoBehaviour
{
private void Start()
{
// 注册
OnCountChangedEvent.Register(OnCountChanged);
//CounterModel.OnCountChanged += OnCountChanged;
transform.Find("btnAdd").GetComponent<Button>().onClick.AddListener(() =>
{
CounterModel.Count++;
//UpdateView();
});
transform.Find("btnSub").GetComponent<Button>().onClick.AddListener(() =>
{
CounterModel.Count--;
//UpdateView();
});
//OnCountChanged(CounterModel.Count);
OnCountChanged();
}
//private void UpdateView()
//{
// transform.Find("CountText").GetComponent().text = CountModel.count.ToString();
//}
//private void OnCountChanged(int newCount)
private void OnCountChanged()
{
transform.Find("CountText").GetComponent<Text>().text = CounterModel.Count.ToString();
}
private void OnDestroy()
{
// 注销
//CounterModel.OnCountChanged -= OnCountChanged;
// 注销
OnCountChangedEvent.UnRegister(OnCountChanged);
}
}
public static class CounterModel
{
private static int mCount = 0;
//public static event Action OnCountChanged;//定义一个委托
public static int Count//count变量变成属性,只要监听OnCountChanged就行了
{
//get => mCount;优化
get
{
return mCount;
}
set
{
if (value != mCount)
{
mCount = value;
//OnCountChanged?.Invoke(value);//每次变化的时候调用一下这个委托
OnCountChangedEvent.Trigger();
}
}
}
}
public class OnCountChangedEvent : Event<OnCountChangedEvent> { }//事件定义,先不传参,只充当通知作用
点击按钮,只需考虑让数据发生变化即可,不用关心表现逻辑怎样,数据变化自然触发表现逻辑,这就是所谓数据驱动。
如果是单个数值变化,用委托的方式更合适,比如金币、分数、等级、经验值等等,
如果是颗粒度较大的更新用事件比较合适,比如从服务器拉取了一个任务列表数据,然后任务列表数据存到了 Model,此时 Model 的任务列表数据发生了变更,这个时候就向 View 发送个事件比较合适。