Initial work on rollback and refactor

This commit is contained in:
2026-08-12 12:27:35 +02:00
parent ad6c1e17ac
commit a21f24b2d9
45 changed files with 1451 additions and 717 deletions
+130
View File
@@ -0,0 +1,130 @@
//! Per-fighter input state: the current and previous frame, plus the buffered
//! commands (smash inputs and friends) that states query when deciding whether
//! to interrupt.
use std::ops::{Index, IndexMut};
use bevy::prelude::*;
use bevy_ggrs::{PlayerInputs, Rollback};
use crate::game_settings::GameSettings;
use crate::input::FighterInputFrame;
use crate::netcode::GGRSCfg;
use crate::player::Player;
struct FighterInputBufferedActions {}
pub enum FighterCommands {
SmashMoveLeft,
SmashMoveRight,
}
/// Frames of life remaining for each buffered command. Indexed by
/// [`FighterCommands`] so adding a command is a two-line change here.
#[derive(Default, Clone, Copy)]
pub struct FighterCommandLifetimes {
smash_move_left: u8,
smash_move_right: u8,
}
impl Index<FighterCommands> for FighterCommandLifetimes {
type Output = u8;
fn index(&self, command: FighterCommands) -> &Self::Output {
match command {
FighterCommands::SmashMoveLeft => &self.smash_move_left,
FighterCommands::SmashMoveRight => &self.smash_move_right,
}
}
}
impl IndexMut<FighterCommands> for FighterCommandLifetimes {
fn index_mut(&mut self, command: FighterCommands) -> &mut Self::Output {
match command {
FighterCommands::SmashMoveLeft => &mut self.smash_move_left,
FighterCommands::SmashMoveRight => &mut self.smash_move_right,
}
}
}
#[derive(Component, Default, Clone, Copy)]
#[require(Rollback)]
pub struct FighterInput {
command_lifetimes: FighterCommandLifetimes,
prev_frame: FighterInputFrame,
current_frame: FighterInputFrame,
frames_in_smash_move_deadzone: u8,
}
impl FighterInput {
pub fn get_last_frame(&self) -> FighterInputFrame {
self.current_frame
}
pub fn push_input_frame(&mut self, frame: FighterInputFrame) {
self.prev_frame = self.current_frame;
self.current_frame = frame;
}
pub fn clear_buffer(&mut self) {
self.command_lifetimes = FighterCommandLifetimes::default()
}
pub fn clear_command(&mut self, command: FighterCommands) {
self.command_lifetimes[command] = 0;
}
pub fn set_lifetime(&mut self, command: FighterCommands, lifetime: u8) {
self.command_lifetimes[command] = lifetime;
}
pub fn has_command(&self, command: FighterCommands) -> bool {
self.command_lifetimes[command] > 0
}
}
/// Turns this frame's rolled-back inputs into per-fighter [`FighterInput`]
/// state. Runs in `GameplaySet::Input`, before any state looks at it.
pub fn postprocess_input(
query: Query<(&mut FighterInput, &Player)>,
inputs: Res<PlayerInputs<GGRSCfg>>,
game_settings: Res<GameSettings>,
) {
for (mut input, player) in query {
input.push_input_frame(inputs[player.handle].0);
// Directional smash detection
let x_abs = input.current_frame.movement.x.abs();
let reset_deadzone = game_settings.input_common.smash_input_reset_axis_threshold;
let axis_threshold = game_settings.input_common.smash_input_axis_threshold;
let frame_threshold = game_settings.input_common.smash_input_frame_threshold;
if x_abs < reset_deadzone {
input.frames_in_smash_move_deadzone = 0xFE;
} else {
let prev_x_abs = input.prev_frame.movement.x.abs();
if prev_x_abs < reset_deadzone
|| input.prev_frame.movement.x.signum() != input.current_frame.movement.x.signum()
{
input.frames_in_smash_move_deadzone = 0;
} else {
input.frames_in_smash_move_deadzone += 1;
}
}
if x_abs >= axis_threshold && input.frames_in_smash_move_deadzone <= frame_threshold {
input.clear_command(FighterCommands::SmashMoveLeft);
input.clear_command(FighterCommands::SmashMoveRight);
let command_to_set = if input.current_frame.movement.x > 0.0 {
FighterCommands::SmashMoveRight
} else {
FighterCommands::SmashMoveLeft
};
input.set_lifetime(command_to_set, game_settings.input_common.input_buffer_size);
if input.current_frame.movement.x > 0.0 {}
}
}
}
+12
View File
@@ -0,0 +1,12 @@
//! Which entity is driving which. Kept separate from the input pipeline
//! itself: this is a relationship between entities, not a source of input.
use bevy::prelude::*;
#[derive(Component, Debug)]
#[relationship(relationship_target = ControlledBy)]
pub struct Controls(pub Entity);
#[derive(Component, Debug)]
#[relationship_target(relationship = Controls)]
pub struct ControlledBy(Entity);
+10
View File
@@ -0,0 +1,10 @@
use bevy::prelude::*;
use serde::{Deserialize, Serialize};
/// One frame of a player's raw input. This is the type GGRS serializes and
/// sends over the wire, so keep it small and keep it `PartialEq`.
#[derive(Copy, Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
pub struct FighterInputFrame {
pub movement: Vec2,
pub directional_attack: Vec2,
}
+55
View File
@@ -0,0 +1,55 @@
//! Keyboard backend: samples the keys into the local input frames GGRS sends
//! over the wire.
use bevy::platform::collections::HashMap;
use bevy::prelude::*;
use bevy_ggrs::{LocalInputs, LocalPlayers};
use crate::input::{BaseInputMap, FighterInputFrame, InputActionState, InputMapAction};
use crate::netcode::GGRSCfg;
pub fn preprocess_keyboard_input(
mut commands: Commands,
key: Res<ButtonInput<KeyCode>>,
input_map: Res<BaseInputMap>,
local_players: Res<LocalPlayers>,
) {
// Keyboard input is only for player 0
let mut local_inputs = HashMap::new();
for handle in &local_players.0 {
let mut input_frame = FighterInputFrame::default();
for keyboard_elment in &input_map.keyboard {
let action_state = if key.just_pressed(keyboard_elment.key) {
InputActionState::JustPressed
} else if key.just_released(keyboard_elment.key) {
InputActionState::JustReleased
} else if key.pressed(keyboard_elment.key) {
InputActionState::Pressed
} else {
InputActionState::Released
};
let action_strength = match action_state {
InputActionState::JustPressed | InputActionState::Pressed => 1.0f32,
InputActionState::JustReleased | InputActionState::Released => 0.0f32,
};
match keyboard_elment.action {
InputMapAction::MovementXDir(sign) => {
input_frame.movement.x += (sign as f32) * action_strength
}
InputMapAction::MovementYDir(sign) => {
input_frame.movement.y += (sign as f32) * action_strength
}
InputMapAction::Jump => todo!(),
}
}
input_frame.movement = input_frame.movement.normalize_or_zero();
input_frame.directional_attack = input_frame.directional_attack.normalize_or_zero();
local_inputs.insert(*handle, input_frame);
}
commands.insert_resource(LocalInputs::<GGRSCfg>(local_inputs));
}
-43
View File
@@ -1,43 +0,0 @@
use bevy::prelude::*;
use crate::input::player::InputFrame;
use super::player::{FighterInput, BaseInputMap, InputActionState};
pub fn postprocess_input(input_state: Query<&mut FighterInput>) {
for _player_input in input_state {
}
}
pub fn preprocess_keyboard_input(key: Res<ButtonInput<KeyCode>>, input_map: Res<BaseInputMap>, mut input_state: Query<&mut FighterInput>) {
// Keyboard input is only for player 0
for mut keeb_input in &mut input_state.iter_mut().take(1) {
let mut input_frame = InputFrame::default();
for keyboard_elment in &input_map.keyboard {
let action_state = if key.just_pressed(keyboard_elment.key) {
InputActionState::JustPressed
} else if key.just_released(keyboard_elment.key) {
InputActionState::JustReleased
} else if key.pressed(keyboard_elment.key) {
InputActionState::Pressed
} else {
InputActionState::Released
};
let action_strength = match action_state {
InputActionState::JustPressed | InputActionState::Pressed => 1.0f32,
InputActionState::JustReleased | InputActionState::Released => 0.0f32,
};
match keyboard_elment.action {
super::player::InputMapAction::MovementXDir(sign) => input_frame.movement.x += (sign as f32) * action_strength,
super::player::InputMapAction::MovementYDir(sign) => input_frame.movement.y += (sign as f32) * action_strength,
super::player::InputMapAction::Jump => todo!(),
}
}
input_frame.movement = input_frame.movement.normalize_or_zero();
input_frame.directional_attack = input_frame.directional_attack.normalize_or_zero();
keeb_input.push_input_frame(input_frame);
}
}
+31
View File
@@ -0,0 +1,31 @@
//! What a physical button means. Device-agnostic: a binding names an
//! [`InputMapAction`], and each device backend (see [`super::keyboard`])
//! decides how to read it.
use bevy::prelude::*;
#[derive(Debug)]
pub enum InputActionState {
JustPressed,
Pressed,
JustReleased,
Released,
}
#[derive(Debug)]
pub enum InputMapAction {
MovementXDir(i32),
MovementYDir(i32),
Jump,
}
#[derive(Debug)]
pub struct KeyboardInputMapElement {
pub key: KeyCode,
pub action: InputMapAction,
}
#[derive(Resource)]
pub struct BaseInputMap {
pub keyboard: Vec<KeyboardInputMapElement>,
}
+51
View File
@@ -0,0 +1,51 @@
//! Input, from physical device to the buffered per-fighter state that the
//! state machine reads.
//!
//! The pipeline is: [`keyboard`] samples devices into [`FighterInputFrame`]s
//! that GGRS ships over the wire, then [`buffer::postprocess_input`] turns the
//! rolled-back frames into [`FighterInput`].
use bevy::prelude::*;
use bevy_ggrs::prelude::*;
pub mod buffer;
pub mod control;
pub mod frame;
pub mod keyboard;
pub mod map;
pub use buffer::{FighterCommands, FighterInput};
pub use frame::FighterInputFrame;
pub use map::{BaseInputMap, InputActionState, InputMapAction, KeyboardInputMapElement};
use crate::schedule::GameplaySet;
/// Owns the input map, the local-input collection that feeds GGRS, and the
/// per-fighter input state derived from the rolled-back inputs.
#[derive(Default)]
pub struct FighterInputPlugin;
impl Plugin for FighterInputPlugin {
fn build(&self, app: &mut App) {
app.insert_resource(BaseInputMap {
keyboard: vec![
// right
KeyboardInputMapElement {
key: KeyCode::KeyD,
action: InputMapAction::MovementXDir(1),
},
// left
KeyboardInputMapElement {
key: KeyCode::KeyA,
action: InputMapAction::MovementXDir(-1),
},
],
})
.add_systems(ReadInputs, keyboard::preprocess_keyboard_input)
.add_systems(
GgrsSchedule,
buffer::postprocess_input.in_set(GameplaySet::Input),
)
.rollback_component_with_clone::<FighterInput>();
}
}
-99
View File
@@ -1,99 +0,0 @@
use std::collections::VecDeque;
use bevy::prelude::*;
#[derive(Default, Clone, Debug)]
pub struct InputFrame {
pub movement: Vec2,
pub directional_attack: Vec2
}
#[derive(Component)]
pub struct FighterInput {
input_buffer: VecDeque<InputFrame>
}
impl Default for FighterInput {
fn default() -> Self {
Self { input_buffer: vec!(InputFrame::default()).into() }
}
}
impl FighterInput {
const INPUT_BUFFER_SIZE: usize = 5;
pub fn get_last_frame(&self) -> Option<&InputFrame> {
Some(self.input_buffer.back().unwrap())
}
pub fn push_input_frame(&mut self, frame: InputFrame) {
while self.input_buffer.len() >= Self::INPUT_BUFFER_SIZE {
self.input_buffer.pop_front();
}
self.input_buffer.push_back(frame);
}
pub fn clear_buffer(&mut self) {
info!("CLEAR");
while self.input_buffer.len() > 1 {
self.input_buffer.pop_front();
}
}
pub fn has_smash_x_movement(&self, reset_deadzone: f32, threshold: f32, frame_threshold: u32) -> Option<f32> {
let iter = self.input_buffer.iter().zip(self.input_buffer.iter().skip(1));
let mut frames_in_deadzone = 0xFE;
for (prev, current) in iter {
let x_abs = current.movement.x.abs();
if x_abs < reset_deadzone {
frames_in_deadzone = 0xFE;
} else {
let prev_x_abs = prev.movement.x.abs();
if prev_x_abs < reset_deadzone || prev.movement.x.signum() != current.movement.x.signum() {
frames_in_deadzone = 0;
} else {
frames_in_deadzone += 1;
}
}
if x_abs >= threshold && frames_in_deadzone <= frame_threshold {
return Some(x_abs.signum());
}
}
None
}
}
#[derive(Debug)]
pub enum InputActionState {
JustPressed,
Pressed,
JustReleased,
Released
}
#[derive(Debug)]
pub enum InputMapAction {
MovementXDir(i32),
MovementYDir(i32),
Jump
}
#[derive(Debug)]
pub struct KeyboardInputMapElement {
pub key: KeyCode,
pub action: InputMapAction
}
#[derive(Resource)]
pub struct BaseInputMap {
pub keyboard: Vec<KeyboardInputMapElement>
}
#[derive(Component, Debug)]
#[relationship(relationship_target = ControlledBy)]
pub struct Controls(pub Entity);
#[derive(Component, Debug)]
#[relationship_target(relationship = Controls)]
pub struct ControlledBy(Entity);
-27
View File
@@ -1,27 +0,0 @@
use bevy::prelude::*;
use crate::input::{
logic::{postprocess_input, preprocess_keyboard_input}, player::{BaseInputMap, InputMapAction, KeyboardInputMapElement},
};
#[derive(Default)]
pub struct FighterInputPlugin;
impl Plugin for FighterInputPlugin {
fn build(&self, app: &mut App) {
app.insert_resource(BaseInputMap {
keyboard: vec![
// right
KeyboardInputMapElement {
key: KeyCode::KeyD,
action: InputMapAction::MovementXDir(1),
},
// left
KeyboardInputMapElement {
key: KeyCode::KeyA,
action: InputMapAction::MovementXDir(-1),
},
],
})
.add_systems(FixedPreUpdate, (preprocess_keyboard_input, postprocess_input).chain());
}
}