游戏编程模式学习笔记

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_24634505/article/details/89398273

命令模式:把命令(行为)抽象为一个对象,将命令的执行者作为参数传入。

优点:
1、游戏行为与执行者解耦,任何对象都可以执行这个行为(敌人、AI等)
2、多态使切换命令更方便(更改快捷键)
3、输入控制器或AI生成一系列命令放入命令流中,调度器或角色调用并消耗命令,实现【自动演示】、【撤销】、【回放】等功能


原型模式:用原型实例制定创建对象的种类,并通过拷贝这些原型创建新的对象,将框架与产生对象分开。 实现一个拷贝接口即可。可以结合json,构建不同的对象。

优点:
1、用new新建对象不能获取当前对象运行时的状态
2、new新对象,在将当前对象的值复制给新对象,效率也不如原型模式高,尤其当对象创建需要读配置文件等复杂操作时。
2、通过new产生一个对象需要非常繁琐的数据准备或访问权限时,使用原型模式可以使客户端与原型对象的具体实现解耦合。

注意:深拷贝和浅拷贝的区别。


单例模式:保证一个类只有一个实例,并提供了访问该实例的全局访问点。

注意:单例模式作为全局变量,会使代码更难理解,更加耦合,对并行不友好。


观察者模式:被观察者维护一个观察者列表并实现一个通知函数。

优点:
1、不必关心是谁接受了通知,观察者和被观察者解耦
注意:
1、如果要同步响应,应当将耗时的工作推入另一个线程或工作队列
2、注意观察者和被观察者的销毁和释放问题
3、注册一个函数指针作为观察者比实现一个Observer接口的实例更加灵活轻便


状态模式:用类定义对象在状态的行为,改变状态时将状态指针指向不同的状态对象。

状态对象:

  1. 使用静态对象还是实例化状态(是否存在多个英雄)
  2. 可以在状态对象中定义入口行为和出口行为,状态对象不必关心上一个和下一个状态
  3. 让状态对象管理状态所使用的资源
    有限状态机:包括所有状态及其转移,输入导致转移。状态机同时只能在一个状态。

并发状态机:存在多个互相独立的状态机,如开火和跑步
分层状态机:多个相似状态存在相同的逻辑,用继承实现
下推自动机:一个状态结束时希望转移回之前的状态,用栈存储一系列指向状态的指针
优点:使用布尔标识,很多可能存在的值的组合是不合法的。 通过enum,每个值都是合法的。


子类沙箱:基类定义抽象的沙箱方法和操作,子类实现沙箱操作。

如对象位置,播放音频等和引擎耦合的代码由基类提供并声明为protected,只许子类调用。定义接口(如超能力)子类只需调用父类方法实现接口。

优点:

  • 更容易让有行为重复的子类分享代码。
  • 基类屏蔽了子类和其他部分的耦合。

注意:如果一个类有很多子类,则应当考虑不再用代码定义不同的能力,用数据驱动的方式更好。


对象池模式:放弃单独的分配和释放对象,从固定的池中重用对象。

池内存管理的策略包括考虑为每个场景调整对象池的大小、为不同的对象创建分离的池、同时只激活固定数量的对象、不创建新对象、强制覆盖一个已有的对象或创建新的溢出池。
使用场景:

  • 需要频繁创建和销毁的大小相近的对象
  • 对象封装了数据库或网络连接等昂贵又可以重用的资源

注意:
当池中对象不再使用时,清除和释放所有引用,并在重用时完全的初始化。考虑在池内还是池外初始化重用对象,如果在池内重新初始化则封装更好,但是对于有很多种初始化函数的对象,池也必须提供多个接口,并把初始化函数转发给对象,比较麻烦。在外部重新初始化接口会比较简单,但是得在外部处理创建新对象失败的情况。

优点:

  • 创建和销毁对象不必分配内存资源
  • 减少内存碎片

对象与池的耦合性:

  • 对象与池耦合:可以让池对象是对象类的友元类,将对象构造器私有化,让的对象只能被池创建。
  • 对象与池不耦合:可以创建通用的对象池。

组件模式:实体被简化为指向组件的指针的容器以及在不同组件间分享的数据。

组件通信的方法:

  1. 将所有共享数据存储在容器类中(必须注意组件运行顺序带来的影响)
  2. 组件间互相引用,无需通过容器类
  3. 通过消息系统告知容器拥有的所有组件

对象获取组件的方法:

  • 对象创建组件能保证对象不会拿不到组件,缺点是不能更换组件,重新设置对象
  • 外部代码设置组件更加灵活,对象可以和组件更加解耦。

优点:

  1. 在实体涉及多个领域时保持领域互相隔离
  2. 方便的增删改组件,通过继承实现组件接口,就能自定义组件从而定义不同的对象
  3. 避免菱形继承或不精确的继承

事件队列:解耦发出消息或事件的时间和处理它的时间。

目的:解耦合,合并处理请求(以得到正确的结果),不阻塞主线程
比起简单的解耦模式如观察者模式,队列提供了推和拉的缓存,以实现延迟处理、忽视请求等功能,不过不能提供回复了。

