PlayerとWorldの間の循環依存関係を回避する方法は?


60

私は、上下左右に移動できる2Dゲームに取り組んでいます。基本的に2つのゲームロジックオブジェクトがあります。

  • プレーヤー:世界に対して相対的な位置を持っている
  • ワールド:マップとプレイヤーを描画します

これまでのところ、WorldPlayerに依存している(つまり、参照している)ため、プレイヤーキャラクターをどこに描画するか、マップのどの部分を描画するかを把握するための位置が必要です。

衝突検出を追加して、プレーヤーが壁を通過できないようにします。

私が考えることができる最も簡単な方法は、意図した動きが可能かどうかをプレイヤー世界に尋ねることです。しかし、それはPlayerWorldの間に循環的な依存関係を導入します(つまり、それぞれが他方への参照を保持します)。私が思いついた唯一の方法は、WorldPlayerを移動させることですが、それはやや直感的ではありません。

私の最良の選択肢は何ですか?または、循環依存関係を回避する価値はありませんか?


4
なぜ循環依存は悪いことだと思いますか?stackoverflow.com/questions/1897537/…–
Fuhrmanator

@Fuhrmanator私はそれらが一般的に悪いことだとは思わないが、私はそれを導入するために私のコードで物事をもう少し複雑にしなければならないだろう。
futlib

私たちの小さな議論について怒っポスト、しかし何も新しい:yannbane.com/2012/11/... ...
jcora

回答:


61

世界はそれ自身を描くべきではありません。レンダラーは世界を描く必要があります。プレーヤーは自分で描画しないでください。レンダラーはプレイヤーをワールドに対して相対的に描画する必要があります。

プレーヤーは、衝突検出について世界に質問する必要があります。あるいは、衝突は静的な世界だけでなく他のアクタに対しても衝突検出をチェックする別のクラスによって処理される必要があります。

世界はおそらくプレイヤーをまったく意識すべきではないと思う。それは、神オブジェクトではなく、低レベルのプリミティブでなければなりません。Playerは、おそらく間接的に(衝突検出、またはインタラクティブオブジェクトのチェックなど)いくつかのWorldメソッドを呼び出す必要があります。


25
@ snake5-「できる」と「すべき」には違いがあります。何でも何でも描画できますが、描画を処理するコードを変更する必要がある場合は、描画中の「すべて」を検索するよりも「レンダラー」クラスに移動する方がはるかに簡単です。「区分化に執着する」は、「結束」を表す別の言葉です。
ネイト

16
@ Mr.Beast、いいえ、彼はそうではありません。彼は良いデザインを提唱しています。クラスの1つの失敗ですべてを詰め込むのは意味がありません。
-jcora

23
おっ、私はそれがそのような反応を引き起こすとは思いませんでした:)私は答えに追加するものは何もありませんが、私がそれを与えた理由を説明することができます-私はそれがより簡単だと思うので。「適切」または「正しい」ではありません。そのように聞こえたくありませんでした。責任が多すぎるクラスに取り組むことに気付いた場合、既存のコードを強制的に読みやすくするよりも、分割の方が速いため、私にとっては簡単です。私は理解できる塊のコードが好きで、@ futlibが経験しているような問題に反応してリファクタリングします。
リオサン

12
@ snake5クラスを追加すると、プログラマのオーバーヘッドが増えると言うのは、私の経験ではしばしば完全に間違っています。私の意見では、情報を提供する名前と明確に定義された責任を持つ10x100の行クラスは、単一の1000行の神クラスよりも読みやすく、プログラマにとってオーバーヘッド少なくなります
マーティン

7
何が何を描くかについてのメモとして、Renderer何らかのソートが必要ですが、それは各物がレンダリングされる方法のロジックがによって処理されることを意味しません、Renderer描かれる必要がある各物はおそらく次のような共通のインターフェースから継承するべきですIDrawableまたはIRenderable(使用している言語に関係なく同等のインターフェース)。世界はそうなるかもしれませんがRenderer、それが責任を踏み越えているように思えIRenderableます。特にそれがすでにそれ自体である場合はそうです。
zzzzBov

35

一般的なレンダリングエンジンがこれらのことを処理する方法を次に示します。

