zhouqijie

几种运行时对象模型架构

Jason:运行时对象模型的实现,可能与工具方的抽象对象模型相似,也可能不相似。(甚至可能不是面向对象编程的)

多数引擎会采用两种基本架构风格:

每个工具方游戏对象,在运行时是以单个类实例或数个相连实例表示的,每个对象含有一组属性及行为,全部都封装在对象的类里。游戏世界只是游戏对象的集合。

每个工具方游戏对象仅以唯一标识符表示。每个对象的属性分布于多个数据列表,每种属性对应一个列表,这些属性以对象标识符为键。



1. 对象为中心的架构

以对象为中心的游戏对象架构,每个游戏对象实现为一个或者一组类实例。

//示例:  
struct Obj
{
    Transform_t transform;
    Mesh* mesh;
    //...
    void (*update)();
    void (*draw)();
}

类层次结构:

Jason:虚幻引擎的游戏对象模型就是一个例子。

最直观的游戏对象归类方式就是使用类层次结构,大部分商业引擎都采用类层次结构。所有游戏对象类都继承自单个基类。

但是类层次结构会越来越庞大,导致很多问题。

类层次结构的问题:

越是在类层次结构中越深的类,越难以理解、维护、修改。因为要理解一个类必须要了解其所有父类。

类层次结构限制了创建新类型时的选择。(例如CollidableObj继承自RenderableObj,如果希望一个游戏对象有碰撞功能,就要继承自CollidableObj,即使它是不可见的不需要RenderObj的功能)

难以扩展现存类的功能。(例如要把一个类A扩展派生为A1和A2两种,那么继承自A类的类B也必须重构成两个类分别继承A1和A2)

对象的分类通常不是单一标准的。单一类层次无法满足多维的分类。

Cre:强行加入类层次树中会出现不同父类但完全相同的多种选择。(比如可渲染的网格对象网格类型的可渲染对象可以分别继承自网格对象可渲染对象

多重继承是多维分类的解决方法之一。但是C++的多重继承有很多问题例如基类成员重复,所以通常禁止或者限制使用多继承。

要加入新的分类标准,又不想使用多继承,就会把相关的属性代码往层次结构上方搬运置于共同基类当中。如果容许一些功能在单一庞大的层次结构中冒泡,多个引擎子系统的封装会变得困难。(虚幻引擎的Actor类是一个经典例子)

多重继承的另一种形式就是使用mix-in类。即所有类都继承自一个共同的基类,但每个类可以有任意数量的mix-in类,mix-in类都是无基类的独立类。

Cre:mix-in类的作用相当于C#中的接口。

更好的做法是使用合成(composition)或者聚合(aggregation),而不是继承。

合成(Composition)的结构:

庞杂的类层次结构是过度使用“是一个(is-a)”关系造成的。一个有用的方法是使用合成(composition),即把”is-a”关系改为“有一个(has-a)”关系。

把游戏对象不同功能分离成独立的类,每个类负责单一的服务。这些类通常称为组件(component)

Jason:组件化设计能让我们可以只选择游戏对象需要的功能,并且每项功能可以独立维护和扩展而不影响其他功能。当某个游戏对象整合多个子系统时,这些子系统能互相保持距离及良好的封装。

Jason:在组件化的设计中游戏对象变为一个枢纽(hub),负责管理其组件的生命周期。

Jason:枢纽含有每个可能组件的指针,并在构造函数中设置为null。其析构函数通常可以自动清理所有组件。

通用组件:

使用通用组件是另一种更灵活的方法。这种设计中所有组件都继承自一个共同的基类Component类,称为通用组件。

在GameObject类中加入通用组件的链表。遍历组件链表时能利用Component类的多态操作,例如查询该类的类型,或者向组件发送事件。

相比硬编码的组件指针,通用组件的设计无需修改游戏对象就能创建新的组件类型,并且支持多个相同组件。

Cre:Unity使用通用组件的设计。

纯组件模型:

如果把GameObject类的几乎所有功能移到多个组件当中,所有组件在逻辑上以标识符分组连接起来,并能通过标识符快速查找组件,这时就不再需要GameObject这个枢纽了。这种架构称为纯组件模型(pure component model)

在纯组件架构中,没有GameObject枢纽来编排多个组件之间的通信。组件可以使用游戏对象的唯一标识符查找该对象的其他组件,或者更高效的机制例如预先把组件连接成循环链表。

纯组件架构中,某游戏对象和另一个游戏对象的通信也比较困难。



2.以属性为中心的架构

Jason:习惯面向对象语言的程序员,常会自然地使用对象属性和行为思考问题,称为以对象为中心(object-centric)的视角。

Jason:如果以属性为中心来思考而不是对象,先定义游戏对象可能含有的属性集合,然后为每个属性建表,这些属性以对象唯一标识符为键。这称为以属性为中心(property-centric)的视角。

Jason:以属性为中心的设计类似关系数据库,每个属性看作数据表的一列,游戏对象唯一标识符为主键。

属性为中心架构怎么实现行为:

每种属性可以实现为属性类,属性可以是简单的单值,也可以是复杂类型例如三角形网格数据。

属性类可以通过硬编码成员函数来实现行为。游戏对象的行为等于其全部属性的行为集合。

另一个方法是把属性值存储于数据表中,然后用脚本代码实现对象行为。

和组件的区别:

Jason:属性为中心的设计和组件为中心的设计很多方面都相似。仅存在一些细微、无关紧要的区别。

属性为中心设计中,子对象(属性)定义游戏对象本身的某个属性(例如血量等)。
组件为中心设计中,子对象(组件)通常代表某底层引擎子系统(例如渲染器、动画等)。

属性为中心的优缺点:

  1. 游戏对象的属性之间的关系不明确,难以凑齐一些细粒度的属性去实现一个复杂行为。
  2. 游戏对象难以Debug。

(END)