Skip to content

Commit

Permalink
Unify leafwing and native input plugins (#935)
Browse files Browse the repository at this point in the history
* unify input buffers

* fix leafwing tests

* cargo fix

* wip change native input design to be similar to leafwing inputs

* unify

* wip tests

* wip

* fix

* compiles

* cargo fix

* tests pass

* leafwing tests pass

* cargo fix

* fix avian 3d

* fix things for examples

* fix examples

* add native input unit test

* cargo fix

* fix host-server -> remote client input broadcasting, with input delay

* all examples seem to work

* fix tests

* gitignore

* fmt + cargo fix

* nits
  • Loading branch information
cBournhonesque authored Mar 3, 2025
1 parent b8e3cb9 commit 4164799
Show file tree
Hide file tree
Showing 60 changed files with 2,568 additions and 2,075 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@
/examples/target/
/examples/Cargo.lock
.aider*
**/CL1
**/CL2
**/SL
10 changes: 8 additions & 2 deletions examples/avian_3d_character/src/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ use serde::{Deserialize, Serialize};
use crate::shared::color_from_id;
use lightyear::client::components::{ComponentSyncMode, LerpFn};
use lightyear::client::interpolation::LinearInterpolator;
use lightyear::prelude::client::{self, LeafwingInputConfig};
use lightyear::prelude::client::{self};
use lightyear::prelude::server::{Replicate, SyncTarget};
use lightyear::prelude::*;
use lightyear::shared::input::InputConfig;
use lightyear::utils::avian3d::{position, rotation};
use lightyear::utils::bevy::TransformLinearInterpolation;
use tracing_subscriber::util::SubscriberInitExt;
Expand Down Expand Up @@ -55,7 +56,12 @@ pub(crate) struct ProtocolPlugin;

impl Plugin for ProtocolPlugin {
fn build(&self, app: &mut App) {
app.add_plugins(LeafwingInputPlugin::<CharacterAction>::default());
app.add_plugins(LeafwingInputPlugin::<CharacterAction> {
config: InputConfig::<CharacterAction> {
rebroadcast_inputs: true,
..default()
},
});

app.register_component::<ColorComponent>(ChannelDirection::ServerToClient)
.add_prediction(ComponentSyncMode::Once);
Expand Down
25 changes: 1 addition & 24 deletions examples/avian_3d_character/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use lightyear::client::connection;
use lightyear::prelude::client::{Confirmed, Predicted};
use lightyear::prelude::server::*;
use lightyear::prelude::*;
use lightyear::server::input::leafwing::InputSystemSet;
use lightyear::server::input::InputSystemSet;
use lightyear::shared::tick_manager;
use lightyear_examples_common::shared::FIXED_TIMESTEP_HZ;

Expand All @@ -36,12 +36,6 @@ pub struct ExampleServerPlugin;
impl Plugin for ExampleServerPlugin {
fn build(&self, app: &mut App) {
app.add_systems(Startup, init);
app.add_systems(
PreUpdate,
// This system will replicate the inputs of a client to other
// clients so that a client can predict other clients.
replicate_inputs.after(InputSystemSet::ReceiveInputs),
);
app.add_systems(FixedUpdate, handle_character_actions);
app.add_systems(Update, handle_connections);
}
Expand Down Expand Up @@ -102,23 +96,6 @@ fn init(mut commands: Commands) {
// ));
}

/// When we receive the input of a client, broadcast it to other clients
/// so that they can predict this client's movements accurately
pub(crate) fn replicate_inputs(
mut receive_inputs: ResMut<Events<ServerReceiveMessage<InputMessage<CharacterAction>>>>,
mut send_inputs: EventWriter<ServerSendMessage<InputMessage<CharacterAction>>>,
) {
// rebroadcast the input to other clients
// we are calling drain() here so make sure that this system runs after the `ReceiveInputs` set,
// so that the server had the time to process the inputs
send_inputs.send_batch(receive_inputs.drain().map(|ev| {
ServerSendMessage::new_with_target::<InputChannel>(
ev.message,
NetworkTarget::AllExceptSingle(ev.from),
)
}));
}

/// Spawn a character whenever a new client has connected.
pub(crate) fn handle_connections(
mut connections: EventReader<ConnectEvent>,
Expand Down
7 changes: 6 additions & 1 deletion examples/avian_physics/src/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,12 @@ impl Plugin for ProtocolPlugin {
// messages
app.register_message::<Message1>(ChannelDirection::Bidirectional);
// inputs
app.add_plugins(LeafwingInputPlugin::<PlayerActions>::default());
app.add_plugins(LeafwingInputPlugin::<PlayerActions> {
config: InputConfig::<PlayerActions> {
rebroadcast_inputs: true,
..default()
},
});
app.add_plugins(LeafwingInputPlugin::<AdminActions>::default());
// components
app.register_component::<PlayerId>(ChannelDirection::Bidirectional)
Expand Down
26 changes: 2 additions & 24 deletions examples/avian_physics/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use leafwing_input_manager::prelude::*;
use lightyear::prelude::client::{Confirmed, Predicted};
use lightyear::prelude::server::*;
use lightyear::prelude::*;
use lightyear::server::input::leafwing::InputSystemSet;
use lightyear::server::input::InputSystemSet;
use lightyear::shared::replication::components::InitialReplicated;

// Plugin for server-specific logic
Expand All @@ -30,12 +30,7 @@ impl Plugin for ExampleServerPlugin {
});

app.add_systems(Startup, (start_server, init));
app.add_systems(
PreUpdate,
// this system will replicate the inputs of a client to other clients
// so that a client can predict other clients
replicate_inputs.after(InputSystemSet::ReceiveInputs),
);

// Re-adding Replicate components to client-replicated entities must be done in this set for proper handling.
app.add_systems(
PreUpdate,
Expand Down Expand Up @@ -87,23 +82,6 @@ pub(crate) fn movement(
}
}

/// When we receive the input of a client, broadcast it to other clients
/// so that they can predict this client's movements accurately
pub(crate) fn replicate_inputs(
mut receive_inputs: ResMut<Events<ServerReceiveMessage<InputMessage<PlayerActions>>>>,
mut send_inputs: EventWriter<ServerSendMessage<InputMessage<PlayerActions>>>,
) {
// rebroadcast the input to other clients
// we are calling drain() here so make sure that this system runs after the `ReceiveInputs` set,
// so that the server had the time to process the inputs
send_inputs.send_batch(receive_inputs.drain().map(|ev| {
ServerSendMessage::new_with_target::<InputChannel>(
ev.message,
NetworkTarget::AllExceptSingle(ev.from),
)
}));
}

// Replicate the pre-predicted entities back to the client
// We have to use `InitialReplicated` instead of `Replicated`, because
// the server has already assumed authority over the entity so the `Replicated` component
Expand Down
4 changes: 2 additions & 2 deletions examples/avian_physics/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ pub(crate) fn get_settings() -> MySettings {
headless: false,
inspector: true,
conditioner: Some(Conditioner {
latency_ms: 200,
jitter_ms: 20,
latency_ms: 50,
jitter_ms: 5,
packet_loss: 0.05,
}),
transport: vec![
Expand Down
138 changes: 57 additions & 81 deletions examples/client_replication/src/client.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use bevy::prelude::*;
use bevy::utils::Duration;
use lightyear::client::input::native::InputSystemSet;
use lightyear::client::input::InputSystemSet;
use lightyear::inputs::native::{ActionState, InputMarker};
use lightyear::prelude::client::*;
use lightyear::prelude::*;

Expand All @@ -17,7 +18,7 @@ impl Plugin for ExampleClientPlugin {
// Inputs need to be buffered in the `FixedPreUpdate` schedule
app.add_systems(
FixedPreUpdate,
buffer_input.in_set(InputSystemSet::BufferInputs),
buffer_input.in_set(InputSystemSet::WriteClientInputs),
);
// all actions related-system that can be rolled back should be in the `FixedUpdate` schedule
app.add_systems(FixedUpdate, (player_movement, delete_player));
Expand Down Expand Up @@ -61,65 +62,58 @@ pub(crate) fn handle_connection(

// System that reads from peripherals and adds inputs to the buffer
pub(crate) fn buffer_input(
tick_manager: Res<TickManager>,
mut input_manager: ResMut<InputManager<Inputs>>,
mut query: Query<&mut ActionState<Inputs>, With<InputMarker<Inputs>>>,
keypress: Res<ButtonInput<KeyCode>>,
) {
let tick = tick_manager.tick();
let mut input = Inputs::None;
let mut direction = Direction {
up: false,
down: false,
left: false,
right: false,
};
if keypress.pressed(KeyCode::KeyW) || keypress.pressed(KeyCode::ArrowUp) {
direction.up = true;
}
if keypress.pressed(KeyCode::KeyS) || keypress.pressed(KeyCode::ArrowDown) {
direction.down = true;
}
if keypress.pressed(KeyCode::KeyA) || keypress.pressed(KeyCode::ArrowLeft) {
direction.left = true;
}
if keypress.pressed(KeyCode::KeyD) || keypress.pressed(KeyCode::ArrowRight) {
direction.right = true;
}
if !direction.is_none() {
input = Inputs::Direction(direction);
}
if keypress.pressed(KeyCode::KeyK) {
input = Inputs::Delete;
}
if keypress.pressed(KeyCode::Space) {
input = Inputs::Spawn;
if let Ok(mut action_state) = query.get_single_mut() {
let mut input = None;
let mut direction = Direction {
up: false,
down: false,
left: false,
right: false,
};
if keypress.pressed(KeyCode::KeyW) || keypress.pressed(KeyCode::ArrowUp) {
direction.up = true;
}
if keypress.pressed(KeyCode::KeyS) || keypress.pressed(KeyCode::ArrowDown) {
direction.down = true;
}
if keypress.pressed(KeyCode::KeyA) || keypress.pressed(KeyCode::ArrowLeft) {
direction.left = true;
}
if keypress.pressed(KeyCode::KeyD) || keypress.pressed(KeyCode::ArrowRight) {
direction.right = true;
}
if !direction.is_none() {
input = Some(Inputs::Direction(direction));
}
if keypress.pressed(KeyCode::KeyK) {
input = Some(Inputs::Delete);
}
action_state.value = input;
}
input_manager.add_input(input, tick);
}

// The client input only gets applied to predicted entities that we own
// This works because we only predict the user's controlled entity.
// If we were predicting more entities, we would have to only apply movement to the player owned one.
fn player_movement(
mut position_query: Query<&mut PlayerPosition, With<Predicted>>,
// InputEvent is a special case: we get an event for every fixed-update system run instead of every frame!
mut input_reader: EventReader<InputEvent<Inputs>>,
mut position_query: Query<(&mut PlayerPosition, &ActionState<Inputs>), With<Predicted>>,
) {
for input in input_reader.read() {
if let Some(input) = input.input() {
for position in position_query.iter_mut() {
// NOTE: be careful to directly pass Mut<PlayerPosition>
// getting a mutable reference triggers change detection, unless you use `as_deref_mut()`
shared_movement_behaviour(position, input);
}
for (position, input) in position_query.iter_mut() {
if let Some(input) = &input.value {
// NOTE: be careful to directly pass Mut<PlayerPosition>
// getting a mutable reference triggers change detection, unless you use `as_deref_mut()`
shared_movement_behaviour(position, input);
}
}
}

/// Spawn a server-owned pre-predicted player entity when the space command is pressed
fn spawn_player(
mut commands: Commands,
mut input_reader: EventReader<InputEvent<Inputs>>,
keypress: Res<ButtonInput<KeyCode>>,
connection: Res<ClientConnection>,
players: Query<&PlayerId, With<PlayerPosition>>,
) {
Expand All @@ -131,57 +125,39 @@ fn spawn_player(
return;
}
}
for input in input_reader.read() {
if let Some(input) = input.input() {
match input {
Inputs::Spawn => {
debug!("got spawn input");
commands.spawn((
PlayerBundle::new(client_id, Vec2::ZERO),
// IMPORTANT: this lets the server know that the entity is pre-predicted
// when the server replicates this entity; we will get a Confirmed entity which will use this entity
// as the Predicted version
PrePredicted::default(),
));
}
_ => {}
}
}
if keypress.just_pressed(KeyCode::Space) {
commands.spawn((
PlayerBundle::new(client_id, Vec2::ZERO),
// add a marker to specify that we will be writing Inputs on this entity
InputMarker::<Inputs>::default(),
// IMPORTANT: this lets the server know that the entity is pre-predicted
// when the server replicates this entity; we will get a Confirmed entity which will use this entity
// as the Predicted version
PrePredicted::default(),
));
}
}

/// Delete the predicted player when the space command is pressed
fn delete_player(
mut commands: Commands,
mut input_reader: EventReader<InputEvent<Inputs>>,
connection: Res<ClientConnection>,
players: Query<
(Entity, &PlayerId),
(Entity, &ActionState<Inputs>),
(
With<PlayerPosition>,
Without<Confirmed>,
Without<Interpolated>,
),
>,
) {
let client_id = connection.id();
for input in input_reader.read() {
if let Some(input) = input.input() {
match input {
Inputs::Delete => {
for (entity, player_id) in players.iter() {
if player_id.0 == client_id {
if let Some(mut entity_mut) = commands.get_entity(entity) {
// we need to use this special function to despawn prediction entity
// the reason is that we actually keep the entity around for a while,
// in case we need to re-store it for rollback
entity_mut.prediction_despawn();
debug!("Despawning the predicted/pre-predicted player because we received player action!");
}
}
}
}
_ => {}
for (entity, inputs) in players.iter() {
if inputs.value.as_ref().is_some_and(|v| v == &Inputs::Delete) {
if let Some(mut entity_mut) = commands.get_entity(entity) {
// we need to use this special function to despawn prediction entity
// the reason is that we actually keep the entity around for a while,
// in case we need to re-store it for rollback
entity_mut.prediction_despawn();
debug!("Despawning the predicted/pre-predicted player because we received player action!");
}
}
}
Expand Down
11 changes: 7 additions & 4 deletions examples/client_replication/src/protocol.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
use std::ops::{Add, Mul};

use bevy::app::{App, Plugin};
use bevy::prelude::{default, Bundle, Color, Component, Deref, DerefMut, Vec2};
use bevy::ecs::entity::MapEntities;
use bevy::prelude::{default, Bundle, Color, Component, Deref, DerefMut, EntityMapper, Vec2};
use serde::{Deserialize, Serialize};

use lightyear::client::components::ComponentSyncMode;
use lightyear::prelude::client::Replicate;
use lightyear::prelude::server::SyncTarget;
use lightyear::prelude::*;

use crate::shared::color_from_id;
Expand Down Expand Up @@ -128,8 +128,11 @@ impl Direction {
pub enum Inputs {
Direction(Direction),
Delete,
Spawn,
None,
}

// Inputs must all implement MapEntities
impl MapEntities for Inputs {
fn map_entities<M: EntityMapper>(&mut self, entity_mapper: &mut M) {}
}

// Protocol
Expand Down
Loading

0 comments on commit 4164799

Please sign in to comment.