ゲームアーキテクチャ/設計の質問-グローバルインスタンスを回避しながら効率的なエンジンを構築する(C ++ゲーム)


28

ゲームアーキテクチャについて質問がありました。異なるコンポーネントを相互に通信させる最良の方法は何ですか?

この質問がすでに何百万回も聞かれている場合、私は本当に謝罪しますが、私が探している正確な種類の情報を持つものを見つけることができません。

私はゲームをゼロから構築しようとして(重要な場合はC ++)、インスピレーションのためのいくつかのオープンソースゲームソフトウェア(Super Maryo Chronicles、OpenTTDなど)を観察しました。これらのゲームデザインの多くは、(レンダリングキュー、エンティティマネージャー、ビデオマネージャーなどのように)世界中でグローバルインスタンスやシングルトンを使用していることに気付きます。グローバルインスタンスとシングルトンを避け、可能な限り疎結合のエンジンを構築しようとしていますが、効果的な設計の経験不足に起因するいくつかの障害に直面しています。(このプロジェクトの動機の一部はこれに取り組むことです:))

GameCore他のプロジェクトで見られるグローバルインスタンスに類似したメンバーを持つメインオブジェクトを1つ持つデザインを構築しました(つまり、入力マネージャー、ビデオマネージャー、GameStageすべてのエンティティとゲームプレイを制御するオブジェクトがあります)現在読み込まれているステージなど)。問題は、すべてがGameCoreオブジェクトに集中しているため、異なるコンポーネントが互いに通信する簡単な方法がないことです。

たとえば、スーパーマリョークロニクルを見ると、ゲームのコンポーネントが別のコンポーネントと通信する必要がある場合(つまり、敵オブジェクトがレンダーキューに追加されてレンダーステージで描画される場合)は、常にグローバルインスタンス。

私は、ゲームオブジェクトに関連情報をオブジェクトに渡しGameCoreて、GameCoreオブジェクトがその情報を必要とするシステムの他のコンポーネントに渡すことができるようにする必要があります(つまり、上記の状況では、各敵オブジェクトレンダリング情報をGameStageオブジェクトに返します。オブジェクトはすべてを収集し、に返します。レンダリング情報は、レンダリングのGameCoreためにビデオマネージャーに渡されます)。これは、本当に恐ろしいデザインのように感じられ、これに対する解決策を考えていました。可能なデザインに関する私の考え:

  1. グローバルインスタンス(Super Maryo Chronicles、OpenTTDなどの設計)
  2. GameCoreオブジェクトを、すべてのオブジェクトが通信する仲介者として機能させる(上記の現在の設計)
  3. コンポーネントと通信する必要がある他のすべてのコンポーネントへのポインターを指定します(つまり、上記のMaryoの例では、敵のクラスは通信する必要があるビデオオブジェクトへのポインターを持ちます)
  4. ゲームをサブシステムに分割する-たとえば、サブシステム内のGameCoreオブジェクト間の通信を処理するマネージャーオブジェクトをオブジェクトに含める
  5. (別のオプション? ....)

上記のオプション4が最適なソリューションであると思いますが、デザインに問題があります。おそらく、グローバルを使用するデザインを見てきました。現在の設計に存在する問題と同じ問題を、各サブシステムで複製しているように感じます。たとえば、上記のGameStageオブジェクトはこれをやや試みたGameCoreものですが、オブジェクトはまだプロセスに関与しています。

誰でもここで設計のアドバイスを提供できますか?

ありがとう!


1
シングルトンは素晴らしいデザインではないというあなたの本能を理解しています。私の経験では、彼らは、システム間での通信を管理するための最も簡単な方法となってきた
エメット・バトラー

4
ベストプラクティスであるかどうかわからないので、コメントとして追加します。InputSystemやGraphicsSystemなどのサブシステムで構成される中央のGameManagerがあります。各サブシステムはGameManagerをコンストラクターのパラメーターとして受け取り、クラスプライベートメンバーへの参照を格納します。その時点で、GameManagerリファレンスを使用して他のシステムにアクセスできます。
イニシアー

この質問はコードであり、ゲームデザインではないため、タグを変更しました。
クライム

このスレッドは少し古いですが、私はまったく同じ問題を抱えています。私はOGREを使用していますが、最良の方法を使用しようとしています。私の意見では、オプション#4が最良のアプローチです。Advanced Ogre Frameworkのようなものをビルドしましたが、これはあまりモジュール化されていません。キーボードのヒットとマウスの動きのみを取得するサブシステムの入力処理が必要だと思います。私が理解していないのは、サブシステム間にこのような「通信」マネージャーを作成するにはどうすればよいですか?
Dominik2000

