Tutorials:Beefball:Client-side Prediction

From Docs
Jump to: navigation, search

<- Back to Beefball Tutorials

Contents

Introduction

As you may have noticed, the Playerballs jitter back and forth when moving quickly, even while playing over 127.0.0.1. This is caused by the latency between the Server and the Client. There are many ways to help remedy the twitching, some harder than others. For this tutorial, we will do a simple version of Client-side prediction, which will help with most of the lag.

The goal of Client-side prediction is to make the Client appear to be updating in realtime, when in reality, they are a few milliseconds behind. The Client will predict what it thinks the Server will do, then update the positioning if the Client is wrong.

Client-side Collision and Control

Our next step is to let the Client predict the outcome of it's own movement, which involves the collision that is usually done by the Server. Lets change the Client to do so.

In GameScreen, change MovementActivity to

private void MovementActivity()
{
	if (GlobalData.GlobalData.GameData.TypeOfGame == GlobalData.GameData.GameType.Server || GlobalData.GlobalData.GameData.TypeOfGame == GlobalData.GameData.GameType.Client)
	{
		PlayerBallList[PlayerBallID].XAcceleration = PlayerBallList[PlayerBallID].GamePad.LeftStick.Position.X * PlayerBallList[PlayerBallID].MovementSpeed;
		PlayerBallList[PlayerBallID].YAcceleration = PlayerBallList[PlayerBallID].GamePad.LeftStick.Position.Y * PlayerBallList[PlayerBallID].MovementSpeed;
	}
	else
	{
		for (int i = 0; i < PlayerBallList.Count; i++)
		{
			PlayerBallList[i].XAcceleration = PlayerBallList[i].GamePad.LeftStick.Position.X * PlayerBallList[i].MovementSpeed;
			PlayerBallList[i].YAcceleration = PlayerBallList[i].GamePad.LeftStick.Position.Y * PlayerBallList[i].MovementSpeed;
		}
	}
}

The Client now changes their own acceleration, instead of setting the CurrentXAcceleration and CurrentYAcceleration.

Now go into PlayerBall.cs and remove

public float CurrentXAcceleration
{
        get;
        set;
}
public float CurrentYAcceleration
{
        get;
        set;
}

Now go back to GameScreen.cs . In ClientActivity(), change

mAgent.WriteMessage(PlayerBallList[PlayerBallID].CurrentXAcceleration);
mAgent.WriteMessage(PlayerBallList[PlayerBallID].CurrentYAcceleration);

to

mAgent.WriteMessage(PlayerBallList[PlayerBallID].XAcceleration);
mAgent.WriteMessage(PlayerBallList[PlayerBallID].YAcceleration);

In ClientActivity(), under

mAgent.SendMessage();

add

//PHYSICS AND OTHER LOGIC FOR PREDICTION
CollisionActivity();

Now the Client predicts its own movement and the collision, but it can't yet predict the movement of the puck or second player. We will add this a bit later.

Fixing Up the Server

We now need to make the Server more efficient. Right now, the server sends each objects position to the other player 60 times a second (60FPS). This is not needed. We can easily send data at 20 – 30 times a second and have the prediction fill in the blanks.

At the top of GameScreen, with the rest of the variables, add

// schedule initial sending of position updates
double nextSendUpdates;

At the bottom of CustomInitialize(), add

nextSendUpdates = NetTime.Now;

In ServerActivity(), above

foreach (NetConnection player in mAgent.Connections)

Add

//Check to send updates (30FPS)
double now = NetTime.Now;

And encompass the whole foreach statement with

if (now > nextSendUpdates)
{
	foreach (NetConnection player in mAgent.Connections)
	{
		…
	}

	//Schedule next update, 20FPS
	nextSendUpdates += (1.0 / 20.0);
}

This limits the rate at which we send the messages to the client to 20 times a second.

Preventing the Twitching

Now the Client predicts it's movement and the Server only sends out the update a limited amount of times a second. But the twitching is still there. After further investigation, by writing the positions to a log, you may find that the Client is receiving positions from a few frames ago. We need to prevent the Client from consuming any already predicted messages.

A simple way of doing this is to keep a list of past predicted values and compare all received messages. To do this, we need a list of Vector2 objects that hold the past X and Y values for each PlayerBall and Puck.

Before we make the lists, lets make the Client predict the other player and puck. The Client needs the Velocity and Acceleration to effectively predict.

Find the following in ServerActivity()

mAgent.WriteMessage((byte)MessageType.Player1);
mAgent.WriteMessage(PlayerBallList[(byte)MessageType.Player1].X);
mAgent.WriteMessage(PlayerBallList[(byte)MessageType.Player1].Y);

And put this below it

mAgent.WriteMessage(PlayerBallList[(byte)MessageType.Player1].XVelocity);
mAgent.WriteMessage(PlayerBallList[(byte)MessageType.Player1].YVelocity);
mAgent.WriteMessage(PlayerBallList[(byte)MessageType.Player1].XAcceleration);
mAgent.WriteMessage(PlayerBallList[(byte)MessageType.Player1].YAcceleration);

This sends the velocity and acceleration to the Client. The Player2, who is the client, does not need to know the acceleration, as it is overwritten soon after, but it may need to the the current velocity. Now add these below sending the X and Y positions for Player2.

mAgent.WriteMessage(PlayerBallList[(byte)MessageType.Player2].XVelocity);
mAgent.WriteMessage(PlayerBallList[(byte)MessageType.Player2].YVelocity);

And Puck, below sending the X and Y positions

