실질적으로 컴포넌트 기반 엔터티 시스템 사용


59

어제 GDC Canada에서 속성 / 행동 엔터티 시스템에 대한 프레젠테이션을 읽었으며 꽤 훌륭하다고 생각합니다. 그러나 이론적으로뿐만 아니라 실제로 사용하는 방법을 잘 모르겠습니다. 우선이 시스템의 작동 방식을 신속하게 설명하겠습니다.


각 게임 개체 (게임 오브젝트)가 구성되어 속성 과 (또한 "외부 코드"에 의해 동작에 의해 액세스 될 수 = 데이터) 동작 (포함 = 로직, OnUpdate()OnMessage()). 예를 들어 브레이크 아웃 클론에서 각 브릭은 (예!) PositionAttribute , ColorAttribute , HealthAttribute , RenderableBehaviour , HitBehaviour로 구성 됩니다. 마지막은 다음과 같이 보일 수 있습니다 (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 button, Entity doors하고 Entity player. 플레이어가 버튼 (충돌에 의해 토글되는 플로어 버튼)과 충돌하면 눌려집니다. 버튼을 누르면 문이 열립니다. 글쎄, 이제 어떻게해야합니까?

플레이어는 CollisionBehaviour 를 가지고 있는데, 플레이어가 무언가와 충돌하는지 확인합니다. 그가 버튼과 충돌 CollisionMessage하면, button엔티티에 a 를 보냅니다 . 메시지는 필요한 모든 정보를 포함합니다 : 누가 버튼과 충돌했는지. 버튼에 ToggleableBehaviour 가 있으며 , 이 버튼 을 받게됩니다 CollisionMessage. 누가 충돌했는지 확인하고 해당 엔티티의 무게가 버튼을 토글하기에 충분히 큰 경우 버튼이 토글됩니다. 이제 버튼 의 ToggledAttribute 를 true로 설정합니다. 알았어,하지만 지금은?

버튼이 다른 모든 객체에 다른 메시지를 보내서 전환되었음을 알려야합니까? 이런 식으로 모든 일을하면 수천 개의 메시지를받을 수 있고 지저분해질 것입니다. 어쩌면 이것이 더 낫습니다. 문은 연결된 버튼이 눌 렸는지 여부를 지속적으로 확인하고 그에 따라 OpenedAttribute를 변경합니다 . 그러나 그것은 문의 OnUpdate()방법이 끊임없이 무언가를 할 것임을 의미합니다 (실제로 문제가 있습니까?).

두 번째 문제는 더 많은 종류의 버튼이 있다면 어떨까요? 하나는 압력에 의해 눌려지고, 다른 하나는 그것을 쏘아서 토글되고, 다른 하나는 물을 부으면 토글됩니다. 이것은 다른 행동을 가져야 함을 의미합니다.

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

이것이 실제 게임이 작동하는 방식입니까 아니면 내가 바보입니까? 어쩌면 하나의 ToggleableBehaviour 만 가질 수 있으며 ButtonTypeAttribute 에 따라 작동합니다 . 따라서이 경우이 작업 ButtonType.Pressure을 수행합니다.이 경우 ButtonType.Shot다른 작업을 수행합니다.

그래서 나는 무엇을 원합니까? 내가 제대로하고 있는지 묻고 싶거나 어리 석고 구성 요소의 요점을 이해하지 못했습니다. 구성 요소가 게임에서 실제로 어떻게 작동하는지에 대한 좋은 예를 찾지 못했습니다. 구성 요소 시스템을 만드는 방법을 설명하지만 사용 방법을 설명하지 않는 자습서가 있습니다.

답변:


46

구성 요소는 훌륭하지만 기분이 좋은 솔루션을 찾는 데 시간이 걸릴 수 있습니다. 걱정하지 마십시오. :)

구성 요소 구성

당신은 올바른 길을 가고 있습니다. 문에서 시작하여 스위치로 끝나서 솔루션을 반대로 설명하려고합니다. 내 구현은 이벤트를 많이 사용합니다. 아래에서는 이벤트를보다 효율적으로 사용하여 문제가되지 않는 방법을 설명합니다.

당신이 그들 사이에 엔티티를 연결하는 메커니즘이 있다면, 스위치가 도어를 눌렀다는 것을 직접 통보하면 도어는 무엇을 해야할지 결정할 수 있습니다.

엔터티를 연결할 수 없으면 솔루션이 내가 한 일에 매우 가깝습니다. 문에서 일반적인 이벤트를 듣고 싶습니다 SwitchActivatedEvent. 스위치가 활성화되면이 이벤트가 게시됩니다.

당신이 스위치의 여러 유형이있는 경우, 내가 가진 것 PressureToggle, WaterToggle그리고 ShotToggle너무 행동,하지만 기본은 확실하지 않다 ToggleableBehaviour좋은, 그래서 나는 그 (멀리 할 것, 물론, 당신은 좋은이없는 경우 보관 이유).

Behaviour -> ToggleOnPressureBehaviour
          -> ToggleOnShotBehaviour
          -> ToggleOnWaterBehaviour

효율적인 이벤트 처리

