To allow a consistent way of handling traffic between client and server, we introduced the actions concept. This based on the Command design pattern and makes it quick for us to implement features that requires functionality on both client & server sides while keeping the code elegantly isolated in single-responsibility classes.
All our actions implement the IGameAction interface and will typically inherit, indirectly, from GameAction which is a PropertyHolder and contains some boilerplate code to take care of common tasks. We further derive ClientAction and ServerAction to specialize the actions based on where they will execute. Regardless of the type of action, they will always have an Invocation ID which is a GUID that will be retained throughout the cycle of execution on both client & server. It’s purpose is to allow us to correlate what’s going on different sides of the internet.
Client Actions are always executed via a centralized ActionProcessor that ensures the correct execution cycle takes place.
When a client action is invoked it’s CanInvoke() method is executed first and only if it returns true is the action allowed to execute. If it is allowed to execute then the Start() method is called on the client. This is where initial client-side work can take place. If this method returns true, then the action is serialized and sent to the server. This can return false when an action is intended to only run on the client, typically as a response to an incoming server event.
After the action is sent to the server and a response comes back the Conclude() method will be invoked. This is where we put the code we want to run after we get the response. We have access to the response details here and in some cases the action will only be invoked to query information from the server, so this is the place to consume that information.
The server actions have more complexity under the hood, but its interface is as simple. An Initialize() method is called before anything touches the event. Then as for client actions the CanInvoke() method is checked to decide whether to invoke the action or send an error response and finally the InvokeInternal() method is called where the action specific code executes and a response is returned.
A server action has access to the client device that made started the action (if any) as well as the associated player ID. It also handles authentication and provides a unified way to report errors back to the client.
When the server receives the action request from the client, the ActionHandler will create an instance of the server action by using the ServerActionFactory which uses the incoming info to create the action based on its type. This means that any new action has to be registered in the factory if it will be kicked off from the client.
Since the client and server communicate by sending and receiving messages, we need to ensure that both sides are on the same page. In particular that the properties they refer are exactly the same on either side. For that purpose we use action wrappers. These follow the decorator pattern and inherit from the GameActionDecorator base class. Their main purpose is to define the constant names of the properties and provide accessors and setters for them. The same wrapper files are referenced by both the client and server code, so any change in one will automatically be picked up on the other thus saving us the error prone hassle of tracking these names independently.