This is part 3 of the multipart series Trap Labs Code Design and Architecture Series
I know many of you have said in the past “Hey! I’ll just make my game single player first (because it’s easy), and if people like it then I’ll add multiplayer!” Let me just repeat something from part 1 upfront, no, you cannot convert a single player game to multiplayer without a rewrite. Yes, I’m sure it has been done before, but I’m also sure your code will end up being a jumbled unmaintainable mess that’ll kill your development velocity going forward. If you want to support multiplayer in the future, you should design it as multiplayer to begin with.
What I’m about to show you is a super simple general purpose multiplayer command engine that I built for Trap Labs. My implementation is only about 600 lines of code long and it handles arbitrary number of clients. I will go over the design and architecture of this module and hopefully you’ll be able to spin your own implementation.
“In object-oriented programming, the command pattern is a behavioral design pattern in which an object is used to encapsulate all information needed to perform an action or trigger an event at a later time.” I encourage you to familiarize yourself with the Command Pattern as it is one of the most useful design patterns around. This pattern serves as the underlining driver of this multiplayer engine.
The primary advantage the command pattern is that it allows you to separate the behavior from its execution. For a video game you may have different commands that executes a certain action for a game element. A great example is MMO where character have myriad of skills and ability. Each time the player executes a skill it’s a different command. Every time a command is issued it can be put into a queue and executed later in some fashion (i.e. skill 1 must execute before skill 2). This essentially forces you to write the code for the skills and ability independently of their time of execution, which improves the code overall maintainability and testability.
Secondly, for a networked multiplayer game (depending your networking model), you’ll have to process data from multiple clients in some sort of ordered fashion. Because of the network latency you may receive data out of sync thus you need some way to buffer up the data and sequence them as they arrive, and then execute them in sequence. This will also allow you to record game command transactions for replays and debugging if you so choose to add this feature.
The advantages for using command pattern goes on and on, and I personally would recommend you to use this pattern for any software that can benefit from this pattern. Even if you are just making a single client software, using the pattern will make your life a lot easier if you choose to extend your software to multi-client, as you’ll about to see.
Here are the basic components to a command engine for single client:
Here is the breakdown:
SequencedCommand
vector. Alternatively, you may have commands that needs to be deferred until some condition is met, then you may wish to have a DeferredCommand
vector as well.CommandManager
and executes available commands. For example, calling Execute()
on CommandExecutor
would execute all SequencedCommands
from the CommandManager
, then remove or archive them from the CommandManager
. You can extend the logic further if you wish to use other execution models.CommandID
that uses some kind of identifier (like a string or enum) which can be used to create executable command objects.CommandManager
. The reason it’s abstract is because the IncomingDataQueue
’s implementation will vary depending on YourGame, so you’ll need to override the IncomingDataQueue
’s processing method depending on your style of incoming data. This ensure that the command engine is agnostic to the type of commands you are trying to process.Dependency Inversion Principle
I do want to take a moment and talk about the Dependency Inversion Principle, which states that high level modules should depend on abstraction, not detail/low level modules. Take a look at the boundary of LibCommand in the diagram above, notice
YourGame
is highly dependent on abstract classes. Abstract classes and interfaces are generally considered stable, because they are designed to not change. In order to add behavior to LibCommand, you must implement theCommandScheduler
andCommandFactory
. IfYourGame
was directly dependent on these implementations then everytime you change the command format or add/remove a command from the factory thenYourGame
is affected. Having these abstract classes allows the dependency to be inverted and thusYourGame
is not longer dependent on the concrete implementations of LibCommand.Another somewhat related note is on the stability of a module. If a module is said to be stable, it means it’s not likely to change. As noted above, interfaces and abstract classes are by nature highly stable. Examples of modules that aren’t likely to change may be certain data structures that you write once and use forever (like a Point/Vector class), or external libraries that are designed to have same long term API. A good rule to follow is to make sure that the module is stable when the dependency on it is on boundaries or very low on the dependency hierarchy (meaning many modules are dependent on it).
Lastly the code to get them all work together would simply something like this inside a game loop:
while (running)
{
...
//Schedule available data from the IncomingDataQueue
commandScheduler.Update();
//Execute all available commands
commandExecutor.Execute();
...
}
Really simple am I right? Not only is this reusable, it will work with any client/server that uses similar command model. For Trap Labs I use this as the basis for both the game engine and the lobby system.
Making a multiplayer command engine is simply scaling up the single player command engine. Basically for every component above, you’d have a multi-client counterpart that is composed of its singular part.
MultiClientCommandManager
is simply a composition of many CommandManager
. This means you’ll end up with a MultiClientCommandExecutor
, MultiClientCommandScheduler
etc. Depending on how you identify different clients (such as using a ClientID
), the real difference on the MultiClient
* version is to delegate work appropriately based on the client identifier. For example, the difference between the single-client vs multi-client function calls:
commandManager.InsertSequencedCommand(command);
multiClientCommandManager.InsertSequencedCommand(clientID, command);
The implementation should be fairly trivial and I’ll leave it up to you to figure it out. The only other thing I’ll add is that it may also be advantageous to add something like a master command executor that executes commands on all clients at the same time, or commands that are not client specific.
You’ll notice that if you implement the multiplayer command engine this way you’ll have clear separation of command handling for each client. In addition, if you choose to record game states through each command, you’ll have full replay-ability for all clients (together or individually). This is extremely powerful as a game feature or debugging assistance.
If you get this part right about your game, you’ll find that it makes it a lot easier to make decoupling decisions about the modules of your game. Every time you add a new command you must put that new feature into its own class which almost ensures testability due to the command pattern’s decoupled nature. So even if you are making a single player game, I would still encourage you to considering using this pattern.