私は最終年度のプロジェクトに取り組んでおり、マルチプレイヤーゲームを作成しています。戦車バトルゲームです。
クライアントには基本的に2種類のタンクがあります。 1つはキーボードで制御可能で、もう1つは敵の戦車と制御不可能です。敵の戦車の位置は、サーバーから位置の変更に関するデータをいつ取得するかによって変わります。
すべての戦車は砲塔を持っています弾とミサイルを発射するために使用されます。また、弾薬の詳細も保存されます。ここで、ユーザーの戦車には、キーボードで回転独立した砲塔が必要です。また、戦車が移動すると移動します。敵の戦車の砲塔は明らかに制御できず、サーバーから更新を取得したときにのみ回転して移動します。
今、これは私にこれを設計し続ける方法について多くの混乱をもたらします。現在のモデルデザインは以下の通りです。
久しぶりにUMLダイアグラムを作成したので、エラーが発生する可能性があります。
私が抱えている問題の1つは、砲塔にあります。敵戦車の砲塔には、firePrimary()、fireSecondary()メソッドがすべて含まれているはずです。これは、これらを呼び出すことはないためです。対戦相手が発砲した場合、弾丸サーバーがメッセージを送信し、私はミサイルをスポーンします。
ユーザータンクのタレットは、これらのメソッドをすべて備えている必要があります。今、私の混乱は、Turret基本クラスにshootableを実装すると、EnemyTankでfirePrimary()やfireSecondary()などのメソッドが使用できなくなることです。 Shootableインターフェースを実装するのではなく、ShootableでUserTurretを構成すると、Tankに抽象クラスTurretがあり、UserTurretまたはEnemyTurretのいずれかを渡すというアーキテクチャが壊れます。
BasicShootableとHeavyShootableの実装は、主にさまざまな種類の弾丸をスポーンするためのものです。
これが正しくない場合は、さまざまなクラスで役に立たないメソッドがないようにするための最善の方法があります。また、戦車や砲塔にその場で動作を追加したい場合は、ゲームエンティティをさらに拡張できます。
注:これは私の前の質問の続きです: 戦略デザインパターンまたは何か他のものを使用する必要がありますか?
これは前の質問の続きであり、これは戦略を使用する場合のisケースです。戦車はすべて同じ機能を公開する必要がありますが、異なるdriverまたはinputを使用してください。プレイヤーの戦車はユーザー入力を使用し、敵の戦車はAIを使用します。
タレットにもさまざまな機能がありますが、あなたの設計はこれを正しく捉えていると思います。共通のインターフェースまたは親クラスがあり、プレーヤーサブクラスは追加の子コンポーネントを持つという追加の複雑さを提供します。
これを実装する方法はいくつかありますが、一般的な考え方は次のようになります。
class TankDriver {
abstract void move(Tank t);
}
class PlayerTankDriver {
...
}
class AiTankDriver {
...
}
class Tank {
TankDriver driver;
Tank(TankDriver d) {
driver = d;
}
void move() {
d.move(this);
}
}
一般的な使用例は、戦車を構築するときにドライバー戦略を指定し、戦車の参照を保持することです。戦車が移動する番になると、移動するように指示します。実装に定義されている戦略に委任します。ユーザーにさらにクエリを送信したり、任意のAIを使用したりできるため、柔軟性があります。1つの敵戦車は非常に攻撃的であり、別の敵戦車はよりリスクが低い可能性があります。非常に深いツリーで先読みアルゴリズムを使用する場合もあれば、1つの動きを先読みする場合もあります(つまり、難易度が異なります)。
重要なのは、プラガブルロジックは、タンクであることの意味の1つの側面にレーザーで焦点を合わせている他のクラスにあるため、まだ1つのタンククラスしか必要ないということです。
最後に、プレーヤーの戦車の砲塔は、AI制御の砲塔とは異なるメソッドが公開される可能性があるという問題があります。これは少し緊密に結合された奇妙な3方向の依存関係に陥る可能性があり、それに対処する方法は複数あります。
これは、適切な設計を維持しながら、「適切な」OO設計を持つという教授の目標を満たします。
これを真のOOファッションで達成するための最良の方法は、戦車に属する砲塔を使用して設計を維持し、戦車自体に砲塔機能のサブセットを公開することだと思います。次に、実際の砲塔に転送します。これは、現実のシナリオで設計する方法ではありませんが、利点があります。
これは デメテルの法則 を満たします。これは基本的に、ドライバー→タンク→タレットがあり、ドライバーがタレットに到達するためにタンクを「通過」してはならないことを示しています。
これにより、ドライバー、タンク、タレットに非線形の依存関係があり、構造、使用法、参照整合性が複雑になる可能性がある厄介な三角形の問題が回避されます。たとえば、運転手に戦車と砲塔があり、戦車の砲塔が運転手とは異なる場合があります。これはバグになります。
generics/templates を回避または簡素化します。これは、戦車が汎用クラスである場合に、戦車がドライバーにどのタイプの砲塔を期待するかを伝えるために使用できます。
しかし、待ってください。今度は、戦車はタレットのインターフェースを認識し、意味のあるメソッドのみを公開する必要があります。それは、このような構成を使用するという点に反します!
allタレットメソッドを公開し、無意味なメソッドには何もしないことをお勧めします。繰り返しますが、完全に理想的ではありませんが、デメテルの法則のために教授を幸せにするでしょう。
戦車を介して砲塔要求を転送するための擬似コード:
interface Turret {
void firePrimary();
void fireSecondary();
// and the other methods in your diagram.
}
class Tank {
Turret turret;
Tank(Turret t) {
turret= t;
}
void firePrimary() {
t.firePrimary();
}
void fireSecondary() {
t.fireSecondary();
}
}
タンクにタレットインターフェースを実装させることもできますが、それはあなた次第です。便利な場合もありますが、Tankが誤用される可能性があります。
タンクにはタレットがあるので、OOの観点から見ると、タンクに「タレットのもの」を実行するように指示でき、それらのコマンドを通過させることができます。
別の戦車を砲塔として追加することで、戦車を悪用する可能性があります。戦車→戦車→砲塔ですが、これは無意味です。静的型システムを使用することにより、この誤用からコンパイル時に保護することは良いことです。
砲塔を実装する戦車、またはドライバーが使用するために戦車に砲塔を返却させることについては、賛否両論があります。これから学ぶことは、これを設計して実装するための正しい方法は1つではないことです。スペクトルの一方の端には「純粋な」OOがあります。これは、教授が教えようとしていることです。スペクトルのもう一方の端には、「ほとんど」OO設計は、あちこちに少しファッジがあり、人生を楽にするためにあります。これは少し正直ですが、これは課題であり、実際のプロジェクトの設計が「実用的な「スペクトルの終わりは、「理論的な」終わりではありません。
私はあなたが役立つかもしれない別の質問に 答え を書きました。 AIとユーザー入力のアプローチでは、必然的に異なる入力が必要になるため、上記の例は少し複雑になります(それでも良いスタートです)。私がリンクした質問/回答を見ると、このアプローチについてもう少し学ぶことができます。