From d589b5563e1618106fa9f4d612eae1e726ea8159 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 | 48 ++++++++---- src/lib.rs | 144 ++++++++++++++++++++++++++++++++++++ src/sync_layer.rs | 2 + tests/stubs.rs | 3 +- tests/stubs_enum.rs | 3 +- 6 files changed, 190 insertions(+), 16 deletions(-) diff --git a/examples/ex_game/ex_game.rs b/examples/ex_game/ex_game.rs index fb2c733..25b2ba7 100644 --- a/examples/ex_game/ex_game.rs +++ b/examples/ex_game/ex_game.rs @@ -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}; @@ -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; } diff --git a/src/input_queue.rs b/src/input_queue.rs index ef9ac79..360754a 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. @@ -11,7 +11,7 @@ pub(crate) struct InputQueue 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, @@ -123,18 +123,37 @@ 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 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; } @@ -252,6 +271,8 @@ mod input_queue_tests { use bytemuck::{Pod, Zeroable}; + use crate::RepeatLastInputPredictor; + use super::*; #[repr(C)] @@ -264,6 +285,7 @@ mod input_queue_tests { impl Config for TestConfig { type Input = TestInput; + type InputPredictor = RepeatLastInputPredictor; type State = Vec; type Address = SocketAddr; } diff --git a/src/lib.rs b/src/lib.rs index a83be8e..7d9d825 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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; @@ -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; + /// The save state type for the session. type State: Clone; @@ -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 { + /// 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) -> Option; +} + +/// 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 InputPredictor for RepeatLastInputPredictor { + fn predict(previous: Option) -> Option { + // 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 InputPredictor for ZeroedInputPredictor +where + I: Zeroable, +{ + fn predict(_previous: Option) -> Option { + 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 InputPredictor for DefaultInputPredictor +where + I: Default, +{ + fn predict(_previous: Option) -> Option { + Some(I::default()) + } +} diff --git a/src/sync_layer.rs b/src/sync_layer.rs index 34f7f17..751038e 100644 --- a/src/sync_layer.rs +++ b/src/sync_layer.rs @@ -299,6 +299,7 @@ impl SyncLayer { mod sync_layer_tests { use super::*; + use crate::RepeatLastInputPredictor; use bytemuck::{Pod, Zeroable}; use std::net::SocketAddr; @@ -312,6 +313,7 @@ mod sync_layer_tests { impl Config for TestConfig { type Input = TestInput; + type InputPredictor = RepeatLastInputPredictor; type State = u8; type Address = SocketAddr; } diff --git a/tests/stubs.rs b/tests/stubs.rs index e184309..be4e28e 100644 --- a/tests/stubs.rs +++ b/tests/stubs.rs @@ -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: &T) -> u64 { let mut s = DefaultHasher::new(); @@ -26,6 +26,7 @@ pub struct StubConfig; impl Config for StubConfig { type Input = StubInput; + type InputPredictor = RepeatLastInputPredictor; type State = StateStub; type Address = SocketAddr; } diff --git a/tests/stubs_enum.rs b/tests/stubs_enum.rs index ed75b4c..8cae59f 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, RepeatLastInputPredictor}; fn calculate_hash(t: &T) -> u64 { let mut s = DefaultHasher::new(); @@ -33,6 +33,7 @@ pub struct StubEnumConfig; impl Config for StubEnumConfig { type Input = EnumInput; + type InputPredictor = RepeatLastInputPredictor; type State = StateStubEnum; type Address = SocketAddr; }