ゲームオブジェクトはどのようにお互いを認識する必要がありますか?


18

ゲームオブジェクトを多形であると同時に多形ではないように整理する方法を見つけるのは難しいと思います。

例は次のとおりです。すべてのオブジェクトにupdate()とが必要であると仮定しますdraw()。そのためには、GameObjectこれら2つの仮想純粋メソッドを持つ基本クラスを定義し、ポリモーフィズムを開始させる必要があります。

class World {
private:
    std::vector<GameObject*> objects;
public:
    // ...
    update() {
        for (auto& o : objects) o->update();
        for (auto& o : objects) o->draw(window);
    }
};

updateメソッドは、特定のクラスオブジェクトが更新する必要がある状態を処理することになっています。事実は、各オブジェクトが周囲の世界について知る必要があるということです。例えば:

  • 鉱山は誰かが衝突しているかどうかを知る必要があります
  • 兵士は、他のチームの兵士が近くにいるかどうかを知る必要があります
  • ゾンビは、半径内で最も近い脳がどこにあるかを知っている必要があります

受動的相互作用(最初の相互作用のような)の場合、衝突検出は、衝突の特定のケースで何をすべきかをオブジェクト自体に委任できると考えていましたon_collide(GameObject*)

他のほとんどの情報(他の2つの例のように)は、updateメソッドに渡されたゲームワールドによって照会できます。現在、世界はオブジェクトのタイプに基づいてオブジェクトを区別しません(すべてのオブジェクトを単一のポリモーフィックコンテナに格納します)。したがって、実際に理想的に返さworld.entities_in(center, radius)れるのはのコンテナですGameObject*。しかしもちろん、兵士は自分のチームから他の兵士を攻撃したくはありませんし、ゾンビは他のゾンビについては言いません。そのため、動作を区別する必要があります。解決策は次のとおりです。

void TeamASoldier::update(const World& world) {
    auto list = world.entities_in(position, eye_sight);
    for (const auto& e : list)
        if (auto enemy = dynamic_cast<TeamBSoldier*>(e))
            // shoot towards enemy
}

void Zombie::update(const World& world) {
    auto list = world.entities_in(position, eye_sight);
    for (const auto& e : list)
        if (auto enemy = dynamic_cast<Human*>(e))
            // go and eat brain
}

しかしもちろん、dynamic_cast<>フレームごとの数は恐ろしく高くなる可能性があり、私たちは皆、どれほど遅いかを知っていdynamic_castます。同じ問題は、前述のon_collide(GameObject*)デリゲートにも当てはまります。

それでは、オブジェクトが他のオブジェクトを認識し、それらを無視したり、タイプに基づいてアクションを実行したりできるように、コードを編成する理想的な方法は何でしょうか?


1
汎用性の高いC ++ RTTIカスタム実装を探していると思います。それにもかかわらず、あなたの質問は賢明なRTTIメカニズムだけに関するものではないようです。あなたが求めるものは、ゲームが使用するほぼすべてのミドルウェア(アニメーションシステム、いくつかの例を挙げると物理学)に必要です。サポートされるクエリのリストに応じて、配列のIDとインデックスを使用してRTTIを回避するか、dynamic_castとtype_infoの安価な代替をサポートする本格的なプロトコルを設計することになります。
テオドロン

ゲームロジックに型システムを使用しないことをお勧めします。たとえば、の結果に依存する代わりに、のdynamic_cast<Human*>ようなものを実装します。これはデフォルトbool GameObject::IsHuman()で戻りfalseますがtrueHumanクラスで返すようにオーバーライドされます。
コンガスボン

余分なもの:興味のある他のエンティティに大量のオブジェクトを送信することはほとんどありません。それはあなたが本当に考慮しなければならない明らかな最適化です。
テオドロン2013年

@congusbongus vtableとカスタムIsAオーバーライドを使用することは、私にとって実際の動的なキャストよりもわずかに優れていることが判明しました。ユーザーがエンティティプール全体で盲目的に反復するのではなく、可能な限り並べ替えられたデータリストを持つことをお勧めします。
テオドロン

