Skip to content

Commit

Permalink
feat: Support custom input prediction
Browse files Browse the repository at this point in the history
Introduces a new associated type parameter for Config that controls the
input prediction approach used.

See extensive documentation on InputPredictor and its implementations.
  • Loading branch information
caspark committed Nov 13, 2024
1 parent dec7a16 commit d589b55
Show file tree
Hide file tree
Showing 6 changed files with 190 additions and 16 deletions.
6 changes: 5 additions & 1 deletion examples/ex_game/ex_game.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use std::net::SocketAddr;

use bytemuck::{Pod, Zeroable};
use ggrs::{Config, Frame, GameStateCell, GgrsRequest, InputStatus, PlayerHandle, NULL_FRAME};
use ggrs::{
Config, Frame, GameStateCell, GgrsRequest, InputPredictor, InputStatus, PlayerHandle,
RepeatLastInputPredictor, NULL_FRAME,
};
use macroquad::prelude::*;
use serde::{Deserialize, Serialize};

Expand Down Expand Up @@ -34,6 +37,7 @@ pub struct Input {
pub struct GGRSConfig;
impl Config for GGRSConfig {
type Input = Input;
type InputPredictor = RepeatLastInputPredictor;
type State = State;
type Address = SocketAddr;
}
Expand Down
48 changes: 35 additions & 13 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 All @@ -11,7 +11,7 @@ pub(crate) struct InputQueue<T>
where
T: Config,
{
/// The head of the queue. The newest `PlayerInput` is saved here
/// The head of the queue. The newest `PlayerInput` is saved here
head: usize,
/// The tail of the queue. The oldest `PlayerInput` still valid is saved here.
tail: usize,
Expand Down Expand Up @@ -123,18 +123,37 @@ 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 a blank input.
let input_prediction =
T::InputPredictor::predict(previous_player_input.map(|pi| (pi.input)));

// Set the frame number of the predicted input to what it was based on
self.prediction = if let Some(predicted_input) = input_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, predicted_input)
} else {
PlayerInput::blank_input(self.prediction.frame)
};

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

use bytemuck::{Pod, Zeroable};

use crate::RepeatLastInputPredictor;

use super::*;

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

impl Config for TestConfig {
type Input = TestInput;
type InputPredictor = RepeatLastInputPredictor;
type State = Vec<u8>;
type Address = SocketAddr;
}
Expand Down
144 changes: 144 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
//#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)]
use std::{fmt::Debug, hash::Hash};

use bytemuck::Zeroable;
pub use error::GgrsError;
pub use network::messages::Message;
pub use network::network_stats::NetworkStats;
Expand Down Expand Up @@ -254,6 +255,13 @@ pub trait Config: 'static {
+ bytemuck::CheckedBitPattern
+ bytemuck::Zeroable;

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

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

Expand All @@ -277,3 +285,139 @@ 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.
///
/// A correct prediction means a rollback will not happen when input is received late from a remote
/// player. An incorrect prediction will 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 the game state (and the
/// more that 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 rollback 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.
///
/// [RepeatLastInputPredictor] is a good default choice for most games; other bundled predictors
/// include [ZeroedInputPredictor] and [DefaultInputPredictor].
///
/// You are welcome to implement your own predictor, which is useful in cases where you are quite
pub trait InputPredictor<I> {
/// Predict the next input for a player based on a possibly-available previous input.
///
/// If this returns `None`, GGRS will attempt to manufacture a suitable fallback input.
/// Currently GGRS will use the zeroed out form of the input (see Bytemuck's
/// [zeroed()](Zeroable::zeroed)) as the fallback.
///
/// 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
/// it is typically good to return `None`, `Some(I::default())`, or a Some with a value of `I`
/// that means "this player sent no input" for your game.
///
fn predict(previous: Option<I>) -> Option<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.
///
/// # Improving Prediction Accuracy
///
/// ## Quantize Inputs
///
/// This predictor 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 this predictor, 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 this
/// input predictor.
///
/// Hence, when using this predictor there is generally 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 this
/// predictor is perfectly suited.
/// * 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.
///
/// ## Store State, Not Transitions
///
/// This predictor works best if your input captures the current state of player input, rather than
/// a transition between states.
///
/// 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, the input predictor 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, the input predictor 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, this input is better suited to the state-based representation.
pub struct RepeatLastInputPredictor;
impl<I> InputPredictor<I> for RepeatLastInputPredictor {
fn predict(previous: Option<I>) -> Option<I> {
// if we have an input, assume it'll be the same next time; if we don't have an input, we
// let GGRS make its best guess.
previous
}
}

/// An input predictor that always predicts that the next input for any given player will be the
/// zeroed out form.
pub struct ZeroedInputPredictor;
impl<I> InputPredictor<I> for ZeroedInputPredictor
where
I: Zeroable,
{
fn predict(_previous: Option<I>) -> Option<I> {
Some(I::zeroed())
}
}

/// 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.
pub struct DefaultInputPredictor;
impl<I> InputPredictor<I> for DefaultInputPredictor
where
I: Default,
{
fn predict(_previous: Option<I>) -> Option<I> {
Some(I::default())
}
}
2 changes: 2 additions & 0 deletions src/sync_layer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,7 @@ impl<T: Config> SyncLayer<T> {
mod sync_layer_tests {

use super::*;
use crate::RepeatLastInputPredictor;
use bytemuck::{Pod, Zeroable};
use std::net::SocketAddr;

Expand All @@ -312,6 +313,7 @@ mod sync_layer_tests {

impl Config for TestConfig {
type Input = TestInput;
type InputPredictor = RepeatLastInputPredictor;
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 @@ -3,7 +3,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, RepeatLastInputPredictor};

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 = RepeatLastInputPredictor;
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, RepeatLastInputPredictor};

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

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

0 comments on commit d589b55

Please sign in to comment.