オブジェクトが空間のどこにあるかと、オブジェクトがどのように描画されるかには基本的な違いがあります。

  1. オブジェクトを描く

    通常、これを行うRendererクラスがあります。オブジェクト(Model)を受け取り、画面に描画します。drawSprite(Sprite)、drawLine(..)、drawModel(Model)など、必要に応じて任意のメソッドを使用できます。レンダラーなので、これらすべてを実行することになっています。また、下にある任意のAPIを使用するため、たとえばOpenGLを使用するレンダラーとDirectXを使用するレンダラーを使用できます。ゲームを別のプラットフォームに移植する場合は、新しいレンダラーを作成して使用するだけです。それは簡単です。

  2. オブジェクトを移動する

    各オブジェクトは、SceneNodeとして参照するものに添付されます。これは、構成によって実現します。SceneNodeにはオブジェクトが含まれます。それでおしまい。SceneNodeとは何ですか?これは、オブジェクト(通常は別のSceneNodeを基準とする)のすべての変換(位置、回転、スケール)と実際のオブジェクトを含む単純なクラスです。

  3. オブジェクトの管理

    SceneNodeはどのように管理されますか?SceneManager。このクラスは、シーン内のすべてのSceneNodeを作成および追跡します。特定のSceneNode(通常は「Player」や「Table」などの文字列名で識別される)またはすべてのノードのリストを要求できます。

  4. 世界を描く

    これは今ではかなり明白なはずです。シーン内のすべてのSceneNodeをウォークスルーし、レンダラーに適切な場所に描画させます。オブジェクトをレンダリングする前にレンダラーにオブジェクトの変換を保存させることにより、適切な場所に描画できます。

  5. 衝突検出

    これは必ずしも些細なことではありません。通常、空間内の特定のポイントにあるオブジェクト、または光線が交差するオブジェクトについてシーンを照会できます。この方法で、プレーヤーから動きの方向に光線を作成し、シーンマネージャーに光線が交差する最初のオブジェクトを尋ねることができます。その後、プレーヤーを新しい位置に移動するか、少し移動させるか(衝突するオブジェクトの隣に移動するか)、まったく移動しないように選択できます。これらのクエリは必ず個別のクラスで処理してください。SceneManagerにSceneNodeのリストを要求する必要がありますが、SceneNodeが空間内のポイントをカバーするか、レイと交差するかを判断するのは別のタスクです。SceneManagerはノードの作成と保存のみを行うことに注意してください。

それで、プレーヤーとは何ですか、そして世界は何ですか?

PlayerはSceneNodeを含むクラスである可能性があり、SceneNodeにはレンダリングされるモデルが含まれます。シーンノードの位置を変更して、プレーヤーを移動します。ワールドは、単にSceneManagerのインスタンスです。すべてのオブジェクトが含まれています(SceneNodesを使用)。衝突検出を処理するには、シーンの現在の状態を照会します。

これは、ほとんどのエンジンの内部で発生することを完全にまたは正確に説明することにはほど遠いですが、SOLIDで強調されているOOP原則を尊重することが基本となる理由を理解するのに役立ちます。コードを再構築するのが難しすぎるという考えや、実際には役に立たないという考えに屈しないでください。コードを慎重に設計することで、将来さらに多くを獲得できます。


+1-ゲームシステムを次のように構築していて、非常に柔軟であることがわかりました。
サイファー

+1、素晴らしい答え。私自身よりも具体的で要点まで。
jcora

+1、私はこの答えから多くのことを学びました。ありがとう@rootlocus
joslinm

16

なぜそれを避けたいのですか?再利用可能なクラスを作成する場合は、循環依存関係を避ける必要があります。ただし、Playerは再利用可能なクラスではありません。世界のないPlayerを使用したいと思いませんか?おそらくない。

クラスは機能のコレクションにすぎないことを忘れないでください。問題は、機能をどのように分割するかだけです。必要なことは何でもしてください。循環的な退廃が必要な場合は、そのようにしてください。(同じように、OOP機能についても同じことが言えます。目的にかなうようにコーディングしてください。単に盲目的にパラダイムに従うのではありません。)

編集
わかりました、質問に答えるために:コールバックを使用して、プレーヤーが衝突チェックのために世界を知る必要があることを避けることができます:

World::checkForCollisions()
{
  [...]
  foreach(entityA in entityList)
    foreach(entityB in entityList)
      if([... entityA and entityB have collided ...])
         entityA.onCollision(entityB);
}

Player::onCollision(other)
{
  [... react on the collision ...]
}

エンティティの速度を公開すると、質問で説明した物理の種類​​を世界で処理できます。

World::calculatePhysics()
{ 
  foreach(entityA in entityList)
    foreach(entityB in entityList)
    {
      [... move entityA according to its velocity as far as possible ...]
      if([... entityA has collided with the world ...])
         entityA.onWorldCollision();
      [... calculate the movement of entityB in order to know if A has collided with B ...]
      if([... entityA and entityB have collided ...])
         entityA.onCollision(entityB);
    }
}

