I haven’t blogged in a while for two reasons:

  1. Work is insanely busy
  2. I’ve been either working hard or playing hard in my spare time
But now, for your reading amusement, is another beautiful trainwreck blog with copious amounts of bad code! This post is going to talk about some of the challenges and the pattern I used to implement multiplayer networking in FlatRedBall using the Lidgren Gen 3 networking library. I primarily used these resources, along with the Lidgrin library documentation, to plan my networking pattern:

First of all, if you want to make a networked multiplayer game, do it from the first line of code. My goal with this project, from the very begining, was to make a networked game and it was hard enough. Trying to shoehorn networking into an existing game would be…very difficult. Second, there are a variety of patterns you can implement that work better for different game types. The pattern described in the PDF linked above is great for RTS games but probably wouldn’t work for an action game.

A strange but important thing to realize is: game instances never match across a network! Realizing this was integral to getting the network to work. I had to evaluate what actually matters to the gameplay and send that. Entities that cannot affect a client do not matter and shouldn’t be updated until the server decides they do matter.

So, this is not “the way” to implement networking with FlatRedBall but it is “a way” that seems to be working pretty well. It is a work in progress but it is off to a pretty good start. Read on and comment with questions or concerns.

 

Step 0: The pattern used

I’m using a Client-Server pattern: one server, multiple clients connected. The server is the master record, both sides simulate the environment and exchange minimal data that describes changes to object states on each end without having to send updates for the entire world every frame. If you haven’t read the Unreal article above, do so. It’s a really good overview of how this works. I will be going into some detail about my specific implementation but that article was written by the pros!

Here’s the overview of how my implementation works:

  • Keyboard input state is compressed into a bitmask each update and only sent to the server if the state changes using UnreliableSequenced send mode
  • Every T seconds, the client sends a reckoning input update using ReliableSequenced
  • Every T seconds, the server forces a “dead reckoning” of all interactive game objects (eventually limited to those affecting a specific client)
  • The value of T is set in Glue so it can be tweaked and tested easily, currently I’m using 0.5 seconds for dead reckoning but that may be too frequent
  • When an entity gets a new state via “dead reckoning” it interpolates to the new position instead of jumping straight there. This makes a big difference in perceived smoothness

 

Step 1: Abstracting the Lidgrin Library for FlatRedBall

I don’t want my entire project to depend on Lidgrin. First, I’d like others to be able to use my abstraction. Second, I might update to a newer version of Lidgren, discover another networking library in the future, or otherwise change dependencies. If that happens, I only need to make changes in one place: my adapter between Lidgren and the FlatRedBall engine. My abstraction is (as of this post) three files: a NetworkAgent, an incoming message class, an outgoing message class (links go to actual codez).

The NetworkAgent is the only object instantiated by a Game Screen. It can be instantiated as a Server or a Client. It provides the Game with a unique ID (which actually comes from Lidgren), latency measurement, and ways to send and receive messages. The NetworkAgent’s GetMessages method returns processes the queue of messages, writes any non-data messages to a provided logger, and pushes all data messages into a List of incoming messages and returns the list. The Game Screen calls this method every Update and processes the List returned.

The incoming/outgoing message classes mostly wrap Lidgren message types. However, they can be tidily extended to send more complex objects than Lidgren allows. For instance, I added methods that can send/receive Vector3 objects, which are actually broken into their three discreet float values and packetized.

The NetworkAgent doesn’t care what type of game you make. It doesn’t care what type of messages can be sent (other than Lidgren’s generic “data” type). All of this game-specific stuff should be implemented in your Game Screen. The Network Agent just makes it easy to send/receive messages without having Lidgren dependencies sprinkled all over your project.

 

Step 2: Input Management

Since my game uses keyboard for movement and mouse for aiming, all movement-related input states are boolean: either a key is down or it’s not. Sending this as a bunch of Boolean values or something even heavier would slow down the network. I pack up to eight different key states into a single byte using bit operations. I can also send the mouse position as two floats. Since the mouse only controls the turret rotation, and that’s not really important unless the player is actively firing a weapon, I’m planning to test sending the mouse state only during the input reckoning or when firing and interpolating the tank rotation between these updates.