mAgent.WriteMessage(PuckInstance.XVelocity);
mAgent.WriteMessage(PuckInstance.YVelocity);
mAgent.WriteMessage(PuckInstance.XAcceleration);
mAgent.WriteMessage(PuckInstance.YAcceleration);

Now we need the Client to receive it. But first, lets add the lists.

Add these to the top of GameScreen with the rest of the variables

List<Vector2> mPlayer1PreviousPositions;
List<Vector2> mPlayer2PreviousPositions;
List<Vector2> mPuckPreviousPositions;

And these to the CustomInitialize()

mPlayer1PreviousPositions = new List<Vector2>();
mPlayer2PreviousPositions = new List<Vector2>();
mPuckPreviousPositions = new List<Vector2>();

Next, add this to the top of ClientActivity()

//Remove the 10th object on the lists, and add the current positions
if (mPlayer1PreviousPositions.Count >= 10)
{
	mPlayer1PreviousPositions.RemoveAt(0);
}
mPlayer1PreviousPositions.Add(new Vector2(PlayerBallList[(byte)MessageType.Player1].Position.X, PlayerBallList[(byte)MessageType.Player1].Position.Y));

if (mPlayer2PreviousPositions.Count >= 10)
{
	mPlayer2PreviousPositions.RemoveAt(0);
}
mPlayer2PreviousPositions.Add(new Vector2(PlayerBallList[(byte)MessageType.Player2].Position.X, PlayerBallList[(byte)MessageType.Player2].Position.Y));
			
if (mPuckPreviousPositions.Count >= 10)
{
	mPuckPreviousPositions.RemoveAt(0);
}
mPuckPreviousPositions.Add(new Vector2(PuckInstance.Position.X, PuckInstance.Position.Y));

These update the list and remove the oldest item from it if there are more than 10 on the list already. You may want to keep more positions on the list, but don't go overboard. The lists are ready, lets change how the Client receives the messages.

In ClientActivity(), below

byte type = incomingMessage.ReadByte();

Add

float x;
float y;
float xVel;
float yVel;
float xAcc;
float yAcc;
bool previous = false;

These variables will help us clear up what we are comparing exactly.

In ClientActivity, change the MessageType.Player1 case to

x = incomingMessage.ReadFloat();
y = incomingMessage.ReadFloat();
xVel = incomingMessage.ReadFloat();
yVel = incomingMessage.ReadFloat();
xAcc = incomingMessage.ReadFloat();
yAcc = incomingMessage.ReadFloat();

foreach (Vector2 pos in mPlayer1PreviousPositions)
{
	if (x == pos.X && y == pos.Y)
	{
		previous = true;
		break;
	}
}
if (!previous)
{
	PlayerBallList[(byte)MessageType.Player1].X = x;
	PlayerBallList[(byte)MessageType.Player1].Y = y;
	PlayerBallList[(byte)MessageType.Player1].XVelocity = xVel;
	PlayerBallList[(byte)MessageType.Player1].YVelocity = yVel;
	PlayerBallList[(byte)MessageType.Player1].XAcceleration = xAcc;
	PlayerBallList[(byte)MessageType.Player1].YAcceleration = yAcc;
}
PlayerBallList[(byte)MessageType.Player1].CooldownCircleRadius = incomingMessage.ReadFloat();
PlayerBallList[(byte)MessageType.Player1].DashPressed = incomingMessage.ReadBoolean();

This first consumes the first part of the message, then checks the value against the stored values in the list. If they are the same, set previous to true, and break. If they were not a previous value, update Player1.

For the Player2 case, change it to

x = incomingMessage.ReadFloat();
y = incomingMessage.ReadFloat();
xVel = incomingMessage.ReadFloat();
yVel = incomingMessage.ReadFloat();

foreach (Vector2 pos in mPlayer2PreviousPositions)
{
	if (x == pos.X && y == pos.Y)
	{
		previous = true;
		break;
	}
}

if (!previous)
{
	PlayerBallList[(byte)MessageType.Player2].X = x;
	PlayerBallList[(byte)MessageType.Player2].Y = y;
	PlayerBallList[(byte)MessageType.Player2].XVelocity = xVel;
	PlayerBallList[(byte)MessageType.Player2].YVelocity = yVel;
}
PlayerBallList[(byte)MessageType.Player2].CooldownCircleRadius = incomingMessage.ReadFloat();
PlayerBallList[(byte)MessageType.Player2].DashPressed = incomingMessage.ReadBoolean();

And the Puck

x = incomingMessage.ReadFloat();
y = incomingMessage.ReadFloat();
xVel = incomingMessage.ReadFloat();
yVel = incomingMessage.ReadFloat();
xAcc = incomingMessage.ReadFloat();
yAcc = incomingMessage.ReadFloat();

foreach (Vector2 pos in mPuckPreviousPositions)
{
	if (x == pos.X && y == pos.Y)
	{
		previous = true;
		break;
	}
}

if (!previous)
{
	PuckInstance.X = x;
	PuckInstance.Y = y;
	PuckInstance.XVelocity = xVel;
	PuckInstance.YVelocity = yVel;
	PuckInstance.XAcceleration = xAcc;
	PuckInstance.YAcceleration = yAcc;
}

Test it! It should have cleared up most of the jittering, but there still may be some. This is because we are using a very simple version of prediction. You can try to save more than 10 frames back, but don't go too high.

Conclusion

This simple prediction should help out quite a bit. In order to clear it up even more, we would need to do more advanced Client-side prediction. We may write tutorials about advanced prediction, but if you find out a good technique that works for you, let us know on the forum so we can add it to this tutorial!

<- Waiting for Player

Personal tools
Namespaces
Variants
Actions
Navigation
Toolbox