Main

April 30, 2020

游戏引擎中预制件的设计

Unity 推广了预制件 Prefab 这个概念,在此之前,Unreal Engine 里有个类似的东西叫做蓝图 Blueprint 。当然它们不完全是一种东西。我现在自己设计游戏引擎,更喜欢 Unity 中的 Prefab ,但我认为 Blueprint 这个名字其实更贴切一些。

当我们说制作一个 Prefab 的时候,其实制作的是一个预制件的模板。引擎运行时对应的那些数据,其实按照这个模板生产出来的。所以,工具制作出来的 Prefab 其实是一个 template ,所以,它本质上就是一张蓝图。但因为 Unity 深入人心,所以我还是打算基于把这个东西叫预制件。

对于 ECS 系统来说,预制件是一组数据,通常保存在文件中,可以以该资源文件的内容为模板,来构造一组 Entity。注意:预制件作为资源文件时,和贴图、模型数据等这些资源文件是不同的。贴图之类的资源,映射到内存后,就是一个数据块或一个引擎中的 handle ,可以被共享使用。但预制件本身只是一个模板,它用于生产数据,本身不是数据。从这个角度讲,如果把预制件文件当作资源纳入资源管理模块统一管理的话,预制件资源对应的是一个 function (生成器)而不是 table (数据集)。

April 15, 2020

资源模块的重构

这篇是对 游戏引擎中的资源生命期管理问题 的延续。

最近对我们游戏引擎的资源模块做了一次重构,大概花了一周的时间,其中核心模块的代码实现花了 2 天。比之前的方案简洁很多。新方案的设计是基于以下原则来实现的:

  1. 引擎应该围绕数据来设计。ECS 更是数据驱动的模型。
  2. 数据全部都用一致的数据结构来表达,方便统一处理。因为我们采用 lua 做开发,所以,一切数据都是 lua table 。我们的引擎与其说是基于 lua 开发,不如说是基于 lua 的数据结构开发。即使某些模块因为性能因素用 C/C++ 实现,操纵的还是 lua table 。读写 lua table 的性能和读写 C struct / array 相比,并无显著的劣势。
  3. 在使用上尽量不区分外部不可修改的静态数据和运行期动态修改的数据。
  4. 惰性加载,延迟异步加载,替代资源,这些尽可能的隐藏起来,不必对外透露细节。尽可能的减少外部干预。
  5. lua 虽然有 metatable 这个神器,可以帮助我们抹平不同数据、不同策略之间的差异。但不要过多依赖语言特性。

January 19, 2020

不适合采用 System 的一种情况

ECS 框架中,System 通常是对某种 Component 的集中处理。大部分情况下,一次性对大量对象中的简单的数据结构做简单的处理,比每次对一个对象的复杂数据结构(通常是简单数据结构的组合)做复杂处理,依次处理所有的对象要更好。

凡事总有例外。我们最近就发现有一类情况不适合这样做。那就是骨骼动画的骨骼计算。

骨骼计算会分为若干步骤,有骨骼数据的展开,骨骼姿态的混合,IK 计算等等。一开始,我们遵循着 ECS 的设计原则,把这些步骤分到不同 System 种。例如并非所有的对象都需要做 IK 计算,这样 IK System 就只用遍历一个子集;同样,也并非所有的动画都需要做多个姿态的混合,等等。

January 13, 2020

ECS 中的概念缺失

经过长时间的思考和实践,最近一个多月,我们的 ECS 框架做了较大的调整。其中一部分工作已经在前一篇消息发布订阅机制中介绍,另一部分工作其实开展的更早,但因为我想多沉淀一段时间再写。到本周基本基本改动完毕,可以总结一下了。

ECS 框架几乎只在游戏开发领域提出,我认为这主要是因为目前只有在游戏领域,周期性的大量对象的状态变换才是主流行为。而在其它人机交互领域,响应外部事件才是主流。这是为何 System 在游戏领域如此重要的原因。

但另一方面,ECS 框架对过去流行的面向对象框架的反思,主要集中在面向数据/数据驱动。这是 Component 概念被如此看重的原因。Component 是行为被拆分出来的对象,重要的是数据本身。对于框架来说,要解决的是更方便的组合、有完善的自省机制,这才能针对数据集本身来编程。因为游戏开发,程序员的工作仅占很少的部分,大部分的工作是策划和美术围绕开发工具给数据添砖加瓦的。

December 24, 2019

ECS 中的消息发布订阅机制

我们在实践 ECS 框架时发现,之所以 ECS 的概念诞生于游戏领域,是因为游戏程序往往都在周期性的处理一批对象,进行运算,根据上个周期的状态得到下个周期的状态。而传统人机交互的应用则是响应型的:即一个外部请求触发一系列的业务运作。