1
こんにちは@ Dominik2000、これはQ&Aサイトであり、フォーラムではありません。質問がある場合は、既存の質問への回答ではなく、実際の質問を投稿してください。詳細については、よくある質問をご覧ください。
ジョシュ

回答:


19

グローバルデータを整理するためにゲームで使用しているのは、ServiceLocatorデザインパターンです。シングルトンパターンと比較したこのパターンの利点は、グローバルデータの実装がアプリケーションの実行中に変更できることです。また、ランタイム中にグローバルオブジェクトを変更することもできます。もう1つの利点は、グローバルオブジェクトの初期化の順序を管理しやすいことです。これは、特にC ++で非常に重要です。

例(C ++またはJavaに簡単に変換できるC#コード)

ものをレンダリングするための一般的な操作を行うレンダリングバックエンドインターフェイスがあるとしましょう。

public interface IRenderBackend
{
    void Draw();
}

そして、あなたはデフォルトのレンダーバックエンド実装を持っていること

public class DefaultRenderBackend : IRenderBackend
{
    public void Draw()
    {
        //do default rendering stuff.
    }
}

設計によっては、レンダリングバックエンドにグローバルにアクセスできるのは合法的なようです。でシングルトンパターン、各手段IRenderBackendの実装では、一意のグローバルインスタンスとして実装されるべきです。ただし、ServiceLocatorパターンを使用する場合、これは必要ありません。

方法は次のとおりです。

public class ServiceLocator<T>
{
    private static T currGlobalInstance;

    public static T Service
    {
        get { return currGlobalInstance; }
        set { currGlobalInstance = value; }
    }
}

グローバルオブジェクトにアクセスできるようにするには、最初にそれを初期化する必要があります。

//somewhere during program initialization
ServiceLocator<IRenderBackend>.Service = new DefaultRenderBackend();

//somewhere else in the code
IRenderBackend currentRenderBackend = ServiceLocator<IRenderBackend>.Service;

ランタイム中に実装がどのように変化するかを示すために、ゲームにはレンダリングが等尺性のミニゲームがあり、IsometricRenderBackendを実装するとします。

public class IsometricRenderBackend : IRenderBackend
{
    void draw()
    {
        //do rendering using an isometric view
    }
}

現在の状態からミニゲーム状態に移行するときは、サービスロケーターによって提供されるグローバルレンダリングバックエンドを変更するだけです。

ServiceLocator<IRenderBackend>.Service = new IsometricRenderBackend();

もう1つの利点は、nullサービスも使用できることです。たとえば、ISoundManagerサービスがあり、ユーザーがサウンドをオフにしたい場合、そのメソッドが呼び出されたときに何もしないNullSoundManagerを実装することができます。したがって、ServiceLocatorのサービスオブジェクトをNullSoundManagerオブジェクトに設定することで実現できますこの結果は、ほとんど労力を必要としません。

要約すると、グローバルデータを削除することが不可能な場合もありますが、それはそれらを適切にオブジェクト指向の方法で整理できないことを意味するものではありません。


私は以前にこれを検討しましたが、実際に私のデザインに実装していません。今回は、するつもりです。ありがとう:)
Awesomania

3
@Erevisしたがって、基本的には、ポリモーフィックオブジェクトへのグローバル参照を記述しています。同様に、これは単なる二重間接指定です(ポインター->インターフェース->実装)。C ++では、として簡単に実装できますstd::unique_ptr<ISomeService>
雨の影13年

1
初期化戦略を「最初のアクセスで初期化」に変更し、外部コードシーケンスにサービスを割り当ててロケーターにプッシュさせる必要を回避できます。サービスに「依存」リストを追加すると、初期化されたときに必要な他のサービスが自動的に設定され、main.cppで誰かがそれを覚えていることを祈らないようにできます。将来の調整のための柔軟性を備えた良い答え。
パトリックヒューズ

4

ゲームエンジンを設計するには多くの方法がありますが、実際にはすべてが最終的に優先されます。