4
@Jefffrey:理想的には、型固有のコードを書かないでください。インターフェイス固有のコード(一般的な意味での「インターフェイス」)を記述しますTeamASoldierandの論理TeamBSoldierは本当に同じです-他のチームの誰かを狙います。他のエンティティに必要なのは、GetTeam()最も具体的なメソッドであり、コンガスボンガスの例では、さらにIsEnemyOf(this)インターフェースの種類にさらに抽象化することができます。コードは、兵士、ゾンビ、プレイヤーなどの分類上の分類を気にする必要はありません。タイプではなく相互作用に焦点を合わせます。
ショーンミドルディッチ

回答:


11

各エンティティ自体の意思決定を実装する代わりに、コントローラーパターンを選択することもできます。すべてのオブジェクト(それらにとって重要)を認識し、それらの動作を制御する中央コントローラークラスがあります。

MovementControllerは、移動可能なすべてのオブジェクトの移動を処理します(ルート検索を行い、現在の移動ベクトルに基づいて位置を更新します)。

MineBehaviorControllerはすべての地雷とすべての兵士をチェックし、兵士が近づきすぎると爆発するように地雷に命令します。

ZombieBehaviorControllerは、すべてのゾンビとその周辺の兵士をチェックし、各ゾンビに最適なターゲットを選択し、そこに移動して攻撃するように命令します(移動自体はMovementControllerによって処理されます)。

SoldierBehaviorControllerは状況全体を分析し、すべての兵士に戦術的な指示を出します(そこに移動し、これを撃ち、その男を癒します...)。これらの高レベルのコマンドの実際の実行は、低レベルのコントローラーでも処理されます。努力すれば、AIを非常に賢明で協調的な意思決定ができ​​るようにすることができます。


1
おそらくこれは、エンティティコンポーネントアーキテクチャ内の特定のタイプのコンポーネントのロジックを管理する「システム」としても知られています。
テオドロン2013年

これはCスタイルのソリューションのように聞こえます。コンポーネントはにグループ化されstd::map、エンティティはIDのみであるため、何らかのタイプシステムを作成する必要があります(レンダラーは何を描画するかを知る必要があるため、タグコンポーネントを使用する必要があります)。そうしたくない場合は、描画コンポーネントが必要になります。ただし、描画する場所を知るために位置コンポーネントが必要になるため、コンポーネント間の依存関係を作成して、非常に複雑なメッセージングシステムで解決します。これはあなたが提案していることですか?

1
@Jefffrey「それはCスタイルのソリューションのように聞こえます」-それが真実であっても、なぜそれが必ずしも悪いことになるのでしょうか?他の懸念は有効かもしれませんが、それらに対する解決策があります。残念ながら、コメントは短すぎて適切に対応できません。
フィリップ

1
@Jefffreyコンポーネント自体にはロジックがなく、「システム」がすべてのロジックを処理するというアプローチを使用すると、コンポーネント間の依存関係が作成されず、超複雑なメッセージングシステムも必要ありません(少なくとも、それほど複雑ではありません) 。例えば参照:gamadu.com/artemis/tutorial.html

1

まず、可能な限りオブジェクトが互いに独立したままになるように機能を実装してください。特に、あなたはマルチスレッドのためにそれをしたいです。最初のコード例では、すべてのオブジェクトのセットをCPUコアの数に一致するセットに分割し、非常に効率的に更新できます。

しかし、あなたが言ったように、いくつかの機能には他のオブジェクトとの相互作用が必要です。つまり、すべてのオブジェクトの状態は、ある時点で同期する必要があります。つまり、アプリケーションはすべての並列タスクが最初に完了するのを待ってから、相互作用を伴う計算を適用する必要があります。これらの同期ポイントの数を減らすと、一部のスレッドが他のスレッドの終了を待たなければならないことを常に意味するため、適切です。

したがって、他のオブジェクトの内部から必要なオブジェクトに関する情報をバッファリングすることをお勧めします。このようなグローバルバッファが与えられた場合、すべてのオブジェクトを互いに独立して更新できますが、オブジェクトとグローバルバッファにのみ依存します。これは、より高速で維持が容易です。固定タイムステップで、たとえば各フレームの後、現在のオブジェクトの状態でバッファを更新します。

