游戏开发框架-QFramework

Unity学习 | 了解QFramework游戏开发框架

【专栏 /视频】QFramework v1.0 使用指南 - GamePix 独立游戏学院

QFramework 简介·

QFramework 是一套渐进式、快速开发框架,适用于任何类型的游戏及应用项目。

QFramework 包含一套 开发架构 和 大量的工具集。

QFramework 特性速览:

  • 开发架构(QFramework.cs v1.0)
    • 简单、易上手、强大
    • MVC
    • IOC、分层支持
    • CQRS 支持
    • 符合 SOLID原则
    • 可以使用 DDD 的方式设计项目
    • 不到 1000 行代码
  • 工具集(QFramework.Toolkits v0.16)
    • UIKit 界面&View快速开发&管理解决方案
      • UI、GameObject 的代码生成&自动赋值
      • 界面管理
      • 层级管理
      • 界面堆栈
      • 默认使用 ResKit 方式管理界面资源
      • 可自定义界面的加载、卸载方式
      • Manager Of Manager 架构集成(不推荐使用)
    • ResKit 资源快速开发&管理解决方案
      • AssetBundle 提供模拟模式,开发阶段无需打包即可加载资源
      • 资源名称代码生成支持
      • 同一个 API 可加载 AssetBundle、Resources、网络 和 自定义来源的资源
      • 提供一套引用计数的资源管理模型
    • AudioKit 音频管理解决方案
      • 提供背景音乐、人声、音效 三种音频播放 API
      • 音量控制
      • 默认使用 ResKit 方式管理音频资源
      • 可自定义音频的加载、卸载方式
    • CoreKit 提供大量的代码工具
      • ActionKit:动作序列执行系统
      • CodeGenKit:代码生成 & 自动序列化赋值工具
      • EventKit:提供基于类、字符串、枚举以及信号类型的事件工具集
      • FluentAPI:对大量的 Unity 和 C# 常用的 API 提供了静态扩展的封装(链式 API)
      • IOCKit:提供依赖注入容器
      • LocaleKit:本地化&多语言工具集
      • LogKit:日志工具集
      • PackageKit:包管理工具,由此可更新框架和对应的插件模块。
      • PoolKit:对象池工具集,提供对象池的基础上,也提供 ListPool 和 Dictionary Pool 等工具。
      • SingletonKit:单例工具集
      • TableKit:提供表格类数据结构的工具集

QFramework 的设计哲学是从每个细节上提升开发效率。

同时 QFramework 还包含丰富的生态。

QFramework 内置模块·

  1. Framework:核心架构(包含一套系统设计架构)
  2. CoreKit: 核心工具库、插件管理
  3. ResKit:资源管理套件(快速开发)
  4. UIKit:UI 管理套件(支持自动绑定、代码生成)
  5. Audio:音频方案

相关下载

  • QFramework 下载
    • 开箱即用的 Unity 开发框架
    • 内置 Res Kit、UI Kit、Audio 以及常用的工具库
  • 插件平台
    • 包含丰富的模块和解决方案

QFramework 架构·

这套架构的特性如下:

  • 基于 MVC
  • 分层
  • (可选)CQRS 支持
  • (可选)事件驱动
  • (可选)数据驱动
  • (可选)IOC 模块化
  • (可选)领域驱动设计(DDD)支持
  • 符合 SOLID 原则
  • 源码不到 1000 行

QFramework 架构提供了四个层级:

  • 表现层:IController
  • 系统层:ISystem
  • 数据层:IModel
  • 工具层:IUtility

除了四个层级,还提供了 Command、Query、Event、BindableProperty 等概念和工具。

层级之间的规则·

  • 表现层:ViewController层,使用IController接口实现。
    负责接收输入和状态变化时的表现,一般情况下,MonoBehaviour 均为表现层

    • 可以获取 System、Model
    • 可以发送 Command、Query
    • 可以监听 Event
  • 系统层:System层。使用ISystem接口实现。
    帮助IController承担一部分逻辑,在多个表现层共享的逻辑,比如计时系统、商城系统、成就系统等

    • 可以获取 System、Model
    • 可以监听Event
    • 可以发送Event
  • 数据层:Model层。使用IModel接口实现。
    负责数据的定义、数据的增删查改方法的提供

    • 可以获取 Utility
    • 可以发送 Event
  • 工具层:Utility层。使用IUtility接口实现。
    负责提供基础设施,比如存储方法、序列化方法、网络连接方法、蓝牙方法、SDK、框架继承等。啥都干不了,可以集成第三方库,或者封装API

  • Command:命令,负责数据的增删改。

    • 可以获取 System、Model
    • 可以发送 Event、Command
  • Query:查询、负责数据的查询

    • 可以获取 System、Model
    • 可以发送 Query
  • 通用规则:

    • IController 更改 ISystem、IModel 的状态必须用Command
    • ISystem、IModel 状态发生变更后通知 IController 必须用事件或BindableProperty
    • IController可以获取ISystem、IModel对象来进行数据查询
    • ICommand、IQuery 不能有状态,
    • 上层可以直接获取下层,下层不能获取上层对象
    • 下层向上层通信用事件
    • 上层向下层通信用方法调用(只是做查询,状态变更用 Command),IController 的交互逻辑为特别情况,只能用 Command

通用规则是理想状态下的一套规则,但是落实的实际项目,很有可能需要对以上规则做一些修改。

交互逻辑和表现逻辑·

交互逻辑,就是从用户输入开始到数据变更的逻辑

顺序是 View->Controller->Model

表现逻辑,就是数据变更到在界面显示的逻辑

顺序是 Model->Controller->View

虽然交互逻辑和表现逻辑理解起来简单,但是它们非常重要,因为 QFramework 接下来的概念都是围绕这两个概念展开的。

View、Model 以及 Controller 的交互逻辑和表现逻辑形成了一个闭环。构成了完整的 MVC 闭环。


而 Controller 本身之所以臃肿,是因为,它负责了两种职责,即改变 Model 数据 的交互逻辑,以及 Model 数据变更之后更新到界面的表现逻辑。

而在一个有一定规模的项目中,表现逻辑和交互逻辑非常多。而一个 Controller 很容易就做到上千行代码。


而大部分的 MVC 方案,解决 Controller 臃肿用的是引入 Command 的方式,即引入命令模式,通过命令来分担 Controller 的交互逻辑的职责。

QFramework 也是使用了同样的方式解决 Controller 臃肿的问题。

案例-计数器·

普通的MVC框架·

Model、View、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
using UnityEngine;
using UnityEngine.UI;

namespace QFramework.Example
{
// Controller
public class CounterAppController : MonoBehaviour
{
// View
private Button mBtnAdd;
private Button mBtnSub;
private Text mCountText;

// Model
private int mCount = 0;

void Start()
{
// View 组件获取
mBtnAdd = transform.Find("BtnAdd").GetComponent<Button>();
mBtnSub = transform.Find("BtnSub").GetComponent<Button>();
mCountText = transform.Find("CountText").GetComponent<Text>();

// 监听输入
mBtnAdd.onClick.AddListener(() =>
{
// 交互逻辑
mCount++;
// 表现逻辑
UpdateView();
});

mBtnSub.onClick.AddListener(() =>
{
// 交互逻辑
mCount--;
// 表现逻辑
UpdateView();
});

UpdateView();
}

void UpdateView()
{
mCountText.text = mCount.ToString();
}
}
}

Model 的代码·

1
2
// Model
private int mCount = 0;

非常简单,只有一个成员变量,但是在这里它其实并不算是一个 Model,他只是要在 View 中显示的一个数据而已,具体为什么不是 Model 我们在后边再说。

View 的代码·

1
2
3
4
// View
private Button mBtnAdd;
private Button mBtnSub;
private Text mCountText;

View 的代码也很简单,View 在 QFramework 的 MVC 定义里就是提供关键组件的引用,比如这三个组件是要在 Controller 代码里要用到的。而其他的例如 Canvas Scaler 等这些组件目前 Controller 不需要,所以就不用声明。

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
void Start()
{
...

// 监听输入
mBtnAdd.onClick.AddListener(() =>
{
// 交互逻辑
mCount++;
// 表现逻辑
UpdateView();
});

mBtnSub.onClick.AddListener(() =>
{
// 交互逻辑
mCount--;
// 表现逻辑
UpdateView();
});

UpdateView();
}

void UpdateView()
{
mCountText.text = mCount.ToString();
}

以上就是 Controller 的代码。

使用了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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
using UnityEngine;
using UnityEngine.UI;

namespace QFramework.Example
{

// 1. 定义一个 Model 对象
public interface ICounterAppModel : IModel
{
BindableProperty<int> Count { get; }
}
public class CounterAppModel : AbstractModel,ICounterAppModel
{
public BindableProperty<int> Count { get; } = new BindableProperty<int>();

protected override void OnInit()
{
var storage = this.GetUtility<IStorage>();

// 设置初始值(不触发事件)
Count.SetValueWithoutEvent(storage.LoadInt(nameof(Count)));

// 当数据变更时 存储数据
Count.Register(newCount =>
{
storage.SaveInt(nameof(Count),newCount);
});
}
}

public interface IAchievementSystem : ISystem
{

}

public class AchievementSystem : AbstractSystem ,IAchievementSystem
{
protected override void OnInit()
{
this.GetModel<ICounterAppModel>() // -+
.Count
.Register(newCount =>
{
if (newCount == 10)
{
Debug.Log("触发 点击达人 成就");
}
else if (newCount == 20)
{
Debug.Log("触发 点击专家 成就");
}
else if (newCount == -10)
{
Debug.Log("触发 点击菜鸟 成就");
}
});
}
}

public interface IStorage : IUtility
{
void SaveInt(string key, int value);
int LoadInt(string key, int defaultValue = 0);
}

public class Storage : IStorage
{
public void SaveInt(string key, int value)
{
PlayerPrefs.SetInt(key,value);
}

public int LoadInt(string key, int defaultValue = 0)
{
return PlayerPrefs.GetInt(key, defaultValue);
}
}


// 2.定义一个架构(提供 MVC、分层、模块管理等)
public class CounterApp : Architecture<CounterApp>
{
protected override void Init()
{
// 注册 System
this.RegisterSystem<IAchievementSystem>(new AchievementSystem());

// 注册 Model
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");
}
}

// 引入 Command
public class IncreaseCountCommand : AbstractCommand
{
protected override void OnExecute()
{
var model = this.GetModel<ICounterAppModel>();

model.Count.Value++;
}
}

public class DecreaseCountCommand : AbstractCommand
{
protected override void OnExecute()
{
this.GetModel<ICounterAppModel>().Count.Value--; // -+
}
}

// Controller
public class CounterAppController : MonoBehaviour , IController /* 3.实现 IController 接口 */
{
// View
private Button mBtnAdd;
private Button mBtnSub;
private Text mCountText;

// 4. Model
private ICounterAppModel mModel;

void Start()
{
// 5. 获取模型
mModel = this.GetModel<ICounterAppModel>();

// View 组件获取
mBtnAdd = transform.Find("BtnAdd").GetComponent<Button>();
mBtnSub = transform.Find("BtnSub").GetComponent<Button>();
mCountText = transform.Find("CountText").GetComponent<Text>();

// 监听输入
mBtnAdd.onClick.AddListener(() =>
{
// 交互逻辑
this.SendCommand<IncreaseCountCommand>();
});

mBtnSub.onClick.AddListener(() =>
{
// 交互逻辑
this.SendCommand(new DecreaseCountCommand(/* 这里可以传参(如果有) */));
});

// 表现逻辑
mModel.Count.RegisterWithInitValue(newCount => // -+
{
UpdateView();

}).UnRegisterWhenGameObjectDestroyed(gameObject);
}

void UpdateView()
{
mCountText.text = mModel.Count.ToString();
}

// 3.
public IArchitecture GetArchitecture()
{
return CounterApp.Interface;
}

private void OnDestroy()
{
// 8. 将 Model 设置为空
mModel = null;
}
}
}

对比分析·

目前像计数器这样的逻辑,使用MVC框架的代码完全没有问题,甚至显得更简单,代码更少。

但是我们要用发展的眼光看待问题。

假如这是一个初创项目,那么接下来很有可能需要添加大量的业务逻辑。

其中很有可能让 mCount 在多个 Controller 中使用,甚至需要针对 mCount 这个数据写一些其他逻辑,比如增加 mCount 则增加 5 个分数,或者 mCount 需要存储等,总之 mCount 在未来可能会发展成一个需要共享的数据,而 mCount 目前只属于 CounterAppController,显然在未来这是不够用的。

我们就需要让 mCount 成员变量变成一个共享的数据,最快的做法是吧 mCount 变量变成静态变量或者单例,但是这样虽然写起来很快,但是在后期维护额度时候会产生很多的问题。

主架构(Architecture<T>)·

Architecture 用于管理模块,或者说 Architecture 提供一整套架构的解决方案,而模块管理和提供 MVC 只是其功能的一小部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 定义一个架构(提供 MVC、分层、模块管理等)
public class CounterApp : Architecture<CounterApp>
{
protected override void Init()
{
// 注册 System
this.RegisterSystem<IAchievementSystem>(new AchievementSystem());

// 注册 Model
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");
}
}

表现层(Controller层)·

负责接收输入和状态变化时的表现,一般情况下,MonoBehaviour 均为表现层。

使用IController接口实现。

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
// Controller
public class CounterAppController : MonoBehaviour , IController /* 3.实现 IController 接口 */
{
// View
private Button mBtnAdd;
private Button mBtnSub;
private Text mCountText;

// 4. Model
private ICounterAppModel mModel;

void Start()
{
// 5. 获取模型
mModel = this.GetModel<ICounterAppModel>();

// View 组件获取
mBtnAdd = transform.Find("BtnAdd").GetComponent<Button>();
mBtnSub = transform.Find("BtnSub").GetComponent<Button>();
mCountText = transform.Find("CountText").GetComponent<Text>();


// 监听输入
mBtnAdd.onClick.AddListener(() =>
{
// 交互逻辑
this.SendCommand<IncreaseCountCommand>();
});

mBtnSub.onClick.AddListener(() =>
{
// 交互逻辑
this.SendCommand(new DecreaseCountCommand(/* 这里可以传参(如果有) */));
});

// 表现逻辑
mModel.Count.RegisterWithInitValue(newCount => // -+
{
UpdateView();

}).UnRegisterWhenGameObjectDestroyed(gameObject);
}

void UpdateView()
{
mCountText.text = mModel.Count.ToString();
}

// 3.
public IArchitecture GetArchitecture()
{
return CounterApp.Interface;
}

private void OnDestroy()
{
// 8. 将 Model 设置为空
mModel = null;
}
}

系统层(System层)·

帮助IController承担一部分逻辑,在多个表现层共享的逻辑,比如计时系统、商城系统、成就系统等

使用ISystem接口实现。

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
// System 层
public interface IAchievementSystem : ISystem
{

}

public class AchievementSystem : AbstractSystem ,IAchievementSystem
{
protected override void OnInit()
{
this.GetModel<ICounterAppModel>() // -+
.Count
.Register(newCount =>
{
if (newCount == 10)
{
Debug.Log("触发 点击达人 成就");
}
else if (newCount == 20)
{
Debug.Log("触发 点击专家 成就");
}
else if (newCount == -10)
{
Debug.Log("触发 点击菜鸟 成就");
}
});
}
}

数据层(Model层)·

负责数据的定义、提供数据的增删查改方法。

使用IModel接口实现。


Model 的引入是为了解决数据共享的问题,但不只是为了让数据和表现分离,这一点是非常重要的一点。

数据共享分两种:空间上的共享和时间上的共享。

空间的共享很简单,就是多个点的代码需要访问 Model 里的数据。

时间上的共享就是存储功能,将上一次关闭 App 之前的数据存储到一个文件里,这次打开时获得上次关闭 App 之前的数据。


数据共享的问题通过 引入 Model 就可以解决了。

但是,需要共享的数据放 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
// Model 层
public interface ICounterAppModel : IModel
{
BindableProperty<int> Count { get; }
}
public class CounterAppModel : AbstractModel,ICounterAppModel
{
public int Count1; // 不使用BindableProperty的形式
public BindableProperty<int> Count { get; } = new BindableProperty<int>();

protected override void OnInit()
{
Count = 0;
var storage = this.GetUtility<IStorage>();

// 设置初始值(不触发事件)
Count.SetValueWithoutEvent(storage.LoadInt(nameof(Count)));

// 当数据变更时 存储数据
Count.Register(newCount =>
{
storage.SaveInt(nameof(Count),newCount);
});
}
}

工具层(Utility层)·

负责提供基础设施,比如存储方法、序列化方法、网络连接方法、蓝牙方法、SDK、框架继承等。也可以集成第三方库,或者封装API。

使用IUtility接口实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Utility 层
public interface IStorage : IUtility
{
void SaveInt(string key, int value);
int LoadInt(string key, int defaultValue = 0);
}

public class Storage : IStorage
{
public void SaveInt(string key, int value)
{
PlayerPrefs.SetInt(key,value);
}

public int LoadInt(string key, int defaultValue = 0)
{
return PlayerPrefs.GetInt(key, defaultValue);
}
}

命令(Command)·

负责数据的增删改。

参考交互逻辑和表现逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 引入 Command
public class IncreaseCountCommand : AbstractCommand
{
protected override void OnExecute()
{
this.GetModel<ICounterAppModel>().Count.Value++; // +
}
}

public class DecreaseCountCommand : AbstractCommand
{
protected override void OnExecute()
{
this.GetModel<ICounterAppModel>().Count.Value--; // -
}
}

大家可能会问,一个简单的数据加减操作,至于创建一个 Command 对象来承担么?看不出来好处呀,反而代码更多了。

如果整个项目只有一个简单的数据加减操作,那使用 Command 有点多此一举,但是一般的项目的交互逻辑,是非常复杂的,代码量也非常多,整个时候使用 Command 词汇发挥作用。

具体发挥什么作用,使用 Command 可以带来很多便利,比如:

  • Command 可以复用,Command 也可以调用 Command
  • Command 可以比较方便实现撤销功能,如果 App 或者 游戏需要的话
  • 如果遵循一定规范,可以实现使用 Command 跑自动化测试。
  • Command 可以定制 Command 队列,也可以让 Command 按照特定的方式执行
  • 一个 Command 也可以封装成一个 Http 或者 TCP 里的一次数据请求
  • Command 可以实现 Command 中间件模式
  • 等等

OK,通过引入 Command,帮助分担了 Controller 的交互逻辑。使得 Controller 成为一个薄薄的一层,在需要修改 Model 的时候,Controller 只要调用一句简单的 Command 即可。

Command 最明显的好处就是:

  • 就算代码再乱,也只是在一个 Command 对象里乱,而不会影响其他的对象。
  • 讲方法封装成命令对象,可以实现对命令对象的组织、排序、延时等操作。

更多好处会随着大家的实践慢慢体会到。

Command拦截·

QFramework 提供了拦截 Command 的 API。

我们尝试在 CounterApp 中实现一个 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()
{
// 注册 System
this.RegisterSystem<IAchievementSystem>(new AchievementSystem());

// 注册 Model
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");
}
}

只需要在 Architecture 中覆写 ExecuteCommand 即可。

运行之后,随意点击了几次按钮,结果如下:

这样就实现了一个非常简单的 Command 日志功能。

