Predicted Command Manager
How to utilize the predicted command manager to easily create server authoritative commands with client prediction.
Basic Concepts
Commands
Predicted Command Manager
Example
Command
import { Keyboard } from "../../UserInput";
import PredictedCustomCommand from "./PredictedCustomCommand";
// The input data for this command. This is added to every character input command.
// We use arrays for these since it's less data to send over the network
type InputCommand = [charging: boolean, completed: boolean];
// The state data associated with this command. We network the progress so that if the client and
// server disagree on how much progress has been made, the client will reconcile to use the server
// progress.
type StateData = [progress: number];
// We define our predicted command by extending PredictedCustomCommand and passing in our data types.
// We will register this predicted command with the PredictedCommandManager later. Keeping mind, this is
// NOT an AirshipBehavior. You don't need to add it to a game object.
export class TestPredictedCommand extends PredictedCustomCommand<InputCommand, StateData> {
// state
private progress = 0; // How far along we are in the charge process.
private completed = false;
// constants
private CHARGE_TIME_SEC = 1; // How long we want to charge for.
// This function is called every time a new instance of this command is created. On replays, this command
// might be destroyed and recreated many time. Keep that in mind since any state you include (like progress
// above) will be reset. ResetToSnapshot is always called after Create(), so you should load any state you
// need from the snapshot there.
Create(): void {
// Slow the character movement speed down while this command is running.
this.character.movement.movementSettings.speed *= 0.1;
}
Destroy(): void {
// Reset the character movement speed back to normal when it ends.
this.character.movement.movementSettings.speed /= 0.1;
}
// Called when the client is generating a new command. This is where you collect any data you
// need to process in OnTick(). Remember that this is being generated on the client, so you should
// pass things like input here.
GetCommand(): false | InputCommand {
// If the player is holding down the ability button, we continue to charge the ability.
if (Airship.Input.IsDown("Ability")) {
return [true]; // Send true since the ability is being held down.
}
return false; // If the ability is not being held down, we just exit the command commpletely by returning false.
}
// Runs on both the client and the server either before (default) or after character movement is processed depending
// on the configuration provided when registering the command with the PredictedCommandManager. We can access
// the data we generated in GetCommand() and perform our logic based on that data.
override OnTick(input: Readonly<InputCommand> | undefined, replay: boolean, fullInput: CharacterInputData) {
if (!input) return false;
this.progress++;
if (this.progress >= this.CHARGE_TIME_SEC / Time.fixedDeltaTime) {
this.character.movement.AddImpulse(Vector3.up.normalized.mul(20));
this.completed = true;
return false;
}
}
// This runs after OnTick() and after the physics simulation completes. Here we return the progress data since that's
// what we want to make sure stays in sync between the client and server.
OnCaptureSnapshot(): StateData {
return [this.progress, this.completed];
}
// This function gets called whenever there's a mismatch between the client and server. It may also get called
// if the client replays or if the server is processing lag compensation. The purpose of this function is to
// reset the state of this command to match whatever snapshot is provided to it.
ResetToSnapshot(state: Readonly<StateData>): void {
this.progress = state[0];
this.completed = state[1];
}
// Called whenever the client needs to compare it's local predicted result to the actual result provided by the server.
// We want to ensure that the state data we have locally actually matches the state data returned by the server.
CompareSnapshots(a: Readonly<StateData>, b: Readonly<StateData>): boolean {
if (a[0] !== b[0]) return false; // If it doesn't match, then we return false
if (a[1] !== b[1]) return false;
return true; // If it matches we are good!
}
}
Registering the Command
Last updated