Sto sviluppando una piccola partita multiplayer. Sarà servito da un server websockets e consumato da più utenti. In quanto tale, devo essere cauto riguardo agli errori di concorrenza.
L'architettura generale del software che ho creato è: Incapsula tutto lo stato di gioco nella classe Game di livello più alto e non consente ad altri thread di accedere agli oggetti gamestate. Invece, i client (thread Websocket sul server) inviano comandi alla classe Game dove vengono aggiunti a un elenco di thread-safe e successivamente elaborati dal metodo Game.Tick che viene chiamato in un thread separato. Ad esempio:
Game.AddCommand assegna un comando alla lista dei comandi in arrivo:
public void AddCommand(Guid id, ICommand command)
{
lock (_commandsLock)
{
_commands[id] = command;
}
}
Game.Tick elabora ciascun comando e cancella l'elenco:
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);
}
}
Il problema è questo: per risolvere se stessi ogni Command ha bisogno di accedere agli interni di Game . Intendo utilizzare un numero di servizi tale che un metodo Resolve possa essere molto limitato o anche una singola riga, ad esempio movementService.Move(entity, target) . Se avessi trasmesso questi servizi nel metodo Resolve , ogni% diCommand sarebbe diventato gonfio di cose di cui non ha bisogno.
Invece, mi piacerebbe usare DI. Ma poiché ogni Command viene creato da un cliente, non possono accedere ai servizi che appartengono a Game . Questi servizi devono essere esposti, e se il client fa qualcosa di sbagliato (cioè chiama Resolve se stessi), potrebbero cambiare gamestate e causare errori di concorrenza.
Ho cercato di risolvere questo problema creando la nozione di CommandResolver , nel qual caso un Command sarebbe un modello semplice con alcuni parametri e nient'altro, risolto da un CommandResolver interno che ha accesso al gamestate. Ma questo richiedeva una mappatura elaborata (ad es. Map MoveCommand a MoveCommandResolver - ogni comando è un tipo diverso con dipendenze diverse) che sfugge al principio Open / Closed- devi modificare il mapping, probabilmente un lunga dichiarazione di switch awkward, piuttosto che semplicemente estendere la funzionalità con una nuova Command & %codice%.
Ora sto scartando il resolver e considerando una delle due possibilità: a) Esporre da CommandResolver a Game che può essere usato per generare comandi, con una sintassi come CommandFactory . I comandi quindi hanno accesso al gamestate e si risolvono da soli. b) I comandi sono costruiti dai client, ma prima di essere risolti viene chiamato un metodo game.GetCommandFor(entity).Move(target) che dà l'accesso Initialise(IUnityContainer) a gamestate. In questo modo ottiene l'accesso al gamestate solo quando il comando è pronto per essere rilasciato. Naturalmente, il client potrebbe ancora mantenere tecnicamente il comando e chiamarlo in un secondo momento.
Qualche idea su quale sia la soluzione migliore? Le mie preoccupazioni principali sono la facile estensibilità e l'integrità dello stato interno di Command .