ただし、おそらく遅かれ早かれ、世界への依存が必要になることに注意してください。それは、世界の機能が必要なときです。最も近い敵がどこにいるかを知りたいですか?次の棚がどれくらい離れているか知りたいですか?依存関係です。


4
+1循環依存は、ここでは実際には問題ではありません。この段階では、それを心配する理由はありません。ゲームが成長し、コードが成熟する場合は、とにかくサブクラスのPlayerクラスとWorldクラスをリファクタリングし、適切なコンポーネントベースのシステム、入力処理用のクラス、レンダリングなどを行うことをお勧めします。スタート、問題ありません。
ローランクーヴィドゥ

4
-1、循環依存関係を導入しない唯一の理由ではありません。それらを導入しないことにより、システムを拡張および変更しやすくなります。
-jcora

4
@Baneその接着剤なしではコーディングできません。違いは、追加するインダイレクションの量です。クラスGame-> World-> Entityがある場合、またはクラスGame-> World、SoundManager、InputManager、PhysicsEngine、ComponentManagerがある場合。すべての(構文上の)オーバーヘッドと、それにより複雑さを暗示するため、読みにくくなります。そして、ある時点で、コンポーネントが相互作用する必要があります。そして、それが、1つのグルークラスが、すべてを多くのクラスに分割するよりも簡単にするポイントです。
APIビースト

3
いいえ、ゴールポストを動かしています。もちろん、何かを呼び出す必要がありますrender(World)。議論は、すべてのコードを1つのクラス内に詰め込むべきか、またはコードを論理ユニットと機能ユニットに分割する必要があるかどうかです。ところで、これらのコンポーネントマネージャー、物理エンジン、および入力マネージャーを再利用することは幸運です。
jcora

1
@Bane新しいクラスを導入する以外に、物事を論理的なチャンクに分割する他の方法があります。同様に、新しい関数を追加したり、コメントブロックで区切られた複数のセクションにファイルを分割したりできます。単純に保つだけでは、コードが混乱するわけではありません。
APIビースト

13

あなたの現在のデザインはソリッドデザインの最初の原則に反しているようです。

「単一責任原則」と呼ばれるこの最初の原則は、一般に、常にデザインを損なうモノリシックなあらゆるオブジェクトを作成しないために従うべき優れたガイドラインです。

具体的には、Worldオブジェクトはゲームの状態の更新と保持、およびすべての描画の両方を担当します。

レンダリングコードが変更された場合、または変更する必要がある場合はどうなりますか?実際にレンダリングに関係のない両方のクラスを更新する必要があるのはなぜですか?Liosanがすでに言ったように、あなたはを持っているべきRendererです。


さて、あなたの実際の質問に答えるために...

これを行うには多くの方法があり、これはデカップリングの1つの方法にすぎません。

  1. 世界はプレイヤーが何であるかを知りません。
    • Objectただし、プレーヤーのあるsのリストはありますが、プレーヤークラスに依存しません(継承を使用してこれを実現します)。
  2. プレーヤーはいくつかによって更新されますInputManager
  3. ワールドは、動きと衝突の検出を処理し、適切な物理的変更を適用し、オブジェクトに更新を送信します。
    • たとえば、オブジェクトAとオブジェクトBが衝突した場合、世界はそれらを通知し、彼らはそれを自分で処理できます。
    • 世界はまだ物理学を処理します(デザインがそのような場合)。
    • 次に、両方のオブジェクトは、衝突がそれらに興味があるかどうかを確認できます。たとえば、オブジェクトAがプレイヤーで、オブジェクトBがスパイクの場合、プレイヤーは自分自身にダメージを与えることができます。
    • ただし、これは他の方法でも解決できます。
  4. Rendererすべてのオブジェクトを描画します。

世界はプレイヤーが何であるかを知らないと言いますが、それが衝突するオブジェクトの1つである場合、プレーヤーのプロパティを知る必要があるかもしれない衝突検出を処理します。
マーカスフォンブロードー

継承、世界は、一般的な方法で説明できる何らかのオブジェクトを認識している必要があります。問題は、世界がプレーヤーへの参照を持っているだけではなく、クラスとしてそれに依存している可能性があることです(つまり、healthこのインスタンスのみがPlayer持っているようなフィールドを使用します)。
jcora

ああ、あなたは世界がプレーヤーへの参照を持たず、必要に応じてプレーヤーとともにICollidableインターフェイスを実装するオブジェクトの配列だけを持っていることを意味します。
マーカスフォンブロードー

