《DFQ》开发随录——ECS

  欢迎参与讨论,转载请注明出处。

前言

  在阅读本文之前,你需要了解一下何为ECS框架,今年年初本人也对此进行了相关研究。到了实际开发时发现确实有此需求,遂应用之。本文便记录其中心得。

ECS的意义

  在讨论实现细节之前,首先要弄明白一个关键问题:为何要用ECS? 对于我而言,使用ECS的意义在于使用传统OOP方式构造一个高度复杂的对象集时异常困难。在我看来,高度复杂的对象集即「多衍生物、衍生物之间多多少少拥有些共性」的存在。游戏中于地图上活跃的对象便是如此,拥有多种形式(物体、特效、子弹、NPC、怪物……),而这些衍生物之间多多少少会拥有一些共性(怪物和NPC都要寻路),如何组织安排好这些功能是很麻烦的一件事。在以往的开发生涯中,这部分我重构过很多遍,尝试过各种形式(将通用的功能做成子对象之类的),最终发现:ECS便是解决此问题的绝佳利器

实现要素

relationship

  上图便是本项目ECS框架的结构了,大致介绍一二:

  • Entity(实体): Entity是对象的主体,Component的容器,在数据结构的形式上就是个哈希表。
  • Component(组件): Component是数据的容器,与Data对接,提取相关数据。Component的形式多样,如Transform、Aspect、Input等。Component只有构造函数。
  • Data(数据): 来自配置文件,其中定义了各Component的配置所需。可由Manager将Data作为参数创建Entity。
  • Group(群组): Group以Component作为条件筛选出合适的Entity集合,如此便可使符合条件的Entity运作相应的业务。
  • System(系统): 业务运作的主体,以Group进行筛选出合适的Entity以执行相应的业务。分为Enter, Init, Exit, Update, LateUpdate, Draw六个业务函数。System的形式多样,如Drawing、Life、Battle等。
  • Lib(库): 存放通用业务函数之处,原则上以具体所需Component为参数,而非Entity,如Hitstop(attacker, identity, time)。如此是为明确函数调用条件,以及可以使Component分别来自不同Entity,实现一些特殊需求。Lib的形式多样,如AI、Battle、Effect等。
  • Manager(中枢): 负责Group与Entity的管理,如AddComponent, DelComponent, NewGroup, NewEntity等。
  • Executor(执行): 整套系统的执行者,负责导入System,定义System的执行顺序以及提供System的执行场所。

使用演示

  以上便是ECS框架的组成元素了,接下来展示一下使用场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
-- attributes.lua
local _ATTRIBUTE = require("actor.lib.attribute") -- Lib
local _Timer = require("util.gear.timer")
local _Base = require("actor.system.base")
local _Attributes = require("core.class")(_Base) -- System
function _Attributes:Ctor(upperEvent)
-- Filter
_Base.Ctor(self, upperEvent, {
battle = true,
attributes = true
})
self._timer = _Timer.New(1000)
end
function _Attributes:Update(dt)
self._timer:Update(dt)
if (not self._timer.isRunning) then
-- List is from group.
for n=1, self._list:GetLength() do
local e = self._list:Get(n) -- Entity
-- Component of Battle
if ((e.battle and not e.battle.isDead) or not e.battle) then
local attributes = e.attributes -- Component of Attributes
_ATTRIBUTE.AddHp(attributes, attributes.hpRecovery)
_ATTRIBUTE.AddMp(attributes, attributes.mpRecovery)
end
end
self._timer:Enter()
end
end
return _Attributes

  可以看到,这是属性相关的System,它提供了每秒回复HP与MP的业务。拥有Battle与Attributes组件的Entity方可执行,并且了ATTRIBUTE这个Lib的函数。采用这种形式只需要将业务分割为一个个System,以不同的Component组成游戏对象即可达到极高的灵活度。对于高度复杂的对象集而言可谓绝佳的解决方案。

子对象问题

  在开发的过程中,总会遇到诸如状态、技能、BUFF之类需要以子对象形式存在的情况。为此应当如何实现是ECS框架绕不开的一个问题。我曾尝试为他们也纳入至ECS框架中,但是这样会使得System的数量膨胀,而且并没有带来什么明显的好处(它们的独立性很高)。也曾试过为它们弄二级ECS框架,但感觉很刻意死板。最终领悟到了一点:ECS框架对我而言的意义,只是降低构建对象的复杂度,若是对象本身的复杂度并不高,采用OOP的方式完全可以接受

后记

  ECS框架我只用在了地图对象,其余部分(如UI)等都是采用传统的面向对象形式,因为他们的构成复杂度并不高,切忌犯了“为用而用”的错误。当然对于Unity那边而言,ECS的意义在于达到高性能(内存连续、非GC、高Cache命中率、多线程),这时候为了高性能是不得不用了。当然Unity的ECS框架我所涉猎并不多,有待后日挖掘。