巨大なプレイヤークラスを回避するにはどうすればよいですか?


46

ほとんどの場合、ゲームにはプレイヤークラスがあります。プレイヤーは通常、ゲームで多くのことを行うことができます。つまり、このクラスは、プレイヤーが実行できる各機能をサポートするための膨大な変数で巨大になります。それぞれのピースはそれ自体ではかなり小さいですが、組み合わされると数千行のコードになり、必要なものを見つけるのが苦痛になり、変更を加えるのが怖くなります。基本的にゲーム全体の一般的なコントロールであるもので、この問題をどのように回避しますか?


26
複数のファイルまたは1つのファイル、コードはどこかに行かなければなりません。ゲームは複雑です。必要なものを見つけるには、適切なメソッド名と説明的なコメントを書きます。変更を恐れずに、テストしてください。そして、あなたの仕事をバックアップしてください:)
クリスマクファーランド

7
どこかに行かなければなりませんが、コードの設計は柔軟性とメンテナンスの点で重要です。数千行のコードのクラスまたはグループを持っていることは、私としてもまったく印象的ではありません。
user441521

17
@ChrisMcFarlandはバックアップを推奨せず、バージョンコードXDを推奨します。
GameDeveloper

1
@ChrisMcFarland GameDeveloperに同意します。Git、svn、TFSなどのバージョン管理があると、大きな変更をより簡単に取り消すことができ、誤ってプロジェクトを削除したり、ハードウェア障害やファイル破損などから簡単に回復できるため、開発が非常に簡単になります。
Nzall

3
@TylerH:私は強く反対します。バックアップでは、多くの探索的変更をマージすることはできません。また、変更セットに有用なメタデータに近い場所を結び付けることも、適切なマルチ開発者ワークフローを有効にすることもできません。非常に強力なポイントインタイムバックアップシステムのようにバージョン管理を使用できますが、これらのツールの多くの可能性が失われています。
Phoshi

回答:


67

通常、エンティティコンポーネントシステムを使用します(エンティティコンポーネントシステムは、コンポーネントベースのアーキテクチャです)。これにより、他のエンティティを簡単に作成できるようになり、敵/ NPCがプレーヤーと同じコンポーネントを持つこともできます。

このアプローチは、オブジェクト指向アプローチとまったく逆の方向に進みます。ゲーム内のすべてはエンティティです。エンティティは、ゲームの仕組みが組み込まれていない単なるケースです。コンポーネントのリストとそれらを操作する方法があります。

たとえば、プレーヤーには位置コンポーネント、アニメーションコンポーネント、および入力コンポーネントがあり、ユーザーがスペースを押したときにプレーヤーをジャンプさせます。

これを実現するには、プレーヤーエンティティにジャンプコンポーネントを指定します。これにより、呼び出されたときにアニメーションコンポーネントがジャンプアニメーションに変更され、プレーヤーが位置コンポーネントで正のy速度を持つようになります。入力コンポーネントで、スペースキーをリッスンし、ジャンプコンポーネントを呼び出します。(これは単なる例であり、移動用のコントローラーコンポーネントが必要です)。

これは、コードをより小さく、再利用可能なモジュールに分割するのに役立ち、プロジェクトをより組織的にすることができます。


コメントは詳細なディスカッション用ではありません。この会話はチャットに移動さました
マイケルハウス

8
移動する必要があるコメントを移動することは理解していますが、回答の正確性に挑戦するコメントを移動しないでください。それは明らかなはずですよね?
バグ

20

ゲームはこの点でユニークではありません。ゴッドクラスはどこでもアンチパターンです。

一般的な解決策は、小さなクラスのツリーで大きなクラスを分解することです。プレイヤーがインベントリを持っている場合、インベントリ管理をの一部にしないでくださいclass Player。代わりに、を作成しclass Inventoryます。これはの1つのメンバーですclass Playerが、内部的にclass Inventory多くのコードをラップできます。

