实际使用基于组件的实体系统


59

昨天,我阅读了GDC Canada上有关属性/行为实体系统的演示,我认为它很棒。但是,我不确定如何实践地使用它,而不仅仅是理论上的。首先,我将快速向您解释该系统的工作方式。


每个游戏实体(游戏对象)都由属性(=数据,可以通过行为,也可以通过“外部代码”访问)和行为(=逻辑,包含OnUpdate()OnMessage())组成。因此,例如,在Breakout克隆中,每个积木将由(例如!)组成:PositionAttributeColorAttributeHealthAttributeRenderableBehaviourHitBehaviour。最后一个看起来像这样(这只是一个用C#编写的无效示例):

void OnMessage(Message m)
{
    if (m is CollisionMessage) // CollisionMessage is inherited from Message
    {
        Entity otherEntity = m.CollidedWith; // Entity CollisionMessage.CollidedWith
        if (otherEntity.Type = EntityType.Ball) // Collided with ball
        {
            int brickHealth = GetAttribute<int>(Attribute.Health); // owner's attribute
            brickHealth -= otherEntity.GetAttribute<int>(Attribute.DamageImpact);
            SetAttribute<int>(Attribute.Health, brickHealth); // owner's attribute

            // If health is <= 0, "destroy" the brick
            if (brickHealth <= 0)
                SetAttribute<bool>(Attribute.Alive, false);
        }
    }
    else if (m is AttributeChangedMessage) // Some attribute has been changed 'externally'
    {
        if (m.Attribute == Attribute.Health)
        {
            // If health is <= 0, "destroy" the brick
            if (brickHealth <= 0)
                SetAttribute<bool>(Attribute.Alive, false);
        }
    }
}

如果您对此系统感兴趣,可以在此处(.ppt)阅读更多内容。


我的问题与该系统有关,但通常与每个基于组件的实体系统有关。我从未见过任何这些在真实的计算机游戏中如何真正发挥作用,因为我找不到任何好的例子,如果找到一个例子,就没有记载,也没有评论,所以我听不懂。

那么,我想问什么?如何设计行为(组件)。我在GameDev SE上已经读到,最常见的错误是制造许多组件,而只是“使所有组件成为组件”。我已经读到,建议不要在组件中进行渲染,而要在组件外部进行渲染(因此,代替RenderableBehaviour,它应该是RenderableAttribute,并且如果实体的RenderableAttribute设置为true,则Renderer(与组件,但引擎本身)应该在屏幕上绘制它?)。

但是,行为/组件如何?比方说,我有一个级别,并在级别,有一个Entity buttonEntity doorsEntity player。当玩家与按钮碰撞时(这是一个按压力切换的地板按钮),将其按下。按下按钮后,它将打开门。好吧,现在该怎么做?

我想出了这样的东西:玩家拥有CollisionBehaviour,它检查玩家是否与某物发生碰撞。如果他有一个按钮碰撞,它会发送CollisionMessagebutton实体。该消息将包含所有必要的信息:谁与按钮碰撞。该按钮具有ToggleableBehaviour,它将收到CollisionMessage。它会检查与谁发生了碰撞,并且该实体的权重是否足以切换按钮,按钮是否会被切换。现在,它将按钮的ToggledAttribute设置为true。好吧,但是现在呢?

该按钮是否应该向其他所有对象发送另一条消息,以告诉他们该消息已被切换?我认为,如果我做这样的一切,我将收到成千上万条消息,并且会变得非常混乱。所以这也许更好:门不断检查链接到它们的按钮是否被按下,并相应地更改其OpenedAttribute。但这意味着门的OnUpdate()方法将不断地做某事(这真的有问题吗?)。

第二个问题:如果我有更多种类的按钮该怎么办。一种是通过压力按压,另一种是通过枪击来切换,第三种是在往其上注水时进行切换,依此类推。这意味着我将不得不具有不同的行为,如下所示:

Behaviour -> ToggleableBehaviour -> ToggleOnPressureBehaviour
                                 -> ToggleOnShotBehaviour
                                 -> ToggleOnWaterBehaviour

这是真实游戏的运作方式还是我只是愚蠢?也许我只有一个ToggleableBehaviour,它会根据ButtonTypeAttribute表现。因此,如果它是a ButtonType.Pressure,则执行此操作;如果它是a ButtonType.Shot,则执行其他操作...

那我想要什么?我想问一下我是否做得对,或者我只是愚蠢而我不了解组件的要点。我没有找到任何有关组件在游戏中如何真正发挥作用的好例子,仅发现了一些教程描述了如何制作组件系统,但没有介绍如何使用它。

Answers:


46

组件很棒,但是要找到适合您的解决方案可能需要一些时间。别担心,您会到达那里。:)

