I'm trying to add multiplayer to my XNA platform game, and have just failed miserably.
The idea behind my method is that the client knows what tick the server is at, and knows how long it takes to get to the server and back. It sends it's input and position to the server with how long ago the input was put in, and then the server adds it to the list.
Each server tick, the server goes through it's list of updates and sends each player the most relevant input. Then when the client receives the update, it uses the information of how long it took for the player to send it to the server, plus how long it takes to get from the server. It uses this Tick amount to run the information through the player's update, and get a position.
Then it should look at where he is and where the server says he is, and average it out.
It sounds right in my head, but my execution went terribly wrong, and I don't like code dumping but this is a last resort: the important code is near the bottom in UpdateNetwork(), but I added some other parts that you can reference to if you need to
/// <summary>
/// Allows the game to run logic such as updating the world,
/// checking for collisions, gathering input, and playing audio.
/// </summary>
/// <param name="gameTime">Provides a snapshot of timing values.</param>
public override void Update(GameTime gameTime)
{
keybState = Keyboard.GetState();
//Keys are stored in enums which get translated
//to a string I can pass over
playerlist[multi.id].GetKeys(keybState);
//Universal commands (respawn, quit, etc)
globalKeyboard();
//Each player, run through them updated
for (int i = 0; i < playerlist.Count; i++)
{
Update_Player(i, 0);
Update_Flags(i);
Update_General(i);
}
//update network processing center
UpdateNetwork();
oldkeybState = keybState;
//timer class, update based on
//gameTime's milliseconds
Trialtime.Count(gameTime);
Update_Camera();
Tick++;
//ping
Connection.clientSend("PING@" + Tick);
//predicted servertime
if (serveroffset < 0)
{
offset.Add(offset.getAvgTick() - serveroffset);
}
else if (serveroffset > 4)
{
offset.Add(serveroffset - 1);
}
else
{
offset.Add(serveroffset);
}
//random variables to display to scree
int predict = ServerTick + latency.getAvgTick() + offset.getAvgTick();
packin.Add(latency.getAvgTick() / 2 + offset.getAvgTick());
packout = packin.getAvgTick() - offset.getAvgTick() * 2;
//Sends position to the server, plus predict
SendPosition(playerlist[multi.id].position, predict);
//sends the predicted server time, which the server then calculates
// if the guess is too high or too low
Connection.clientSend("PREDICT@" + predict.ToString());
base.Update(gameTime);
}
/// <summary>
/// First thing that happens is the player updates his client position
/// </summary>
/// <param name="id"></param>
/// <param name="tickloop"></param>
void Update_Player(int id, int tickloop)
{
//If the sprintTimer is initialized,
//update the timer
if (sprintTimer > 0)
{
SprintTime(id);
}
//Checks player state and reacts
//accordingly
for (int i = 0; i <= tickloop; i++)
{
if (playerlist[id].state == DEAD)
{
deathTimer(id);
}
else if (playerlist[id].state == GROUND)
{
Control_Keyboard_Ground(id);
Control_Update_Ground(id);
}
else if (playerlist[id].state == AIR)
{
Control_Keyboard_Air(id);
Control_Update_Air(id);
}
else if (playerlist[id].state == WALL)
{
Control_Keyboard_Wall(id);
Control_Update_Wall(id);
}
}
}
/// <summary>
/// Here's an example of how the informationn is processed
///
/// </summary>
private void Control_Keyboard_Air(int id)
{
GamePadState currentState = GamePad.GetState(PlayerIndex.One);
Vector2 ts = currentState.ThumbSticks.Left;
//First Check if right is held down
if (playerlist[id].KeysDown[(int)KeyType.Right] == 1)
{
List<blockClass> Details = Collision(new Rectangle((int)playerlist[id].position.X + 28, (int)playerlist[id].position.Y,
20, 20), id);
if (Details.Count == 0)
{
if (playerlist[id].velocity < 6)
playerlist[id].velocity += .25f;
playerlist[id].orientation = SpriteEffects.None;
}
}
if (playerlist[id].KeysDown[(int)KeyType.Left] == 1)
{
List<blockClass> Details = Collision(new Rectangle((int)playerlist[id].position.X + -18, (int)playerlist[id].position.Y,
20, 20), id);
if (Details.Count == 0)
{
if (playerlist[id].velocity > -6)
playerlist[id].velocity -= .25f;
playerlist[id].orientation = SpriteEffects.FlipHorizontally;
}
}
}
///<summary>
///Process for analyzing collisions mid air
///--Ground for gravity
///--Wall
/// </summary>
void Control_Update_Air(int id)
{
//update animation
if (playerlist[id].prevstate != AIR)
{
playerlist[id].prevstate = AIR;
playerlist[id].animJump();
}
if (playerlist[id].gravity < 0)
{
//p_type is a variable that initiates a boost
if (p_type == 1 && playerlist[id].gravity < -3)
{
createBoost(playerlist[id].position, Color.DarkRed);
}
//Check collision above, if you hit a ceiling then stop
List<blockClass> Details = Collision(new Rectangle((int)playerlist[id].position.X + 8, (int)playerlist[id].position.Y - 10,
24, 20), id);
if (Details.Count > 0)
{
playerlist[id].gravity = 0;
p_type = 0;
}
}
//Update players vertical position
playerlist[id].position += up * playerlist[id].gravity;
//update gravity
playerlist[id].gravity += 0.3f;
//If player is moving right
if (playerlist[id].velocity > 0)
{
//Check collision to the right
List<blockClass> Details = Collision(new Rectangle((int)playerlist[id].position.X + 28, (int)playerlist[id].position.Y,
20, 20), id);
//If there is a collision
if (Details.Count > 0)
{
//and one of the blocks is type WALL
if (Details[0].type == 2)
{
//Set the things to initiate wall controls
playerlist[id].position.X = Details[0].position.X - 30;
playerlist[id].gravity = 0;
playerlist[id].speed = playerlist[id].velocity;
playerlist[id].velocity = 0;
playerlist[id].state = WALL;
playerlist[id].orientation = SpriteEffects.None;
p_type = 0;
}
}
else
{
//if there is no collision, move right
playerlist[id].position += right * playerlist[id].velocity;
}
} //if player is moving left
else if (playerlist[id].velocity < 0)
{
List<blockClass> Details = Collision(new Rectangle((int)playerlist[id].position.X + -18, (int)playerlist[id].position.Y,
20, 20), id);
if (Details.Count > 0)
{
if (Details[0].type == 2)
{
playerlist[id].position.X = Details[0].position.X + 28;
playerlist[id].speed = -playerlist[id].velocity;
playerlist[id].gravity = 0;
playerlist[id].velocity = 0;
playerlist[id].state = WALL;
playerlist[id].orientation = SpriteEffects.FlipHorizontally;
outputDebug = "WALL LEFT";
p_type = 0;
}
else
{
outputDebug = "wall type fail";
}
}
else
{
//position
playerlist[id].position += right * playerlist[id].velocity;
}
}
//Now check for ground
List<blockClass> cb = Collision(new Rectangle((int)playerlist[id].position.X, (int)playerlist[id].position.Y + 30,
32, 20), id);
if (cb.Count > 0)
{
if (cb[0].type == 1 || cb[0].type == 3)
{
p_type = 0;
float displace = 27 - (cb[0].position.Y - playerlist[id].position.Y);
playerlist[id].position.Y -= displace;
playerlist[id].state = GROUND;
}
}
}
/// <summary>
/// Now onto the ugly part that's messing everything up.
///
/// </summary>
public void UpdateNetwork()
{
//Run the networking input
Connection.GetMessage();
//incoming is a list<String> of all the messages
//in the network buffer
if (Connection.incoming.Count == 0)
{
//does nothing
//correctPosition();
}
else
{
//For each message
for (int i = 0; i < Connection.incoming.Count; i++)
{
//I'm using strings in this form:
//COMMAND@INFORMATION
String[] convert = Connection.incoming[i].Split('@');
//if the command is "UP" which is UDPATE
if (convert[0] == "UP")
{
update = true;
//0 is UP
//1 is the server's TICK
//2 is all the players information
//this checks and sets what string
//is the latest one
if (seconds < Int32.Parse(convert[1]))
{
seconds = Int32.Parse(convert[1]);
packet = i;
update_str = convert;
}
}
else if (convert[0] == "TICK")
{
if (seconds < Int32.Parse(convert[1]))
{
seconds = Int32.Parse(convert[1]);
tick = true;
}
} //gets the latency round about
else if (convert[0] == "PING")
{
latency.Add(Tick - Int32.Parse(convert[1]));
serveroffset = Int32.Parse(convert[2]);
}
Connection.incoming.Remove(Connection.incoming[i]);
}
if (tick)
{
snapshots.Add(seconds - ServerTick);
ServerTick = seconds;
tick = false;
}
//Here's the part that's NEW:
//vvvvvvvvvvvvvvvvvvvvvvvvv
if (update)
{
if (update_str != null)
{
String[] data = update_str[2].Split('$');
//0 = id
//1 = predicted tick
//2 = avg latency tick
//3 = position X
//4 = position Y
//5 = keystrokes
int id = Int32.Parse(data[0]);
float x = float.Parse(data[1]);
float y = float.Parse(data[2]);
String strokes = data[3];
//last position
playerlist[id].clientPosition = playerlist[id].position;
//set the players keys to what the server says
playerlist[id].SetKeys(strokes);
//reset the position to where it was at the server
playerlist[id].position = new Vector2(x, y);
//run the player through the Update through the amount of ticks
//it took to get through server and to player
Update_Player(id, latency.getAvgTick());
//If the distance is too big, try to average it out
if (Vector2.Distance(playerlist[id].position, playerlist[id].clientPosition) > 2) {
playerlist[id].position = new Vector2(playerlist[id].position.X + (playerlist[id].position.X - playerlist[id].clientPosition.X) / 2,
playerlist[id].position.Y + (playerlist[id].position.Y - playerlist[id].clientPosition.Y) / 2);
}
}
update = false;
}
}
}
The result is the player flickering back and forth like a madman