コンポーネントベースのエンティティシステムを実際に使用する


59

昨日、私はGDCカナダから、属性/行動エンティティシステムに関するプレゼンテーションを読みました。ただし、理論上だけでなく、実際に使用する方法もわかりません。まず、このシステムの仕組みを簡単に説明します。


各ゲームエンティティ(ゲームオブジェクト)は、属性(=データ、動作によってアクセスできるだけでなく、「外部コード」によってもアクセス可能)および動作(= OnUpdate()およびを含むロジック)で構成されますOnMessage()。したがって、たとえば、ブレイクアウトクローンでは、各ブリックは(例!)で構成されます: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()方法が絶えず何かをしていることを意味します(それは本当に問題ですか?)。

2番目の問題:ボタンの種類が増えた場合はどうなるか。1つは圧力で押され、2つ目はそれを撃つことで切り替えられ、3つ目は水がかけられると切り替えられます。

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

これは実際のゲームの仕組みですか、それとも私はただのバカですか?たぶん、ToggleableBehaviourを1つだけ持つことができ、ButtonTypeAttributeに従って動作します。それがaの場合ButtonType.Pressure、これを行い、aの場合ButtonType.Shot、何か他のことをします...

だから私は何が欲しいのですか?私はそれを正しくやっているのか、それとも愚かでコンポーネントのポイントを理解していないのかを尋ねたいと思います。コンポーネントがゲームで実際にどのように機能するかの良い例は見つかりませんでした。コンポーネントシステムの作成方法を説明するチュートリアルをいくつか見つけましたが、使用方法は説明しませんでした。

回答:


46

コンポーネントは優れていますが、使いやすいソリューションを見つけるには時間がかかる場合があります。心配しないで、そこに着くでしょう。:)

コンポーネントの整理

あなたはほぼ正しい軌道に乗っています、と私は言います。ドアから始めてスイッチで終わる、ソリューションを逆に説明しようとします。私の実装ではイベントを多用しています。以下では、イベントが問題にならないように、イベントをより効率的に使用する方法について説明します。

エンティティをエンティティ間で接続するメカニズムがある場合、スイッチが押されたことを直接ドアに通知し、ドアが処理を決定できるようにします。

エンティティを接続できない場合、ソリューションは私がやろうとしていることにかなり近いです。私はドアに一般的なイベント(SwitchActivatedEvent多分)を聞いてもらいたい。スイッチがアクティブになると、このイベントをポストします。

あなたが複数のタイプのスイッチを持っているなら、私は持っていますPressureToggleWaterToggleそしてShotToggleビヘイビアもありますが、ベースToggleableBehaviourが良いかどうかはわかりませんので、私はそれを廃止します(もちろん、あなたが良いそれを維持する理由)。

Behaviour -> ToggleOnPressureBehaviour
          -> ToggleOnShotBehaviour
          -> ToggleOnWaterBehaviour

効率的なイベント処理

あまりにも多くのイベントが飛び交うのではないかと心配する場合、できることは1つだけです。発生するすべてのイベントをすべてのコンポーネントに通知するのではなく、コンポーネントが適切なタイプのイベントであるかどうかをコンポーネントにチェックさせるのではなく、別のメカニズムがあります...

あなたは持つことができるEventDispatchersubscribe、この(擬似コード)のようになります方法を:

EventDispatcher.subscribe(event_type, function)

次に、イベントをポストすると、ディスパッチャはそのタイプをチェックし、その特定のタイプのイベントにサブスクライブした機能のみを通知します。これを、イベントのタイプを関数のリストに関連付けるマップとして実装できます。

これにより、システムの効率が大幅に向上します。イベントごとの関数呼び出しが大幅に少なくなり、コンポーネントは適切なタイプのイベントを受け取り、再確認する必要がなくなります。

StackOverflowにこの前の簡単な実装を投稿しました。これは、Pythonで書かれていますが、多分それはまだあなたを助けることができます。
https://stackoverflow.com/a/7294148/627005

この実装は非常に汎用的です。コンポーネントの関数だけでなく、あらゆる種類の関数で機能します。それが必要ない場合は、の代わりにfunction、メソッドにbehaviorパラメーターを含めることができsubscribeます。これは、通知が必要な動作インスタンスです。

属性と動作

単純な古いコンポーネントの代わりに、自分属性と動作を使用するようになりました。ただし、ブレイクアウトゲームでシステムを使用する方法の説明から、あなたはそれをやりすぎていると思います。

属性を使用するのは、2つの動作が同じデータにアクセスする必要がある場合のみです。この属性は、動作を分離し、コンポーネント間の依存関係(属性または動作)が絡まないようにするのに役立ちます。これらは非常に単純で明確な規則に従っているためです。

  • 属性は他のコンポーネントを使用せず(他の属性も動作もしない)、自己充足的です。

  • ビヘイビアは、他のビヘイビアを使用または認識しません。一部の属性(厳密に必要な属性)のみを知っています。

一部のデータが1つのビヘイビアーのみで必要な場合、属性に入れる理由がないので、ビヘイビアーに保持させます。


@ 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機能はありません。必要はありません。その目的は、複雑なゲームロジックを実行するのではなく、データを保持することです。

属性にいくつかの簡単なロジックを実行させることもできます。この例ではHealthAttribute0 <= 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?それはいいですね...しかし、私はレンガがその健康に応じて色を変えたい場合、健康は属性である必要がありますよね?ありがとうございました!
トムソントム

2
@TomsonTomはい、それだけです。イベントがリスナーが知る必要のあるデータを保持することは非常に良い解決策です。
ポールマンタ

3
これは素晴らしい答えです!(pdfと同様)-機会があれば、このシステムでのレンダリングの処理方法について少し詳しく説明していただけますか?この属性/動作モデルは私にとって完全に新しいものですが、非常に興味深いものです。
マイケル

1
@TomsonTomレンダリングについては、マイケルに行った答えを参照してください。衝突に関しては、私は個人的に近道を取りました。Box2Dと呼ばれるライブラリを使用しました。これは非常に使いやすく、衝突をはるかに上手く処理します。ただし、ゲームロジックコードでライブラリを直接使用することはありません。すべてにEntityは、EntityBodyすべてのいビットを抽象化するがあります。その後、ビヘイビアはから位置を読み取り、EntityBodyそれに力を加え、体が持つ関節やモーターを使用します。Box2Dのような非常に忠実な物理シミュレーションを行うことは、確かに新しい課題をもたらしますが、とても楽しいです。
ポールマンタ

1
@thelinuxlichあなたはArtemisの開発者です!:D ボードでComponent/ Systemスキームが数回参照されているのを見ました。実際、私たちの実装には多くの類似点があります。
ポールマンタ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.