基于组件的设计:处理对象交互


9

我不确定对象在基于组件的设计中如何精确地处理其他对象。

说我有Obj课。我做:

Obj obj;
obj.add(new Position());
obj.add(new Physics());

那么,我又如何才能使另一个对象不仅使球移动,而且还要应用那些物理方法。我不是在寻找实现细节,而是抽象地讲对象如何通信。在基于实体的设计中,您可能只有:

obj1.emitForceOn(obj2,5.0,0.0,0.0);

为了更好地掌握组件驱动的设计以及如何进行基本操作的任何文章或说明都将非常有帮助。

Answers:


10

通常使用消息来完成。您可以在此站点上的其他问题中找到很多详细信息,例如此处此处

为了回答您的特定示例,一种方法是定义Message对象可以处理的小类,例如:

struct Message
{
    Message(const Objt& sender, const std::string& msg)
        : m_sender(&sender)
        , m_msg(msg) {}
    const Obj* m_sender;
    std::string m_msg;
};

void Obj::Process(const Message& msg)
{
    for (int i=0; i<m_components.size(); ++i)
    {
        // let components do some stuff with msg
        m_components[i].Process(msg);
    }
}

这样,您就不会“污染” Obj类与组件相关的方法的接口。有些组件可以选择处理消息,有些则可以忽略它。

您可以从另一个对象直接调用此方法开始:

Message msg(obj1, "EmitForce(5.0,0.0,0.0)");
obj2.ProcessMessage(msg);

在这种情况下,obj2Physics会挑消息,做任何处理,它需要做的。完成后,它将:

  • 向自己发送“ SetPosition”消息,Position组件将选择该消息;
  • 或直接访问Position组件以进行修改(对于纯基于组件的设计来说,这是非常错误的,因为您不能假定每个对象都有一个Position组件,但是该Position组件可能是的要求Physics)。

将消息的实际处理延迟下一个组件的更新通常是一个好主意。立即处理该消息可能意味着将消息发送到其他对象的其他组件,因此仅发送一条消息可能会很快意味着一个不可分割的意大利面条堆栈。

稍后可能需要使用更高级的系统:异步消息队列,将消息发送到对象组,按组件注册/从消息中注销等。

Message类可以是一个通用的容器,用于一个简单的字符串如上述所示,但在运行时处理的字符串是没有真正有效的。您可以使用一个通用值的容器:字符串,整数,浮点数...使用名称或更佳的ID来区分不同类型的消息。或者,您也可以派生基类以满足特定需求。在您的情况下,您可以想象一个EmitForceMessageMessage所需力矢量派生并添加所需力矢量的,但要小心RTTI的运行时成本。


3
我不用担心直接访问组件的“非纯度”。组件用于满足功能和设计需求,而不是学术界。您要检查组件是否存在(例如,对于get组件调用,检查返回值是否不为null)。
肖恩·米德迪奇

我一直想着您上次使用RTTI时所说的,但是有很多人对RTTI说了很多坏话
jmasterx 2012年

@SeanMiddleditch当然,我会这样做,只是要说清楚,当您访问同一实体的其他组件时,您应始终仔细检查您在做什么。
洛朗·库维杜

@Milo编译器实现的RTTI及其dynamic_cast 可能成为瓶颈,但是我现在暂时不必担心。如果仍然有问题,您仍然可以稍后对其进行优化。基于CRC的类标识符的工作原理像一个超级按钮。
Laurent Couvidou 2012年

´template <typename T> uint32_t class_id(){static uint32_t v; 返回(uint32_t)&v; }´-无需RTTI。
2012年

3

为了解决与您显示的问题类似的问题,我要做的是添加一些特定的组件处理程序并添加某种事件解析系统。

因此,对于您的“ Physics”对象,在初始化该对象时会将其自身添加到Physics objetcs的中央管理器中。在游戏循环中,此类管理器具有自己的更新步骤,因此,在更新此PhysicsManager时,它将计算所有物理相互作用,并将它们添加到事件队列中。

产生所有事件之后,您可以解决事件队列,只需检查发生了什么并相应地采取措施,就您的情况而言,应该有一个事件表明对象A和B以某种方式发生了交互,因此您可以调用generateForceOn方法。

这种方法的优点:

  • 从概念上讲,它很容易遵循。
  • 给您提供特定优化的空间,例如使用四边形或您需要的任何东西。
  • 最终实际上是“即插即用”。具有物理的对象不会与非物理的对象进行交互,因为对于管理器而言它们不存在。

缺点:

  • 您最终会得到很多引用,因此,如果不小心清理所有内容(从组件到组件所有者,从经理到组件,从事件到参与者等),正确清理所有内容可能会有些混乱。 )。
  • 您必须特别考虑解决所有问题的顺序。我想这不是您的情况,但我遇到了多个无限循环,其中一个事件创建了另一个事件,而我只是将其直接添加到事件队列中。

我希望这有帮助。

PS:如果有人有更清洁/更好的方法来解决此问题,我真的很想听听。


1
obj->Message( "Physics.EmitForce 0.0 1.1 2.2" );
// and some variations such as...
obj->Message( "Physics.EmitForce", "0.0 1.1 2.2" );
obj->Message( "Physics", "EmitForce", "0.0 1.1 2.2" );

关于此设计的一些注意事项:

  • 组件的名称是第一个参数-这是为了避免在消息上处理过多的代码-我们不知道任何消息可能触发哪些组件-并且我们不希望所有组件都以90%的失败率来咀嚼一条消息转换为许多不必要的分支和strcmp的速率。
  • 消息名称是第二个参数。
  • 第一个点(在#1和#2中)不是必需的,只是为了使阅读更容易(对于人,而不是计算机)。
  • 兼容sscanf,iostream和you-name。没有句法糖不能简化消息的处理。
  • 一个字符串参数:就内存需求而言,传递本机类型并不便宜,因为您必须支持未知数量的相对未知类型的参数。
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.