读书笔记----软件设计原则、设计模式
这个作业属于哪个课程 | https://edu.cnblogs.com/campus/gdgy/2021Softwarecodedevelopmenttechnology/ |
---|---|
这个作业要求在哪里 | https://edu.cnblogs.com/campus/gdgy/2021Softwarecodedevelopmenttechnology/homework/11833 |
这个作业的目标 | 理解软件设计原则和设计模式 |
阅读《游戏编程模式》与《设计模式》笔记
《游戏编程模式》主要介绍了游戏开发中常用的四大类型,19个设计模式的用法,使用场景
设计原则
LSP:里氏代换原则
Liskov Substitution Principle,LSP
- 若在任何情况下,子类或实现类与基类都可以互换,那么继承的使用是合适的
- 子类不能添加任何父类没有的附加约束
- 子类对象必须可以替换基类对象
例子
正方形继承长方形类,违背LSP原则,导致出现错误。
OCP:开闭原则
Open Closed Principle,OCP
软件实体应该是可扩展的,但不可修改的
- 对于扩展是开放的
- 对于更改是封闭的
- 对模块行为扩展时,不必改动模块源代码或二进制代码
OCP的关键在于抽象
- 抽象技术:abstract class,Interface
- 抽象预见了可能的所以扩展(闭)
- 抽象随时可导出新的类(开 )
例子:
手开关抽屉,门,冰箱。。。
那如果手直接和可开关的具体类关联,则每次添加新的具体类都需要修改手的源代码
解决方案:新增一个接口
SRP:单一职责原则
Single Responsibility Principle,SRP
- SRP体现了内聚性,即一个模块的组成元素直接的功能相关性
- 类的职责定义为“变化的原因”,当一个类承担了多于一个职责,那么引起变化的原因会有多个
例子
解决方案
ISP:接口隔离原则
Interface Segregation Principle,ISP
- 客户不应该依赖他们用不到的方法,只给每个用户它所需要的接口
- 为避免肥接口,应该一个类实现多个接口,每个客户仅知道必须的接口
- 要为各个类建立它们需要的专用接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。
例子:接口污染
DIP:依赖倒置原则
Dependence Inversion Principle,DIP
- 高层模块不应该依赖底层模块,二者应该依赖于抽象
- 抽象不应该依赖于细节,细节应该依赖于抽象
- 针对接口编程,不要针对实现编程
例子:传统依赖关系
解决方案
LKP:最少知识原则
Least Knowledge Principle,又叫迪米特法则(Law of Demeter,LoD)
启发式原则
- 依赖于抽象,即所有依赖关系都应该止于抽象类或接口
- 任何类不应该从具体类派生
- 任何变量不应该拥有指向具体类的指针或者引用
- 任何方法都不应该改写任何基类中已经实现的方法
通用
外观模式
引入外观角色,为子系统的访问提供简单而单一的接口,是“迪米特法则”的体现
降低原有系统的复杂度,同时降低客户类与子系统类的耦合度,提高了客户端使用的便捷性,使得客户端无须关心子系统的工作细节
代理模式
原有类无法做的事情,交给代理类执行
例子:模拟实现MonoBehavior的帧更新和协程
装饰器模式
继承原有类,不改变原有接口,扩展新功能
适配器模式
保留现有类所提供的服务,改变原有接口,从而适配用户需求
两种适配方式
- 对象适配器:包含原有类
- 类适配器:继承原有类
装饰器模式和适配器模式都不改变原有类的代码
单例模式
确保一个类只有一个实例,并为其提供一个全局访问入口
观察者模式
应用
- 游戏成就系统
- 通过使用观察者模式,让成就系统与游戏中各个系统解耦
- Unity里的消息中心
- sendMessage和send
原型模式
使用特定原型实例来创建特定种类的对象,并且通过拷贝原型来创建新对象
游戏
序列型
双缓冲
存储两块缓冲区,通过交换缓冲区指针或引用,实现无缝过渡效果
应用
- 应用情景
- 我们需要维护一些被逐步改变着的状态量
- 同个状态可能会在其被修改同时被访问
- 希望避免访问状态的代码能看到具体工作过程
- 希望能够读取状态但不希望等待写入操作的完成
- 图形学
- 图像渲染采用双缓冲模式,一个缓存用于展示当前帧,另一个正在被渲染代码写入数据,避免出现视觉错误
- 处理动态模糊,当前帧和先前帧的一部分混合,从而使得产生的图像更接近真实摄像机拍摄效果
- AI
游戏循环
游戏循环模式,实现游戏运行过程中对用户输入处理和时间处理的解耦。
使用场合
任何游戏或游戏引擎都拥有自己的游戏循环,因为游戏循环是游戏运行的主心骨。
更新方式
更新方法,通过每次处理一帧的行为来模拟一系列独立对象。
要点
- 更新方法模式:在游戏中保持游戏对象的集合。每个对象实现一个更新方法,以处理对象在一帧内的行为。每一帧中,游戏循环对集合中的每一个对象进行更新。
- 当离开每帧时,我们也许需要存储下状态,以备不时之需。
行为型
字节码
字节码模式,将行为编码为虚拟机器上的指令,来赋予其数据的灵活性。从而让数据易于修改,易于加载,并与其他可执行部分相隔离。有两种大风格:基于栈和基于寄存器
使用情况
- 编译语言太底层,编写起来繁琐易错
- 因编译时间太长或工具问题,导致迭代缓慢
- 它的安全性太依赖编码者
子类沙盒
用一系列由基类提供的操作定义子类中的行为。
类型对象
创造一个类A来允许灵活的创造新的类,而类A的每个实例都代表了不同类型的对象。
解耦型
命令模式
原型
玩家脚本里一个具体按键对应执行一个具体事件函数
public void CheckInput()
{
if(Input.GetButtonDown(KeyCode.X))
{
Jump();
}
......
}
改善
- 可扩展性
- 命令类Command,所有命令继承它
- 输入处理类InputHandle,每个键位对应指针,可修改对应具体按键
- 若支持无行为按键,可设指针为空,检查指针是否空来决定是否执行事件
- 解耦
- 将命令的发起者不再局限于玩家,执行者也不局限于玩家
- 将执行命令改为返回命令 命令延迟至调用具体化
- 传入命令发起者指针
- 将命令的发起者不再局限于玩家,执行者也不局限于玩家
应用
- AI方面
- 引入命令流,类似命令队列,不同类型AI可以混搭不同命令
- 联机方面
- 命令流序列化,从而传送数据流,实现命令回放
- 撤销和重做
- 引入命令栈,仅记录命令涉及变化的状态变量(持久化数据结构
组件模式
在单一实体跨越了多个领域时,为了保持领域之间相互解耦,可以将每部分代码放入各自的组件类中,将实体简化为组件的容器。
事件队列
在先入先出的队列中存储一系列通知或请求。发送通知时,将请求放入队列并返回。处理请求的系统在稍晚些的时候从队列中获取请求并进行处理。 这样就解耦了发送者和接收者,既静态又及时。
服务器定位器
服务类定义了一堆操作的抽象接口。具体的服务提供者实现这个接口。 分离的服务定位器提供了通过查询合适的提供者, 获取服务的方法,同时隐藏了提供者的具体细节和需要定位它的进程。
优化型
享元模式
通过将对象数据切分成两种类型,然后在每个对象实例之间共享内部状态数据来节省内存
- 内部状态
- 又可称为上下文无关的状态/数据,即是不属于单一实例,能够被所有对象共享的数据
- 外部状态
- 即是一个实例的状态,是唯一的,表现实例的差异性
应用
- 场景渲染
- 当你有太多对象并考虑对其进行轻量化时,如大型森林场景,树木内部状态数据是重复的,若通过共享可以很大程度减少数据存储量
对象池/缓冲池模式
使用固定的对象池重用对象,取代单独地分配和释放对象
数据局部性
- 现代的CPU有缓存来加速内存读取,其可以更快地读取最近访问过的内存毗邻的内存。基于这一点,我们通过保证处理的数据排列在连续内存上,以提高内存局部性,从而提高性能。
- 为了保证数据局部性,就要避免的缓存不命中。也许你需要牺牲一些宝贵的抽象。你越围绕数据局部性设计程序,就越放弃继承、接口和它们带来的好处。没有银弹,只有权衡。
脏标记模型
- 脏标记,就是用来表示被标记的内容是否有被修改过的一个标志位。
- 脏标识模式:考虑情况,当前有一组原始数据随着时间变化而改变。由这些原始数据计算出目标数据需要耗费一定的计算量。这个时候,可以用一个脏标识,来追踪目前的原始数据是否与之前的原始数据保持一致,而此脏标识会在被标记的原始数据改变时改变。那么,若这个标记没被改变,就可以使用之前缓存的目标数据,不用再重复计算。反之,若此标记已经改变,则需用新的原始数据计算目标数据。
空间分区
- 对于一系列对象,每个对象都有空间上的位置。将它们存储在根据位置组织对象的空间数据结构中,让我们有效查询在某处或者附近的对象。 当对象的位置改变时,更新空间数据结构,这样它可以继续找到对象。
- 最简单的空间分区:固定网格。想象某即时战略类游戏,一改在单独的数组中存储我们的游戏对象的常规思维,我们将它们存到网格的格子中。每个格子存储一组单位,它们的位置在格子的边界内部。当我们处理战斗时,一般只需考虑在同一格子或相邻格子中的单位,而不是将每个游戏中的单位与其他所有单位比较,这样就大大节约了计算量。
心得体会
在过去所开发的游戏中
观察者模式可以制作成就系统;
与事件队列配合可以制作事件中心;
频繁生成销毁的对象用对象池再好不过了;
命令模式可以使得玩家输入与逻辑处理解耦;
享元与双缓冲用于图形渲染。
使用了这些设计模式后,开发的逻辑变得更加清晰有条理;新的开发者加入也能迅速使用这些代码模块;而且优化了游戏渲染速度。
因此,合理巧妙的使用设计模式会让代码开发事半功倍。