IDを使用して異なる場所で同じゲームエンティティの複数の参照を管理する


8

私は同様のトピックについて素晴らしい質問を見てきましたが、この特定の方法に対処するものはありませんでした:

[XNA Game Studio]ゲームにゲームエンティティの複数のコレクションがあり、多くのエンティティが複数のリストに属している場合、エンティティが破棄されるたびに追跡し、所属するリストから削除する方法を検討しています。

多くの潜在的なメソッドがずさんで複雑なように見えますが、以前に見た方法では、ゲームエンティティの複数のコレクションを使用する代わりに、ゲームエンティティIDのコレクションを使用する方法を思い出しました。これらのIDは、中央の「データベース」(おそらくハッシュテーブル)を介してゲームエンティティにマッピングされます。

したがって、コードの一部がゲームエンティティのメンバーにアクセスする必要がある場合は常に、まずデータベース内にあるかどうかを確認します。そうでない場合は、それに応じて反応します。

これは健全なアプローチですか?オブジェクトにアクセスするたびにルックアップのコストがかかるトレードオフで、複数のリストを格納することのリスク/手間の多くが排除されるようです。

回答:


3

短い答え:はい。

長い答え:実際、私が見たすべてのゲームはこのように機能しました。ハッシュテーブルの検索は十分に高速です。実際のID値を気にしない場合(つまり、他の場所で使用されていない場合)、ハッシュテーブルの代わりにベクトルを使用できます。これはさらに高速です。

追加のボーナスとして、この設定によりゲームオブジェクトを再利用できるようになり、メモリ割り当て率が低下します。ゲームオブジェクトが破壊されると、それを「解放」するのではなく、フィールドをクリーンアップして「オブジェクトプール」に入れます。次に、新しいオブジェクトが必要な場合は、プールから既存のオブジェクトを取得します。新しいオブジェクトが常に生成される場合、これによりパフォーマンスが著しく向上します。


一般的に、これは十分に正確ですが、最初に他のことを考慮する必要があります。迅速に作成および破棄されるオブジェクトが多く、ハッシュ関数が優れている場合、ハッシュテーブルはベクトルよりも適している可能性があります。疑問がある場合は、ベクターを使用してください。
hokiecsgrad 2010年

