Seamless local and multiplayer, Part 1

While working on multiplayer, I have stumbled over some scenarios that I havn't been thinking about before. Those have led me to change my architecture and probably cleaned up a lot of how parts in my engine connects. And, they have also introduced more overhead for simple things. I can probably live with that.

Server and client at the same time

I want a system that it divided up into two layers: Client and server.

A single instance of the game should be able to run both, otherwise you wouldn't be able to play single player. On the same time, I also wanted to include both server-side and client-side logic into the same classes. I figured this would probably make it a lot easier to work with the logic in one place.

So, instead of just having Start(), Update(GameTime) etc, I decided to add versions that are specific to the side you are on. So Update() became UpdateClient(), UpdateServer() and Update().

    public class PlayerActor
    {
        public void UpdateClient(GameTime time)
        {
            // update position on meshes that have been synced from server
            Mesh.Position = Position;
        }
        public void UpdateServer(GameTime time)
        {
            // send out position to the client
            SendToClient(Position);
        }
        public void Update(GameTime time)
        {
            // do stuff that happens on both layers
        }
    }

Server and client layer

The server layer is responsible for basic game logic. Positioning of things in the physical world, creation of actor, starting actions, etc. Let's say you start a new a new game. A map is loaded and has some different things on it. The player, environment geometry, some enemies, a burning church, etc, etc. Maybe all of this comes from a save where some things are in a different state than it was when you started out that map. The server layer is responsible for loading all of this information and portion it out to the differents systems/components.

Examples

The environment meshes are sent to the physics engine on both the server and client side. On the client side, the meshes are loaded to the GPU so be displayed when they are in view.

The loaded data for actors are sent to the server layer and to the client layer. The server layer creates physics entities and adds them to the AI system. On the client side, physics entities are added to the physics system and meshes are loaded for the actors.

When the game is running, the server layers copies the positions and velocities for the physics objects and sends them to the client layer for synchronization.

Message service / system / component

The server layer has loaded a part of a save and that part describes an actor. Let's call that actor George. Right now, George is just a collection of data, nothing more. The server layer creates a CreateActorMessage and puts all the data it has loaded into it. George is now on his way to existance. Since actors needs to exists on both layers, the CreateActorMessage is sent with MessageService.SendToBoth(georgeMessage).

The ActorComponent on the server layer receives the message and creates an CharacterActor which in turn loads the right behaviours and creates the right physics entity based on the data in the message. 

The client layer, however, is not on the same machine as the server layer is, since this is a multiplayer game. The message service serializes the message and sends it out over TCP to instance of the game running there. On the remote machine, the message service receives the CreateActorMessage and forwards it to the ActorComponent. The ActorComponent parses the message and creates a new CharacterActor based on message. Since the ActorComponent knows that it's client only, it doesn't add the behaviours for George to the AI System. However, it loads the meshes needed to represent George visually.

George doesn't care that he's split up over multiple machines, he's just happy that he's back.

The machine with the server layer might of course have a client layer too. The messages are sent to all client layers that should process them. This can be decided by what the respective client layer (i.e player) can see from their position.

Some things to ponder

First, I decided that all entites in the physics engine should be connected between layers. That started an interesting problem when both layers where running on the same machine. That would send a synchronization message from the physics system to itself, updating an entity with a position/velocity that were fresh a couple of nanoseconds ago.

One solution is to skip sending the message to the local client layer, but still send it to all remote client layers. I.e: SendToClient(message, Destination.RemoteOnly). This is probably the best.

Another solution would be to not run a physics simulation on the client layer and only update the mesh via a message to the actor. Then run some lightweight interpolation of velocity on the actor until a new message arrives. That would give the client machine more computational power to the rendering pipeline.

The roundtrip via a message system adds some extra time to your calls and the messages adds some garbage.