From a8ad53bfe9c7ff3753b45669ba480ace41996a3e Mon Sep 17 00:00:00 2001 From: Caspar Krieger Date: Wed, 13 Nov 2024 14:22:05 +0800 Subject: [PATCH] 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 | 45 ++++++++---- src/lib.rs | 138 ++++++++++++++++++++++++++++++++++++ src/sync_layer.rs | 2 + tests/stubs.rs | 3 +- tests/stubs_enum.rs | 3 +- 6 files changed, 182 insertions(+), 15 deletions(-) diff --git a/examples/ex_game/ex_game.rs b/examples/ex_game/ex_game.rs index 2817ad0..a84fb08 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 f06c72f..67eaa1b 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. @@ -123,18 +123,36 @@ 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 - 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; } @@ -252,6 +270,8 @@ mod input_queue_tests { use serde::{Deserialize, Serialize}; + use crate::PredictRepeatLast; + use super::*; #[repr(C)] @@ -264,6 +284,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 427d935..9b30b1c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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; + /// The save state type for the session. type State: Clone + Send + Sync; @@ -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; + /// The save state type for the session. type State; @@ -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 { + /// 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 a27d928..0770228 100644 --- a/src/sync_layer.rs +++ b/src/sync_layer.rs @@ -382,6 +382,7 @@ impl SyncLayer { mod sync_layer_tests { use super::*; + use crate::PredictRepeatLast; use serde::{Deserialize, Serialize}; use std::net::SocketAddr; @@ -395,6 +396,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 d254da8..35cebc9 100644 --- a/tests/stubs.rs +++ b/tests/stubs.rs @@ -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: &T) -> u64 { let mut s = DefaultHasher::new(); @@ -26,6 +26,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 723fa7f..164e90a 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::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; }