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
.