Verzögerungen in Online-Spielen durch Client-Side Prediction kompensieren

Seite 2: Bewegungssimulation

Inhaltsverzeichnis

Aus den Änderungen ergeben sich allerdings einige neue Umstände:

  1. Der Client zeigt die Figur des Spielers in der Zukunft, da die Eingaben direkt angewandt werden.
  2. Der Client muss die Bewegungssimulation ausführen können und Ergebnisse erzeugen, die möglichst identisch zu denen auf dem Server sind.
  3. Die Zeit, die die Eingabedaten zum Server brauchen, kann schwanken.

Punkt 3 lässt sich technisch einfach beheben, wenn die Client-Side Prediction bereits von Beginn an eingeplant wurde. Dazu muss die Bewegungssimulation folgende Voraussetzungen erfüllen:

  • Die Bewegung wird ausschließlich von den Eingabedaten des Spielers gesteuert.
  • Der gesamte Bewegungszustand (z.B. Position, Rotation, Geschwindigkeit,...) ist in einer Datenstruktur gekapselt.
  • Die Bewegungssimulation ist deterministisch bei gleichem initialen Zustand und gleichen Eingabedaten.

Der letzte Punkt lässt sich dadurch erreichen, dass sich Client und Server eine Codebasis teilen. Die beiden anderen betreffen den Aufbau der zustandslosen Simulation, die für ein gegebenes Paar an Bewegungszustand und Eingabedaten einen neuen Bewegungszustand ermittelt:

MoveState TickSimulation(MoveState state, 
Input input, float deltaTime);

Anhand einer einfachen Bewegungssimulation sollte der Implementierungsaufbau klar werden. Erstellt hat das Beispiel Gabriel Gambetta, der in seiner Live Demo den Aufbau auf das Wesentliche beschränkt. Die Spielfigur hat in ihr lediglich die Möglichkeit, sich auf der Horizontalen nach links oder rechts zu bewegen. Selbst eine Beschleunigung besitzt sie nicht, stattdessen bewegt sie sich sofort mit voller Geschwindigkeit in die gewünschte Richtung. Der MoveState besteht somit nur aus der x-Position und einer festgelegten Geschwindigkeit:

public class MoveState
{
// Position.
public float X;
}

In einer komplexeren Simulation hätte der Bewegungszustand höchstwahrscheinlich neben einer Position in weiteren Dimensionen noch eine derzeitige Geschwindigkeit sowie eine Rotation in einer oder mehreren Dimensionen.

Die Eingabedaten sind durch die eingeschränkten Bewegungsmöglichkeiten ähnlich einfach.

public class Input
{
// -1 (left), 0, 1 (right)
public int HorizontalAxis;
}

Danach fehlt noch die Simulation, die lediglich die Position anhand der Eingabedaten und der verstrichenen Zeit verändert.

public class MovePhysics
{
// Fixed speed if moved.
public const float Speed = 2.0f;

public MoveState TickSimulation(MoveState state, Input input,
float deltaTime)
{
MoveState newState = new MoveState();
newState.X = state.X + deltaTime * input.HorizontalAxis *
this.Speed;
return newState;
}
}

Ohne Client-Side Prediction holt der Client zunächst regelmäßig die Eingabedaten und schickt sie an den Server. Dabei vergibt er eine InputNumber, die es ermöglicht, die einzelen Eingabedaten eindeutig zu identifizieren:

public class ClientInput
{
public int InputNumber;

public Input Input;
}

public class Client
{
private int inputCounter;

public int ClientId { get; set; }

public void FixedUpdate(float deltaTime)
{
// Get input.
Input input = this.FetchInput();

// Send to server.
ClientInput clientInput = new ClientInput
{
InputNumber = this.inputCounter++,
Input = input,
ClientId = this.ClientId
};
this.network.SendToServer(clientInput);
}
}

Der Server empfängt die Eingabedaten seiner Clients und wendet sie auf die Figuren in der Spielwelt an. Außerdem übermittelt er den Zustand der Spielwelt regelmäßig an die Clients:

public class ConnectedClient
{
public MoveState MoveState { get; set; }

public int ClientId { get; set; }

public Input Input { get; set; }

public int InputNumber { get; set; }
}

public class Server
{
private readonly List<ConnectedClient> clients = new
List<ConnectedClient>();

public void OnClientInputReceived(ClientInput clientInput)
{
// Get client this input is for.
ConnectedClient client =
this.clients.FirstOrDefault(connectedClient =>
connectedClient.ClientId == clientInput.ClientId);
if (client == null)
{
return;
}
client.Input = clientInput.Input;
client.InputNumber = clientInput.InputNumber;
}

public void UpdateWorld(float updateInterval)
{
// Update all clients.
foreach (var client in this.clients)
{
client.MoveState = this.physics.TickSimulation(
client.MoveState, client.Input, updateInterval);
}

// Send world state to clients.
this.SendWorldState();
}
}