Skip to content

Commit a8ad53b

Browse files
committed
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.
1 parent 2cca0b2 commit a8ad53b

6 files changed

Lines changed: 182 additions & 15 deletions

File tree

examples/ex_game/ex_game.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
use std::net::SocketAddr;
22

3-
use ggrs::{Config, Frame, GameStateCell, GgrsRequest, InputStatus, PlayerHandle, NULL_FRAME};
3+
use ggrs::{
4+
Config, Frame, GameStateCell, GgrsRequest, InputStatus, PlayerHandle, PredictRepeatLast,
5+
NULL_FRAME,
6+
};
47
use macroquad::prelude::*;
58
use serde::{Deserialize, Serialize};
69

@@ -33,6 +36,7 @@ pub struct Input {
3336
pub struct GGRSConfig;
3437
impl Config for GGRSConfig {
3538
type Input = Input;
39+
type InputPredictor = PredictRepeatLast;
3640
type State = State;
3741
type Address = SocketAddr;
3842
}

src/input_queue.rs

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::frame_info::PlayerInput;
2-
use crate::{Config, Frame, InputStatus, NULL_FRAME};
2+
use crate::{Config, Frame, InputPredictor, InputStatus, NULL_FRAME};
33
use std::cmp;
44

55
/// 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<T: Config> InputQueue<T> {
123123
return (self.inputs[offset].input, InputStatus::Confirmed);
124124
}
125125

