实践中,发现面向组件-状态机-消息驱动,如果整合起来,能够更加自然和简单的进行抽象。这些都是以面向对象为基础,更进一步的抽象扩展。最初的灵感是在看这本书的时候产生的,Practical UML Statecharts in C/C++, Second Edition。本文会分别简单介绍一下,面向组件,状态机,消息驱动的各自特点。然后,结合Mojoc的代码,看看是如何把三者整合起来的使用的。
显然游戏引擎的架构是一个很切合的使用场景,通常有以下特点。
* 基类Component提供组件生命周期的绑定,子类负责实现特定的功能。
* Entity对象使用组合模式,负责管理多个Component,包括Component的状态切换和生命周期。
* 代码功能以组件为单位得到复用与自由组合。
* Entity作为组件的抽象集合,可与其它Entity发生交互和消息交换。
面向组件,其实就是把继承得到的功能复用模式,拆散到组件里,然后使用组合模式来绑定组件使用。这样就解决了面向对象的以下几个问题。
* 为了一个功能去继承,获得了父类其它无用的,甚至不相关的功能,形成了继承模板的冗余。
* 继承链超过3层的时候,对象职能无法保持单一,不便于记忆和使用,增加了心智负担,让人心理感觉复杂。
* 继承链中,父类属性个数增长迅速,不仅有潜在的冲突,还会互相影响形成意外的结果。
* 对于复用功能层面,继承复用显得力度过大,也不利于类的抽象设计。
面向组件是抽象力度更小的描述,组合模式相比继承带来了隔离性,不会传递继承链上的属性和功能。如果在面向对象的设计中,抽象更多的工具类,然后组合起来使用,并且增加这些工具类的生命周期和状态管理,这已经是面向组件的设计思想了。
有了组件化的静态描述,有了状态机的动态描述,那么剩下的就是消息传递来产生交互了。消息的传递,驱动了状态的变化,驱动依靠的是行为也就是函数,状态变化则就是属性变化的结果。有了消息驱动,就让静态描述,表现出了动态变化,产生了交互。消息驱动,一般通过,观察者模式,消息订阅,或是消息轮询来实现。
如果,我们把以上三者合起来,抽象成一个最基本的结构。可以想象,一个原子化的组件,实现了单一功能,有自己的状态变化,可以发送消息,也能够处理消息。这个原子组件,可以自由的组合,形成更大一些组件,然后递归的组合,形成更丰富的组件。并且这一切都是动态变化的,无论是更大的组件还是更小的组件,都是由原子组件所构成,有自己状态和交互性。这将会形成很强的抽象和描述能力。
Mojoc实现了这个模式,Component.h,在编写游戏逻辑的时候感觉是清晰而明确的,可以参看,Hero.c 和 Enemy.c。下面介绍一下Mojoc的实现代码。
这里并没有使用Entity去管理Component,而是把Entity的功能嵌入了Component,因为我觉得这样也行。Component形成一个递归的树形结构,可以管理子Component。
struct Component
{
int order;
Component* parent;
ArrayIntMap(order, Component*) childMap[1];
};
struct AComponent
{
void (*AddChild) (Component* parent, Component* child, int order);
void (*AppendChild) (Component* parent, Component* child);
void (*RemoveChild) (Component* parent, Component* child);
void (*RemoveAllChildren) (Component* parent);
void (*ReorderAllChildren)(Component* parent);
};
struct ComponentState
{
int id;
void (*Update) (Component* component, float deltaSeconds);
void (*UpdateAfter)(Component* component, float deltaSeconds);
bool (*OnMessage) (Component* component, void* sender, int subject, void* extraData);
};
struct Component
{
ComponentState* curState;
ComponentState* preState;
ComponentState* defaultState;
ArrayIntMap(stateId, ComponentState*) stateMap[1];
};
struct AComponent
{
void (*SetState)(Component* component, int stateId);
ComponentState* (*AddState)(Component* component, int stateId, ComponentStateOnMessage onMessage, ComponentStateUpdate update);
};
struct Component
{
ArrayIntSet(Component*) observerSet[1];
};
struct AComponent
{
void (*AddObserver) (Component* sender, Component* observer);
void (*RemoveObserver)(Component* sender, Component* observer);
bool (*SendMessage) (Component* component, void* sender, int subject, void* extraData);
void (*Notify) (Component* sender, int subject, void* extraData);
};
组件,状态机,消息驱动,是常用基础的设计模式,只不过大部分时候是独立分散的使用。这里我把它们放到一起,作为一个最基础的结构,强制性使用,可能会浪费一些空间。但换一种视角,现实世界所有的一切,的确无论大到宇宙星球,还是小到微观粒子,都有状态,能接受信息,也能释放信息。这种抽象或许就是应对了现实世界的运作模式。
当然,这个模式可以使用任何语言实现,好不好用,尝试了才知道。
「组件-状态-消息」