The important thing here is to understand the chain of events. If a player is holding down a single key, input state does not change until they release it. This type of update is only sent on state change and is sent UnreliableSequenced. This state update is received by the server and forwarded to other clients. However, since it is Unreliable it may not be delivered at all, which would result in the server not realizing the player had released the forward key and keep moving the tank forever. This is why I also need an input reckoning. This forces a reliable update to the server of the current state.

Note that the server also broadcasts its own input updates. And, when it forwards an input update, it forwards it to all clients (including the one that sent it). This means that a client must check the ID of the entity the update is affecting and discard it if it applies to the entity the client owns.

 

This is the Input State object:

// An enum who's values are ready for bitwise ops
[Flags]
public enum InputTypes
{
	None = 0,
	UpPress = 1,
	DownPress = 2,
	LeftPress = 4,
	RightPress = 8,
	FirePress = 16,
	SpecialPress = 32
}

// extension method to make input testing easy!
public static class InputTypeExtensions
{
	public static bool ContainsFlag(this InputTypes flagSource, InputTypes testFlag)
	{
		return ((flagSource & testFlag) == testFlag);
	}
}

 

The Game Screen does this each update cycle to create a new state:

private void HandleInput()
{
	// for readability
	Keyboard kb = InputManager.Keyboard;
	Mouse ms = InputManager.Mouse;
	mCurrentMousePosition = new Vector3(ms.WorldXAt(0), ms.WorldYAt(0), 0);

	// Use bitwise and a ternary to "OR" input states into the object
	mPreviousInputState = mCurrentInputState;
	mCurrentInputState = InputTypes.None;
	mCurrentInputState |= (kb.KeyDown(Keys.W) ? InputTypes.UpPress : InputTypes.None);
	mCurrentInputState |= (kb.KeyDown(Keys.S) ? InputTypes.DownPress : InputTypes.None);
	mCurrentInputState |= (kb.KeyDown(Keys.A) ? InputTypes.LeftPress : InputTypes.None);
	mCurrentInputState |= (kb.KeyDown(Keys.D) ? InputTypes.RightPress : InputTypes.None);
	mCurrentInputState |= (ms.ButtonDown(Mouse.MouseButtons.LeftButton) ? InputTypes.FirePress : InputTypes.None);
	mCurrentInputState |= (ms.ButtonDown(Mouse.MouseButtons.RightButton) ? InputTypes.SpecialPress : InputTypes.None);

	// only send input state if we own an entity
	if (mOwnedTank != null)
	{
		// apply the input state to our owned entity
		mOwnedTank.ChangeInputState(mCurrentInputState, mCurrentMousePosition);

		// send the server an update
		if (mCurrentInputState != mPreviousInputState)
		{
			// false means don't send reliably
			SendInputUpdateMessage(false);
		}
	}
}

 

How the Game Screen sends an input state:

// mInputMessage is an Outgoing Message instance
// mNetwork is a NetworkAgent instance
// note that we also send the entity owner id
private void SendInputUpdateMessage(bool isReliableMessage)
{
	mInputMessage = mNetwork.CreateMessge();
	mInputMessage.Write((byte)NetworkMessageType.InputState);
	mInputMessage.Write(mNetwork.GetId());
	mInputMessage.Write((byte)mCurrentInputState);
	mInputMessage.Write(mCurrentMousePosition);
	mInputMessage.SendToAll(isReliableMessage);
}

 

Whether it comes directly from the Game Screen or via a network message, an input state is handled by an Entity like this (just a snippit):

public void ApplyInputState()
{
	// uses extension method to check input state
	if (CurrentInputState.ContainsFlag(InputTypes.UpPress))
	{
		CurrentSpeed += AccelRate;
		TankTracks.Animate = true;
	}
	// handle other key states here...not shown

	// apply turret rotation
	TankTurret.RelativeRotationZ = (float)Math.Atan2(mTarget.Y - Position.Y, mTarget.X - Position.X) - RotationZ;

	// affect velocity with speed and rotation
	Velocity.X = (float)(Math.Cos(RotationZ) * CurrentSpeed);
	Velocity.Y = (float)(Math.Sin(RotationZ) * CurrentSpeed);

	// interpolate from reckoning (see next section)
	InterpolateReckoningDelta();
}

 