別の例:プレイヤーキャラクターはNPCと関係があるためclass RelationPlayerオブジェクトとオブジェクトの両方を参照しているが、NPCどちらにも属していない場合があります。


ええ、私はこれを行う方法についてのアイデアを探していました。私の考えでは、小さなピースの機能がたくさんあるので、コーディング中にこれらの小さなピースを分割するのは自然ではありません。ただし、これらの小さな機能がすべてプレーヤークラスを巨大化し始めていることが明らかになります。
user441521

1
ゲーム内の他のすべてのクラス/オブジェクトが含まれて管理されている場合、人々は通常、何かが神のクラスまたは神のオブジェクトであると言います。
バリント

11

1)プレーヤー:ステートマシン+コンポーネントベースのアーキテクチャ。

Playerの通常のコンポーネント:HealthSystem、MovementSystem、InventorySystem、ActionSystem。これらはすべてのようなクラスですclass HealthSystem

使用することはお勧めしません(通常の場合、フレームごとに何らかのアクションが必要な場合を除き、ヘルスシステムを更新することは意味Update()がありません。これらはめったに起こりません。時々健康を失うために-コルーチンを使用することをお勧めします別の人は常に健康またはランニングパワーを再生します、あなたはちょうど現在の健康またはパワーを取り、時が来たらそのレベルまで満たすためにコルーチンを呼び出すだけです。彼は破損していたか、再び走り始めました

状態:LootState、RunState、WalkState、AttackState、IDLEState。

すべての状態はから継承しinterface IStateます。IStateこの例では、例として4つのメソッドがあります。Loot() Run() Walk() Attack()

また、class InputControllerユーザーのすべての入力を確認する場所があります。

さて実際の例に移りましょう:InputControllerプレイヤーがのいずれかを押したかどうかを確認しWASD or arrows、次にを押したかどうかを確認しShiftます。彼が押された場合にのみWASD、我々は呼んで_currentPlayerState.Walk();、このhappendsと我々が持っているときcurrentPlayerStateに等しくなるようにWalkStateして、その後WalkState.Walk() 、この場合には-私たちはこのような状態のために必要なすべてのコンポーネントを持ってMovementSystem、我々はプレーヤーの動きをするので、public void Walk() { _playerMovementSystem.Walk(); }あなたは私たちがここに持っているものを参照してください- ?動作の2番目の層があり、コードの維持とデバッグに非常に適しています。

次に2番目のケース:WASD+をShift押したらどうなりますか?しかし、以前の状態はでしたWalkState。この場合、Run()が呼び出されますInputController(これを混同しないでください。これは、のためではなく+ チェックインRun()があるために呼び出されます)。我々は呼び出すときに-私たちは切り替える必要があることを知っているにして私たちがそうするのと、私たちはこのフレームを失うアクションにしたくないので、別の状態で今、このメソッドの内部で再びそれを呼び出すけど。そして今、もちろん私たちは呼び出します。WASDShiftInputControllerWalkState_currentPlayerState.Run();WalkState_currentPlayerStateRunStateRun()WalkState_playerMovementSystem.Run();

しかしLootState、プレーヤーがボタンを放すまで歩くことも走ることもできない場合はどうでしょうか?さて、この場合、略奪を始めたとき、たとえばボタンEが押されたとき、私たち_currentPlayerState.Loot();はスイッチにLootState電話し、そこから呼び出されます。そこで、たとえば、範囲内に略奪するものがあるかどうかを取得するためにcollsionメソッドを呼び出します。そして、アニメーションがある場所または開始する場所でコルーチンを呼び出します。また、コルーチンブレークでない場合はプレーヤーがボタンを保持していることを確認します。しかし、プレーヤーが押すとWASDどうなりますか?- _currentPlayerState.Walk();が呼び出されますが、ここではステートマシンについてのかわいいものがありますLootState.Walk()何もしない、または機能として実行する空のメソッドがあります。プレイヤーは、「おい、まだこれを略奪していない、待てますか?」と言います。彼が略奪を終了すると、に変わりIDLEStateます。

また、class BaseState : IStateこれらのデフォルトメソッドのすべての動作を実装した、呼び出される別のスクリプトをvirtual実行できoverrideますがclass LootState : BaseState、クラスのタイプで使用できるようにそれらを実行します。


コンポーネントベースのシステムは素晴らしいです、それについて私を悩ます唯一のものはインスタンスです、それらの多く。また、ガベージコレクターのためにより多くのメモリと作業が必要です。たとえば、敵のインスタンスが1000個ある場合。それらはすべて4つのコンポーネントを持っています。1000個ではなく4000個のオブジェクト。Mbは、単一のゲームオブジェクトが持つすべてのコンポーネントを考慮すれば、それほど大したことではありません(パフォーマンステストは実行していません)。


2)継承ベースのアーキテクチャ。コンポーネントを完全に取り除くことはできないことに気づくでしょうが、きれいで機能するコードを作成したい場合は実際には不可能です。また、適切な場合に使用することを強くお勧めするデザインパターンを使用する場合(それらを使いすぎないでください、それは過剰生成と呼ばれます)。

