Created
November 21, 2024 13:22
-
-
Save RJ/e533e6f75cff3e5822a59c7972f0d5a3 to your computer and use it in GitHub Desktop.
bevy plugin managing mouse inputs etc
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use std::ops::Mul; | |
/// Handles acting on player inputs. Moving and shooting. | |
use crate::prelude::*; | |
use avian2d::prelude::*; | |
use bevy::{ecs::query::QueryData, prelude::*}; | |
use client::{is_in_rollback, Predicted}; | |
use leafwing_input_manager::{Actionlike, InputControlKind}; | |
use lightyear::client::input::leafwing::InputSystemSet; | |
use lightyear::{ | |
inputs::leafwing::input_buffer::InputBuffer, shared::replication::components::Controlled, | |
}; | |
pub mod prelude { | |
pub use super::PlayerActions; | |
pub use super::PlayerCommandsPlugin; | |
pub const PLAYER_THRUSTER_POWER: f32 = 32000. * 3.0; | |
pub const PLAYER_ROTATIONAL_SPEED: f32 = 4.0; | |
pub const PLAYER_MASS: f32 = 960.31; | |
pub use super::{capture_mouse_input, client_player_movement, server_player_movement}; | |
} | |
use prelude::{PLAYER_ROTATIONAL_SPEED, PLAYER_THRUSTER_POWER}; | |
pub struct PlayerCommandsPlugin; | |
impl Plugin for PlayerCommandsPlugin { | |
fn build(&self, app: &mut App) { | |
#[cfg(feature = "client")] | |
{ | |
app.add_systems( | |
FixedPreUpdate, | |
// make sure we update the ActionState before buffering them | |
capture_mouse_input | |
.before(InputSystemSet::BufferClientInputs) | |
.run_if(not(is_in_rollback)), | |
); | |
// we don't spawn bullets during rollback. | |
// if we have the inputs early (so not in rb) then we spawn, | |
// otherwise we rely on normal server replication to spawn them | |
app.add_systems( | |
FixedUpdate, | |
( | |
client_player_movement, | |
shared_player_firing.run_if(not(is_in_rollback)), | |
) | |
.chain() | |
.in_set(FixedSet::Main), | |
); | |
} | |
#[cfg(feature = "server")] | |
{ | |
info!("Server movement systems"); | |
app.add_systems( | |
FixedUpdate, | |
(server_player_movement, shared_player_firing) | |
.chain() | |
.in_set(FixedSet::Main), | |
); | |
} | |
} | |
} | |
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy, Hash, Reflect)] | |
pub enum PlayerActions { | |
Thrust, | |
Down, | |
Left, | |
Right, | |
Fire, | |
MousePositionRelative, | |
} | |
impl Actionlike for PlayerActions { | |
fn input_control_kind(&self) -> InputControlKind { | |
match self { | |
PlayerActions::Thrust | |
| PlayerActions::Down | |
| PlayerActions::Fire | |
| PlayerActions::Left | |
| PlayerActions::Right => InputControlKind::Button, | |
PlayerActions::MousePositionRelative => InputControlKind::DualAxis, | |
} | |
} | |
} | |
#[allow(dead_code)] | |
pub fn client_player_movement( | |
mut q: Query<ApplyInputsQuery, (With<Player>, With<Predicted>, Without<Docked>)>, | |
tick_manager: Res<TickManager>, | |
mut commands: Commands, | |
) { | |
// let (camera, camera_transform) = camera_query.single(); | |
for aiq in q.iter_mut() { | |
// mouse_look(camera, camera_transform, &aiq); | |
shared_movement_behaviour(aiq, &tick_manager, &mut commands); | |
} | |
} | |
#[allow(dead_code)] | |
pub fn server_player_movement( | |
mut q: Query<ApplyInputsQuery, (With<Player>, Without<Docked>)>, | |
tick_manager: Res<TickManager>, | |
mut commands: Commands, | |
) { | |
for aiq in q.iter_mut() { | |
shared_movement_behaviour(aiq, &tick_manager, &mut commands); | |
} | |
} | |
#[derive(QueryData)] | |
#[query_data(mutable, derive(Debug))] | |
pub struct ApplyInputsQuery { | |
pub entity: Entity, | |
pub ex_force: &'static mut ExternalForce, | |
pub ang_vel: &'static mut AngularVelocity, | |
pub rot: &'static mut Rotation, | |
pub action: &'static ActionState<PlayerActions>, | |
pub opt_ib: Option<&'static InputBuffer<PlayerActions>>, | |
pub pos: &'static Position, | |
} | |
/// Filtered out if docked or on autopilot | |
pub fn capture_mouse_input( | |
// tick_manager: Res<TickManager>, | |
mut action_state_query: Query< | |
(&Position, &mut ActionState<PlayerActions>), | |
( | |
With<Controlled>, | |
With<Predicted>, | |
Without<Autopilot>, | |
Without<TargetPositionPid>, | |
Without<Docked>, | |
), | |
>, | |
q_window: Query<&Window, With<bevy::window::PrimaryWindow>>, | |
q_camera: Query<(&Camera, &GlobalTransform, &Transform)>, | |
) { | |
let Ok((player_pos, mut action_state)) = action_state_query.get_single_mut() else { | |
return; | |
}; | |
let (camera, camera_global_transform, camera_transform) = | |
q_camera.get_single().expect("Expected just one camera"); | |
let window = q_window.single(); | |
if let Some(world_position) = window | |
.cursor_position() | |
.and_then(|cursor| camera.viewport_to_world(camera_global_transform, cursor)) | |
.map(|ray| ray.origin.truncate()) | |
{ | |
let mouse_position_relative = world_position - player_pos.0; | |
let camera_scale = camera_transform.scale.x; | |
let pair = mouse_position_relative * camera_scale; | |
action_state.set_axis_pair(&PlayerActions::MousePositionRelative, pair); | |
// info!(tick = ?tick_manager.tick(), ?mouse_position_relative, "Relative mouse position"); | |
} | |
} | |
// TODO what if we don't have inputs for this tick, we should use InputBuffer get_last! | |
pub fn shared_movement_behaviour( | |
aiq: ApplyInputsQueryItem, | |
_tick_manager: &TickManager, | |
_commands: &mut Commands, | |
) { | |
let ApplyInputsQueryItem { | |
entity: _, | |
mut ex_force, | |
ang_vel: _, | |
mut rot, | |
action, | |
opt_ib: _, | |
pos: _, | |
} = aiq; | |
// turn towards the mouse cursor, with a maximum turning speed | |
let relative_mouse_pos = action.axis_pair(&PlayerActions::MousePositionRelative); | |
let mouse_dir = relative_mouse_pos.xy().normalize_or_zero(); | |
if mouse_dir != Vec2::ZERO { | |
// info!("mouse_dir = {mouse_dir:?}"); | |
let current_dir = Vec2::new(rot.cos, rot.sin); | |
let angle_diff = current_dir.angle_between(mouse_dir); | |
let max_rotation = PLAYER_ROTATIONAL_SPEED * 1.0 / FIXED_TIMESTEP_HZ as f32; | |
let limited_rotation = angle_diff.clamp(-max_rotation, max_rotation); | |
let new_rotation = current_dir.rotate(Vec2::from_angle(limited_rotation)); | |
rot.cos = new_rotation.x; | |
rot.sin = new_rotation.y; | |
} | |
if action.pressed(&PlayerActions::Thrust) { | |
// info!("UP PRESSED"); | |
ex_force | |
.apply_force(rot.mul(Vec2::X * PLAYER_THRUSTER_POWER)) | |
.with_persistence(false); | |
} | |
} | |
/// NB we are not restricting this query to `Controlled` entities on the clients, because we hope to | |
/// receive PlayerActions for remote players ahead of the server simulating the tick (lag, input delay, etc) | |
/// in which case we prespawn their bullets on the correct tick, just like we do for our own bullets. | |
/// | |
/// When spawning here, we add the `PreSpawnedPlayerObject` component, and when the client receives the | |
/// replication packet from the server, it matches the hashes on its own `PreSpawnedPlayerObject`, allowing it to | |
/// treat our locally spawned one as the `Predicted` entity (and gives it the Predicted component). | |
/// | |
/// This system doesn't run in rollback, so without early player inputs, their bullets will be | |
/// spawned by the normal server replication (triggering a rollback). | |
pub fn shared_player_firing( | |
q: Query< | |
(Entity, &ActionState<PlayerActions>, &Weapon), | |
Or<(With<Predicted>, With<ReplicationTarget>)>, | |
>, | |
mut commands: Commands, | |
tick_manager: Res<TickManager>, | |
) { | |
if q.is_empty() { | |
return; | |
} | |
let current_tick = tick_manager.tick(); | |
for (player_entity, action, weapon) in q.iter() { | |
if !action.pressed(&PlayerActions::Fire) { | |
continue; | |
} | |
if !weapon.ready_to_fire(current_tick) { | |
continue; | |
} | |
commands.trigger_targets(WeaponFireEvent, player_entity); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment