Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support custom input prediction #83

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion examples/ex_game/ex_game.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use std::net::SocketAddr;

use ggrs::{Config, Frame, GameStateCell, GgrsRequest, InputStatus, PlayerHandle, NULL_FRAME};
use ggrs::{
Config, Frame, GameStateCell, GgrsRequest, InputStatus, PlayerHandle, PredictRepeatLast,
NULL_FRAME,
};
use macroquad::prelude::*;
use serde::{Deserialize, Serialize};

Expand Down Expand Up @@ -33,6 +36,7 @@ pub struct Input {
pub struct GGRSConfig;
impl Config for GGRSConfig {
type Input = Input;
type InputPredictor = PredictRepeatLast;
type State = State;
type Address = SocketAddr;
}
Expand Down
45 changes: 33 additions & 12 deletions src/input_queue.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::frame_info::PlayerInput;
use crate::{Config, Frame, InputStatus, NULL_FRAME};
use crate::{Config, Frame, InputPredictor, InputStatus, NULL_FRAME};
use std::cmp;

/// The length of the input queue. This describes the number of inputs GGRS can hold at the same time per player.
Expand Down Expand Up @@ -123,18 +123,36 @@ impl<T: Config> InputQueue<T> {
return (self.inputs[offset].input, InputStatus::Confirmed);
}

// The requested frame isn't in the queue. This means we need to return a prediction frame. Predict that the user will do the same thing they did last time.
if requested_frame == 0 || self.last_added_frame == NULL_FRAME {
// basing new prediction frame from nothing, since we are on frame 0 or we have no frames yet
self.prediction = PlayerInput::blank_input(self.prediction.frame);
} else {
// basing new prediction frame from previously added frame
let previous_position = match self.head {
0 => INPUT_QUEUE_LENGTH - 1,
_ => self.head - 1,
// The requested frame isn't in the queue. This means we need to return a prediction frame.
// Fetch the previous input if we have one, so we can use it to predict the next frame.
let previous_player_input =
if requested_frame == 0 || self.last_added_frame == NULL_FRAME {
None
} else {
// basing new prediction frame from previously added frame
let previous_position = match self.head {
0 => INPUT_QUEUE_LENGTH - 1,
_ => self.head - 1,
};
Some(self.inputs[previous_position])
};
self.prediction = self.inputs[previous_position];
}

// Ask the user to predict the input based on the previous input (if any); if we don't
// get a prediction from the user, default to the default input.
let input_prediction = previous_player_input
.map(|pi| T::InputPredictor::predict(pi.input))
.unwrap_or_default();

// Set the frame number of the predicted input to what it was based on
self.prediction = {
let frame_num = if let Some(previous_player_input) = previous_player_input {
previous_player_input.frame
} else {
self.prediction.frame
};
PlayerInput::new(frame_num, input_prediction)
};

// update the prediction's frame
self.prediction.frame += 1;
}
Expand Down Expand Up @@ -252,6 +270,8 @@ mod input_queue_tests {

use serde::{Deserialize, Serialize};

use crate::PredictRepeatLast;

use super::*;

#[repr(C)]
Expand All @@ -264,6 +284,7 @@ mod input_queue_tests {

impl Config for TestConfig {
type Input = TestInput;
type InputPredictor = PredictRepeatLast;
type State = Vec<u8>;
type Address = SocketAddr;
}
Expand Down
138 changes: 138 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,11 @@ pub trait Config: 'static + Send + Sync {
/// a player, including when a player is disconnected.
type Input: Copy + Clone + PartialEq + Default + Serialize + DeserializeOwned + Send + Sync;

/// How GGRS should predict the next input for a player when their input hasn't arrived yet.
///
/// [PredictRepeatLast] is a good default; see [InputPredictor] for more information.
type InputPredictor: InputPredictor<Self::Input>;

/// The save state type for the session.
type State: Clone + Send + Sync;

Expand Down Expand Up @@ -244,6 +249,11 @@ pub trait Config: 'static {
/// a player, including when a player is disconnected.
type Input: Copy + Clone + PartialEq + Default + Serialize + DeserializeOwned;

/// How GGRS should predict the next input for a player when their input hasn't arrived yet.
///
/// [PredictRepeatLast] is a good default; see [InputPredictor] for more information.
type InputPredictor: InputPredictor<Self::Input>;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to self: I also need to update the Sync+Send variant of the Config struct to include this param.


/// The save state type for the session.
type State;

Expand All @@ -267,3 +277,131 @@ where
/// The pairs `(A, Message)` indicate from which address each packet was received.
fn receive_all_messages(&mut self) -> Vec<(A, Message)>;
}

/// An [InputPredictor] allows GGRS to predict the next input for a player based on previous input
/// received.
///
/// # Bundled Predictors
///
/// [PredictRepeatLast] is a good default choice for most action games where inputs consist of the
/// buttons player are holding down; if your game input instead consists of sporadic one-off events
/// which are almost never repeated, then [PredictDefault] may better suit.
///
/// You are welcome to implement your own predictor to exploit known properties of your input.
///
/// # Understanding Predictions
///
/// A correct prediction means a rollback will not happen when input is received late from a remote
/// player. An incorrect prediction will later cause GGRS to request your game to rollback. It is
/// normal and expected that some predictions will be incorrect, but the more incorrect predictions
/// are given to GGRS, the more work your game will have to do to resimulate past game states (and
/// the more rollbacks may be noticeable to your human players).
///
/// For example, if your chosen input predictor says a player's input always makes them crouch, but
/// in your game players only crouch in 1% of frames, then:
///
/// * GGRS will make it seem to your game as if all remote players crouch on every frame.
/// * When GGRS receives input from a remote player and finds out they are not crouching, it will
/// ask your game to roll back to the frame that input was from and resimulate it plus all
/// subsequent frames up to and including the present frame.
/// * Therefore 99% of frames will be resimulated.
///
/// # Improving Prediction Accuracy
///
/// ## Quantize Inputs
///
/// Input prediction based on repeating past inputs works best if your inputs are discrete (or
/// quantized), as this increases the chances of them being the same from frame to frame.
///
/// For example, say your game allows players to move forward or stand still using an analog
/// joystick; here are two ways you could represent player input:
///
/// * `moving_forward: bool` set to `true` when the joystick is pressed forward and `false`
/// otherwise.
/// * `forward_speed: f32` with a range from `0.0` to `1.0` depending on how far the joystick is
/// pressed forward.
///
/// The former works well with [PredictRepeatLast], but the (fairly) continuous nature of a 32-bit
/// floating point number plus the precision of an analog joystick plus the inability of most humans
/// to hold a joystick perfectly still means that the value of `forward_speed` from one frame to the
/// next will almost always differ; this in turn will cause many mispredictions when used with
/// [PredictRepeatLast].
///
/// Quantization generally incurs a tradeoff between input precision and prediction accuracy, with
/// the right choice depending on the game's design:
///
/// * in a keyboard-only game, move-forward input is likely a binary "move or not" anyway, so
/// quantizing is unnecessary.
/// * in a 2D fighting game played with analog joysticks, it might be fine for movement to be
/// represented as "stand still", "walk forward", and "run forward" based on how far the joystick
/// is pressed forward.
/// * in a platformer played with analog joysticks, 5 to 10 discrete moving forward speeds may be
/// required in order for the game to feel precise enough.
///
/// ## State-based vs Transition-based Input
///
/// The bundled predictors works best if your input either captures the current state of player
/// input ([PredictRepeatLast]) OR captures transitions between states ([PredictDefault]).
///
/// For example, say your game allows players to hold a button to crouch; here are two ways you
/// could represent player input:
///
/// * state-based: `crouching_button_held`, set to `true` as long as the player is crouching
/// * transition-based: `crouching_button_pressed` and `crouching_button_released`, which are set to
/// true on the frames where the player first presses and and releases the crouch button
/// (respectively)
///
/// Given a sequence of these inputs over time, these two representations capture the same
/// information (with some bookkeeping, your game can trivially convert between the two). But,
/// consider a single instance of a player crouching for several frames in a row:
///
/// In the first case (state-based), [PredictRepeatLast] will make two mispredictions: once on the
/// first frame when crouching begins, and once on the last frame when the player releases the
/// crouch button.
///
/// But in the second case (transition-based), [PredictRepeatLast] will make four mispredictions:
///
/// * When the player first presses the crouch button
/// * The frame immediately after the crouch button was pressed
/// * When the player releases the crouch button
/// * The frame immediately after the crouch button was released
///
/// Therefore, [PredictRepeatLast] is better suited to a state-based representation of input, and
/// [PredictDefault] is better suited to a transition-based representation of input.
///
/// If your input is a mix of both states and transitions, then consider implementing your own
/// prediction strategy that exploits that.
pub trait InputPredictor<I> {
/// Predict the next input for a player based on a previous input.
///
/// The previous input may not be available, for example in the case where no input from a
/// remote player has been received in this session yet (notably, the very first simulation of
/// the first frame of a session will never have any inputs from remote players). In such a case
/// GGRS will use [I::default()](Default::default) instead of calling the predictor.
///
fn predict(previous: I) -> I;
}

/// An [InputPredictor] that predicts that the next input for any player will be identical to the
/// last received input for that player.
///
/// This is a good default choice, and a sane starting point for any custom input prediction logic.
pub struct PredictRepeatLast;
impl<I> InputPredictor<I> for PredictRepeatLast {
fn predict(previous: I) -> I {
previous
}
}

/// An input predictor that always predicts that the next input for any given player will be the
/// [Default](Default::default()) input, regardless of what the previous input was.
///
/// This is appropriate if your inputs capture transitions between rather than states themselves;
/// see the discussion at [PredictRepeatLast] (which is better suited for inputs that capture
/// state) for a concrete example.
pub struct PredictDefault;
impl<I: Default> InputPredictor<I> for PredictDefault {
fn predict(_previous: I) -> I {
I::default()
}
}
2 changes: 2 additions & 0 deletions src/sync_layer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,7 @@ impl<T: Config> SyncLayer<T> {
mod sync_layer_tests {

use super::*;
use crate::PredictRepeatLast;
use serde::{Deserialize, Serialize};
use std::net::SocketAddr;

Expand All @@ -395,6 +396,7 @@ mod sync_layer_tests {

impl Config for TestConfig {
type Input = TestInput;
type InputPredictor = PredictRepeatLast;
type State = u8;
type Address = SocketAddr;
}
Expand Down
3 changes: 2 additions & 1 deletion tests/stubs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::net::SocketAddr;

use ggrs::{Config, Frame, GameStateCell, GgrsRequest, InputStatus};
use ggrs::{Config, Frame, GameStateCell, GgrsRequest, InputStatus, PredictRepeatLast};

fn calculate_hash<T: Hash>(t: &T) -> u64 {
let mut s = DefaultHasher::new();
Expand All @@ -26,6 +26,7 @@ pub struct StubConfig;

impl Config for StubConfig {
type Input = StubInput;
type InputPredictor = PredictRepeatLast;
type State = StateStub;
type Address = SocketAddr;
}
Expand Down
3 changes: 2 additions & 1 deletion tests/stubs_enum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::net::SocketAddr;

use ggrs::{Config, Frame, GameStateCell, GgrsRequest, InputStatus};
use ggrs::{Config, Frame, GameStateCell, GgrsRequest, InputStatus, PredictRepeatLast};
use serde::{Deserialize, Serialize};

fn calculate_hash<T: Hash>(t: &T) -> u64 {
Expand All @@ -28,6 +28,7 @@ pub struct StubEnumConfig;

impl Config for StubEnumConfig {
type Input = EnumInput;
type InputPredictor = PredictRepeatLast;
type State = StateStubEnum;
type Address = SocketAddr;
}
Expand Down
Loading