有了 Command 拦截功能,我们可以做非常多的事情,比如:

  • Command 日志可以用来方便调试
  • 可以实现 Command 中间件模式 可以写各种各样额度 Command 中间件,比如 Command 日志中间件
  • 可以方便你先撤销功能
  • 可以用 Command 做自动化测试
  • 等等

事件(Event)·

在一个项目中,表现逻辑的调用次数,至少会和交互逻辑的调用次数一样多。因为只要修改了数据,对应地就要把数据的变化在界面上表现出来。

而这部分使用的表现逻辑的代码也会很多,所以我们引入一个事件机制来解决这个问题。

这个事件机制的使用其实是和 Command 一起使用的,这里有一个简单的小模式,如下图所示:

即通过 Command 修改数据,当数据发生修改后发送对应的数据变更事件

这个是简化版本的 CQRS 原则,即 Command Query Responsibility Separiation,读写分离原则。

引入这项原则会很容易实现 事件驱动、数据驱动 架构。

对象(BindableProperty)·

BindableProperty 是包含 数据 + 数据变更事件 的一个对象。

一般情况下,像主角的金币、分数等数据非常适合用 BindableProperty 的方式实现。


简单的用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
var age = new BindableProperty<int>(10);

age.Register(newAge=>{
Debug.Log(newAge)
}).UnRegisterWhenGameObjectDestoryed(gameObject);

age++;
age--;

// 输出结果
// 11
// 10

非常简单,就是当调用 age++ 和 age-- 的时候,就会触发数据变更事件。

BindableProperty 除了提供 Register 这个 API 之外,还提供了 RegisterWithInitValue API,意思是 注册时 先把当前值返回过来。

具体用法如下:

1
2
3
4
5
6
7
8
var age = new BindableProperty<int>(5);

age.RegisterWithInitValue(newAge => {
Debug.Log(newAge);
});

// 输出结果
// 5

这个 API 就是,没有任何变化的情况下,age 先返回一个当前的值,比较方便用于显示初始界面。

BindableProperty 是一个独立的工具,可以脱离 QFramework 架构使用,也就是说不用非要用 QFramework 的 MVC 才能用 BindableProperty,而是可以再自己项目中随意使用。


具体用法:

1
2
3
4
5
6
7
8
// 设置Count初始值(不触发事件)
Count.SetValueWithoutEvent(storage.LoadInt(nameof(Count)));

// 当Count数据变更时 存储数据
Count.Register(newCount =>
{
storage.SaveInt(nameof(Count),newCount);
});

用接口设计模块(依赖倒置原则)·

QFramework 本身支持依赖倒置原则,就是所有的模块访问和交互都可以通过接口来完成。

所有的模块注册、模块获取等代码都是通过接口完成,这一点符合 SOLID 原则中的 依赖倒置原则。

通过接口设计模块可以让我们更容易思考模块之间的交互和职责本身,而不是具体实现,在设计的时候可以减少很多的干扰。

当然面向接口的方式去做开发也有很多其他的好处,这当然是大家随着使用时长会慢慢体会的。

其中有一个重要的点,就是我们之前说的 Storage,如果想把存储的 API 从 PlayerPrefs 切换成 EasySave,那么我们就不需要去修改 Storage 对象,而是扩展一个 IStorage 接口即可,伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class EasySaveStorage : IStorage
{
public void SaveInt(string key, int value)
{
// todo
}

public int LoadInt(string key, int defaultValue = 0)
{
// todo
throw new System.NotImplementedException();
}
}

注册模块的伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 定义一个架构(用于管理模块)
public class CounterApp : Architecture<CounterApp>
{
protected override void Init()
{
// 注册成就系统
this.RegisterSystem<IAchievementSystem>(new AchievementSystem());

this.RegisterModel<ICounterAppModel>(new CounterAppModel());

// 注册存储工具对象,从Storage换成了EasySaveStorage
// this.RegisterUtility<IStorage>(new Storage());
this.RegisterUtility<IStorage>(new EasySaveStorage());
}
}

