之所以不在 Component 上绑定方法,是因为在不同的 System 下,对 Component 的操作方法是不固定的。A system 可能对某种组件的数据有一组操作方法;而 B system 用不到这些,但有另外的方法。我们需要的是把对 Component 的操作方法分类,按 System 去组织,而不是拒绝用面向对象的语法去处理 Component 的数据。
如果是 C++ 这种静态语言,我们可以仅在 Component 的基类中保存数据,而把方法都添加在
的派生类中,派生类之添加方法不准增加成员变量;在 Lua 这种动态语言中,能有更灵活的方式来解决这个问题。后面我会介绍这种手法。
- Component_System
另一方面,一个 System 可以关注 Entity 上多个 Component 的组合。如果我们的方法需要同时操作 Component A,B,C ,那么这个方法组织在 A 下或 B,C 下都看起来不是很合适。这类方法还是属于 System 的,也可以由多个 System 共享。在守望先锋的框架中,把它们归类为 Util 方法。
大部分 System 还需要一个单件来储存相关数据。看起来这违背了 System 只有方法没有状态的原则,但实践中单件却大量存在。比如说,空间裁剪器是一个 System ,但我们必须有一个单件来保存空间信息;输入设备管理器也是这样,必须有个位置储存输入状态。所以,我们不妨给所有 System 都绑定一个单件数据结构,这个数据结构的方法仅供该 System 调用(改变状态),但数据可以被其它 System 读取(不改变状态)。
理解了 ECS 的设计和需求后,最后谈谈 Lua 的语法技巧。
我们可以用 lua table 来实现 entity 对象,但这个对象只能由 System 访问,在做引用的时候都必须使用 entity id 。只有 table 对象才好附加操作的方法,数字 id 是不行的。
Component 放在 Entity 对象中,是纯数据结构,不需要方法。所有针对 Component 操作的方法都由 Entity 来发起。
例如,有一类 Component 叫 transform ,它可以有一个方法叫 rotate ,用来旋转。我们在实现 rotate 方法时候,通常会写成:
- function transform:rotate(deg)
- ...
- end
但我们不必真的把这个方法通过 metatable 绑定到 component 对象上,设置给 Entity 其实是一样的。比如在使用的时候,可以要求使用的人这样写:
- entity = world[entity_id] -- 通过 entity id 取得 entity 对象。
- entity:transform_rotate(30) -- 旋转 entity 30 度,这里调用的是上面的 transform.rotate 方法。
调用的是
- entity:transform_rotate(30)
;我们只需要让框架生成这个
- entity.transform_rotate(entity, 30)
函数,让它实际调用
- transform_rotate
就可以了。这对有闭包支持的 lua 来说,生成这个 proxy 函数是小菜一碟。甚至我们可以用 self[1] 来访问 self.transform ,只需要把 .transform 组件固定在 entity 的一号位, 或许能略微提升一点性能。
- return transform.rotate(self.transform, 30)
每个 system 都可以有一组专属的 entity 操作方法,这组方法(包括动态构造出上述的转发函数)是框架在初始化阶段完成的。我们怎样做到在不同的 system 中 entity 的行为不同(可以用到的方法不一样)呢?这就是 Lua 的 metatable 巧妙之处了。
我们不必为每个 entity 对象都产生若干代理对象交给不同的 System 使用,而仅需要在 System 切换的时候,修改一下 entity 类的 metatable 中的
就够了。因为 entity:method() 其实是调用的
- __index
。对于所有 entity 对象,getmetatable(entity) 是一致的,共享同一张表,我们只需要一行代码就可以切换当下所有 entity 的行为。
- getmetatable(entity).__index.method(entity)
如果有必要,我们还可以在这个
中作进一步的运行时检查,检查当前 System 有没有访问没有声明的 Component 数据。这种运行时检查也可以方便的开关。
- __index
ECS 框架中,System 是依次运行的,System 相互之间也禁止调用。所以 System 的切换是完全可控,且发生频率很低的。这种通过切换
的方式来改变不同 System 下 Entity 行为的方法简单可行。
- __index
和传统的 OOP 方式,定义类的数据结构和方法不同。对于 ECS 系统,我们需要定义的是:
这里 4,5,6 定义的方法集,只能在 3 提到的 Update/Notify 函数中调用。
来源: http://www.tuicool.com/articles/2yQnuym