私は小さなマルチプレイヤーゲームを開発しています。 1つのWebSocketサーバーによって提供され、複数のコンシューマーによって消費されます。そのため、並行性エラーに注意する必要があります。
私が思いついた一般的なソフトウェアアーキテクチャは次のとおりです。すべてのゲーム状態を最高レベルのGame
クラスにカプセル化し、他のスレッドがゲーム状態オブジェクトにアクセスできないようにします。代わりに、クライアント(サーバー上のWebSocketスレッド)はコマンドをGame
クラスに送信し、そこでスレッドセーフリストに追加され、後で別のスレッドで呼び出されるGame.Tick
メソッドによって処理されます。例えば:
Game.AddCommand
は、着信コマンドのリストにコマンドを割り当てます。
public void AddCommand(Guid id, ICommand command)
{
lock (_commandsLock)
{
_commands[id] = command;
}
}
Game.Tick
は各コマンドを処理し、リストをクリアします。
public void Tick()
{
IList<ICommand> commands;
lock (_commandsLock)
{
commands = _commands.Values.ToList();
_commands = new Dictionary<Guid, ICommand>();
}
foreach (var command in commands)
{
command.Resolve();
Observer.Update(_map);
}
}
問題は次のとおりです。自分自身を解決するには、各Command
がGame
の内部にアクセスする必要があります。 Resolve
メソッドが非常に少なくなるか、movementService.Move(entity, target)
のように単一行になるように、いくつかのサービスを使用するつもりです。これらのサービスをResolve
メソッドに渡すと、各Command
は不要なもので肥大化してしまいます。
代わりに、DIを使用したいと思います。ただし、各Command
はクライアントによって作成されるため、Game
に属するサービスにアクセスすることはできません。これらのサービスは公開する必要があり、クライアントが何か間違ったことをした場合(つまり、自分でResolve
を呼び出す)、ゲーム状態が変更され、同時実行エラーが発生する可能性があります。
CommandResolver
の概念を作成してこれを解決しようとしました。この場合、Command
はいくつかのパラメーターを含む単純なモデルであり、内部のCommandResolver
によって解決されます。 gamestateにアクセスできます。しかし、これには、Open/Closedの原則に反する複雑なマッピング(たとえば、map MoveCommand
からMoveCommandResolver
-各コマンドは異なる依存関係を持つ異なるタイプ)が必要です変更 =マッピング、おそらく単にではなく、長く厄介なswitchステートメント拡張新しいCommand
&CommandResolver
を備えた機能。
現在、リゾルバーを破棄し、次の2つの可能性のいずれかを検討しています。a)コマンドの生成に使用できるGame
からCommandFactory
を公開します。おそらく、game.GetCommandFor(entity).Move(target)
のような構文を使用します。 。その後、コマンドはgamestateにアクセスし、自分自身を解決します。 b)コマンドはクライアントによって作成されますが、解決される前にInitialise(IUnityContainer)
メソッドが呼び出され、Command
にgamestateへのアクセスが許可されます。このようにして、コマンドを発行する準備ができて初めて、gamestateにアクセスできます。もちろん、クライアントは技術的にコマンドを保持し、後日呼び出すことができます。
最善の解決策について何か考えはありますか?私の主な関心事は、Game
の内部状態の整合性を確保しながら簡単に拡張できることです。
ここで完全に安全なソリューションを作成することはできません。コマンドを抽象化して柔軟性を犠牲にするか、柔軟性を高めながらゲーム状態のカプセル化を緩めます。
ここでの問題は、コマンドオブジェクトがゲームの状態を壊さないように信頼できるかどうかです。それらを信頼できない場合は、いくつかの正常なコマンドを効果的にホワイトリストに登録するコマンドファクトリを使用することが望ましい場合があります。ファクトリメソッドは、検証を実行して不正なコマンドを拒否できます。
しかし、ほとんどの場合、自分のコードを信頼できます。物事を成し遂げるためにカプセル化を少し犠牲にすることは問題ありません。たとえば、次のようなデザインがあります。
_interface IGameCommand
{
void Resolve(GameState game);
}
_
次に、Game.Tick()
で:
_foreach (var command in commands)
{
command.Resolve(state);
...
}
_
これには、ゲームの状態で必要な操作とサービスが公開されている必要があります。このゲーム状態オブジェクトは、Game
の通常のパブリックインターフェイスと同じである必要はありません。別のクラスにすることができます。これは、後でコマンドをキューに入れるのではなく、ゲームのメソッドを誤って直接呼び出すことを回避するのに役立つ場合があります。技術的には、これを使用してコマンドの有効期間を超えて参照を保持できますが、これがすべてコードである場合は、単にそうしないでください。
マルチプレイヤーゲームの場合は、セキュリティモデルについて慎重に検討してください。メッセージ指向のソリューションは、メッセージを検証でき、安全な接続を介してメッセージを交換できるため(「安全」の特定の値に対して)、良い考えです。プログラミング言語の型システムはセキュリティの障壁ではありません。信頼できないコードを実行して、個人情報にアクセスできないと想定することはできません。したがって、メッセージにコードを含めることはできません。代わりに、メッセージを動作にマッピングするために説明したようなリゾルバーアプローチを使用する必要があります。