如果你把游戏业务塞到响应型框架中,就会发现,不得不用时间去触发,业务响应的是 timer 。但这种情况下,timer 几乎没有携带任何状态,对单个 timer 的响应,是不可能做成无状态的:它本身就是整个游戏世界对上个状态的迭代。

这种情况下,响应式框架就很低效。

August 16, 2019

游戏引擎中的资源生命期管理问题

最近我们开发中的游戏引擎在修理资源管理模块中的 bug 时,我提出了一些想法,希望可以简化资源对象的生命期管理。

其实这个模块已经被重构过几次了。我想理一下它的发展轨迹。

最开始,我们不想太考虑资源的生命期问题,全部都不释放。当然,谁都明白,这种策略只适合做 demo ,不可能用在产品中。

因为我们整个引擎的框架是用 lua 搭建,那么,最直接的想法就是利用 lua 自带的 gc 来回收那些不被引用的资源对象。我不太喜欢这个简单粗暴的方法。因为首先, gc 不会太及时,其次 gc 方法触发的时机很难控制,容易干扰正常的运行流程。图形显示模块是时间敏感的,如果因为资源释放占用了 cpu 的话,很容易变成肉眼可查的卡顿。

另一个促使我们认真考虑资源管理模块的设计的原因是,当我们从 demo 过渡到现实世界的大游戏场景时,过多的资源量触发了 bgfx 的一个内部限制:如果你在一个渲染帧内调用了过多资源 api (例如创建新的 buffer texture 等),会超出 bgfx 的多线程渲染内部的一个消息管道上限,直接让程序崩溃。

所以我们不得不比计划提前实现资源的异步加载模块,它属于资源管理模块的一部分,所以也就顺理成章的考虑整个资源管理模块的设计。

February 24, 2019

跟踪 Component 的修改

在 ECS 框架中,每个 System 在每次更新时,都遍历一类 Component 依次处理。这对于游戏的大多数场景都适用,因为游戏引擎要处理的对象通常是易变的。对于每个对象单独判断是否应该处理反而有性能负担。

但是,总有一些应用场景下,只对一类 Component 中的一小部分做修改,而没有被修改的对象可以保持上次的数据,而不必重复运算。在需要重算的对象数量远小于总量时,每个更新就很不划算了。

为了减少运算量,我们通常的解决方案是增加一个脏标记,修改时设置,设置它。这样就可以在处理的时候只处理被标记过的对象。常规的脏标记实现方案有两种,一是在 Component 上加一个 bool 字段,遍历的时候跳过没有标记的对象;二是在 Entity 上动态添加一个用于 tag 的 Component ,视作脏标记,遍历后再清除这个 tag 。

不过这两种方案在 lua 实现的 ecs 框架中,使用代价都比较大。

February 18, 2019

最近对 ECS 框架的一些想法

我们的游戏引擎采用 ECS 框架。最近一年的开发,为 ECS 框架的应用积累了不少经验。我在 blog 上也写过数篇 ECS 相关的东西:

Lua 下的 ECS 框架

ECS 中的 Entity

最近两个月,结合过去的经验,我们对最初设计的框架做了较大的调整。这主要是源于对框架要解决的事情的更深入的理解,以及在实践过程中针对典型场景总结出来的模式。

November 30, 2018

ECS 中的 Entity

我认为 ECS 框架针对的问题是传统面向对象框架中,对象数量很多而对象的特性非常繁杂,而针对对象的不同方面 aspect 编写处理逻辑会非常繁杂。每个针对特定的方面执行业务,都需要从众多对象中挑选出能够操作的子集,这样性能低下,且不相关的特性间耦合度很高。

所以 ECS 框架改变了数据组织方式,把同类数据聚合在一起,并用专门的业务处理流程只针对特定数据进行处理。这就是 C 和 S 的概念:Component 就是对象的一个方面 aspect 的数据集,而 System 就是针对特定一个或几个 aspect 处理方法。

那么,Entity 是什么呢?

我认为 Entity 主要解决了三个问题。

December 16, 2017

Lua 实现 ECS 框架的一些技巧

最近在用 Lua 实现一个 ECS 框架,用到了一些有趣的 Lua 语法技巧。

在 ECS 框架中,Component 是没有方法只有数据的,方法全部写在 System 中。Entity 本身仅仅是 Component 的组合,通常用一个 id 表示。

但实际写代码的时候,使用面向对象的语法(用 Lua 的冒号 这个语法糖)却是比较自然的写法。比如我们在操作一个 Component 数据的时候,用 component:foobar() 比用 foobar(component) 要舒服一些。好在 Lua 是一门非常动态的语言,我们有一些语法技巧在保持上面 ECS 原则的前提下,兼顾编码的书写体验。