这样,底层所有存储的代码都切换成了 EasySave 的存储,替换一套方案非常简单。

Controller·

IController已在QFramework框架中给出,可直接使用

1
2
public class CounterAppController : MonoBehaviour , IController
{ # ... }

System·

ISystem已在QFramework框架中给出,请先继承再使用

1
2
3
4
public interface IAchievementSystem : ISystem
{ }
public class AchievementSystem : AbstractSystem ,IAchievementSystem
{ # ... }

Model·

IModel已在QFramework框架中给出,请先继承再使用

1
2
3
4
5
6
public interface ICounterAppModel : IModel
{
BindableProperty<int> Count { get; }
}
public class CounterAppModel : AbstractModel,ICounterAppModel
{ # ... }

Utility·

IUtility已在QFramework框架中给出,请先继承再使用

1
2
3
4
5
6
7
8
public interface IStorage : IUtility
{
void SaveInt(string key, int value);
int LoadInt(string key, int defaultValue = 0);
}

public class Storage : IStorage
{ # ... }

查询(Query)·

负责数据的查询

Query 是 CQRS 中的 Q,也就是 Command Query Responsibility Saperation 中的 Query。

首先 Controller 中的表现逻辑更多是接收到数据变更事件之后,对 Model 或者 System 进行查询,而查询的时候,有的时候需要组合查询,比如多个 Model 一起查询,查询的数据可能还需要转换一下,这种查询的代码量比较多。尤其是像模拟警用或者非常重数据的项目,所以 QFramework 支持通过 Query 这样的一个概念,来解决这部分问题。


Query 是一个可选的概念,如果游戏中数据的查询逻辑并不是很重的话,直接在 Controller 的表现逻辑里写就可以了,但是查询数据比较重,或者项目规模非常大的话,最好是用 Query 来承担查询的逻辑。

Command 一般负责数据的 增 删 改,而 Query 负责数据的 查。

如果游戏需要从服务器同步数据,一般拉取服务器数据的请求可以写在 Query 中,而增删改服务器输的请求可以写在 Command 中。


使用的方式也很简单,和 Command 用法一致,这里我们写一个小的 App, 叫做 QueryExampleApp 代码如下:

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
71
72
73
74
75
76
77
using System.Collections.Generic;
using UnityEngine;

namespace QFramework.Example
{
public class QueryExampleController : MonoBehaviour,IController
{
public class StudentModel : AbstractModel
{

public List<string> StudentNames = new List<string>()
{
"张三",
"李四"
};

protected override void OnInit()
{

}
}

public class TeacherModel : AbstractModel
{
public List<string> TeacherNames = new List<string>()
{
"王五",
"赵六"
};

protected override void OnInit()
{

}
}

// Architecture
public class QueryExampleApp : Architecture<QueryExampleApp>
{
protected override void Init()
{
this.RegisterModel(new StudentModel());
this.RegisterModel(new TeacherModel());
}
}


/// <summary>
/// 获取学校的全部人数
/// </summary>
public class SchoolAllPersonCountQuery : AbstractQuery<int>
{
protected override int OnDo()
{
return this.GetModel<StudentModel>().StudentNames.Count +
this.GetModel<TeacherModel>().TeacherNames.Count;
}
}

private int mAllPersonCount = 0;

private void OnGUI()
{
GUILayout.Label(mAllPersonCount.ToString());

if (GUILayout.Button("查询学校总人数"))
{
mAllPersonCount = this.SendQuery(new SchoolAllPersonCountQuery());
}
}

public IArchitecture GetArchitecture()
{
return QueryExampleApp.Interface;
}
}
}

运行之后,当按下查询按钮时,结果就会显示。

调试界面(Editor)·

将底层的系统原型与简易的Editor界面配合,把数据和接口调好后,再与UGUI对接。

CoreKit 工具集·

TypeEventSystem·

15.内置工具:TypeEventSystem

Easyevent·

16.内置工具:Easyevent

IOCContainer·

18.内置工具:IOCContainer