Skip to content

Instantly share code, notes, and snippets.

@RJ
Created November 21, 2024 13:22
Show Gist options
  • Save RJ/e533e6f75cff3e5822a59c7972f0d5a3 to your computer and use it in GitHub Desktop.
Save RJ/e533e6f75cff3e5822a59c7972f0d5a3 to your computer and use it in GitHub Desktop.
bevy plugin managing mouse inputs etc
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