したがって、フレームごとに1回行うことは、1。現在のオブジェクトの状態をグローバルにバッファリングする、2。自分自身とバッファに基づいてすべてのオブジェクトを更新する、3。オブジェクトを描画し、バッファの更新からやり直します。


1

コンポーネントベースのシステムを使用します。このシステムでは、動作を定義する1つ以上のコンポーネントを含むベアボーンGameObjectがあります。

たとえば、あるオブジェクトが常に左右に移動することになっている場合(プラットフォーム)、そのようなコンポーネントを作成してGameObjectにアタッチすることができます。

ゲームオブジェクトは常にゆっくりと回転することになっているとしましょう。それを行う別のコンポーネントを作成して、それをGameObjectにアタッチできます。

コードを複製しないと実行が困難になる従来のクラス階層で、回転する移動プラットフォームが必要な場合はどうでしょう。

このシステムの利点は、RotatableクラスまたはMovingPlatformクラスを使用する代わりに、これらのコンポーネントの両方をGameObjectにアタッチして、AutoRotatesするMovingPlatformを使用できるようになったことです。

すべてのコンポーネントには「requiresUpdate」というプロパティがあり、trueの場合、GameObjectはそのコンポーネントの「update」メソッドを呼び出します。たとえば、Draggableコンポーネントがある場合、このコンポーネントはマウスダウン(GameObjectの上にある場合)で「requiresUpdate」をtrueに設定し、マウスアップでfalseに設定できます。マウスが押されているときにのみマウスを追跡できるようにします。

Tony Hawk Pro Skaterの開発者の1人がデファクトを書いており、読む価値があります:http ://cowboyprogramming.com/2007/01/05/evolve-your-heirachy/


1

継承よりも合成を優先します。

これとは別に私の最も強力なアドバイスは次のようになります。「これを非常に柔軟にしたい」という考え方に引き込まれないでください。柔軟性は優れていますが、あるレベルで、ゲームなどの有限システムでは、全体を構築するために使用されるアトミックパーツが存在することに注意してください。何らかの方法で、処理はこれらの事前定義されたアトミックタイプに依存します。言い換えれば、「可能」であれば、「あらゆる」タイプのデータを処理しても、それを処理するコードがなければ、長期的には役に立ちません。基本的に、すべてのコードは既知の仕様に基づいてデータを解析/処理する必要があります...つまり、事前定義されたタイプのセットを意味します。そのセットの大きさは?あなた次第。

この記事では、堅牢でパフォーマンスの高いエンティティコンポーネントアーキテクチャを介した、ゲーム開発における継承よりも構成の原則についての洞察を提供します。

事前定義されたコンポーネントのスーパーセットの(異なる)サブセットからエンティティを構築することにより、それらのアクターのコンポーネントの状態を読み取ることで、AIとその周辺のアクターを具体的かつ断片的に把握する方法をAIに提供します。


1

個人的には、描画関数をObjectクラス自体に含めないことをお勧めします。私は、オブジェクトの位置/座標をオブジェクト自体の外に置くことをお勧めします。