2
+1良い答え。しかし、「良いソフトウェア設計は重要ではないと言うすべての人々を無視してください」。一般。誰もそれを言っていません。
ローランクーヴィドゥ

2
編集済み!とにかく不必要に思えた
...-jcora

1

プレイヤーは、衝突検出のようなものについて世界に尋ねるべきです。循環依存を回避する方法は、WorldがPlayerに依存しないようにすることです。世界は、それ自体がどこに描画されているかを知る必要があります。おそらく、追跡するエンティティへの参照を保持できるCameraオブジェクトへの参照を使用して、さらに抽象化したいでしょう。

循環参照の観点から避けたいのは、相互参照を保持することではなく、コード内で明示的に相互参照することです。


1

2つの異なるタイプのオブジェクトが互いに質問できる場合。それらはメソッドを呼び出すためにもう一方への参照を保持する必要があるため、お互いに依存します。

ワールドにプレイヤーに質問させることで循環依存を回避できますが、プレイヤーはワールドに質問することはできません。このようにして、ワールドはプレイヤーへの参照を持ちますが、プレイヤーはワールドへの参照を必要としません。またはその逆。しかし、これは問題を解決するものではありません。なぜなら、世界はプレイヤーに何か質問があるかどうかを尋ね、次の電話で伝える必要があるからです...

したがって、この「問題」を実際に回避することはできません。それについて心配する必要はないと思います。できるだけ長く、愚かなデザインをしてください。


0

プレイヤーとワールドの詳細を取り除いて、2つのオブジェクト間に循環依存関係を導入したくないという単純なケースがあります(言語によっては問題にならない場合もあります。Fuhrmanatorのコメントのリンクを参照してください)。この問題および同様の問題に適用される少なくとも2つの非常に単純な構造的解決策があります。

1)シングルトンパターンをワールドクラスに導入します。これにより、プレーヤー(および他のすべてのオブジェクト)は、高価な検索や永続的に保持されたリンクなしで、ワールドオブジェクトを簡単に見つけることができます。このパターンの要点は、クラスがそのクラスの唯一のインスタンスへの静的参照を持っているということです。これは、オブジェクトのインスタンス化時に設定され、削除時にクリアされます。

開発言語と必要な複雑さに応じて、これをスーパークラスまたはインターフェイスとして簡単に実装し、プロジェクト内に複数あるとは思わない多くの主要なクラスで再利用できます。

2)開発中の言語がサポートしている場合(多くの場合)、弱参照を使用します。これは、ガベージコレクションなどには影響しない参照です。まさにこれらの場合に役立ちます。弱参照しているオブジェクトがまだ存在するかどうかについて仮定をしないようにしてください。

特定のケースでは、プレイヤーは世界への弱い参照を保持できます。この利点は(シングルトンの場合と同様に)、各フレームで何らかの方法でワールドオブジェクトを検索したり、ガベージコレクションなどの循環参照の影響を受けるプロセスを妨げる永続的な参照を保持する必要がないことです。


0

他の人が言ったように、私はあなたが思うWorldあまりにも多くの一つのことをやっている:それはしようとしている両方のゲームが含まれているMap(個別のエンティティであるべきである)なるRendererと同時に。

そのため、新しいオブジェクト(GameMapおそらく)を作成し、その中にマップレベルのデータを保存します。その中に、現在のマップと対話する関数を記述します。

次に、オブジェクト必要Rendererです。このオブジェクトを、を含む(および)の両方することができ、それらを描画することもできます。Renderer GameMapPlayerEnemies


-6

変数をメンバーとして追加しないことにより、循環依存を回避できます。プレーヤーなどに静的なCurrentWorld()関数を使用します。ただし、すでにWorldに実装されているインターフェイスとは異なるインターフェイスを発明しないでください。これは完全に不要です。

また、プレーヤーオブジェクトを破棄する前または破棄するときに参照を破棄して、循環参照によって引き起こされる問題を効果的に停止することもできます。


1
同感です。OOPが過大評価されています。基本的な制御フローを学習した後、チュートリアルと教育はすぐにオブジェクト指向にジャンプします。OOプログラムは一般に手続き型コードよりも低速です。オブジェクト間に官僚主義があり、多くのポインタアクセスがあり、キャッシュミスが発生するからです。ゲームは動作しますが、非常に遅いです。キャッシュミスを回避するために、プレーンなグローバルアレイと、すべてに最適化された手作業で最適化された機能を使用した、リアルで非常に高速で機能豊富なゲーム。その結果、パフォーマンスが10倍向上します。
カルマリオス
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.