I'm developing a small multiplayer game. It'll be served by one websockets server and consumed by multiple consumers. As such I need to be cautious about concurrency errors.
The general software architecture I've come up with is: Encapsulate all game state into the highest-level Game
class and don't allow other threads to access gamestate objects. Instead, clients (websocket threads on the server) send commands to the Game
class where they're added to a threadsafe list and later processed by the Game.Tick
method which is called in a separate thread. For example:
Game.AddCommand
assigns a command to the list of incoming commands:
public void AddCommand(Guid id, ICommand command)
{
lock (_commandsLock)
{
_commands[id] = command;
}
}
Game.Tick
processes each command and clears the list:
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);
}
}
The issue is this: To resolve themselves each Command
needs access to the internals of Game
. I intend to use a number of services such that a Resolve
method can be very few or even a single line, such as movementService.Move(entity, target)
. If I passed these services into the Resolve
method, each Command
would become bloated with things that it doesn't need.
Instead, I'd like to use DI. But since each Command
is created by a client, they can't access the services that belong to Game
. These services need to be exposed, and if the client does something wrong (ie call Resolve
themselves) they could change gamestate and cause concurrency errors.
I tried to resolve this by creating the notion of a CommandResolver
, in which case a Command
would be a simple model with some parameters and nothing more, resolved by an internal CommandResolver
which has access to gamestate. But this required elaborate mapping (eg map MoveCommand
to MoveCommandResolver
- each command is a different type with different dependencies) which defies the Open/Closed principle- you have to modify the mapping, likely a long awkward switch statement, rather than simply extending the functionality with a new Command
& CommandResolver
.
Now I'm scrapping the resolver and considering one of two possibilities: a) Expose from Game
a CommandFactory
that can be used to generate commands, prehaps with a syntax like game.GetCommandFor(entity).Move(target)
. The commands then have access to gamestate and resolve themselves. b) Commands are constructed by clients, but before being resolved an Initialise(IUnityContainer)
method is called that gives the Command
access to gamestate. This way it only gains access to gamestate once the command is ready to be issued. Of course, the client could still technically hold onto the command and call it at a later date.
Any thoughts on what the best solution is? My primary concerns are easy extensibility while ensuring the integrity of Game
's internal state.