そのdraw()メソッドは、OpenGL、OpenGL ES、Direct3D、これらのAPIのラッピングレイヤー、またはエンジンAPIの低レベルレンダリングAPIを処理します。その間にスワップする必要があるかもしれません(たとえば、OpenGL + OpenGL ES + Direct3Dをサポートしたい場合。

そのGameObjectには、Meshや、シェーダー入力、アニメーション状態などを含むより大きなバンドルなど、視覚的な外観に関する基本情報が含まれている必要があります。

また、柔軟なグラフィックパイプラインが必要になります。カメラまでの距離に基づいてオブジェクトを並べたい場合はどうなりますか。またはその材料タイプ。「選択した」オブジェクトを別の色で描きたい場合はどうなりますか。オブジェクトで描画関数を呼び出すときに実際にsooとしてレンダリングするのではなく、代わりにレンダーが実行するアクションのコマンドリストに入れた場合はどうでしょう(スレッド化に必要な場合があります)。他のシステムでもそのようなことができますが、それはPITAです。

直接描画する代わりに、必要なすべてのオブジェクトを別のデータ構造にバインドすることをお勧めします。このバインディングには、オブジェクトの場所とレンダリング情報への参照が必要なだけです。

レベル/チャンク/エリア/マップ/ハブ/ホールワールド/空間インデックスが与えられるものは何でも、これにはオブジェクトが含まれ、座標クエリに基づいてそれらを返します。単純なリストまたはOctreeのようなものです。また、物理シーンとしてサードパーティの物理エンジンによって実装されたもののラッパーにもなります。「カメラのビューにあるすべてのオブジェクトにいくつかの余分な領域を追加してクエリを実行する」などの操作や、リスト全体を取得してすべてをレンダリングできる単純なゲームを実行できます。

Spacial Indexには、実際の位置情報を含める必要はありません。それらは、オブジェクトを他のオブジェクトの場所に関連してツリー構造で保存することにより機能します。それらは、その位置に基づいてオブジェクトをすばやく検索できる一種の不可逆キャッシュとしても使用できます。実際のX、Y、Z座標を複製する必要はありません。続けたいならできると言って

実際、ゲームオブジェクトには独自の位置情報を含める必要さえありません。たとえば、レベルに入れられていないオブジェクトは、x、y、z座標を持つべきではありません。これは意味がありません。それを特別なインデックスに含めることができます。実際の参照に基づいてオブジェクトの座標を検索する必要がある場合は、オブジェクトとシーングラフの間にバインドが必要になります(シーングラフは、座標に基づいてオブジェクトを返すためのものですが、オブジェクトに基づいて座標を返すのが遅いです) 。

オブジェクトをレベルに追加するとき。次のことを行います。

1)ロケーション構造を作成します。

 class Location { 
     float x, y, z; // Or a special Coordinates class, or a vec3 or whatever.
     SpacialIndex& spacialIndex; // Note this could be the area/level/map/whatever here
 };

これは、サードパーティの物理エンジンのオブジェクトへの参照にもなります。または、別の場所への参照を持つオフセット座標にすることもできます(追跡カメラまたは接続されたオブジェクトまたは例の場合)。多態性では、静的オブジェクトか動的オブジェクトかに依存します。ここで座標が更新されたときに空間インデックスへの参照を保持することにより、空間インデックスも更新できます。

動的メモリ割り当てが心配な場合は、メモリプールを使用してください。

2)オブジェクト、その場所、シーングラフ間のバインド/リンク。

typedef std::pair<Object, Location> SpacialBinding.

3)バインディングは、適切なポイントでレベル内の空間インデックスに追加されます。

レンダリングの準備をしているとき。

1)カメラを取得します(位置はプレイヤーキャラクターを追跡し、レンダラーはそれを特別に参照しますが、実際に必要なのはそれだけです)。

2)カメラのSpacialBindingを取得します。

3)バインディングから空間インデックスを取得します。

4)(おそらく)カメラから見えるオブジェクトを照会します。

5A)視覚情報を処理する必要があります。GPUにアップロードされたテクスチャなど。これは事前に行うのが最適です(レベルのロード時など)が、おそらく実行時に実行できます(オープンワールドの場合は、チャンクに近づいたときにロードできますが、事前に実行する必要があります)。

5B)オプションで、キャッシュされたレンダーツリーを構築します。深度/マテリアルの並べ替えや、近くにあるオブジェクトを追跡したい場合は、後で表示される可能性があります。それ以外の場合は、ゲーム/パフォーマンス要件に依存するたびに、空間インデックスを照会するだけです。

レンダラーには、オブジェクトと座標の間をリンクするRenderBindingオブジェクトが必要になる可能性があります

class RenderBinding {
    Object& object;
    RenderInformation& renderInfo;
    Location& location // This could just be a coordinates class.
}

次に、レンダリングするときに、リストを実行します。

上記の参照を使用しましたが、スマートポインター、生のポインター、オブジェクトハンドルなどがあります。

編集:

class Game {
    weak_ptr<Camera> camera;
    Level level1;