组织组件

我会说,您在正确的轨道上。我将尝试从门开始到开关结束来反向描述解决方案。我的实现大量使用事件。下面我将描述如何更有效地使用事件,以免它们成为问题。

如果您有一种在实体之间连接实体的机制,我希望开关直接通知门已被按下,然后门可以决定要做什么。

如果您无法连接实体,则您的解决方案与我的工作非常接近。我会让门听一个通用事件(SwitchActivatedEvent,也许)。激活开关后,它们将发布此事件。

如果您有多种类型的开关,我也会有PressureToggleWaterToggle并且也有一种ShotToggle行为,但是我不确定该基础ToggleableBehaviour是否有什么好处,所以我会废除它(当然,除非您有一个好的选择保留它的原因)。

Behaviour -> ToggleOnPressureBehaviour
          -> ToggleOnShotBehaviour
          -> ToggleOnWaterBehaviour

高效的事件处理

至于担心到处都有太多的事件,您可以做一件事。与其通知每个组件发生的每个事件,不如让组件检查它是否是正确的事件类型,而是一种不同的机制...

您可以使用EventDispatcher带有如下形式的subscribe方法(伪代码):

EventDispatcher.subscribe(event_type, function)

然后,当您发布事件时,调度程序将检查其类型,并仅通知已订阅该特定事件类型的那些函数。您可以将其实现为将事件类型与功能列表相关联的映射。

这样,系统效率大大提高:每个事件的函数调用减少了很多,并且组件可以确保它们接收到正确的事件类型,而不必重复检查。

不久前,我在StackOverflow上发布了一个简单的实现。它是用Python编写的,但也许它仍然可以帮助你:
https://stackoverflow.com/a/7294148/627005

该实现非常通用:它可以与任何类型的功能一起使用,而不仅仅是组件中的功能。如果不需要,则function可以behaviorsubscribe方法中有一个参数-需要通知的行为实例。

属性和行为

我来自己使用属性和行为,而不是普通的旧组件。但是,从您对在Breakout游戏中如何使用系统的描述来看,我认为您已经过头了。

仅在两种行为需要访问同一数据时才使用属性。该属性有助于使行为保持分离,并且组件之间的依赖关系(无论是属性还是行为)不会纠缠在一起,因为它们遵循非常简单明了的规则:

  • 属性不使用任何其他组件(既没有其他属性,也没有行为),它们是自给自足的。

  • 行为不使用或不了解其他行为。他们只知道某些属性(他们严格需要的属性)。

当只有一种行为且只有一种行为需要某些数据时,我认为没有理由将其放在属性中,而是让行为保留它。


@ 树heishe的评论

普通零件也不会出现该问题吗?

无论如何,我不必检查事件类型,因为每个函数都一定会始终接收正确的事件类型。

而且,行为的依赖关系(即行为所需的属性)在构造时已解决,因此您不必每次每次更新都寻找属性。

最后,我将Python用于我的游戏逻辑代码(尽管该引擎是C ++),所以不需要强制转换。Python完成了它的鸭式操作,一切正常。但是,即使我不使用鸭嘴式语言,也可以这样做(简化示例):

