【Unity 框架】QFramework v1.0 使用指南 时间:2023年9月12日23:16:19
视频URL:https://www.bilibili.com/video/BV1cG4y1H7uU/
参考链接:
架构篇 ———- 基础 ———- 视频URL:https://www.bilibili.com/video/BV1cG4y1H7uU/
01. QFramework 架构简介 https://www.bilibili.com/read/cv19092948/
QFramework 架构是一套简单、强大、易上手的系统设计架构。
这套架构的特性如下:
基于 MVC
分层
(可选)CQRS 支持
(可选)事件驱动
(可选)数据驱动
(可选)IOC 模块化
(可选)领域驱动设计(DDD)支持
符合 SOLID 原则
源码不到 1000 行
提供的架构图
举个例子(一图胜千言😂)
02. QFramework 的 MVC https://www.bilibili.com/read/cv19093399/
效果
代码:没有MVC 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 using System.Collections;using System.Collections.Generic;using UnityEngine;using UnityEngine.UI;namespace QFramework.Example { public class P2_CounterAppControllerNoMVC : MonoBehaviour { public Button BtnAdd; public Button BtnSub; public Text CountText; public int Count = 0 ; void Start () { BtnAdd.onClick.AddListener(() => { Count++; UpdateView(); }); BtnSub.onClick.AddListener(() => { Count--; UpdateView(); }); UpdateView(); } void UpdateView () { CountText.text = Count.ToString(); } } }
代码:引入MVC 导入 QFramework 的方式非常简单,只需要复制 QFramework.cs 的代码到 Unity 工程中即可。
QFramework.cs 地址:
Gitee: https://gitee.com/liangxiegame/QFramework/blob/master/QFramework.cs
Github: https://github.com/liangxiegame/QFramework/blob/master/QFramework.cs
Controller
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 using System.Collections;using System.Collections.Generic;using UnityEngine;using UnityEngine.UI;namespace QFramework.Example{ public class P2_CounterAppControllerMVC : MonoBehaviour, IController { public Button BtnAdd; public Button BtnSub; public Text CountText; private P2_CounterModel mModel; void Start () { mModel = this .GetModel <P2_CounterModel>(); BtnAdd.onClick.AddListener (() => { mModel.Count++; UpdateView (); }); BtnSub.onClick.AddListener (() => { mModel.Count--; UpdateView (); }); UpdateView (); } void UpdateView () { CountText.text = mModel.Count.ToString (); } public IArchitecture GetArchitecture () { return P2_CounterApp.Interface; } } }
Model
1 2 3 4 5 6 7 8 9 10 11 12 13 using QFramework;namespace QFramework.Example { public class P2_CounterModel : AbstractModel { public int Count = 0 ; protected override void OnInit () { Count = 0 ; } } }
Architecture
看成是IOC容器
1 2 3 4 5 6 7 8 9 10 namespace QFramework.Example { public class P2_CounterApp : Architecture <P2_CounterApp > { protected override void Init () { this .RegisterModel(new P2_CounterModel()); } } }
MVC优势 这里要注意一点,Model 的引入是为了解决数据共享的问题,而不是说单只是为了让数据和表现分离,这一点是非常重要的一点。
数据共享分两种:空间上的共享和时间上的共享。
空间的共享很简单,就是多个点的代码需要访问 Model 里的数据。
时间上的共享就是存储功能,将上一次关闭 App 之前的数据存储到一个文件里,这次打开时获得上次关闭 App 之前的数据。
03. 引入 Command https://www.bilibili.com/read/cv19094196/
无Command问题 现在,数据共享的问题通过 引入 Model 解决了。
这里再次强调一下,需要共享的数据放 Model 里,不需要共享的,能不放就不放。
虽然引入了 Model,但是这套代码随着项目规模的发展还是有很多的问题。
其中最严重也最常见的就是 ==Controller 会越来越臃肿==。
Command优势 使用 Command 可以带来很多便利,比如:
Command 可以复用,Command 也可以调用 Command
Command 可以比较方便实现撤销功能,如果 App 或者 游戏需要的话
如果遵循一定规范,可以实现使用 Command 跑自动化测试。
Command 可以定制 Command 队列,也可以让 Command 按照特定的方式执行
一个 Command 也可以封装成一个 Http 或者 TCP 里的一次数据请求
Command 可以实现 Command 中间件模式
······
Command 最明显的好处就是:
就算代码再乱,也只是在一个 Command 对象里乱,而不会影响其他的对象。
将方法封装成命令对象,可以实现对命令对象的组织、排序、延时等操作。
代码 Command对象
1 2 3 4 5 6 7 8 9 10 11 12 13 using QFramework;using QFramework.Example;namespace QFramework.Example { public class P3_IncreaseCountCommand : AbstractCommand { protected override void OnExecute () { this .GetModel<P2_CounterModel>().Count++; } } }
1 2 3 4 5 6 7 8 9 10 11 12 using QFramework;namespace QFramework.Example { public class P3_DecreaseCountCommand : AbstractCommand { protected override void OnExecute () { this .GetModel<P2_CounterModel>().Count--; } } }
修改Controller
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 void Start (){ mModel = this .GetModel<P2_CounterModel>(); BtnAdd.onClick.AddListener(() => { this .SendCommand<P3_IncreaseCountCommand>(); UpdateView(); }); BtnSub.onClick.AddListener(() => { this .SendCommand<P3_DecreaseCountCommand>(); UpdateView(); }); UpdateView(); }
04. 引入 Event https://www.bilibili.com/read/cv19105066/
Event优势 每次调用逻辑之后,表现逻辑部分都需要手动调用一次(UpdateView 方法)。
在一个项目中,表现逻辑的调用次数,至少会和交互逻辑的调用次数一样多。因为只要修改了数据,对应地就要把数据的变化在界面上表现出来。
而这部分调用表现逻辑的代码也会很多,所以我们引入一个事件机制来解决这个问题。
这个事件机制的使用其实是和 Command 一起使用的,这里有一个简单的小模式,如下图所示:
即通过 Command 修改数据,当数据发生修改后发送对应的数据变更事件。
这个是简化版本的 CQRS 原则,即 Command Query Responsibility Separiation,读写分离原则。
引入这项原则会很容易实现 事件驱动、数据驱动 架构。
代码 新增事件结构体
1 2 3 4 5 6 7 namespace QFramework.Example { public struct P4_CountChangeEvent { } }
修改Command类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 using QFramework;using QFramework.Example;namespace QFramework.Example { public class P4_IncreaseCountCommand : AbstractCommand { protected override void OnExecute () { this .GetModel<P2_CounterModel>().Count++; this .SendEvent<P4_CountChangeEvent>(); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 using QFramework;namespace QFramework.Example { public class P4_DecreaseCountCommand : AbstractCommand { protected override void OnExecute () { this .GetModel<P2_CounterModel>().Count--; this .SendEvent<P4_CountChangeEvent>(); } } }
修改Controller
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 void Start (){ mModel = this .GetModel<P2_CounterModel>(); BtnAdd.onClick.AddListener(() => { this .SendCommand<P4_IncreaseCountCommand>(); }); BtnSub.onClick.AddListener(() => { this .SendCommand<P4_DecreaseCountCommand>(); }); this .RegisterEvent<P4_CountChangeEvent>(e => { UpdateView(); }).UnRegisterWhenGameObjectDestroyed(gameObject); UpdateView(); }
05. 引入 Utility https://www.bilibili.com/read/cv19105101/
需求:存储Model层数据
未引入Utility 修改CounterModel
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 using QFramework;using UnityEngine;namespace QFramework.Example { public class P5_CounterModel : AbstractModel { private int mCount = 0 ; public int Count { get { return mCount; } set { if (mCount != value ) { mCount = value ; PlayerPrefs.SetInt(nameof (Count), value ); } } } protected override void OnInit () { Count = PlayerPrefs.GetInt(nameof (Count), 0 ); } } }
其他类相应修改成 P5_CounterModel
CounterApp、CounterAppControllerMVC、DecreaseCountCommand、IncreaseCountCommand
这样就支持了非常基本的数据存储功能。
当然还是有一些问题,如果时候未来我们需要存储的数据非常多的时候,Model 层就会充斥大量存储、加载相关的代码。
还有就是,我们以后如果不想使用 PlayperPrefs 了,想使用 EasySave 或者 SQLite 的时候,就会造成大量的修改工作量。
于是 QFramework 提供了一个 Utility 层,专门用来解决上述两个问题的,使用方法非常简单,如下:
引入Utility 新增Storage
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 using UnityEngine;namespace QFramework.Example { public class P5_Storage : IUtility { public void SaveInt (string key, int value ) { PlayerPrefs.SetInt(key, value ); } public int LoadInt (string key, int defaultValue = 0 ) { return PlayerPrefs.GetInt(key, defaultValue); } } }
修改CounterApp,将Storage注册进IOC
1 2 3 4 5 6 7 8 9 10 11 namespace QFramework.Example { public class P5_CounterApp : Architecture <P5_CounterApp > { protected override void Init () { this .RegisterUtility(new P5_Storage()); this .RegisterModel(new P5_CounterModel()); } } }
修改CounterModel
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 QFramework;using UnityEngine;namespace QFramework.Example { public class P5_CounterModel : AbstractModel { private int mCount = 0 ; public int Count { get { return mCount; } set { if (mCount != value ) { mCount = value ; this .GetUtility<P5_Storage>().SaveInt(nameof (Count), value ); } } } protected override void OnInit () { Count = this .GetUtility<P5_Storage>().LoadInt(nameof (Count), 0 ); } } }
06. 引入 System https://www.bilibili.com/read/cv19105164/
未引入System 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 using QFramework;using QFramework.Example;using UnityEngine;namespace QFramework.Example { public class P6_IncreaseCountCommand : AbstractCommand { protected override void OnExecute () { this .GetModel<P5_CounterModel>().Count++; this .SendEvent<P4_CountChangeEvent>(); } } }
这个功能很快就完成了。
但是这个时候策划说,希望再增加一个当点击 - 号到 -10 时,触发一个 点击菜鸟成就,然后策划还说,点击达人 和 点击专家 成就太容易达成了,需要分别改成 1000 次 和 2000 次。
而这次策划提出的需求,需要我们修改两处的代码,即 IncreaseCountCommand 里需要修改数值为 1000 和 2000,然后再 DecreaseCountCommand 增加一个判断逻辑。
一次提出的需求,结果造成了多处修改,这说明代码有问题。
首先像这种规则类的逻辑,比如分数统计或者成就统计等代码,不适合分散写在 Command 里,而适合统一写在一个对象里,而这种对象,在 QFramework 里有提供,就是 System 对象。
引入System 新增AchievementSystem
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 using UnityEngine;namespace QFramework.Example { public class P6_AchievementSystem : AbstractSystem { protected override void OnInit () { var model = this .GetModel<P5_CounterModel>(); this .RegisterEvent<P4_CountChangeEvent>(e => { if (model.Count == 10 ) { Debug.Log("点击达人成就达成!" ); } else if (model.Count == 20 ) { Debug.Log("点击成就专家达成!" ); } else if (model.Count == -10 ) { Debug.Log("点击菜鸟成就达成!" ); } }); } } }
修改CounterApp,CounterAppControllerMVC相应代码
修改IncreaseCommand相关代码
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 using QFramework;using QFramework.Example;using UnityEngine;namespace QFramework.Example { public class P6_IncreaseCountCommand : AbstractCommand { protected override void OnExecute () { this .GetModel<P5_CounterModel>().Count++; this .SendEvent<P4_CountChangeEvent>(); } } }
框架基础总结 概念总结 到此,大家应该能看懂[这两张图了](#01. QFramework 架构简介)
QFramework 总共分四个层级,即
表现层:IController
系统层:ISystem
数据层:IModel
工具层:IUtility
除了四个层级,还接触了为 Controller 的交互逻辑减负的 Command 和 为表现逻辑减负的 Event。
还有一个非常重要的 CQRS 原则的简易版本,Command->Model->State Changed Event。
到目前为止 QFramework 的基本用法我们过了一遍了。
从下一篇开始,我们开始介绍 QFramework 架构提供的剩余功能,这些功能是可选的。
这篇就到这里。
完整代码 CounterAppController 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 59 60 61 62 63 64 using System.Collections;using System.Collections.Generic;using Unity.VisualScripting;using UnityEngine;using UnityEngine.UI;namespace QFramework.Example { public class CounterAppController : MonoBehaviour , IController { public Button BtnAdd; public Button BtnSub; public Text CountText; private CounterModel mModel; void Start () { mModel = this .GetModel<CounterModel>(); BtnAdd.onClick.AddListener(() => { this .SendCommand<IncreaseCountCommand>(); }); BtnSub.onClick.AddListener(() => { this .SendCommand<DecreaseCountCommand>(); }); this .RegisterEvent<CountChangeEvent>(e => { UpdateView(); }).UnRegisterWhenGameObjectDestroyed(gameObject); UpdateView(); } void UpdateView () { CountText.text = mModel.Count.ToString(); } public IArchitecture GetArchitecture () { return CounterApp.Interface; } } }
CounterModel 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 using QFramework;using UnityEngine;namespace QFramework.Example { public class CounterModel : AbstractModel { private int mCount = 0 ; public int Count { get { return mCount; } set { if (mCount != value ) { mCount = value ; this .GetUtility<Storage>().SaveInt(nameof (Count), value ); } } } protected override void OnInit () { Count = this .GetUtility<Storage>().LoadInt(nameof (Count), 0 ); } } }
CounterApp 1 2 3 4 5 6 7 8 9 10 11 12 namespace QFramework.Example { public class CounterApp : Architecture <CounterApp > { protected override void Init () { this .RegisterUtility(new Storage()); this .RegisterModel(new CounterModel()); this .RegisterSystem(new AchievementSystem()); } } }
Command 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 using QFramework;using QFramework.Example;using UnityEngine;namespace QFramework.Example { public class IncreaseCountCommand : AbstractCommand { protected override void OnExecute () { this .GetModel<CounterModel>().Count++; this .SendEvent<CountChangeEvent>(); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 using QFramework;namespace QFramework.Example { public class DecreaseCountCommand : AbstractCommand { protected override void OnExecute () { this .GetModel<CounterModel>().Count--; this .SendEvent<CountChangeEvent>(); } } }
Event 1 2 3 4 5 6 7 namespace QFramework.Example { public struct CountChangeEvent { } }
Storage 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 using UnityEngine;namespace QFramework.Example { public class Storage : IUtility { public void SaveInt (string key, int value ) { PlayerPrefs.SetInt(key, value ); } public int LoadInt (string key, int defaultValue = 0 ) { return PlayerPrefs.GetInt(key, defaultValue); } } }
AchievementSystem 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 using UnityEngine;namespace QFramework.Example { public class AchievementSystem : AbstractSystem { protected override void OnInit () { var model = this .GetModel<CounterModel>(); this .RegisterEvent<CountChangeEvent>(e => { if (model.Count == 10 ) { Debug.Log("点击达人成就达成!" ); } else if (model.Count == 20 ) { Debug.Log("点击成就专家达成!" ); } else if (model.Count == -10 ) { Debug.Log("点击菜鸟成就达成!" ); } }); } } }
———- 进阶 ———- 07. 使用 BindableProperty 优化事件 https://www.bilibili.com/read/cv19105225/
在这篇我们介绍一个新的概念 BindableProperty。
BindableProperty 是包含 数据 + 数据变更事件 的一个对象。
一般情况下,像主角的金币、分数等数据非常适合用 BindableProperty 的方式实现。
基础用法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 var age = new BindableProperty<int >(5 );age.Register(newAge => { Debug.Log(newAge); }).UnRegisterWhenGameObjectDestroyed(gameObject); age.SetValueWithoutEvent(10 ); age.Value++; age.Value--;
带初始调用
1 2 3 4 5 6 7 8 9 10 var age = new BindableProperty<int >(5 );age.RegisterWithInitValue(newAge => { Debug.Log(newAge); });
08. 用接口设计模块(依赖倒置原则) https://www.bilibili.com/read/cv19105281/
QFramework 本身支持依赖倒置原则,就是所有的模块访问和交互都可以==通过接口来完成==。
09. Query 介绍 https://www.bilibili.com/read/cv19119047/
Query 是 CQRS 中的 Q,也就是 Command Query Responsibility Saperation 中的 Query。
关于 Command 我们已经介绍了。
而 Query 是和 Command 对应的查询对象。
首先 Controller 中的表现逻辑更多是接收到数据变更事件之后,对 Model 或者 System 进行查询,而查询的时候,有的时候需要组合查询,比如多个 Model 一起查询,查询的数据可能还需要转换一下,这种查询的代码量比较多。尤其是像模拟经营或者非常重数据的项目,所以 QFramework 支持通过 Query 这样的一个概念,来解决这部分问题。
Command 一般负责数据的 ==增 删 改==,而 Query 负责数据的 ==查==。
如果游戏需要从服务器同步数据,一般拉取服务器数据的请求可以写在 Query 中,而增删改服务器输的请求可以写在 Command 中。
创建Query
1 2 3 4 5 6 7 8 public class SchoolAllPersonCountQuery : AbstractQuery <int >{ protected override int OnDo () { return this .GetModel<StudentModel>().StudentNames.Count + this .GetModel<TeacherModel>().TeacherNames.Count; } }
Query请求
1 2 3 4 5 6 7 8 9 private void OnGUI (){ GUILayout.Label(mAllPersonCount.ToString()); if (GUILayout.Button("查询学校总人数" )) { mAllPersonCount = this .SendQuery(new SchoolAllPersonCountQuery()); } }
10. 架构规范 与 推荐用法 https://www.bilibili.com/read/cv19119101/
通用规则:
IController 更改 ISystem、IModel 的状态必须用Command
ISystem、IModel 状态发生变更后通知 IController 必须用事件或BindableProperty
IController可以获取ISystem、IModel对象来进行数据查询
ICommand、IQuery 不能有状态,
上层可以直接获取下层,下层不能获取上层对象
下层向上层通信用事件
上层向下层通信用方法调用(只是做查询,状态变更用 Command),IController 的交互逻辑为特别情况,只能用 Command
11. 光速实现 EditorCounterApp 和 给主程看的开发模式 https://www.bilibili.com/read/cv19119108/
代码 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 System;using System.Collections;using System.Collections.Generic;using UnityEditor;using UnityEngine;namespace QFramework.Example { public class P11_EditorCounterAppController : EditorWindow , IController { private ICounterModel mModel; [MenuItem("CounterApp/Window" ) ] public static void Open () { var counterApp = GetWindow<P11_EditorCounterAppController>(); counterApp.Show(); } private void OnEnable () { mModel = this .GetModel<ICounterModel>(); } private void OnDisable () { mModel = null ; } private void OnGUI () { if (GUILayout.Button("+" )) { this .SendCommand<IncreaseCountCommand>(); } GUILayout.Label(mModel.Count.Value.ToString()); if (GUILayout.Button("-" )) { this .SendCommand<DecreaseCountCommand>(); } } public IArchitecture GetArchitecture () { return CounterApp.Interface; } } }
12. 纸上设计 https://www.bilibili.com/read/cv19119119/
13. Architecture 的好处 https://www.bilibili.com/read/cv19119132/
14. Command 拦截 https://www.bilibili.com/read/cv19135184/
有了 Command 拦截功能,我们可以做非常多的事情,比如:
Command 日志可以用来方便调试
可以实现 Command 中间件模式 可以写各种各样额度 Command 中间件,比如 Command 日志中间件
可以方便你写撤销功能
可以用 Command 做自动化测试
等等
代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class CounterApp : Architecture <CounterApp >{ protected override void Init () { this .RegisterSystem<IAchievementSystem>(new AchievementSystem()); this .RegisterModel<ICounterAppModel>(new CounterAppModel()); this .RegisterUtility<IStorage>(new Storage()); } protected override void ExecuteCommand (ICommand command ) { Debug.Log("Before " + command.GetType().Name + "Execute" ); base .ExecuteCommand(command); Debug.Log("After " + command.GetType().Name + "Execute" ); } }
15. 内置工具:TypeEventSystem https://www.bilibili.com/read/cv19135216/
1.基础 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 System;using UnityEngine;namespace QFramework.Example { public class TypeEventSystemBasicExample : MonoBehaviour { public struct EventA { public int Count; } private void Start () { TypeEventSystem.Global.Register<EventA>(e => { Debug.Log(e.Count); }).UnRegisterWhenGameObjectDestroyed(gameObject); } private void Update () { if (Input.GetMouseButton(0 )) { TypeEventSystem.Global.Send<EventA>(); TypeEventSystem.Global.Send<EventA>(new EventA() { Count = 10 }); } } } }
2.事件继承 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 System;using UnityEngine;namespace QFramework.Example { public class TypeEventSystemInheritExample : MonoBehaviour { public interface IEvent { } public struct EventA : IEvent { public int Count; } private void Start () { TypeEventSystem.Global.Register<IEvent>(e => { if (e is EventA) { Debug.Log("receive A: " + ((EventA)e).Count); } }).UnRegisterWhenGameObjectDestroyed(gameObject); } private void Update () { if (Input.GetMouseButton(0 )) { TypeEventSystem.Global.Send<IEvent>(new EventA() { Count = 10 }); } } } }
3.多事件 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 59 using System;using UnityEngine;namespace QFramework.Example { public class TypeEventSystemMoreExample : MonoBehaviour , IOnEvent <TypeEventSystemMoreExample.EventA >, IOnEvent <TypeEventSystemMoreExample.EventB > { public struct EventA { public int Count; } public struct EventB { public int Age; } private void Start () { this .RegisterEvent<EventA>().UnRegisterWhenGameObjectDestroyed(gameObject); this .RegisterEvent<EventB>(); } private void OnDestroy () { this .UnRegisterEvent<EventB>(); } private void Update () { if (Input.GetMouseButton(0 )) { TypeEventSystem.Global.Send<EventA>(); TypeEventSystem.Global.Send(new EventB() { Age = 10 }); } } public void OnEvent (EventA e ) { Debug.Log("A: " + e.Count); } public void OnEvent (EventB e ) { Debug.Log("B: " + e.Age); } } }
4.非GameObject 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 System;using System.Collections.Generic;using UnityEngine;namespace QFramework.Example { public class TypeEventSystemNoneMonoExample : IUnRegisterList { public List<IUnRegister> UnregisterList { get ; } = new List<IUnRegister>(); public struct EventA { public int Count; } private void Start () { TypeEventSystem.Global.Register<EventA>(e => { Debug.Log(e.Count); }).AddToUnregisterList(this ); } void OnDestroy () { this .UnRegisterAll(); } } }
小结 如果想手动注销,必须要创建一个用于接收事件的方法。
而用自动注销则直接用委托即可。
这两个各有优劣,按需使用。
另外,事件的定义最好使用 struct,因为 struct 的 gc 更少,可以获得更好的性能。
接口事件拥有更好的约束,也可以通过 IDE 的代码生成来提高开发效率。
总之 TypeEventSystem 是一个非常强大的事件工具。
16. 内置工具:EasyEvent https://www.bilibili.com/read/cv19135257/
EasyEvent优势 EasyEvent 是 C# 委托和事件的替代。
EasyEvent 相比 C# 委托和事件,优势是可以自动注销。
相比 TypeEventSystem,优势是更轻量,大多数情况下不用声明事件类,而且性能更好(接近 C# 委托)。
缺点则是其携带的参数没有名字,需要自己定义名字。
在设计一些通用系统的时候,EasyEvent 会派上用场,比如背包系统、对话系统,TypeEventSystem 是一个非常好的例子。
在一个项目早期做原型验证时,EasyEvent 也会起非常大的作用,QFramework 架构中的事件,其实写起来有点繁琐,而在项目早期快速迭代原型是重点,此时用 EasyEvent 可以获得更快的开发效率,而使用 QFramework 架构中的事件在项目规模更大的时候会发挥很大的作用,它更方便协作更容易维护,也更容易标准化。
代码 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 System;using UnityEngine;namespace QFramework.Example { public class EasyEventExample : MonoBehaviour { public EasyEvent EasyEventA = new EasyEvent(); public EasyEvent<int > OnCountChangeEvent = new EasyEvent<int >(); private void Start () { EasyEventA.Register(() => { Debug.Log("EasyEventA Trigger!" ); }).UnRegisterWhenGameObjectDestroyed(gameObject); OnCountChangeEvent.Register(count => { Debug.Log(count); }); EasyEvents.Register<EasyEvent<int , string >>(); EasyEvents.Get<EasyEvent<int , string >>().Register((age, name) => { Debug.Log("name: " + name + " age: " + age); }).UnRegisterWhenGameObjectDestroyed(gameObject); } public void OnEasyEventB (int age, string name ) { Debug.Log("name: " + name + " age: " + age); } private void Update () { if (Input.GetMouseButton(0 )) { EasyEventA.Trigger(); OnCountChangeEvent.Trigger(10 ); EasyEvents.Get<EasyEvent<int , string >>().Trigger(18 , "小聪" ); } } } }
17. 内置工具:BindableProperty https://www.bilibili.com/read/cv19135363/
代码 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;namespace QFramework.Example { public class BindablePropertyExample : MonoBehaviour { private BindableProperty<int > mSomeValue = new BindableProperty<int >(0 ); private BindableProperty<string > mName = new BindableProperty<string >("QFramework" ); void Start () { mSomeValue.Register(newValue => { Debug.Log(newValue); }).UnRegisterWhenGameObjectDestroyed(gameObject); mName.RegisterWithInitValue(newName => { Debug.Log(mName); }).UnRegisterWhenGameObjectDestroyed(gameObject); } void Update () { if (Input.GetMouseButtonDown(0 )) { mSomeValue.Value++; } } } }
18. 内置工具:IOCContainer https://www.bilibili.com/read/cv19135635/
QFramework 架构的模块注册与获取是通过 IOCContainer 实现的。
IOC 的意思是控制反转,即控制反转容器。
其技术的本质很简单,本质就是一个字典,Key 是 Type,Value 是 Object,即:Dictionary<Type,object>。
QFramework 架构中的 IOCContainer 是一个非常简易版本的控制翻转容器,仅支持了注册对象为单例的模式。
一般情况下,其他的控制反转容器会有各种各样的对象注册模式,有的甚至会内置对象池和对象工厂,比如 Zenject。
19. 心中有架构 https://www.bilibili.com/read/cv19160067/
20. QFramework.cs 的更多内容 https://www.bilibili.com/read/cv19160129/
工具篇 视频链接:https://www.bilibili.com/video/BV19D4y1476u/
QFramework.Toolkits 是包含 QFramework.cs 和 大量工具集的解决方案。
https://www.bilibili.com/read/cv19160267/
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 QFramework;using UnityEngine;using UnityEngine.UI;namespace liangxiegame { public partial class UIGamePanel : UIPanel { private ResLoader mResLoader; protected override void OnInit (IUIData uiData = null ) { mResLoader = ResLoader.Allocate(); mResLoader.LoadSync<GameObject>("GameplayRoot" ) .Instantiate() .Identity() .GetComponent<GameplayRoot>() .InitGameplayRoot(); BtnPause.onClick.AddListener(() => { AudioKit.PlaySound("btn_click" ); ActionKit.Sequence() .Callback(() => BtnPause.interactable = false ) .Callback(() => BtnPause.PlayBtnFadeAnimation()) .Delay(0.3f ) .Callback(() => UIKit.OpenPanel<UIPausePanel>()) .Start(this ); }); } protected override void OnClose () { mResLoader.Recycle2Cache(); mResLoader = null ; } } }
02. 下载与安装 Ctrl+E https://www.bilibili.com/read/cv19160423/
打开工具集:Ctrl + E
03. CodeGenKit 脚本生成 https://www.bilibili.com/read/cv19160448/
ViewController脚本:管理者,自动将所有的成员赋值定义在分部类内
Bind脚本:成员,一个脚本只能绑定一个最近的ViewController(最近:父类中最接近的一级)。名称自动为对象名。
Other Binds脚本:配合ViewController使用,可以主动绑定任何对象。名称可自定义。
04. ActionKit 时序动作执行系统 https://www.bilibili.com/read/cv19182026/
发现:如果Start时传入self,会导致运行时在self创建MonoUpdateActionExecutor脚本。即使动作执行完成,该脚本仍然存在。在意的话可以使用扩展方法,添加一个自销毁逻辑。
建议直接运行项目的ActionKit的Example包内容!!
1.基础用法 1 2 3 4 5 6 Debug.Log("Start Time:" + Time.time); ActionKit.Delay(1.0f , () => { Debug.Log("End Time:" + Time.time); }).Start(this );
2.使用Sequence 1 2 3 4 5 6 7 8 Debug.Log("Sequence Start:" + Time.time); ActionKit.Sequence() .Callback(() => Debug.Log("Delay Start:" + Time.time)) .Delay(1.0f ) .Callback(() => Debug.Log("Delay Finish:" + Time.time)) .Start(this , _ => { Debug.Log("Sequence Finish:" + Time.time); });
3.帧延时 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 Debug.Log("Delay Frame Start FrameCount:" + Time.frameCount); ActionKit.DelayFrame(1 , () => { Debug.Log("Delay Frame Finish FrameCount:" + Time.frameCount); }) .Start(this ); ActionKit.Sequence() .DelayFrame(10 ) .Callback(() => Debug.Log("Sequence Delay FrameCount:" + Time.frameCount)) .Start(this ); ActionKit.NextFrame(() => { }).Start(this );
4.条件执行 1 2 3 4 5 6 ActionKit.Sequence() .Callback(() => Debug.Log("Before Condition" )) .Condition(() => Input.GetMouseButtonDown(0 )) .Callback(() => Debug.Log("Mouse Clicked" )) .Start(this );
5.重复执行 1 2 3 4 5 6 7 8 9 10 11 12 13 14 ActionKit.Repeat() .Condition(() => Input.GetMouseButtonDown(0 )) .Callback(() => Debug.Log("Mouse Clicked" )) .Start(this ); ActionKit.Repeat(5 ) .Condition(() => Input.GetMouseButtonDown(1 )) .Callback(() => Debug.Log("Mouse right clicked" )) .Start(this , () => { Debug.Log("Right click finished" ); });
6.并行执行 1 2 3 4 5 6 7 8 9 10 11 Debug.Log("Parallel Start:" + Time.time); ActionKit.Parallel() .Delay(1.0f , () => { Debug.Log(Time.time); }) .Delay(2.0f , () => { Debug.Log(Time.time); }) .Delay(3.0f , () => { Debug.Log(Time.time); }) .Start(this , () => { Debug.Log("Parallel Finish:" + Time.time); });
7.综合示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 ActionKit.Sequence() .Callback(() => Debug.Log("Sequence Start" )) .Callback(() => Debug.Log("Parallel Start: " + Time.time)) .Parallel(p => { p.Delay(1.0f , () => Debug.Log("Delay 1s Finished" )) .Delay(2.0f , () => Debug.Log("Delay 2s Finished" )); }) .Callback(() => Debug.Log("Parallel Finished: " + Time.time)) .Callback(() => Debug.Log("Check Mouse Clicked" )) .Sequence(s => { s.Condition(() => Input.GetMouseButton(0 )) .Callback(() => Debug.Log("Mouse Clicked" )); }) .Start(this , () => { Debug.Log("Finish" ); });
8.自定义动作 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 59 60 61 62 63 64 65 66 67 68 69 70 using UnityEngine;namespace QFramework.Example { public class CustomExample : MonoBehaviour { class SomeData { public int ExecuteCount = 0 ; } private void Start () { ActionKit.Custom(a => { a .OnStart(() => { Debug.Log("OnStart" ); }) .OnExecute(dt => { Debug.Log("OnExecute" ); a.Finish(); }) .OnFinish(() => { Debug.Log("OnFinish" ); }); }).Start(this ); ActionKit.Custom<SomeData>(a => { a .OnStart(() => { a.Data = new SomeData() { ExecuteCount = 0 }; }) .OnExecute(dt => { Debug.Log(a.Data.ExecuteCount); a.Data.ExecuteCount++; if (a.Data.ExecuteCount >= 5 ) { a.Finish(); } }).OnFinish(() => { Debug.Log("Finished" ); }); }) .Start(this ); } } }
9.协程支持 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 System.Collections;using UnityEngine;namespace QFramework.Example { public class CoroutineExample : MonoBehaviour { private void Start () { ActionKit.Coroutine(SomeCoroutine).Start(this ); SomeCoroutine().ToAction().Start(this ); ActionKit.Sequence() .Coroutine(SomeCoroutine) .Start(this ); } IEnumerator SomeCoroutine () { yield return new WaitForSeconds (1.0f ) ; Debug.Log("Hello:" + Time.time); } } }
10.全局Mono生命周期 将MonoBehaviour的生命周期函数写在一起
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;namespace QFramework.Example { public class GlobalMonoEventsExample : MonoBehaviour { void Start () { ActionKit.OnUpdate.Register(() => { if (Time.frameCount % 30 == 0 ) { Debug.Log("Update" ); } }).UnRegisterWhenGameObjectDestroyed(gameObject); ActionKit.OnFixedUpdate.Register(() => { }).UnRegisterWhenGameObjectDestroyed(gameObject); ActionKit.OnLateUpdate.Register(() => { }).UnRegisterWhenGameObjectDestroyed(gameObject); ActionKit.OnGUI.Register(() => { GUILayout.Label("See Example Code" ); GUILayout.Label("请查看示例代码" ); }).UnRegisterWhenGameObjectDestroyed(gameObject); ActionKit.OnApplicationFocus.Register(focus => { Debug.Log("focus:" + focus); }).UnRegisterWhenGameObjectDestroyed(gameObject); ActionKit.OnApplicationPause.Register(pause => { Debug.Log("pause:" + pause); }).UnRegisterWhenGameObjectDestroyed(gameObject); ActionKit.OnApplicationQuit.Register(() => { Debug.Log("quit" ); }).UnRegisterWhenGameObjectDestroyed(gameObject); } } }
05. ResKit 资源管理&开发解决方案 https://www.bilibili.com/read/cv19182075/
这里注意,一次标记就是一个 AssetBundle,如果想要让 AssetBundle 包含多个资源,可以将多个资源放到一个文件夹中,然后标记文件夹。
06. UIKit 界面管理&快速开发解决方案 https://www.bilibili.com/read/cv19182123/