December 10, 2017

ECS 的 entity 集合维护

最近在基于 ECS 模型做一些基础工作。实际操作时有一个问题不太明白,那就是涉及对象 (entity) 集合本身的 System 到底应该怎样处理才合适。

仔细阅读了能找到的关于 ECS 的资料,网上能找到的大多是几年前甚至 10 年前的。关于 ECS 的资料都不断地强调一些基本原则:C 里面不可以有方法(纯数据结构),S 里面不可以有状态(纯函数)。从这个角度看,Unity 其实只是一个 EC 系统,而不是 ECS 系统。从 Unity 中寻找关于 System 的设计模式恐怕并不合适。

重看了一遍暴雪在今年 GDC 上的演讲 Overwatch Gameplay Architecture and Netcode —— 这可能是最新公开的采用 ECS 模式的成功(守望先锋)实践了—— 我想我碰到的基础问题应该在里面都有答案。

December 03, 2017

Lua 下的 ECS 框架

前段时间,我写了一篇 浅谈《守望先锋》中的 ECS 构架 。最近想试试在 Lua 中实现一个简单的 ECS 框架,又仔细琢磨了一下。

我思考后的结论是:ECS 并不是一个新概念,它的提出其实是和语言相关的。ECS 概念的诞生起于游戏行业,相关框架基本都是基于 C++ 来开发的。它其实是对 C++ 对象模型的一个反思。ECS 针对组件组合对象,而反对 C++ 固有的基于继承的对象模型。对象模型才是 ECS 的设计核心理念。而离开 C++ 的对象模型,ECS 并不是什么新鲜的东西。

我的这个观点也不新鲜,在 ECS 的 Wikipedia 页上也有类似的说法:

In the original talk at GDC Scott Bilas compares C++ object system and his new Custom component system. This is consistent with a traditional use of this term in general Systems engineering with Common Lisp Object System and Type system as examples. Therefore, the ideas of "Systems" as a first-class element is a personal opinion essay. Overall, ECS is a mixed personal reflection of orthogonal well-established ideas in general Computer science and Programming language theory. For example, components can be seen as a mixin idiom in various programming languages. Alternatively, components are just a small case under the general Delegation (object-oriented programming) approach and Meta-object protocol. I.e. any complete component object system can be expressed with templates and empathy model within The Orlando Treaty vision of Object-oriented programming,

June 26, 2017

浅谈《守望先锋》中的 ECS 构架

今天读了一篇 《守望先锋》架构设计与网络同步 。这是根据 GDC 2017 上的演讲 Overwatch Gameplay Architecture and Netcode 视频翻译而来的,所以并没有原文。由于是个一小时的演讲,不可能讲得面面俱到,所以理解起来有些困难,我反复读了三遍,然后把英文视频找来(订阅 GDC Vault 可以看,有版权)看了一遍,大致理解了 ECS 这个框架。写这篇 Blog 记录一下我对 ECS 的理解,结合我自己这些年做游戏开发的经验,可能并非等价于原演讲中的思想。

Entity Component System (ECS) 是一个 gameplay 层面的框架,它是建立在渲染引擎、物理引擎之上的,主要解决的问题是如何建立一个模型来处理游戏对象 (Game Object) 的更新操作。

传统的很多游戏引擎是基于面向对象来设计的,游戏中的东西都是对象,每个对象有一个叫做 Update 的方法,框架遍历所有的对象,依次调用其 Update 方法。有些引擎甚至定义了多种 Update 方法,在同一帧的不同时机去调用。

这么做其实是有极大的缺陷的,我相信很多做过游戏开发的程序都会有这种体会。因为游戏对象其实是由很多部分聚合而成,引擎的功能模块很多,不同的模块关注的部分往往互不相关。比如渲染模块并不关心网络连接、游戏业务处理不关心玩家的名字、用的什么模型。从自然意义上说,把游戏对象的属性聚合在一起成为一个对象是很自然的事情,对于这个对象的生命期管理也是最合理的方式。但对于不同的业务模块来说,针对聚合在一起的对象做处理,把处理方法绑定在对象身上就不那么自然了。这会导致模块的内聚性很差、模块间也会出现不必要的耦合。

我觉得守望先锋之所以要设计一个新的框架来解决这个问题,是因为他们面对的问题复杂度可能到了一个更高的程度:比如如何用预测技术做更准确的网络同步。网络同步只关心很少的对象属性,没必要在设计同步模块时牵扯过多不必要的东西。为了准确,需要让客户端和服务器跑同一套代码,而服务器并不需要做显示,所以要比较容易的去掉显示系统;客户端和服务器也不完全是同样的逻辑,需要共享一部分系统,而在另一部分上根据分别实现……