126-
// 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.
127-
if requested_frame == 0 || self.last_added_frame == NULL_FRAME {
128-
// basing new prediction frame from nothing, since we are on frame 0 or we have no frames yet
129-
self.prediction = PlayerInput::blank_input(self.prediction.frame);
130-
} else {
131-
// basing new prediction frame from previously added frame
132-
let previous_position = match self.head {
133-
0 => INPUT_QUEUE_LENGTH - 1,
134-
_ => self.head - 1,
126+
// The requested frame isn't in the queue. This means we need to return a prediction frame.
127+
// Fetch the previous input if we have one, so we can use it to predict the next frame.
128+
let previous_player_input =
129+
if requested_frame == 0 || self.last_added_frame == NULL_FRAME {
130+
None
131+
} else {
132+
// basing new prediction frame from previously added frame
133+
let previous_position = match self.head {
134+
0 => INPUT_QUEUE_LENGTH - 1,
135+
_ => self.head - 1,
136+
};
137+
Some(self.inputs[previous_position])
135138
};
136-
self.prediction = self.inputs[previous_position];
137-
}
139+
140+
// Ask the user to predict the input based on the previous input (if any); if we don't
141+
// get a prediction from the user, default to the default input.
142+
let input_prediction = previous_player_input
143+
.map(|pi| T::InputPredictor::predict(pi.input))
144+
.unwrap_or_default();
145+
146+
// Set the frame number of the predicted input to what it was based on
147+
self.prediction = {
148+
let frame_num = if let Some(previous_player_input) = previous_player_input {
149+
previous_player_input.frame
150+
} else {
151+
self.prediction.frame
152+
};
153+
PlayerInput::new(frame_num, input_prediction)
154+
};
155+
138156
// update the prediction's frame
139157
self.prediction.frame += 1;
140158
}
@@ -252,6 +270,8 @@ mod input_queue_tests {
252270

253271
use serde::{Deserialize, Serialize};
254272

273+
use crate::PredictRepeatLast;
274+
255275
use super::*;
256276

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

265285
impl Config for TestConfig {
266286
type Input = TestInput;
287+
type InputPredictor = PredictRepeatLast;
267288
type State = Vec<u8>;
268289
type Address = SocketAddr;
269290
}

src/lib.rs

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,11 @@ pub trait Config: 'static + Send + Sync {
210210
/// a player, including when a player is disconnected.
211211
type Input: Copy + Clone + PartialEq + Default + Serialize + DeserializeOwned + Send + Sync;
212212

213+
/// How GGRS should predict the next input for a player when their input hasn't arrived yet.
214+
///
215+
/// [PredictRepeatLast] is a good default; see [InputPredictor] for more information.
216+
type InputPredictor: InputPredictor<Self::Input>;
217+
213218
/// The save state type for the session.
214219
type State: Clone + Send + Sync;
215220

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

252+
/// How GGRS should predict the next input for a player when their input hasn't arrived yet.
253+
///
254+
/// [PredictRepeatLast] is a good default; see [InputPredictor] for more information.
255+
type InputPredictor: InputPredictor<Self::Input>;
256+
247257
/// The save state type for the session.
248258
type State;
249259

@@ -267,3 +277,131 @@ where
267277
/// The pairs `(A, Message)` indicate from which address each packet was received.
268278
fn receive_all_messages(&mut self) -> Vec<(A, Message)>;
269279
}
280+
281+
/// An [InputPredictor] allows GGRS to predict the next input for a player based on previous input
282+
/// received.
283+
///
284+
/// # Bundled Predictors
285+
///
286+
/// [PredictRepeatLast] is a good default choice for most action games where inputs consist of the
287+
/// buttons player are holding down; if your game input instead consists of sporadic one-off events
288+
/// which are almost never repeated, then [PredictDefault] may better suit.
289+
///
290+
/// You are welcome to implement your own predictor to exploit known properties of your input.
291+
///
292+
/// # Understanding Predictions
293+
///
294+
/// A correct prediction means a rollback will not happen when input is received late from a remote
295+
/// player. An incorrect prediction will later cause GGRS to request your game to rollback. It is
296+
/// normal and expected that some predictions will be incorrect, but the more incorrect predictions
297+
/// are given to GGRS, the more work your game will have to do to resimulate past game states (and
298+
/// the more rollbacks may be noticeable to your human players).
299+
///
300+
/// For example, if your chosen input predictor says a player's input always makes them crouch, but
301+
/// in your game players only crouch in 1% of frames, then:
302+
///
303+
/// * GGRS will make it seem to your game as if all remote players crouch on every frame.
304+
/// * When GGRS receives input from a remote player and finds out they are not crouching, it will
305+
/// ask your game to roll back to the frame that input was from and resimulate it plus all
306+
/// subsequent frames up to and including the present frame.
307+
/// * Therefore 99% of frames will be resimulated.
308+
///
309+
/// # Improving Prediction Accuracy
310+
///
311+
/// ## Quantize Inputs
312+
///
313+
/// Input prediction based on repeating past inputs works best if your inputs are discrete (or
314+
/// quantized), as this increases the chances of them being the same from frame to frame.
315+
///
316+
/// For example, say your game allows players to move forward or stand still using an analog
317+
/// joystick; here are two ways you could represent player input:
318+
///
319+
/// * `moving_forward: bool` set to `true` when the joystick is pressed forward and `false`
320+
/// otherwise.
321+
/// * `forward_speed: f32` with a range from `0.0` to `1.0` depending on how far the joystick is
322+
/// pressed forward.
323+
///
324+
/// The former works well with [PredictRepeatLast], but the (fairly) continuous nature of a 32-bit
325+
/// floating point number plus the precision of an analog joystick plus the inability of most humans
326+
/// to hold a joystick perfectly still means that the value of `forward_speed` from one frame to the
327+
/// next will almost always differ; this in turn will cause many mispredictions when used with
328+
/// [PredictRepeatLast].
329+
///
330+
/// Quantization generally incurs a tradeoff between input precision and prediction accuracy, with
331+
/// the right choice depending on the game's design:
332+
///
333+
/// * in a keyboard-only game, move-forward input is likely a binary "move or not" anyway, so
334+
/// quantizing is unnecessary.
335+
/// * in a 2D fighting game played with analog joysticks, it might be fine for movement to be
336+
/// represented as "stand still", "walk forward", and "run forward" based on how far the joystick
337+
/// is pressed forward.
338+
/// * in a platformer played with analog joysticks, 5 to 10 discrete moving forward speeds may be
339+
/// required in order for the game to feel precise enough.
340+
///
341+
/// ## State-based vs Transition-based Input
342+
///
343+
/// The bundled predictors works best if your input either captures the current state of player
344+
/// input ([PredictRepeatLast]) OR captures transitions between states ([PredictDefault]).
345+
///
346+
/// For example, say your game allows players to hold a button to crouch; here are two ways you
347+
/// could represent player input:
348+
///
349+
/// * state-based: `crouching_button_held`, set to `true` as long as the player is crouching
350+
/// * transition-based: `crouching_button_pressed` and `crouching_button_released`, which are set to
351+
/// true on the frames where the player first presses and and releases the crouch button
352+
/// (respectively)
353+
///
354+
/// Given a sequence of these inputs over time, these two representations capture the same
355+
/// information (with some bookkeeping, your game can trivially convert between the two). But,
356+
/// consider a single instance of a player crouching for several frames in a row:
357+
///
358+
/// In the first case (state-based), [PredictRepeatLast] will make two mispredictions: once on the
359+
/// first frame when crouching begins, and once on the last frame when the player releases the
360+
/// crouch button.
361+
///
362+
/// But in the second case (transition-based), [PredictRepeatLast] will make four mispredictions:
363+
///
364+
/// * When the player first presses the crouch button
365+
/// * The frame immediately after the crouch button was pressed
366+
/// * When the player releases the crouch button
367+
/// * The frame immediately after the crouch button was released
368+
///
369+
/// Therefore, [PredictRepeatLast] is better suited to a state-based representation of input, and
370+
/// [PredictDefault] is better suited to a transition-based representation of input.
371+
///
372+
/// If your input is a mix of both states and transitions, then consider implementing your own
373+
/// prediction strategy that exploits that.
374+
pub trait InputPredictor<I> {
375+
/// Predict the next input for a player based on a previous input.
376+
///
377+
/// The previous input may not be available, for example in the case where no input from a
378+
/// remote player has been received in this session yet (notably, the very first simulation of
379+
/// the first frame of a session will never have any inputs from remote players). In such a case
380+
/// GGRS will use [I::default()](Default::default) instead of calling the predictor.
381+
///
382+
fn predict(previous: I) -> I;
383+
}
384+
385+
/// An [InputPredictor] that predicts that the next input for any player will be identical to the
386+
/// last received input for that player.
387+
///
388+
/// This is a good default choice, and a sane starting point for any custom input prediction logic.
389+
pub struct PredictRepeatLast;
390+
impl<I> InputPredictor<I> for PredictRepeatLast {
391+
fn predict(previous: I) -> I {
392+
previous
393+
}
394+
}
395+
396+
/// An input predictor that always predicts that the next input for any given player will be the
397+
/// [Default](Default::default()) input, regardless of what the previous input was.
398+
///
399+
/// This is appropriate if your inputs capture transitions between rather than states themselves;
400+
/// see the discussion at [PredictRepeatLast] (which is better suited for inputs that capture
401+
/// state) for a concrete example.
402+
pub struct PredictDefault;
403+
impl<I: Default> InputPredictor<I> for PredictDefault {
404+
fn predict(_previous: I) -> I {
405+
I::default()
406+
}
407+
}

