C ++エンジンプログラミングでシングルトンを正しく使用するにはどうすればよいですか?


16

私はシングルトンが悪いことを知っています。私の古いゲームエンジンは、すべてのデータを保持してから実際のゲームループまですべてを処理するシングルトン「ゲーム」オブジェクトを使用していました。今、私は新しいものを作っています。

問題は、window.draw(sprite)ウィンドウがである場所で使用するSFMLで何かを描画することですsf::RenderWindow。ここに表示される2つのオプションがあります。

  1. ゲーム内のすべてのエンティティが取得するシングルトンゲームオブジェクトを作成します(以前使用したもの)
  2. これをエンティティのコンストラクタにします:(Entity(x, y, window, view, ...etc)これはばかげて面倒です)

エンティティのコンストラクタをxとyだけに保ちながらこれを行う適切な方法は何ですか?

メインのゲームループで作成したすべてを追跡して、ゲームループでスプライトを手動で描画することもできますが、それも面倒で、エンティティの描画機能全体を完全に完全に制御したいと考えています。


1
'render'関数の引数としてウィンドウを渡すことができます。
ダリ

25
シングルトンは悪くない!それらは役に立つことがあり、時には必要になることもあります(もちろん議論の余地があります)。
ExOfDe

3
シングルトンを単純なグローバルに置き換えてください。「必要に応じて」グローバルに必要なリソースを作成することには意味がありません。エンティティについては、「レベル」クラスを使用して、すべてのエンティティに関連する特定のものを保持できます。
snake5

メインでウィンドウと他の依存関係を宣言し、他のクラスにポインターを設定します。
KaareZ

1
@JAB main()からの手動初期化により簡単に修正されました。遅延初期化は未知の瞬間にそれを実現しますが、これはコアシステムにとっては良い考えではありません。
snake15

回答:


3

各エンティティ内にスプライトをレンダリングするために必要なデータのみを保存し、エンティティから取得して、レンダリングのためにウィンドウに渡します。エンティティ内にウィンドウやビューデータを保存する必要はありません。

Levelクラス(現在使用中のすべてのエンティティを保持)とRendererクラス(ウィンドウ、ビュー、およびレンダリング用のその他のものを含む)を保持するトップレベルのGameクラスまたはEngineクラスを作成できます。

したがって、トップレベルクラスのゲーム更新ループは次のようになります。

EntityList entities = mCurrentLevel.getEntities();
for(auto& i : entities){
  // Run game logic...
  i->update(...);
}
// Render all the entities
for(auto& i : entities){
  mRenderer->draw(i->getSprite());
}

3
シングルトンについて理想的なものはありません。実装する必要がないのに、なぜ実装内部を公開するのですか?なぜLogger::getInstance().Log(...)ただではなく書くのLog(...)ですか?手動で一度だけ実行できるかどうかを尋ねられたときにクラスをランダムに初期化するのはなぜですか?静的グローバルを参照するグローバル関数は、作成と使用がはるかに簡単です。
snake5

@ snake5 Stack Exchangeでシングルトンを正当化することは、ヒトラーに同情するようなものです。
ウィリーヤギ

30

単純なアプローチは、以前Singleton<T>はグローバルであったものをT代わりに作成することです。グローバルにも問題がありますが、些細な制約を強制するための余分な作業や定型コードの束を表すものではありません。これは基本的に、エンティティコンストラクターに(潜在的に)関与することのない唯一のソリューションです。

より困難ですが、おそらくより良いアプローチは、依存関係を必要な場所渡すことです。はい、これにはWindow *aをオブジェクトの束(エンティティなど)に大まかに見える方法で渡すことが含まれます。見た目がグロスに見えるという事実から、何かがわかります。あなたのデザインはグロスかもしれません。

これが(より多くのタイピングを伴うことよりも)難しい理由は、これがインターフェースをリファクタリングすることにつながり、より少ないリーフレベルのクラスで「必要」なものが必要になるためです。これにより、レンダラーをすべてに渡すことのinherentさの多くが失われ、依存関係とカップリングの量を減らすことでコードの一般的な保守性も向上します。 。依存関係がシングルトンまたはグローバルである場合、システムがどのように相互接続されているかは明らかではありませんでした。

しかし、それは潜在的に大きな仕事です。事実の後にシステムにそれを行うことは、実に痛みを伴うことがあります。現時点では、シングルトンを使用してシステムをそのままにしておく方がはるかに実用的かもしれません(特に問題なく動作するゲームを実際に出荷しようとしている場合、プレイヤーは一般的に気にしませんシングルトンまたはそこに4つ)。

既存のデザインでこれを実行したい場合、これらの変更を行うための一般的なチェックリストがないため、現在の実装に関する詳細を投稿する必要があります。または、チャットで話し合ってください。