class SomeBehavior
{
  public:
    SomeBehavior(std::map<std::string, Attribute*> attribs, EventDispatcher* events)
        // For the purposes of this example, I'll assume that the attributes I
        // receive are the right ones. 
        : health_(static_cast<HealthAttribute*>(attribs["health"])),
          armor_(static_cast<ArmorAttribute*>(attribs["armor"]))
    {
        // Boost's polymorphic_downcast would probably be more secure than
        // a static_cast here, but nonetheless...
        // Also, I'd probably use some smart pointers instead of plain
        // old C pointers for the attributes.

        // This is how I'd subscribe a function to a certain type of event.
        // The dispatcher returns a `Subscription` object; the subscription 
        // is alive for as long this object is alive.
        subscription_ = events->subscribe(event::type<DamageEvent>(),
            std::bind(&SomeBehavior::onDamageEvent, this, _1));
    }

    void onDamageEvent(std::shared_ptr<Event> e)
    {
        DamageEvent* damage = boost::polymorphic_downcast<DamageEvent*>(e.get());
        // Simplistic and incorrect formula: health = health - damage + armor
        health_->value(health_->value() - damage->amount() + armor_->protection());
    }

    void update(boost::chrono::duration timePassed)
    {
        // Behaviors also have an `update` function, just like
        // traditional components.
    }

  private:
    HealthAttribute* health_;
    ArmorAttribute* armor_;
    EventDispatcher::Subscription subscription_;
};

与行为不同,属性没有任何update功能-它们不需要,它们的目的是保存数据,而不是执行复杂的游戏逻辑。

您仍然可以让属性执行一些简单的逻辑。在此示例中,a HealthAttribute可以确保0 <= value <= max_health始终为真。HealthCriticalEvent当它下降到25%以下时,它还可以将a发送到同一实体的其他组件,但是它执行的逻辑不会比这复杂。


属性类的示例:

class HealthAttribute : public EntityAttribute
{
  public:
    HealthAttribute(Entity* entity, double max, double critical)
        : max_(max), critical_(critical), current_(max)
    { }

    double value() const {
        return current_;
    }    

    void value(double val)
    {
        // Ensure that 0 <= current <= max 
        if (0 <= val && val <= max_)
            current_ = val;

        // Notify other components belonging to this entity that
        // health is too low.
        if (current_ <= critical_) {
            auto ev = std::shared_ptr<Event>(new HealthCriticalEvent())
            entity_->events().post(ev)
        }
    }

  private:
    double current_, max_, critical_;
};

谢谢!这正是我想要的解决方案。与将简单消息传递给所有实体相比,我也更喜欢您对EventDispatcher的想法。现在,您要告诉我的最后一件事:在本示例中,您基本上说Health和DamageImpact不必是属性。因此,它们不是属性而是属性的私有变量?那意味着,“ DamageImpact”会通过事件传递吗?例如EventArgs.DamageImpact?听起来不错...但是,如果我希望砖根据其健康状况来改变颜色,那么健康就必须成为一种属性,对吗?谢谢!
TomsonTom,2012年

2
@TomsonTom是的,就是这样。让事件保留听众需要知道的任何数据是一个很好的解决方案。
保罗·曼塔

3
这是一个很好的答案!(与pdf一样)-如果有机会,您能否详细说明如何使用此系统处理渲染?这个属性/行为模型对我来说是全新的,但非常有趣。
迈克尔

1
@TomsonTom关于渲染,请参阅我给Michael的答案。至于碰撞,我个人采取了捷径。我使用了一个名为Box2D的库,该库非常易于使用,并且比我能更好地处理碰撞。但是我没有在我的游戏逻辑代码中直接使用该库。每个Entity都有一个EntityBody,抽象出所有丑陋的位。然后,行为可以从中读取位置EntityBody,对其施加作用力,使用人体具有的关节和马达等。进行像Box2D这样的高保真物理模拟无疑会带来新的挑战,但是imo确实很有趣。
保罗·曼塔

1
@thelinuxlich所以您是Artemis的开发人员!:D我已经看到Component/ System方案在板上多次引用。我们的实现确实有很多相似之处。
保罗·曼塔
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.