src/sync_layer.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,7 @@ impl<T: Config> SyncLayer<T> {
382382
mod sync_layer_tests {
383383

384384
use super::*;
385+
use crate::PredictRepeatLast;
385386
use serde::{Deserialize, Serialize};
386387
use std::net::SocketAddr;
387388

@@ -395,6 +396,7 @@ mod sync_layer_tests {
395396

396397
impl Config for TestConfig {
397398
type Input = TestInput;
399+
type InputPredictor = PredictRepeatLast;
398400
type State = u8;
399401
type Address = SocketAddr;
400402
}

tests/stubs.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use std::collections::hash_map::DefaultHasher;
44
use std::hash::{Hash, Hasher};
55
use std::net::SocketAddr;
66

7-
use ggrs::{Config, Frame, GameStateCell, GgrsRequest, InputStatus};
7+
use ggrs::{Config, Frame, GameStateCell, GgrsRequest, InputStatus, PredictRepeatLast};
88

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

2727
impl Config for StubConfig {
2828
type Input = StubInput;
29+
type InputPredictor = PredictRepeatLast;
2930
type State = StateStub;
3031
type Address = SocketAddr;
3132
}

tests/stubs_enum.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::collections::hash_map::DefaultHasher;
22
use std::hash::{Hash, Hasher};
33
use std::net::SocketAddr;
44

5-
use ggrs::{Config, Frame, GameStateCell, GgrsRequest, InputStatus};
5+
use ggrs::{Config, Frame, GameStateCell, GgrsRequest, InputStatus, PredictRepeatLast};
66
use serde::{Deserialize, Serialize};
77

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

2929
impl Config for StubEnumConfig {
3030
type Input = EnumInput;
31+
type InputPredictor = PredictRepeatLast;
3132
type State = StateStubEnum;
3233
type Address = SocketAddr;
3334
}

0 commit comments

Comments
 (0)