当前位置: 首页 > 工具软件 > Mojoc > 使用案例 >

「游戏引擎Mojoc」(4)面向组件-状态机-消息驱动3合1编程模型

郝承悦
2023-12-01

实践中,发现面向组件-状态机-消息驱动,如果整合起来,能够更加自然和简单的进行抽象。这些都是以面向对象为基础,更进一步的抽象扩展。最初的灵感是在看这本书的时候产生的,Practical UML Statecharts in C/C++, Second Edition。本文会分别简单介绍一下,面向组件,状态机,消息驱动的各自特点。然后,结合Mojoc的代码,看看是如何把三者整合起来的使用的。

面向组件

显然游戏引擎的架构是一个很切合的使用场景,通常有以下特点。
* 基类Component提供组件生命周期的绑定,子类负责实现特定的功能。
* Entity对象使用组合模式,负责管理多个Component,包括Component的状态切换和生命周期。
* 代码功能以组件为单位得到复用与自由组合。
* Entity作为组件的抽象集合,可与其它Entity发生交互和消息交换。

面向组件,其实就是把继承得到的功能复用模式,拆散到组件里,然后使用组合模式来绑定组件使用。这样就解决了面向对象的以下几个问题。
* 为了一个功能去继承,获得了父类其它无用的,甚至不相关的功能,形成了继承模板的冗余。
* 继承链超过3层的时候,对象职能无法保持单一,不便于记忆和使用,增加了心智负担,让人心理感觉复杂。
* 继承链中,父类属性个数增长迅速,不仅有潜在的冲突,还会互相影响形成意外的结果。
* 对于复用功能层面,继承复用显得力度过大,也不利于类的抽象设计。

面向组件是抽象力度更小的描述,组合模式相比继承带来了隔离性,不会传递继承链上的属性和功能。如果在面向对象的设计中,抽象更多的工具类,然后组合起来使用,并且增加这些工具类的生命周期和状态管理,这已经是面向组件的设计思想了。

状态机

  • 任何事物都有状态,并在某一刻处在某一种状态下。
  • 状态机是一种视角,通过变化的切入点来抽象和描述。
  • 如果说组件和对象是一种静态描述,那么状态机就是一种动态描述。
  • 状态机起到了隔离并捕捉变化的作用,让状态描述和抽象更加单一和内聚。

消息驱动

有了组件化的静态描述,有了状态机的动态描述,那么剩下的就是消息传递来产生交互了。消息的传递,驱动了状态的变化,驱动依靠的是行为也就是函数,状态变化则就是属性变化的结果。有了消息驱动,就让静态描述,表现出了动态变化,产生了交互。消息驱动,一般通过,观察者模式,消息订阅,或是消息轮询来实现。

3合1

如果,我们把以上三者合起来,抽象成一个最基本的结构。可以想象,一个原子化的组件,实现了单一功能,有自己的状态变化,可以发送消息,也能够处理消息。这个原子组件,可以自由的组合,形成更大一些组件,然后递归的组合,形成更丰富的组件。并且这一切都是动态变化的,无论是更大的组件还是更小的组件,都是由原子组件所构成,有自己状态和交互性。这将会形成很强的抽象和描述能力。

Mojoc的实现

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);
};
  • Component通过childMap去管理子Component。
  • 平行的子Component通过Order排序。

状态机

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);
};
  • 每一个状态,都可以通过OnMessage去接受和处理消息。
  • 状态并没有,OnEnter和OnExit的回调,是因为被整合进了OnMessage。
  • 包括事件处理,Component之间的消息,都是通过OnMessage来处理。
  • 层次的Components,整体上看,可以组成一个层次状态机。

消息驱动

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);
};
  • 每一个Component都是可以发布消息,或订阅消息的。
  • SendMessage是向Component和子Component发送消息。
  • Notify是向订阅者发布消息。

总结

组件,状态机,消息驱动,是常用基础的设计模式,只不过大部分时候是独立分散的使用。这里我把它们放到一起,作为一个最基础的结构,强制性使用,可能会浪费一些空间。但换一种视角,现实世界所有的一切,的确无论大到宇宙星球,还是小到微观粒子,都有状态,能接受信息,也能释放信息。这种抽象或许就是应对了现实世界的运作模式。

当然,这个模式可以使用任何语言实现,好不好用,尝试了才知道。


「组件-状态-消息」

 类似资料: