QFramework个人总结
QFramework是基于Unity的游戏开发框架,简单却强大,非常值得新手游戏程序员学习。
概览
- 开发架构原则
- MVC
- IOC、分层支持
- CQRS 支持
- 符合 SOLID原则
- 可以使用 DDD 的方式设计项目
- 工具集
- UIKit 界面&View快速开发&管理解决方案
- ResKit 资源快速开发&管理解决方案
- 提供一套引用计数的资源管理模型
- AudioKit 音频管理解决方案
- CoreKit 提供大量的代码工具
- ActionKit:动作序列执行系统
- CodeGenKit:代码生成 & 自动序列化赋值工具
- EventKit:提供基于类、字符串、枚举以及信号类型的事件工具集
- FluentAPI:对大量的 Unity 和 C# 常用的 API 提供了静态扩展的封装(链式 API)
- IOCKit:提供依赖注入容器
- LocaleKit:本地化&多语言工具集
- LogKit:日志工具集
- PackageKit:包管理工具,由此可更新框架和对应的插件模块。
- PoolKit:对象池工具集,提供对象池的基础上,也提供 ListPool 和 Dictionary Pool 等工具。
- SingletonKit:单例工具集
- TableKit:提供表格类数据结构的工具集
从架构原则谈起
上图是QFramework的架构实例图,清晰的表现了框架的分层特性,理清每层的含义,有利于我们将游戏业务完美的填充进框架里,既方便我们的逻辑拆分,也可以让后续开发切实体会到框架所带来的健壮性和可扩展性的好处。
上图就是凉鞋(作者)举的一个简单的例子,涵盖了基本四层架构,通信的方向,通信的方式(直接调用,使用命令类和事件)。
具体含义,凉鞋的教程写的非常精彩了,推荐大家去看。
MVC
MVC来源于传统软件开发,其思想就是将常见业务逻辑分三层,用户界面,程序逻辑,数据。拿网站举例,网页前端显示的是View层,你的账号信息是Model层,你可以进行的操作为Controller层。
因为游戏业务逻辑的特殊性,游戏表现与控制往往联系的很紧密,除了UI功能,常常没有纯粹的View层,所以这里统一为ViewController层,基本上我们所写的大部分Mono类都是属于这一层。而原教旨的Controller层的作用,下发给了System层来承担。所以我们在编写Mono类的脚本时,要尽可能保证遵循单一职责原则,防止将过多的逻辑交给了ViewController层。
Model层为数据层,代表应用程序的数据结构,通常包括数据存储、数据操作。本地远程存档,数据库交互处于这一层。相对独立和底层。
接下来简述一下层级以及层级规则,这套规则为何这样规定暂且不管,只需要明白层级及其规则是框架的核心,其他的特性例如事件,数据,CQRS都是为实现规则而服务的。
层级顺序为表现层->系统层->数据层->工具层
上层可以获取下层,直接调用下层,一些特殊操作使用Command
下层只能上层使用事件Event间接通信(解耦的方式)
事件驱动
事件驱动含义就是类之间通信除了直接调用,还可以通过事件这一中介,来间接调用,实现了对象之间的解耦。观察者模式讲的就是事件中心的具体实现方式。框架中也给我们提供好了事件的接口调用,事件在框架中的作用就是下层往长层通信使用,其理由也很好理解,底层的改变往往牵一发而动全身,广播事件间接通知上层对象符合现实逻辑。
数据驱动
如果理解了事件驱动,数据驱动就更好理解了,数据层作为除工具层之外的最底层,改变数据的影响是最大的,其发送事件是最频繁的。框架提供了自带事件的数据类BindableProperty,方便表现层更简单的注册和监听事件。
CQRS支持
CQRS读写分离,数据驱动的另一种体现。将重要的读写操作包装成读写类来管理。什么叫重要的读写操作呢,读写重要数据的操作,重要数据都分装到了Model层中,所以QFramework建议对Model的写操作实现Command类进行,要做权限管理和批量查询的操作实现Query类进行。
IOC模块化
IOC是"Inversion of Control"(控制反转)的缩写。在软件开发中,IOC是一种设计原则,它指的是将控制权从自己手动交给框架或容器来管理。
工厂模式就是IOC原则的一个例子,什么叫工厂模式,就是你不生产对象,工厂生产对象,工厂负责维护它所生产对象的引用,你只需要用的时候从工厂申请就行了。
这样做的好处就是对象之间的耦合度降低了,互相之间想要通信,获取引用都可以通过工厂或者说框架来获取了。Unity的实例化方法就属于工厂方法,各种Get也是通过Unity这间大工厂去获取其他对象的引用。
回到QFramework也是一样的,框架通过Architecture的Init方法作为系统层,数据层,工具层的工厂方法,管理这些重要类。表现层由Unity工厂管理。再通过框架内各种层Get方法的访问权限,控制了哪些对象引用可以得到。这就实现了层级之间直接通信的规则。
这种IOC原则同样也应用在UI管理中。
SOLIO原则(GPT如是说道)
SOLID原则是面向对象设计中的五个基本原则,既体现在框架代码中,也应该是我们写代码的原则。分别是:
单一职责原则(Single Responsibility Principle):一个类应该只有一个引起变化的原因,即一个类应该只负责一项职责。
-
开放封闭原则(Open/Closed Principle):软件实体(类、模块、函数等)应该对扩展开放,对修改关闭,即可以通过扩展来实现新功能,而不是修改已有的代码。
在C#中,开放封闭原则是面向对象设计中的重要原则之一,它指导着软件设计者编写易于扩展的代码,同时不修改现有代码。简单来说,开放封闭原则要求软件实体(类、模块、函数等)应该对扩展开放,对修改封闭。
在C#中,可以通过以下方式来解释开放封闭原则:
使用抽象类和接口:通过定义抽象类和接口,可以让代码更容易扩展。其他类可以实现这些抽象类和接口,从而扩展系统的功能,而不需要修改原有的代码。
使用继承和多态:通过继承和多态的特性,可以实现代码的扩展而不修改原有代码。子类可以重写父类的方法,实现新的功能,而不会影响到已有的代码。
使用设计模式:设计模式如策略模式、装饰器模式等可以帮助实现开放封闭原则。这些设计模式提供了灵活的方式来扩展系统功能,同时保持原有代码的稳定性。
拿继承举例来说。假设我们有一个图形绘制应用程序,其中包含不同类型的图形(如圆形、矩形)和一个绘图类 Drawing,用于绘制这些图形。
现在我们需要向应用程序中添加一个新的图形类型:三角形。按照开放封闭原则的要求,我们希望能够添加新的图形类型而不需要修改 Drawing 类的现有代码。
下面是一个简单的示例代码:
using System;
// 抽象图形类
public abstract class Shape
{
public abstract void Draw();
}
// 圆形类
public class Circle : Shape
{
public override void Draw()
{
Console.WriteLine("Drawing a circle");
}
}
// 矩形类
public class Rectangle : Shape
{
public override void Draw()
{
Console.WriteLine("Drawing a rectangle");
}
}
// 新增的三角形类
public class Triangle : Shape
{
public override void Draw()
{
Console.WriteLine("Drawing a triangle");
}
}
// 绘图类
public class Drawing
{
public void DrawShape(Shape shape)
{
shape.Draw();
}
}
class Program
{
static void Main()
{
Drawing drawing = new Drawing();
// 绘制圆形
drawing.DrawShape(new Circle());
// 绘制矩形
drawing.DrawShape(new Rectangle());
// 绘制三角形
drawing.DrawShape(new Triangle());
}
}
在这个示例中,我们通过定义抽象类 Shape 和具体图形类 Circle、Rectangle、Triangle 来实现不同类型的图形。通过向 Drawing 类传递不同的图形对象,我们可以实现绘制不同类型的图形,而无需修改 Drawing 类的代码,符合开放封闭原则。
这样,当需要添加新的图形类型时,只需创建新的图形类并继承 Shape 类,然后在 Drawing 类中使用新的图形对象即可,而不需要修改原有代码。
-
里氏替换原则(Liskov Substitution Principle):子类必须能够替换掉父类并且仍然表现出父类的行为,即子类可以替换父类并且不影响程序的正确性。
在面向对象编程中,里氏替换原则是由Barbara Liskov提出的一个重要原则,它强调子类对象必须能够替换其父类对象而不影响程序的正确性。换句话说,子类应该能够替代父类并表现出父类的行为,而不需要修改程序的期望行为。
在C#中,可以通过以下方式来解释里氏替换原则:
子类必须保持父类的行为:子类在继承父类时,应该保持父类的行为和约束条件。子类可以扩展父类的行为,但不能改变父类已有的行为。
子类可以重写父类方法:子类可以通过重写父类的方法来实现自己特定的行为,但是重写的方法不能改变原有方法的预期行为。
子类可以引入新的方法:子类可以添加新的方法来扩展功能,但不能修改已有方法的含义。
举个简单的例子来说明里氏替换原则:
using System;
// 父类:动物类
public class Animal
{
public virtual void MakeSound()
{
Console.WriteLine("Animal makes a sound");
}
}
// 子类:狗类
public class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine("Dog barks");
}
}
class Program
{
static void Main()
{
Animal animal = new Dog();
animal.MakeSound(); // 输出 "Dog barks"
}
}
在这个例子中,我们有一个父类 Animal 和一个子类 Dog。根据里氏替换原则,我们可以将 Dog 类的实例赋值给 Animal 类型的变量,而且调用 MakeSound() 方法时,会执行 Dog 类中重写的方法,而不是父类中的方法。
通过遵循里氏替换原则,我们可以确保代码的可扩展性和可维护性,同时提高代码的灵活性和可重用性。
-
接口隔离原则(Interface Segregation Principle):一个类对另一个类的依赖应该建立在最小的接口上,即不应该强迫客户端实现它们用不到的方法。
接口隔离原则是面向对象设计中的一个原则,它强调一个类不应该强迫其客户端依赖它们不需要的接口。换句话说,接口隔离原则要求将大而臃肿的接口拆分成更小、更具体的接口,以便客户端只需知道与其相关的方法。
在C#中,可以通过以下方式来解释接口隔离原则:
接口应该精简:接口应该只包含客户端需要的方法,避免将不相关的方法放在同一个接口中。
避免“胖接口”:不应该设计一个“胖接口”(fat interface),即包含大量方法的接口。这样的接口会导致实现类需要实现不需要的方法,违反了接口隔离原则。
根据功能拆分接口:将一个大接口拆分成多个小接口,每个小接口代表一个特定的功能或角色,以便客户端可以根据需要选择性地实现这些接口。
举个简单的例子来说明接口隔离原则:
using System;
// 接口隔离前
public interface IWorker
{
void Work();
void Eat();
void Rest();
}
public class Worker : IWorker
{
public void Work()
{
Console.WriteLine("Working");
}
public void Eat()
{
Console.WriteLine("Eating");
}
public void Rest()
{
Console.WriteLine("Resting");
}
}
// 接口隔离后
public interface IWorkable
{
void Work();
}
public interface IEatable
{
void Eat();
}
public interface IRestable
{
void Rest();
}
public class Worker : IWorkable, IEatable, IRestable
{
public void Work()
{
Console.WriteLine("Working");
}
public void Eat()
{
Console.WriteLine("Eating");
}
public void Rest()
{
Console.WriteLine("Resting");
}
}
在这个例子中,我们首先展示了一个违反接口隔离原则的情况,即一个包含 Work、Eat 和 Rest 方法的大接口 IWorker,然后通过拆分成 IWorkable、IEatable 和 IRestable 三个小接口来遵循接口隔离原则。这样做可以让类根据需要选择性地实现各个小接口,而不需要实现不需要的方法。
-
依赖反转原则(Dependency Inversion Principle):高层模块不应该依赖于低层模块,二者都应该依赖于抽象;抽象不应该依赖于具体实现,具体实现应该依赖于抽象。
依赖反转原则强调高层模块不应该依赖于低层模块,二者都应该依赖于抽象;而且抽象不应该依赖于细节,细节应该依赖于抽象。简而言之,依赖反转原则要求通过抽象来减少模块间的耦合性。
在C#中,可以通过以下方式来解释依赖反转原则:
高层模块定义抽象接口:高层模块定义抽象接口或抽象类,低层模块实现这些接口或类。
低层模块依赖于抽象:低层模块只依赖于抽象定义的接口或类,而不依赖于具体实现。
抽象不依赖于细节:抽象接口或类不应该依赖于具体实现的细节,而是定义通用的方法和属性。
举个简单的例子来说明依赖反转原则:
using System;
// 依赖反转前
public class Light
{
public void TurnOn()
{
Console.WriteLine("Light is on");
}
}
public class Switch
{
private Light light = new Light();
public void Toggle()
{
light.TurnOn();
}
}
// 依赖反转后
public interface ISwitchable
{
void TurnOn();
}
public class Light : ISwitchable
{
public void TurnOn()
{
Console.WriteLine("Light is on");
}
}
public class Switch
{
private ISwitchable switchable;
public Switch(ISwitchable switchable)
{
this.switchable = switchable;
}
public void Toggle()
{
switchable.TurnOn();
}
}
class Program
{
static void Main()
{
ISwitchable light = new Light();
Switch switchButton = new Switch(light);
switchButton.Toggle();
}
}
在这个例子中,我们通过引入接口 ISwitchable,使得 Switch 类不再直接依赖于 Light 类,而是通过接口来实现对灯光的控制。这样做符合依赖反转原则,降低了模块之间的耦合性,并使得代码更容易扩展和维护。
官方架构推荐使用规范
QFramework系统设计架构分为四层及其规则:
表现层:ViewController层。IController接口,负责接收输入和状态变化时的表现,一般情况下,MonoBehaviour 均为表现层
可以获取System
可以获取Model
可以发送Command
可以监听Event
系统层:System层。ISystem接口,帮助IController承担一部分逻辑,在多个表现层共享的逻辑,比如计时系统、商城系统、成就系统等
可以获取System
可以获取Model
可以监听Event
可以发送Event
数据层:Model层。IModel接口,负责数据的定义、数据的增删查改方法的提供
可以获取Utility
可以发送Event
工具层:Utility层。IUtility接口,负责提供基础设施,比如存储方法、序列化方法、网络连接方法、蓝牙方法、SDK、框架继承等。啥都干不了,可以集成第三方库,或者封装API
除了四个层级,还有一个核心概念——Command
可以获取System
可以获取Model
可以发送Event
可以发送Command
层级规则:
IController 更改 ISystem、IModel 的状态必须用Command
ISystem、IModel状态发生变更后通知IController必须用事件或BindableProperty
IController可以获取ISystem、IModel对象来进行
数据查询
ICommand不能有状态
上层可以直接获取下层,下层不能获取上层对象
下层向上层通信用事件
上层向下层通信用方法调用(只是做查询,状态变更用Command),IController的交互逻辑为特别情况,只能用Command