ゲームで終了するために必要なすべてのプロパティを持つPlayerクラスがあるとします。ヘルス、マナ、エネルギーを持ち、移動、実行、能力を使用でき、インベントリを持ち、アイテムを作成し、アイテムを略奪することができ、バリケードやタレットを構築することもできます。

まず、インベントリ、クラフティング、ムーブメント、ビルディングは、次のようなメソッドを持つことはプレイヤーの責任ではないため、コンポーネントベースにする必要がありますAddItemToInventoryArray()-プレイヤーは、PutItemToInventory()前述のメソッド(2レイヤー-異なるレイヤーに応じていくつかの条件を追加します)。

建物の別の例。プレイヤーはのようなものを呼び出すことができますOpenBuildingWindow()Building、残りのすべてを処理し、ユーザーが特定の建物を構築することを決定すると、必要なすべての情報をプレイヤーに渡しBuild(BuildingInfo someBuildingInfo)、プレイヤーは必要なすべてのアニメーションで構築を開始します。

ソリッド-OOP原則。S-単一の責任:前の例で見たこと。ええ、でも継承はどこにありますか?

ここで:プレイヤーの健康と他の特性は別のエンティティによって処理されるべきですか?私はそうは思いません。体力のないプレイヤーはいません。もしあれば、私たちは継承しません。例えば、我々が持っていますIDamagableLivingEntityIGameActorGameActorIDamagableもちろんありTakeDamage()ます。

class LivinEntity : IDamagable {

   private float _health; // For fields that are the same between Instances I would use Flyweight Pattern.

   public void TakeDamage() {
       ....
   }
}

class GameActor : LivingEntity, IGameActor {
    // Here goes state machine and other attached components needed.
}

class Player : GameActor {
   // Inventory, Building, Crafting.... components.
}

したがって、ここでは実際にコンポーネントを継承から分割することはできませんでしたが、ご覧のようにそれらを混在させることができます。たとえば、システムの種類が異なり、必要以上のコードを記述したくない場合は、システムの構築用の基本クラスを作成することもできます。実際、さまざまな種類の建物を持つこともできますが、実際にはコンポーネントベースでそれを行う良い方法はありません!

OrganicBuilding : BuildingTechBuilding : Building。2つのコンポーネントを作成して、建物の一般的な操作やプロパティのためにコードを2回記述する必要はありません。そして、それらを異なる方法で追加し、継承の力を使用し、後でポリモーフィズムとカプセル化を使用できます。


間に何かを使用することをお勧めします。コンポーネントを使いすぎないようにします。


ゲームプログラミングパターンに関するこの本を読むことを強くお勧めします。これはWEBで無料です。


