【Unity框架】QFramework v1.0 使用指南
聪头 游戏开发萌新

【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 行

提供的架构图

image

举个例子(一图胜千言😂)

image

02. QFramework 的 MVC

https://www.bilibili.com/read/cv19093399/

效果

image

代码:没有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
{
//Controller
public class P2_CounterAppControllerNoMVC : MonoBehaviour
{
//View
public Button BtnAdd;
public Button BtnSub;
public Text CountText;

//Model
public int Count = 0;

// Start is called before the first frame update
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
{
//View
public Button BtnAdd;
public Button BtnSub;
public Text CountText;

//Model
private P2_CounterModel mModel;
// public int Count = 0;

// Start is called before the first frame update
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 会越来越臃肿==。

image

Command优势

使用 Command 可以带来很多便利,比如:

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

Command 最明显的好处就是:

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

代码

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(() =>
{
//交互逻辑
// mModel.Count++;
this.SendCommand<P3_IncreaseCountCommand>();

//表现逻辑
UpdateView();
});

BtnSub.onClick.AddListener(() =>
{
//交互逻辑
// mModel.Count--;
this.SendCommand<P3_DecreaseCountCommand>();

//表现逻辑
UpdateView();
});

UpdateView();
}

04. 引入 Event

https://www.bilibili.com/read/cv19105066/

Event优势

每次调用逻辑之后,表现逻辑部分都需要手动调用一次(UpdateView 方法)。

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

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

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

image

即通过 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(() =>
{
//交互逻辑
// mModel.Count++;
this.SendCommand<P4_IncreaseCountCommand>();

//表现逻辑
// UpdateView();
});

BtnSub.onClick.AddListener(() =>
{
//交互逻辑
// mModel.Count--;
this.SendCommand<P4_DecreaseCountCommand>();

//表现逻辑
// UpdateView();
});

// 表现逻辑
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); //nameof 表达式可生成变量、类型或成员的名称作为字符串常量。
}
}
}
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;
// PlayerPrefs.SetInt(nameof(Count), value); //nameof 表达式可生成变量、类型或成员的名称作为字符串常量。
this.GetUtility<P5_Storage>().SaveInt(nameof(Count), value);
}
}
}
protected override void OnInit()
{
// Count = PlayerPrefs.GetInt(nameof(Count), 0);
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>();

// var model = this.GetModel<P5_CounterModel>();
// if (model.Count == 10)
// {
// Debug.Log("点击达人成就达成!");
// }
// else if (model.Count == 20)
// {
// Debug.Log("点击成就专家达成!");
// }
// else if (model.Count == -10)
// {
// Debug.Log("点击菜鸟成就达成!");
// }
}
}
}

这个功能很快就完成了。

但是这个时候策划说,希望再增加一个当点击 - 号到 -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>();

// var model = this.GetModel<P5_CounterModel>();
// if (model.Count == 10)
// {
// Debug.Log("点击达人成就达成!");
// }
// else if (model.Count == 20)
// {
// Debug.Log("点击成就专家达成!");
// }
// else if (model.Count == -10)
// {
// Debug.Log("点击菜鸟成就达成!");
// }
}
}
}

框架基础总结

概念总结

到此,大家应该能看懂[这两张图了](#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
{
//View
public Button BtnAdd;
public Button BtnSub;
public Text CountText;

//Model
private CounterModel mModel;
// public int Count = 0;

// Start is called before the first frame update
void Start()
{
mModel = this.GetModel<CounterModel>();

BtnAdd.onClick.AddListener(() =>
{
//交互逻辑
// mModel.Count++;
this.SendCommand<IncreaseCountCommand>();

//表现逻辑
// UpdateView();
});

BtnSub.onClick.AddListener(() =>
{
//交互逻辑
// mModel.Count--;
this.SendCommand<DecreaseCountCommand>();

//表现逻辑
// UpdateView();
});

// 表现逻辑
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;
// PlayerPrefs.SetInt(nameof(Count), value); //nameof 表达式可生成变量、类型或成员的名称作为字符串常量。
this.GetUtility<Storage>().SaveInt(nameof(Count), value);
}
}
}
protected override void OnInit()
{
// Count = PlayerPrefs.GetInt(nameof(Count), 0);
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>();

// var model = this.GetModel<P5_CounterModel>();
// if (model.Count == 10)
// {
// Debug.Log("点击达人成就达成!");
// }
// else if (model.Count == 20)
// {
// Debug.Log("点击成就专家达成!");
// }
// else if (model.Count == -10)
// {
// Debug.Log("点击菜鸟成就达成!");
// }
}
}
}
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--;

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

带初始调用

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

age.RegisterWithInitValue(newAge => {

Debug.Log(newAge);

});

// 输出结果
// 5

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/

image

代码

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

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<EventA>();
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
});
}

// if (Input.GetMouseButton(1))
// {
// this.UnRegisterEvent<EventB>();
// }
}

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()
{
//用法1.使用EasyEvent,并自主销毁
EasyEventA.Register(() =>
{
Debug.Log("EasyEventA Trigger!");
}).UnRegisterWhenGameObjectDestroyed(gameObject);

//用法2.使用EasyEvent,非自主销毁
OnCountChangeEvent.Register(count =>
{
Debug.Log(count);
});

//用法3.使用EasyEvents管理EasyEvent(较少使用)
//将EasyEvent添加到EasyEvents容器
EasyEvents.Register<EasyEvent<int, string>>();

//调用EasyEvents提供的全局事件系统,获取EasyEvent(相当于委托),再将方法注册到EasyEvent上
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++;
           }
       }
   }
}


// 输出结果
// QFramework
// 按下鼠标左键,输出:
// 1
// 按下鼠标左键,输出:
// 2

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 和 大量工具集的解决方案。

01.QFramework.Toolkits简介

https://www.bilibili.com/read/cv19160267/

image

典型的 QFrameowrk.Toolkits 代码

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

image

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
//1.基础用法
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
//2.使用Sequence
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
//3.帧延时
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.Sequence()
// .NextFrame()
// .Start(this);

ActionKit.NextFrame(() => { }).Start(this);

4.条件执行

1
2
3
4
5
6
//4.条件执行
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) //整体逻辑执行5次结束,故调用5次clicked,1次finished
.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
//6.并行执行
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
//7.综合示例
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);
           
           // OnStart
           // OnExecute
           // OnFinish

           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);
           
           // 0
           // 1
           // 2
           // 3
           // 4
           // Finished

           // 还支持 Sequence、Repeat、Spawn 等
           // Also support sequence repeat spawn
           // ActionKit.Sequence()
           //     .Custom(c =>
           //     {
           //         c.OnStart(() => c.Finish());
           //     }).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);
       }
   }
}

// 输出结果
// Hello:1.002077
// Hello:1.002077
// Hello:1.002077

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(() =>
           {
               // fixed update code here
               // 这里写 fixed update 相关代码
           }).UnRegisterWhenGameObjectDestroyed(gameObject);
           
           ActionKit.OnLateUpdate.Register(() =>
           {
               // late update code here
               // 这里写 late update 相关代码
           }).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/

 评论