あなたが投稿したことから、「シングルトンなし」の方向への大きなステップは、エンティティがウィンドウまたはビューにアクセスする必要を避けることだと思います。それは、彼らが自分自身を描くことを示唆しており、あなたはエンティティに自分自身を描く必要はありません。エンティティが許可する情報のみを含む方法を採用できますそれらは何らかの外部システム(ウィンドウとビューの参照を持つ)によって描画されます。エンティティは、その位置と使用するスプライト(または、インスタンスの重複を避けるために実際のスプライトをレンダラー自体にキャッシュする場合は、そのスプライトへの何らかの参照)を公開するだけです。レンダラーは、エンティティの特定のリストを描画するように単に指示され、ループし、データを読み取り、内部で保持されているウィンドウオブジェクトを使用drawして、エンティティを検索したスプライトで呼び出します。


3
私はC ++に精通していませんが、この言語の快適な依存性注入フレームワークはありませんか?
bgusach

1
私はそれらを「快適」とは言いませんし、一般的には特に有用だとは思いませんが、他の人は経験が異なるかもしれないので、それらを育てるのが良い点です。

1
エンティティが自己を描画せずに情報を保持し、単一のシステムがすべてのエンティティの描画を処理するように彼が説明する方法は、今日のほとんどの人気のあるゲームエンジンで多く使用されています。
パトリックW.マクマホン

1
「グロスに見えるという事実は、あなたに何かを伝えるはずです。あなたのデザインはグロスかもしれません。」
Shadow503

理想的なケースと実用的な答えの両方を提供した+1。

6

sf :: RenderWindowから継承

SFMLは、実際にはクラスから継承することをお勧めします。

class GameWindow: public sf::RenderWindow{};

ここから、エンティティを描画するためのメンバー描画関数を作成します。

class GameWindow: public sf::RenderWindow{
public:
 void draw(const Entity& entity);
};

これで次のことができます。

GameWindow window;
Entity entity;

window.draw(entity);

エンティティにsf :: Spriteを継承させることで、エンティティが独自の一意のスプライトを保持する場合、これをさらに一歩進めることができます。

class Entity: public sf::Sprite{};

sf::RenderWindowだけのエンティティを描くことができ、およびエンティティが、今のような機能を持っているsetTexture()し、setColor()。エンティティは、スプライトの位置を独自の位置として使用することもできますsetPosition()。これにより、エンティティの移動とスプライトの両方に関数を使用できます。


最後に、あなたが持っているだけでかなりいいです:

window.draw(game);

以下にいくつかの簡単な実装例を示します

class GameWindow: public sf::RenderWindow{
 sf::Sprite entitySprite; //assuming your Entities don't need unique sprites.
public:
 void draw(const Entity& entity){
  entitySprite.setPosition(entity.getPosition());
  sf::RenderWindow::draw(entitySprite);
 }
};

または

class GameWindow: public sf::RenderWindow{
public:
 void draw(const Entity& entity){
  sf::RenderWindow::draw(entity.getSprite()); //assuming Entities hold their own sprite.
 }
};

3

ゲーム開発では、他のすべての種類のソフトウェア開発でシングルトンを回避するのと同じ方法で、シングルトンを回避します依存関係を渡します

道のうちで、あなたは(のような裸の型として直接の依存関係を渡すために選択することができますintWindow*など)か、などの(一の以上のカスタムラッパー型でそれらを渡すために選択することができますEntityInitializationOptions)。

前者の方法は(気づいたように)迷惑になる可能性がありますが、後者の方法では、すべてのエンティティコンストラクタを変更することなく、1つのオブジェクトですべてを渡し、フィールドを変更(さらにオプションタイプを特殊化)できます。後者の方が良いと思います。


3

シングルトンは悪くありません。代わりに、簡単に悪用されます。一方、グローバルはさらに悪用されやすく、より多くの問題を抱えています。

シングルトンをグローバルに置き換える唯一の正当な理由は、宗教的なシングルトン嫌いを和らげることです。

問題は、1つのグローバルインスタンスのみが存在し、どこからでもアクセスできる必要があるクラスを含むデザインを持っていることです。これは、たとえば分割画面を実装するゲームや、単一のロガーが必ずしもそのような素晴らしいアイデアとは限らないことに気付いた場合の十分に大規模なエンタープライズアプリケーションなど、シングルトンの複数インスタンスを持つようになるとすぐにバラバラになります。

要するに、参照によって合理的に渡すことができない単一のグローバルインスタンスがあるクラスを実際に持っている場合、シングルトンは次善のソリューションのプールの中でより良いソリューションの1つであることがよくあります。


1
私は宗教的なシングルトン嫌いであり、グローバルな解決策も考えていません。:S
ダンパントリー

1

依存関係を注入します。これを行うことの利点は、ファクトリを介してこれらの依存関係のさまざまなタイプを作成できることです。残念なことに、シングルトンを使用するクラスからシングルトンをリッピングすることは、カーペットの上で後ろ足で猫を引っ張るようなものです。しかし、それらを注入すれば、おそらくその場で実装を交換できます。

RenderSystem(IWindow* window);

これで、さまざまなタイプのウィンドウを挿入できます。これにより、さまざまな種類のウィンドウを使用してRenderSystemに対してテストを記述できるため、RenderSystemがどのように破損または実行するかを確認できます。「RenderSystem」の内部でシングルトンを直接使用する場合、これは不可能であり、より困難です。

現在では、よりテストしやすく、モジュール化され、特定の実装から切り離されています。具体的な実装ではなく、インターフェイスにのみ依存します。

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