Initial work on rollback and refactor
This commit is contained in:
@@ -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 {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>,
|
||||
}
|
||||
@@ -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>();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user