队列存储的对象:

  • 事件(怪物死了):描述已经发生的事,有点像异步的观察者模式,多个监听者对这个事件做出回应,队列一般是全局可见的
  • 消息(播放声音):描述想要,将要发生的事,类似于异步的服务,需要对谁能拿走消息做出限定,可能只有一个监听者。

对象的生命周期:

  • 传递所有权:
    消息入队时,其内存的所有权由发送者转到队列,队列负责销毁(unique_ptr)

  • 共享所有权:用shared_ptr,只要存在消息的引用,消息就不会释放,直到不用时自动释放。

  • 队列拥有消息:对象池模式

注意:
1、中心事件队列类似操作系统中的总线,作为全局变量可能引发风险。并且由于缓存队列,接收者不能假设现在的状态反映了事件发生时的世界,比起同步的系统需要存储更多数据。
2、如果有多个写入者,则应更小心消息环路


脏标识模式:通过延后对被修改的数据的处理,减少重复工作。
请求数据时,如果脏标识被设置则重新计算并清除标识。
当任务的计算或同步开销昂贵的时候可以考虑用脏标记模式节省开销。

优点:

  • 避免导出数据前原始数据多次变化带来的不必要计算
    注意:
  • 脏标识向子节点的传递
  • 合理控制脏标识的粒度
  • 清除脏标识的计算是否消耗大量时间造成卡顿?可以用一个计时器控制清除脏标识的频率。

游戏循环

固定时间步长、动态渲染:以固定步长更新,将输入、更新、渲染分开,如果游戏时间赶不上真实时间,可以扔掉一些渲染帧。虽然低端硬件上画面会抖动,但游戏速度与硬件无关。渲染器需要知道当前每个游戏对象的速度以在渲染时得到正确的位置。

动态时间步长:根据真实世界过去的时间推动游戏世界中的时间。虽然游戏可以以固定的时间运行(和真实时间成比例),但是不同机器上每一帧耗费的时间不一样,导致误差累计不同,在物理和联网应用上影响较大


更新方法

游戏有很多对象需要独立的同时运行,并且需要根据时间进行模拟。这时就应当把每个对象的行为抽象出来,用一个update完成(类似Unity的工作方式),每次传入一个时间步长,甚至可以用数据文件配置一个关卡。

注意:
对游戏对象的顺序更新和增删等管理。如果在当前帧进行游戏对象的增加或删除,可能造成错误的效果,更好的办法是将增删的对象放入一个缓存,下一帧开头或这一帧结尾进行处理。


服务定位器:对(日志、内存管理、工具函数等)服务提供全局接入点,避免耦合。

服务类定义操作的接口,具体的服务提供者实现这个接口。通过服务定位器,使用者只需知道接口,无需知道服务提供者是谁和定位它的过程。因此通过继承父类,可以将服务者替换为空服务,或者logged服务,可以在运行时改变服务,而逻辑不受到任何影响。
可以用静态函数返回一个服务提供者。

灵活度取决于是返回服务对象还是返回服务对象的指针。
注意:服务要能在循环的任何部分,任何环境下正确工作。可以提供一个空服务,来取消服务,或者使服务在未注册之前什么都不做,但不会崩溃。


数据局部性:保证数据以处理顺序排列在连续内存上,从而充分利用CPU缓存来加速内存读取。

在游戏循环中进行的更新最好可以在连续的内存上进行:

  • 不好的做法:游戏实体中存储组件的指针,每一帧对所有游戏实体进行更新,造成多次缓存不命中(我们不知道指针指向的对象在内存中如何布局)

  • 好的做法:将同类组件放在一个数组里,更新某个模块(物理、音乐、ai)时只需直接遍历数组,数据在其中是紧挨着放的,无需在内存中跳来跳去。

  • 更好的做法:如果数组中存在多个不活跃的要跳过的对象,则加载到内存中的数据会存在很多浪费。维护一个数组中活跃对象数量的变量,每次有激活对象时将其和队列第一个不活跃对象进行交换。对于每帧都要更新的对象又可能这是更快的做法。

组件模式下或任何需要接触很多数据的代码都应当多考虑数据局部性。数组里存储的是数据实体而不是指针,因此围绕数据局部性设计程序可能要牺牲一些继承、接口带来的好处。


空间分区:用数据结构将空间位置相近的对象存储在一起,在碰撞等判定中提高效率,是空间换时间的做法。

可以将地图划分为多个格子,用双向链表将处于同一格子的对象存储在一起,在对象位置发生变换的时候可以快速的增删。
检查时只检查邻近一半的格子,避免重复的检查:

x x -
x o -
x - -

更多划分策略:
四叉树,二分空间查找等。除了平面策略还可以层次划分(如果一个区域中的对象数量超过阈值则继续划分)。
平面划分更新更容易,层次策略对于空区域和高密度区域的处理更有效率。

猜你喜欢

转载自blog.csdn.net/qq_24634505/article/details/89398273