From 3f9d218bc379c422e222cfbe6d9092710a714060 Mon Sep 17 00:00:00 2001 From: Caspar Krieger Date: Wed, 13 Nov 2024 14:22:05 +0800 Subject: [PATCH 1/2] feat: Support custom input prediction Introduces a new associated type parameter for Config that controls the input prediction approach used. See extensive documentation on InputPredictor and its implementations. --- examples/ex_game/ex_game.rs | 6 +- src/input_queue.rs | 38 +++++++--- src/lib.rs | 143 +++++++++++++++++++++++++++++++++++- src/sync_layer.rs | 2 + tests/stubs.rs | 3 +- tests/stubs_enum.rs | 3 +- 6 files changed, 181 insertions(+), 14 deletions(-) diff --git a/examples/ex_game/ex_game.rs b/examples/ex_game/ex_game.rs index f0ee57d..606107c 100644 --- a/examples/ex_game/ex_game.rs +++ b/examples/ex_game/ex_game.rs @@ -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}; @@ -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; } diff --git a/src/input_queue.rs b/src/input_queue.rs index ad28d96..3b9e49f 100644 --- a/src/input_queue.rs +++ b/src/input_queue.rs @@ -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. @@ -131,14 +131,31 @@ impl InputQueue { 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 - self.prediction = self.inputs[Self::prev_pos(self.head)]; - } + // 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 + Some(self.inputs[Self::prev_pos(self.head)]) + }; + + // 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; } @@ -248,6 +265,8 @@ mod input_queue_tests { use serde::{Deserialize, Serialize}; + use crate::PredictRepeatLast; + use super::*; #[repr(C)] @@ -260,6 +279,7 @@ mod input_queue_tests { impl Config for TestConfig { type Input = TestInput; + type InputPredictor = PredictRepeatLast; type State = Vec; type Address = SocketAddr; } diff --git a/src/lib.rs b/src/lib.rs index 66c08b9..546911f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -33,6 +33,7 @@ //! pub struct GgrsConfig; //! impl ggrs::Config for GgrsConfig { //! type Input = Input; +//! type InputPredictor = ggrs::PredictRepeatLast; //! type State = GameState; //! type Address = std::net::SocketAddr; //! } @@ -42,7 +43,7 @@ //! //! ```rust,no_run //! # use ggrs::{SessionBuilder, PlayerType, UdpNonBlockingSocket}; -//! # struct GgrsConfig; impl ggrs::Config for GgrsConfig { type Input = u8; type State = u8; type Address = std::net::SocketAddr; } +//! # struct GgrsConfig; impl ggrs::Config for GgrsConfig { type Input = u8; type InputPredictor = ggrs::PredictRepeatLast; type State = u8; type Address = std::net::SocketAddr; } //! let socket = UdpNonBlockingSocket::bind_to_port(7000).unwrap(); //! let mut session = SessionBuilder::::new() //! .with_num_players(2).unwrap() @@ -59,7 +60,7 @@ //! ```rust,no_run //! # use ggrs::*; //! # struct GgrsConfig; -//! # impl Config for GgrsConfig { type Input = u8; type State = u8; type Address = std::net::SocketAddr; } +//! # impl Config for GgrsConfig { type Input = u8; type InputPredictor = PredictRepeatLast; type State = u8; type Address = std::net::SocketAddr; } //! # let socket = UdpNonBlockingSocket::bind_to_port(7000).unwrap(); //! # let mut session: P2PSession = SessionBuilder::new() //! # .add_player(PlayerType::Local, 0).unwrap() @@ -346,6 +347,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; + /// The save state type for the session. type State: Clone + Send + Sync; @@ -417,6 +423,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; + /// The save state type for the session. type State; @@ -477,3 +488,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 { + /// 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 InputPredictor 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 InputPredictor for PredictDefault { + fn predict(_previous: I) -> I { + I::default() + } +} diff --git a/src/sync_layer.rs b/src/sync_layer.rs index cd1bcda..bcc362c 100644 --- a/src/sync_layer.rs +++ b/src/sync_layer.rs @@ -378,6 +378,7 @@ impl SyncLayer { mod sync_layer_tests { use super::*; + use crate::PredictRepeatLast; use serde::{Deserialize, Serialize}; use std::net::SocketAddr; @@ -391,6 +392,7 @@ mod sync_layer_tests { impl Config for TestConfig { type Input = TestInput; + type InputPredictor = PredictRepeatLast; type State = u8; type Address = SocketAddr; } diff --git a/tests/stubs.rs b/tests/stubs.rs index 2061900..4819a33 100644 --- a/tests/stubs.rs +++ b/tests/stubs.rs @@ -6,7 +6,7 @@ use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use ggrs::{ Config, Frame, GameStateCell, GgrsError, GgrsRequest, InputStatus, P2PSession, PlayerType, - SessionBuilder, SessionState, SpectatorSession, UdpNonBlockingSocket, + PredictRepeatLast, SessionBuilder, SessionState, SpectatorSession, UdpNonBlockingSocket, }; fn calculate_hash(t: &T) -> u64 { @@ -29,6 +29,7 @@ pub struct StubConfig; impl Config for StubConfig { type Input = StubInput; + type InputPredictor = PredictRepeatLast; type State = StateStub; type Address = SocketAddr; } diff --git a/tests/stubs_enum.rs b/tests/stubs_enum.rs index caf2a2c..3fd2e55 100644 --- a/tests/stubs_enum.rs +++ b/tests/stubs_enum.rs @@ -2,7 +2,7 @@ use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; -use ggrs::{Config, Frame, GameStateCell, GgrsRequest, InputStatus}; +use ggrs::{Config, Frame, GameStateCell, GgrsRequest, InputStatus, PredictRepeatLast}; use serde::{Deserialize, Serialize}; fn calculate_hash(t: &T) -> u64 { @@ -28,6 +28,7 @@ pub struct StubEnumConfig; impl Config for StubEnumConfig { type Input = EnumInput; + type InputPredictor = PredictRepeatLast; type State = StateStubEnum; type Address = SocketAddr; } From f3a8554d880e6112d1d2f3e89411d8acaefbd528 Mon Sep 17 00:00:00 2001 From: Caspar Krieger Date: Thu, 26 Mar 2026 19:19:36 +0800 Subject: [PATCH 2/2] docs: document InputPredictor in setup, sessions, and requests-and-events --- docs/requests-and-events.md | 4 ++-- docs/sessions.md | 2 +- docs/setup.md | 12 ++++++++++++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/docs/requests-and-events.md b/docs/requests-and-events.md index a9cc7cc..862e0ab 100644 --- a/docs/requests-and-events.md +++ b/docs/requests-and-events.md @@ -53,7 +53,7 @@ GgrsRequest::AdvanceFrame { inputs } => { for (player_handle, (input, status)) in inputs.iter().enumerate() { match status { InputStatus::Confirmed => { /* actual received input */ } - InputStatus::Predicted => { /* predicted — may be rolled back */ } + InputStatus::Predicted => { /* predicted by your InputPredictor — may be rolled back */ } InputStatus::Disconnected => { /* player disconnected; input is T::Input::default() */ } } game.apply_input(player_handle, input); @@ -62,7 +62,7 @@ GgrsRequest::AdvanceFrame { inputs } => { } ``` -`Disconnected` inputs have `T::Input::default()` as the input value. A sensible convention is for `Default` to represent "no buttons pressed" so disconnected players simply stop moving. +`Predicted` inputs are generated by the [`InputPredictor`](https://docs.rs/ggrs/latest/ggrs/trait.InputPredictor.html) configured on your `Config` trait — see [Setup](setup.md#input-prediction) for details on choosing a predictor. `Disconnected` inputs always use `T::Input::default()` regardless of the predictor. A sensible convention is for `Default` to represent "no buttons pressed" so disconnected players simply stop moving. --- diff --git a/docs/sessions.md b/docs/sessions.md index dde067c..caa6b72 100644 --- a/docs/sessions.md +++ b/docs/sessions.md @@ -6,7 +6,7 @@ GGRS provides three session types. All are constructed with [`SessionBuilder`](h ### `P2PSession` -The main session type for multiplayer games. All participating clients create their own `P2PSession` and connect to each other in a peer-to-peer mesh. Each client sends only its own local inputs; GGRS handles prediction and rollback transparently. +The main session type for multiplayer games. All participating clients create their own `P2PSession` and connect to each other in a peer-to-peer mesh. Each client sends only its own local inputs; GGRS handles prediction and rollback transparently. The prediction strategy is controlled by the `InputPredictor` type on your `Config` — see [Setup](setup.md#input-prediction). ### `SpectatorSession` diff --git a/docs/setup.md b/docs/setup.md index d327d0d..d90db30 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -57,6 +57,7 @@ pub struct GgrsConfig; impl ggrs::Config for GgrsConfig { type Input = Input; // transmitted over the network each frame + type InputPredictor = ggrs::PredictRepeatLast; // how to predict missing remote inputs type State = GameState; // saved/loaded during rollbacks type Address = std::net::SocketAddr; } @@ -64,4 +65,15 @@ impl ggrs::Config for GgrsConfig { `Input` must implement `Default` — GGRS uses the default value to represent "no input" for disconnected players. +### Input Prediction + +When remote inputs haven't arrived yet, GGRS must predict what a player's input will be so the game can keep running without waiting. The `InputPredictor` associated type on `Config` controls this prediction strategy. + +GGRS ships with two predictors: + +- **`PredictRepeatLast`** — predicts that the player will repeat their last known input. This is a good default for most action games where inputs represent held state (e.g., buttons currently pressed). +- **`PredictDefault`** — always predicts `Input::default()`, regardless of the previous input. This is better suited for transition-based inputs where events are one-off (e.g., "button just pressed this frame"). + +You can also implement the [`InputPredictor`](https://docs.rs/ggrs/latest/ggrs/trait.InputPredictor.html) trait yourself to exploit known properties of your input format. See the rustdoc on `InputPredictor` for detailed guidance on improving prediction accuracy through input quantization and choosing between state-based and transition-based input representations. + See [Sessions](sessions.md) for the next step.