享元模式是什么
英文版本是flyweight,意味着轻量级,听起来是个好东西。
怎么做到轻量级,为什么翻译又叫享元?
核心机制:
- 将一个“巨无霸”对象的数据拆分成两份(理论上也可以是多份)
- 分为变和不变的部分,不变部分也称固有状态、上下文无关状态
- 不变的部分可以单独拆出去共享其他实例,实例只保存变的部分
- 意味着大家都一样的部分就不用每个人都保存了,共享一份即可
享元模式的一些使用场景
森林的渲染
森林由许多树组成,每棵树都有一系列与之相关的成员:
- 定义树干,树枝和树叶形状的多边形网格
- 树皮和树叶的纹理
- 在森林中树的位置和朝向
- 大小和色彩之类的调节参数,让每棵树都看起来与众不同
如果每棵树都这么与众不同,且有以上所有实例,先不管内存兄弟的感受,cpu可就有福了,cpu到gpu的交互将会产生很多drawcall,光准备阶段就能忙死它。
Draw call其实是CPU与GPU的通信方式,他们通过CommandBuffer作为通信的“信道”,每一次GPU与CPU的通信的过程有很多步骤。如果一帧中间DrawCall数量太多,CPU就会在设置渲染状态-提交drawcall上花费大量时间,造成性能问题,这里的性能问题其实是GPU在等待CPU的处理。
实际上游戏中森林每棵树长得都差不太多,只是一些位置、朝向、缩放的不同,那么完全可以拆成变化状态和固有状态,这里树的网格等数据即固有的,相同的树共享一份网格数据。
实际上图形api也是支持这种设计思想,“使用同一模型渲染每个实例”,传入每个实例不同的部分,调用一次渲染,绘制整个森林。(这个API是由显卡直接实现的,意味着享元模式也许是唯一的有硬件支持的GoF设计模式)
GPU Instanceing
在Unity中使用该模式的feature
GPU Instancing 在同一个Draw call中渲染完全相同的网格。可以通过添加变量来减少重复的外观,每个实例可以具有不同的属性,例如颜色或缩放。在Frame Debugger中如有Draw calls显示多个实例时会显示“Darw Mesh(Instanced)”。
使用了多个GPU Instanceing组合,如果材质球属性要有变化,请用MaterialPropertyBlock
一些限制:
- 和SRP Batcher不兼容,需要测试哪种情况下效率更高
- SkinnedMeshRenderers不支持GPU Instancing技术,但可以通过GPU顶点动画的方式去支持,官方提供了Animation-Instancing
- 支持全局光照系统,但有一些限制
- GPU Instancing每个批次的Instance数量是有上限的,但好像最新的文档没有提及,虽然测试还是有上限
地形数据
游戏里常见地形有草,泥土,丘陵,湖泊,河流。基于区块建立地表:世界的表面被划分为由微小区块组成的巨大网格
每种地形类型都有一系列特性会影响游戏玩法:
- 决定了玩家能够多快地穿过它的移动开销
- 表明能否用船穿过的水域标识
- 用来渲染它的纹理
基于数据的设计,可能会将地形抽象成枚举,然后每个区块保存一份枚举
1 | enum |
然后具体使用的时候在每个方法里判断枚举去switch得到不同的特性,比如如果是草,移动开销则为*
听起来运行效率很高,但是地形数据太分散了,没有OOP的优雅
地形完全可以用一个类来描述,将它的数据和功能聚合在一起
1 | class Terrain |
和森林的例子一样,每个区块并不需要实例化一份地形数据,因为位置1的草和位置7的草并没有什么不同
1 | class World |
性能方面几乎没有额外的开销,如果要较真的话,跟踪这样的指针会导致缓存不命中,降低运行速度。
早期的马里奥的卷轴地图和如今的tilemap的实现思路想必都有使用该模式