Step 3: Reckoning

If you didn’t read the articles linked at the top and you haven’t figured it out by now: reckoning is the process of forcing clients and server back into sync with each other. A complete override of all client states by the server is called a “dead reckoning.” Input reckoning is not a full-on dead reckoning because it doesn’t reposition or change the entire state of an entity, merely updates it’s input. The entity could still be way out of position. Thus the server needs to force a reckoning on all entities in the game to make sure they are where they should be. This is simple in concept: every N seconds, the server loops through it’s entities and broadcasts a state message for each entity to all clients. The problem is, this state _never_ exactly matches the entity state on the client. Plus, the message will take a bit of time to transfer and be applied. So, even if the entity states exactly match when the server broadcasts a message, they probably won’t by the time the message is received. This means that even with latency close to zero and no packet loss you get jerky performance. There are two ways of mitigating this and neither works perfectly by itself:

Prediction: use the average latency, rotation and velocity to broadcast where the entity should be by the time the message arrives. This helps a little but if the player is actively turning or changing direction it can compound the difference between states.

Interpolation: Instead of immediately forcing the entity to a new position, interpolate the current position into the destination position smoothly. This is more (and less) complicated than it sounds because the entity’s current position and the received reckoning position are a snapshot of a moment in time. These values are actively changing. So you have to interpolate where the entity is going to be given the Client’s state with where the entity is going to be given the Server’s state, while also accounting for new input even as the interpolation happens!

The solution I found (after about 5 tries) was actually really simple in retrospect. I simply take the delta of the current position and the destination position and apply a percentage of the delta each update until there is no more discrepancy. This means that the client and server instances of an Entity do not match but are frequently, smoothly, shifted closer together. Making these shifts smoother makes them much harder to perceive, improving gameplay. If the discrepancy is big enough to cause collision problems with world geometry or enemy fire, the server position ultimately rules. This sucks but it is present in all games, how often have you died in a FPS because the enemy wasn’t actually where you were shooting?

// reckoning difference
Vector3 mPositionDelta;

// called by the client's Game Screen when it receives a reckoning message
public void ForceNewState(Vector3 position, float rotation, float speed, int health, InputTypes input)
{
	// the important line: set the delta
	mPositionDelta = Vector3.Subtract(position, Position);

	RotationZ = rotation;
	CurrentInputState = input;
	CurrentSpeed = speed;
	CurrentHealth = health;
}

// each frame the entity applies its current input state
// after the normal movement is applied this interpolation is called
// Interpolation rate is a global var that all entities can reference
private void InterpolateReckoningDelta()
{
	if (mPositionDelta.X != 0)
	{
		Position.X += mPositionDelta.X * GlobalData.GameData.InterpolationRate;
		mPositionDelta.X -= mPositionDelta.X * GlobalData.GameData.InterpolationRate;
	}

	if (mPositionDelta.Y != 0)
	{
		Position.Y += mPositionDelta.Y * GlobalData.GameData.InterpolationRate;
		mPositionDelta.Y -= mPositionDelta.Y * GlobalData.GameData.InterpolationRate;
	}
	// some clamping could happen here for really small deltas
	// but I have tuned interpolation rate so that it finishes up
	// just in time for the next reckoning!
}

 

Hopefully this has made some level of sense. Currently the game doesn’t implement shooting, collision, etc so there is still much to be done that will likely affect my pattern.

Love or hate? Drop it in a comment!

Update: Forgot to mention one little thing. Entity creation. When the client receives a reckoning update it checks the ID and then searches its list of entities for a matching ID and applies the reckoning to the matching ID. If it doesn’t find an ID it creates a new entity with that ID and applies the reckoning. This results in a slight delay in new entity appearance if you connect to an existing game between reckonings. Lots of ways to improve this with more explicit entity creation requests but I haven’t gotten there yet :)