私は今夜​​遅く掘りますが、参考までに私は団結を使用していませんので、うまくいくように調整する必要があります。
user441521

ああ、ここがUnityタグだと思ったのは残念だ。唯一のものはMonoBehaviorです。これは、Unityエディターのシーンのすべてのインスタンスの基本クラスにすぎません。Physics.OverlapSphere()に関しては、フレーム中に球体コライダーを作成し、その接触をチェックするメソッドです。コルーチンは偽の更新プログラムのようなもので、その呼び出しはプレーヤーPCのfpsよりも少ない量に減らすことができます-パフォーマンスに優れています。Start()-インスタンスが作成されたときに一度だけ呼び出されるメソッド。他のすべてが他のすべての場所に適用されるべきです。次のパートでは、Unityで何も使用しません。スライ。これが何かを明らかにすることを願っています。
率直な月_Max_

以前Unityを使用したことがあるので、この考えを理解しています。私もコルーチンを持っているLuaを使用しているので、物事はかなりうまく翻訳されるはずです。
user441521

この回答は、Unityタグがないことを考えると、Unity特有のことです。それをより一般化し、統一的なものを例にした場合、これははるかに良い答えになります。
ファラプ

@CandidMoonええ、それはましです。
ファラプ

4

この問題に特効薬はありませんが、さまざまなアプローチがあり、そのほとんどが「懸念の分離」の原則を中心に展開しています。他の回答では、一般的なコンポーネントベースのアプローチについてすでに説明していますが、コンポーネントベースのソリューションの代わりに、またはコンポーネントベースのソリューションと共に使用できる他のアプローチがあります。この問題に対する私が推奨する解決策の1つであるエンティティコントローラーアプローチについて説明します。

第一に、Playerクラスの概念そのものがそもそも誤解を招くものです。多くの人は、プレイヤーキャラクター、NPCキャラクター、モンスター/敵を異なるクラスであると考える傾向があります。すべてに目録などがあります。

この考え方は、プレイヤーキャラクター、非プレイヤーキャラクター、モンスター/敵がすべてEntity異なる扱いを受けるのではなく、' s' として扱われるアプローチにつながります。当然ながら、それらは異なる振る舞いをする必要があります-プレイヤーキャラクターは入力を介して制御されなければならず、npcsにはaiが必要です。

これに対する解決策は、Controllerを制御するために使用されるクラスを持つことEntityです。これを行うことにより、すべての重いロジックがコントローラーに格納され、すべてのデータと共通性がエンティティに保存されます。

また、サブクラス化することControllerInputControllerしてAIController、それはプレイヤーが効果的にいずれかを制御することができますEntity部屋に。また、このアプローチは、ネットワークストリームからのコマンドを介して動作するRemoteControllerまたはNetworkControllerクラスを持つことにより、マルチプレイヤーを支援します。

これにより、Controller注意しないと、多くのロジックが1つになってしまう可能性があります。これを回避する方法はController、他Controllerので構成されるを使用するか、Controller機能をのさまざまなプロパティに依存させることControllerです。例えば、AIControllerだろうDecisionTree、それに取り付けられており、PlayerCharacterController様々な他から構成することができるControllerような、S MovementControllerJumpController(を含むステートマシン、状態とは、昇降、OnGround) InventoryUIController。これの追加の利点はController、新しい機能が追加されると新しいsを追加できることです。インベントリシステムなしでゲームを開始し、1つ追加すると、そのコントローラを後で追加できます。


このアイデアは気に入っていますが、すべてのコードをコントローラークラスに転送したため、同じ問題が残っています。
user441521

@ user441521追加したい段落があることに気付いたが、ブラウザがクラッシュしたときにそれを失った。今すぐ追加します。基本的に、異なるコントローラーで集約コントローラーに構成できるため、各コントローラーが異なる処理を実行できます。例AggregateController.Controllers = {JumpController(keybinds)、MoveController(keybinds)、InventoryUIController(keybinds、uisystem)}
ファラプ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.