ほとんどの場合、ゲームにはプレイヤークラスがあります。プレイヤーは通常、ゲームで多くのことを行うことができます。つまり、このクラスは、プレイヤーが実行できる各機能をサポートするための膨大な変数で巨大になります。それぞれのピースはそれ自体ではかなり小さいですが、組み合わされると数千行のコードになり、必要なものを見つけるのが苦痛になり、変更を加えるのが怖くなります。基本的にゲーム全体の一般的なコントロールであるもので、この問題をどのように回避しますか?
ほとんどの場合、ゲームにはプレイヤークラスがあります。プレイヤーは通常、ゲームで多くのことを行うことができます。つまり、このクラスは、プレイヤーが実行できる各機能をサポートするための膨大な変数で巨大になります。それぞれのピースはそれ自体ではかなり小さいですが、組み合わされると数千行のコードになり、必要なものを見つけるのが苦痛になり、変更を加えるのが怖くなります。基本的にゲーム全体の一般的なコントロールであるもので、この問題をどのように回避しますか?
回答:
通常、エンティティコンポーネントシステムを使用します(エンティティコンポーネントシステムは、コンポーネントベースのアーキテクチャです)。これにより、他のエンティティを簡単に作成できるようになり、敵/ NPCがプレーヤーと同じコンポーネントを持つこともできます。
このアプローチは、オブジェクト指向アプローチとまったく逆の方向に進みます。ゲーム内のすべてはエンティティです。エンティティは、ゲームの仕組みが組み込まれていない単なるケースです。コンポーネントのリストとそれらを操作する方法があります。
たとえば、プレーヤーには位置コンポーネント、アニメーションコンポーネント、および入力コンポーネントがあり、ユーザーがスペースを押したときにプレーヤーをジャンプさせます。
これを実現するには、プレーヤーエンティティにジャンプコンポーネントを指定します。これにより、呼び出されたときにアニメーションコンポーネントがジャンプアニメーションに変更され、プレーヤーが位置コンポーネントで正のy速度を持つようになります。入力コンポーネントで、スペースキーをリッスンし、ジャンプコンポーネントを呼び出します。(これは単なる例であり、移動用のコントローラーコンポーネントが必要です)。
これは、コードをより小さく、再利用可能なモジュールに分割するのに役立ち、プロジェクトをより組織的にすることができます。
ゲームはこの点でユニークではありません。ゴッドクラスはどこでもアンチパターンです。
一般的な解決策は、小さなクラスのツリーで大きなクラスを分解することです。プレイヤーがインベントリを持っている場合、インベントリ管理をの一部にしないでくださいclass Player
。代わりに、を作成しclass Inventory
ます。これはの1つのメンバーですclass Player
が、内部的にclass Inventory
多くのコードをラップできます。
別の例:プレイヤーキャラクターはNPCと関係があるためclass Relation
、Player
オブジェクトとオブジェクトの両方を参照しているが、NPC
どちらにも属していない場合があります。
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()
があるために呼び出されます)。我々は呼び出すときに-私たちは切り替える必要があることを知っているにして私たちがそうするのと、私たちはこのフレームを失うアクションにしたくないので、別の状態で今、このメソッドの内部で再びそれを呼び出すけど。そして今、もちろん私たちは呼び出します。WASD
Shift
InputController
WalkState
_currentPlayerState.Run();
WalkState
_currentPlayerState
RunState
Run()
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-単一の責任:前の例で見たこと。ええ、でも継承はどこにありますか?
ここで:プレイヤーの健康と他の特性は別のエンティティによって処理されるべきですか?私はそうは思いません。体力のないプレイヤーはいません。もしあれば、私たちは継承しません。例えば、我々が持っていますIDamagable
、LivingEntity
、IGameActor
、GameActor
。IDamagable
もちろんあり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 : Building
、TechBuilding : Building
。2つのコンポーネントを作成して、建物の一般的な操作やプロパティのためにコードを2回記述する必要はありません。そして、それらを異なる方法で追加し、継承の力を使用し、後でポリモーフィズムとカプセル化を使用できます。
間に何かを使用することをお勧めします。コンポーネントを使いすぎないようにします。
ゲームプログラミングパターンに関するこの本を読むことを強くお勧めします。これはWEBで無料です。
この問題に特効薬はありませんが、さまざまなアプローチがあり、そのほとんどが「懸念の分離」の原則を中心に展開しています。他の回答では、一般的なコンポーネントベースのアプローチについてすでに説明していますが、コンポーネントベースのソリューションの代わりに、またはコンポーネントベースのソリューションと共に使用できる他のアプローチがあります。この問題に対する私が推奨する解決策の1つであるエンティティコントローラーアプローチについて説明します。
第一に、Player
クラスの概念そのものがそもそも誤解を招くものです。多くの人は、プレイヤーキャラクター、NPCキャラクター、モンスター/敵を異なるクラスであると考える傾向があります。すべてに目録などがあります。
この考え方は、プレイヤーキャラクター、非プレイヤーキャラクター、モンスター/敵がすべてEntity
異なる扱いを受けるのではなく、' s' として扱われるアプローチにつながります。当然ながら、それらは異なる振る舞いをする必要があります-プレイヤーキャラクターは入力を介して制御されなければならず、npcsにはaiが必要です。
これに対する解決策は、Controller
を制御するために使用されるクラスを持つことEntity
です。これを行うことにより、すべての重いロジックがコントローラーに格納され、すべてのデータと共通性がエンティティに保存されます。
また、サブクラス化することController
にInputController
してAIController
、それはプレイヤーが効果的にいずれかを制御することができますEntity
部屋に。また、このアプローチは、ネットワークストリームからのコマンドを介して動作するRemoteController
またはNetworkController
クラスを持つことにより、マルチプレイヤーを支援します。
これにより、Controller
注意しないと、多くのロジックが1つになってしまう可能性があります。これを回避する方法はController
、他Controller
ので構成されるを使用するか、Controller
機能をのさまざまなプロパティに依存させることController
です。例えば、AIController
だろうDecisionTree
、それに取り付けられており、PlayerCharacterController
様々な他から構成することができるController
ような、S MovementController
、JumpController
(を含むステートマシン、状態とは、昇降、OnGround) InventoryUIController
。これの追加の利点はController
、新しい機能が追加されると新しいsを追加できることです。インベントリシステムなしでゲームを開始し、1つ追加すると、そのコントローラを後で追加できます。