    void init() {
        Camera camera(75.0_deg, 1.025_ratio, 1000_meters);
        auto template_player = loadObject("Player.json")
        auto player = level1.addObject(move(player), Position(1.0, 2.0, 3.0));
        level1.addObject(move(camera), getRelativePosition(player));

        auto template_bad_guy = loadObject("BadGuy.json")
        level1.addObject(template_bad_guy, {10, 10, 20});
        level1.addObject(template_bad_guy, {10, 30, 20});
        level1.addObject(move(template_bad_guy), {50, 30, 20});
    }

    void render() {
        camera->getFrustrum();
        auto level = camera->getLocation()->getLevel();
        auto object = level.getVisible(camera);
        for(object : objects) {
            render(objects);
        }
    }

    void render(Object& object) {
        auto ri = object.getRenderInfo();
        renderVBO(ri.getVBO());
    }

    Object loadObject(string file) {
        Object object;
        // Load file from disk and set the properties
        // Upload mesh data, textures to GPU. Load shaders whatever.
        object.setHitPoints(// values from file);
        object.setRenderInfo(// data from 3D api);
    }
}

class Level {
    Octree octree;
    vector<ObjectPtr> objects;
    // NOTE: If your level is mesh based there might also be a BSP here. Or a hightmap for an openworld
    // There could also be a physics scene here.
    ObjectPtr addObject(Object&& object, Position& pos) {
        Location location(pos, level, object);
        objects.emplace_back(object);
        object->setLocation(location)
        return octree.addObject(location);
    }
    vector<Object> getVisible(Camera& camera) {
        auto f = camera.getFtrustrum();
        return octree.getObjectsInFrustrum(f);
    }
    void updatePosition(LocationPtr l) {
        octree->updatePosition(l);
    }
}

class Octree {
    OctreeNode root_node;
    ObjectPtr add(Location&& object) {
        return root_node.add(location);
    }
    vector<ObjectPtr> getObjectsInRadius(const vec3& position, const float& radius) { // pass to root_node };
    vector<ObjectPtr> getObjectsinFrustrum(const FrustrumShape frustrum;) {//...}
    void updatePosition(LocationPtr* l) {
        // Walk up from l.octree_node until you reach the new place
        // Check if objects are colliding
        // l.object.CollidedWith(other)
    }
}

class Object {
    Location location;
    RenderInfo render_info;
    Properties object_props;
    Position getPosition() { return getLocation().position; }
    Location getLocation() { return location; }
    void collidedWith(ObjectPtr other) {
        // if other.isPickup() && object.needs(other.pickupType()) pick it up, play sound whatever
    }
}

class Location {
    Position position;
    LevelPtr level;
    ObjectPtr object;
    OctreeNote octree_node;
    setPosition(Position position) {
        position = position;
        level.updatePosition(this);
    }
}

class Position {
    vec3 coordinates;
    vec3 rotation;
}

class RenderInfo {
    AnimationState anim;
}
class RenderInfo_OpenGL : public RenderInfo {
    GLuint vbo_object;
    GLuint texture_object;
    GLuint shader_object;
}

class Camera: public Object {
    Degrees fov;
    Ratio aspect;
    Meters draw_distance;
    Frustrum getFrustrum() {
        // Use above to make a skewed frustum box
    }
}

物事をお互いに「気づかせる」ことに関して。それが衝突検出です。たぶんOctreeで実装されるでしょう。メインオブジェクトにコールバックを提供する必要があります。このようなものは、Bulletなどの適切な物理エンジンによって最適に処理されます。その場合、OctreeをPhysicsSceneに、PositionをCollisionMesh.getPosition()などのリンクに置き換えてください。


うわー、これはとてもよさそうだ。私は基本的な考え方を理解したと思いますが、これ以上の例がなければ、これの外見をかなり理解することはできません。これに関する参考資料や実例はありますか?(しばらくの間、この回答を読み続けます)。

本当に例はありません。時間があるときに私がやろうとしていることです。全体的なクラスをさらにいくつか追加し、それが役立つかどうかを確認します。これこれがあります。オブジェクトクラスの関係やレンダリングよりも、オブジェクトクラスの方が重要です。私はそれを自分で実装していないので、落とし穴、ワークアウトが必要なビット、またはパフォーマンスのものがあるかもしれませんが、全体的な構造は大丈夫だと思います。
デビッドC.ビショップ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.