주위에 너무 많은 이벤트가 있다는 것을 걱정하기 위해 할 수있는 한 가지가 있습니다. 모든 구성 요소에 발생하는 모든 단일 이벤트를 알리는 대신 구성 요소가 올바른 유형의 이벤트인지 확인하도록하십시오. 여기에는 다른 메커니즘이 있습니다.

당신은 할 수 있습니다 EventDispatcherA를 subscribe이 (의사)과 같은 형태 방법 :

EventDispatcher.subscribe(event_type, function)

그런 다음, 이벤트를 게시하면 디스패처가 해당 유형을 확인하고 해당 특정 유형의 이벤트에 등록한 기능 만 알립니다. 이를 이벤트 유형과 함수 목록을 연관시키는 맵으로 구현할 수 있습니다.

이러한 방식으로 시스템의 효율성이 크게 향상됩니다. 이벤트 당 함수 호출이 훨씬 적으며 구성 요소가 올바른 유형의 이벤트를 수신했으며 이중 점검 할 필요가 없는지 확인할 수 있습니다.

얼마 전에 StackOverflow에 간단한 구현을 게시했습니다. 파이썬으로 작성되었지만 여전히 도움이 될 수 있습니다.
https://stackoverflow.com/a/7294148/627005

구현은 매우 일반적입니다. 구성 요소의 함수뿐만 아니라 모든 종류의 함수와 작동합니다. 그 대신이 필요하지 않은 경우 function, 당신은 할 수 behavior귀하의 매개 변수를 subscribe통보해야하는 비헤이비어 인스턴스 - 방법을.

속성과 행동

평범한 오래된 구성 요소 대신 속성과 동작을 직접 사용했습니다 . 그러나 Breakout 게임에서 시스템을 사용하는 방법에 대한 설명에서 나는 그것을 과도하게 사용하고 있다고 생각합니다.

두 동작이 동일한 데이터에 액세스해야하는 경우에만 특성을 사용합니다. 이 속성은 동작을 개별적으로 유지하는 데 도움이되며 구성 요소 간의 종속성 (속성 또는 동작)은 매우 간단하고 명확한 규칙을 따르기 때문에 얽 히지 않습니다.

  • 속성은 다른 구성 요소 (다른 속성이나 동작 모두)를 사용하지 않으며 자체적으로 충분합니다.

  • 행동은 다른 행동을 사용하거나 알지 못합니다. 그들은 속성 중 일부 에 대해서만 알고 있습니다 (엄격히 필요한 속성).

일부 데이터가 동작 중 하나만 필요로하는 경우에는 속성에 넣을 이유가 없으며 동작을 유지합니다.


@ heishe 님의 댓글

일반 구성 요소에서도 문제가 발생하지 않습니까?

어쨌든 모든 함수가 항상 올바른 유형의 이벤트를 수신해야하기 때문에 이벤트 유형을 확인할 필요가 없습니다 .

또한 동작의 종속성 (필요한 속성)은 구성시 해결되므로 모든 업데이트마다 속성을 찾을 필요가 없습니다.

마지막으로, 게임 로직 코드 (엔진은 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기능이 없습니다. 필요하지는 않지만 복잡한 게임 로직을 수행하지 않고 데이터를 보유하는 것이 목적입니다.

여전히 속성이 간단한 로직을 수행하도록 할 수 있습니다. 이 예에서는 항상 사실임을 HealthAttribute보장 할 수 있습니다 0 <= value <= max_health. 또한 HealthCriticalEvent25 % 이하로 떨어질 때 동일한 엔터티의 다른 구성 요소에 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

2
@TomsonTom 그렇습니다. 이벤트가 리스너가 알아야 할 모든 데이터를 보유하도록하는 것은 매우 좋은 솔루션입니다.
Paul Manta

3
이것은 좋은 답변입니다! (pdf 그대로)-기회가있을 때이 시스템으로 렌더링 을 처리하는 방법에 대해 좀 더 자세히 설명해 주 시겠습니까? 이 속성 / 동작 모델은 완전히 새로운 것이지만 매우 흥미 롭습니다.
Michael

1
@TomsonTom 렌더링에 대해 Michael에게 준 답변을 참조하십시오. 충돌에 관해서는 개인적으로 지름길을 가졌습니다. Box2D라는 라이브러리를 사용했는데 사용하기 쉽고 충돌보다 훨씬 잘 처리합니다. 그러나 게임 로직 코드에서 직접 라이브러리를 사용하지 않습니다. 모든 Entity에는 EntityBody모든 못생긴 비트를 추상화합니다. 그런 다음 행동은에서 위치를 읽고 EntityBody힘을 가하며 몸에있는 관절과 모터 등을 사용할 수 있습니다. Box2D와 같은 고 충실도 물리 시뮬레이션은 새로운 도전을 가져 오지만 확실히 재미 있습니다.
Paul Manta

1
@thelinuxlich 그래서 당신은 Artemis의 개발자입니다! : D 보드 에서 Component/ System체계가 몇 번 참조되는 것을 보았습니다 . 우리의 구현은 실제로 상당히 유사합니다.
Paul Manta
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.