はい、私は同意する必要があります:ベクトルはハッシュテーブルよりも絶対的に高速ではありません。しかし、99%高速です(-8
Nevermind

スロットを再利用したい場合(確実に必要な場合)、無効なIDが認識されることを確認する必要があります。例:ID 7のオブジェクトはリストA、B、Cにあります。オブジェクトは破棄され、同じスロットに別のオブジェクトが作成されます。このオブジェクトは自身をリストAおよびCに登録します。リストBの最初のオブジェクトのエントリは、新しく作成されたオブジェクトを「ポイント」しますが、この場合は間違っています。これは、Scott Bilasのscottbilas.com/publications/gem-resmgrの Handle-Managerで完全に解決できます。私はすでに商用ゲームでこれを使用しましたが、それは魅力のように機能します。
DarthCoder、2011年

3

これは健全なアプローチですか?オブジェクトにアクセスするたびにルックアップのコストがかかるトレードオフで、複数のリストを格納することのリスク/手間の多くが排除されるようです。

行ったのは、チェックを実行する負担を破棄時から使用時まで移動することだけです。私はこれまでこれまでやったことはないと主張するつもりはありませんが、それが「健全」であるとは考えていません。

より堅牢な代替手段の1つを使用することをお勧めします。リストの数がわかっている場合は、すべてのリストからオブジェクトを削除するだけで済みます。オブジェクトが指定されたリストにない場合、それは無害であり、オブジェクトを正しく削除したことが保証されます。

リストの数が不明または実用的でない場合は、オブジェクトへのリストへの後方参照を保存します。これの一般的な実装はオブザーバーパターンですが、デリゲートでも同様の効果が得られる可能性があります。デリゲートでは、必要に応じて、含まれているリストからアイテムを削除するためにコールバックします。

または、実際には複数のリストがまったく必要ない場合もあります。多くの場合、オブジェクトを1つのリストに保持し、動的クエリを使用して、必要なときに他の一時的なコレクションを生成できます。例えば。キャラクターのインベントリ用に個別のリストを用意する代わりに、「持ち越し」が現在のキャラクターと等しいすべてのオブジェクトを引き出すことができます。これはかなり非効率に聞こえるかもしれませんが、リレーショナルデータベースには十分であり、巧妙なインデックス付けによって最適化できます。


ええ、私の現在の実装では、ゲームエンティティへの参照を必要とするすべてのリストまたは単一のオブジェクトは、その破棄されたイベントをサブスクライブし、それに応じて反応する必要があります。これは、IDルックアップと同じか、いくつかの点で優れています。
vargonian 2010年

1
私の本では、これは堅牢なソリューションの正反対です。lookup-by-idと比較すると、より多くのコードがあり、より多くのバグの場所があり、何かを忘れる機会が多く、起動を全員に通知せずにオブジェクトを破壊する可能性があります。lookup-by-idを使用すると、ほとんど変更されない少しのコード、単一の破棄ポイント、および破棄されたオブジェクトにアクセスしないことが100%保証されます。
Nevermind

1
lookup-by-idを使用すると、オブジェクトを使用する必要があるすべての場所に追加のコードがあります。オブザーバーベースのアプローチでは、リスト全体に項目を追加および削除する場合を除いて、追加のコードはありません。これは、プログラム全体で数箇所になる可能性があります。アイテムを作成、破棄、またはリスト間で移動する以上にアイテムを参照すると、オブザーバーアプローチによりコードが少なくなります。
カイロタン

オブジェクトを作成/破棄する場所よりも、オブジェクトを参照する場所のほうが多いでしょう。ただし、各参照に必要なコードはごくわずかですが、プロパティで既にルックアップがラップされている場合はほとんど必要ありません(ほとんどの場合)。さらに、オブジェクトの破棄をサブスクライブすることを忘れることはできますが、オブジェクトを検索することを忘れることはできません。
Nevermind '11

1

これは、「リストからオブジェクトを削除し、繰り返し処理する」問題のより具体的な例のようです。これは簡単に解決できます(逆の順序で繰り返すと、おそらくC#のような配列スタイルのリストの最も簡単な方法になります。List)。

エンティティが複数の場所から参照される場合は、とにかく参照タイプである必要があります。そのため、複雑なIDシステムを経由する必要はありません。C#のクラスはすでに必要な間接参照を提供しています。

そして最後に-なぜわざわざ「データベースをチェックする」のですか?各エンティティにIsDeadフラグを付けてチェックするだけです。


C#は本当にわかりませんが、このアーキテクチャはCとC ++でよく使用されます。オブジェクトが破棄された場合、言語の通常の参照型(ポインター)を使用しても安全ではありません。これに対処するにはいくつかの方法がありますが、それらはすべて間接層を伴いますが、これは一般的な解決策です。(Kylotanが示唆するバックポインターのリストは別のものです-どちらを選択するかは、メモリと時間のどちらを費やすかによって異なります。)

C#はガベージコレクションであるため、インスタンスを放棄しても問題はありません。管理されていない(つまり、ガベージコレクターによる)リソースの場合、IDisposableパターンが存在します。これは、オブジェクトをとしてマークするのとよく似ていますIsDead。インスタンスをGCするのではなくプールする必要がある場合は、より複雑な戦略が必要になります。
Andrew Russell

IsDeadフラグは、過去に成功して使用したものです。私は喜んで続行するのではなく、デッドステートのチェックに失敗した場合にゲームがクラッシュし、奇妙でデバッグが困難になる可能性があるというアイデアが好きです。前者はデータベース検索の使用です。IsDeadは後者にフラグを立てます。
vargonian 2010年

3
@vargonian Disposableパターンでは、通常、クラスの各関数とプロパティの上部でIsDisposedをチェックします。インスタンスがObjectDisposedException破棄された場合は、スローします(これは通常、ハンドされない例外で「クラッシュ」します)。チェックをオブジェクト自体に移動することにより(オブジェクトを外部でチェックするのではなく)、オブジェクトが独自の「am I dead」動作を定義できるようにし、呼び出し元からのチェックの負担を取り除きます。目的に応じIDisposableて、を実装したりIsDead、同様の機能を使用して独自のフラグを作成したりできます。
Andrew Russell

1

私は自分のC#/。NETゲームでこのアプローチをよく使用します。ここで説明する他の利点(および危険!)の他に、シリアル化の問題を回避するのにも役立ちます。

.NET Frameworkの組み込みバイナリシリアル化機能を利用する場合は、エンティティIDを使用すると、書き出されるオブジェクトグラフのサイズを最小限に抑えることができます。デフォルトでは、.NETのバイナリフォーマッタは、オブジェクトグラフ全体をフィールドレベルでシリアル化します。シリアル化したいとしましょうShipインスタンス。所有者を参照Shipする_ownerフィールドがある場合Player、そのPlayerインスタンスもシリアル化されます。場合にPlayer含まれている_ships(と言うのフィールドをICollection<Ship>)、その後、すべてのプレイヤーの船のも、フィールドレベル(再帰的)で参照される他のオブジェクトとともに、書き出されます。巨大なオブジェクトグラフの一部だけをシリアル化したい場合、誤って巨大なオブジェクトグラフをシリアル化するのは簡単です。

代わりに、_ownerIdフィールドがある場合は、その値を使用してPlayer参照をオンデマンドで解決できます。私のパブリックAPIは変更しないままにすることもでき、Ownerプロパティは単にルックアップを実行します。

ハッシュベースのルックアップは一般に非常に高速ですが、ルックアップが頻繁に行われる非常に大規模なエンティティセットでは、追加されたオーバーヘッドが問題になる可能性があります。問題が発生した場合は、シリアル化されないフィールドを使用して参照をキャッシュできます。たとえば、次のようなことができます。

public class Ship
{
    private int _ownerId;
    [NonSerialized] private Lazy<Player> _owner;

    public Player Owner
    {
        get { return _owner.Value; }
    }

    public Ship(Player owner)
    {
        _ownerId = owner.PlayerID;
        EnsureCache();
    }

    private void EnsureCache()
    {
        if (_owner == null)
            _owner = new Lazy<Player>(() => Game.Current.Players[_ownerId]);
    }

    [OnDeserialized]
    private void OnDeserialized(StreamingContext context)
    {
        EnsureCache();
    }
}

もちろん、このアプローチはシリアル化を難しくすることもできます実際に大きなオブジェクトグラフをシリアル化する必要がある場合にます。そのような場合、必要なオブジェクトがすべて含まれるように、何らかの「コンテナ」を考案する必要があります。


0

クリーンアップを処理する別の方法は、制御を反転させてDisposableオブジェクトを使用することです。おそらくエンティティを割り当てる要素があるので、追加する必要があるすべてのリストをファクトリに渡します。エンティティは、それが含まれるすべてのリストへの参照を保持し、IDisposableを実装します。disposeメソッドでは、すべてのリストから自分自身を削除します。これで、工場(作成)と廃棄という2つの場所でのリスト管理のみを考慮する必要があります。オブジェクトの削除は、entity.Dispose()と同じくらい簡単です。

C#での名前と参照の問題は、コンポーネントまたは値のタイプを管理する場合にのみ重要です。1つのエンティティにリンクされているコンポーネントのリストがある場合、IDフィールドをハッシュマップキーとして使用すると便利です。ただし、エンティティのリストしかない場合は、参照付きのリストを使用してください。C#の参照は安全で、操作が簡単です。

リストからアイテムを削除または追加するには、キューまたは他の任意の数のソリューションを使用して、リストの追加と削除を処理できます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.