基本を邪魔にならないようにするため、一部の開発者は、ピラミッドのように設計することを好みます。このクラスでは、一連のサブシステムを作成、所有、初期化するカーネル、コア、またはフレームワーククラスと呼ばれるトップコアクラスがあります。オーディオ、グラフィックス、ネットワーク、物理学、AI、タスク、エンティティ、リソース管理として。一般に、これらのサブシステムはこのフレームワーククラスによって公開され、通常、必要に応じてこのフレームワーククラスをコンストラクター引数として独自のクラスに渡します。

オプション#4を考えて、あなたは正しい軌道に乗っていると思います。

通信自体に関しては、常に直接的な関数呼び出し自体を意味する必要はありません。通信が発生する可能性があり、多くの間接的な方法は、それが使用して、いくつかの間接的な方法であるかどうか、がありますSignal and Slotsまたは使用しますMessages

ゲームでは、アクションを非同期的に発生させて、ゲームループをできるだけ速く動かし、フレームレートが肉眼で滑らかになるようにすることが重要な場合があります。プレイヤーはスローで途切れるシーンが好きではないので、私たちは物事を流し続けるが、ロジックを流し続けるが、チェックして注文する方法を見つけなければなりません。非同期操作には場所がありますが、すべての操作に対する答えではありません。

同期通信と非同期通信の両方が混在することに注意してください。適切なものを選択してください。ただし、サブシステム間で両方のスタイルをサポートする必要があることを理解してください。両方のサポートを設計することは、将来にわたって役立ちます。


1

逆または周期的な依存関係がないことを確認する必要があります。たとえば、クラスCoreがあり、これにCoreLevelあり、LevelにのリストがEntityある場合、依存関係ツリーは次のようになります。

Core --> Level --> Entity

したがって、この最初の依存ツリーを考えると、にEntity依存しLevelたりCore、に依存したりLevelすることはありませんCore。いずれかの場合LevelまたはEntity依存関係ツリーの上位までのデータへのアクセスを持っている必要があり、それは参照することにより、パラメータとして渡す必要があります。

次のコード(C ++)を検討してください。

class Core;
class Entity;
class Level;

class Level
{
    public:
        Level(Core& coreIn) : core(coreIn) {}

        Core& core;
}

class Entity
{
    public:
        Entity(Level& levelIn) : level(levelIn) {}

        Level& level;
}

この技術を使用して、それぞれがあることがわかりますEntityへのアクセス権を持っているLevel、とLevelへのアクセスを持っていますCore。それぞれEntityが同じ参照を保存し、Levelメモリを浪費することに注意してください。これに気付いたら、それぞれにEntity本当にアクセスする必要があるかどうかを質問する必要がありますLevel

私の経験では、A)逆依存関係を回避するための本当に明らかな解決策、またはB)グローバルインスタンスとシングルトンを回避する方法はありません。


何か不足していますか?「エンティティがレベルに依存しないようにする必要があります」と言いますが、それからそのエンティティを「Entity(Level&levelIn)」と記述します。依存関係はrefによって渡されることを理解していますが、それでも依存関係です。
アダムネイラー

@AdamNaylorポイントは、逆依存関係が本当に必要な場合があり、参照を渡すことでグローバルを回避できることです。ただし、一般に、これらの依存関係を完全に回避することが最善であり、これを行う方法が常に明確であるとは限りません。
リンゴ

0

だから、基本的に、グローバルな可変状態を避けたいですか?ローカルにするか、不変にするか、まったく状態にしないことができます。後者は最も効率的で柔軟です。これは実装非表示として知られています。

class ISomeComponent // abstract base class
{
    //...
};

extern ISomeComponent & g_SomeComponent; // will be defined somewhere else;

0

問題は、実際には、パフォーマンスを犠牲にすることなくカップリングを減らす方法に関するようです。すべてのグローバルオブジェクト(サービス)は通常、ゲームの実行中に変更可能な一種のコンテキストを形成します。この意味で、サービスロケーターパターンは、コンテキストのさまざまな部分を、アプリケーションのさまざまな部分に分散します。別の現実世界のアプローチは、次のような構造を宣言することです。

struct sEnvironment
{
    owning<iAudio*> m_Audio;
    owning<iRenderer*> m_Renderer;
    owning<iGameLevel*> m_GameLevel;
    ...
}

そして、それを所有していない生のポインタとして渡しますsEnvironment*。ここでは、ポインターがインターフェイスを指しているため、サービスロケーターと同様の方法で結合が減少します。ただし、すべてのサービスは1か所にあります(良い場合も悪い場合もあります)。これは別のアプローチです。

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