diff --git a/src/core/dev.rs b/src/core/dev.rs index 62975501..f7a2ab15 100644 --- a/src/core/dev.rs +++ b/src/core/dev.rs @@ -2,9 +2,8 @@ use bevy::{dev_tools::states::log_transitions, prelude::*}; -use crate::screen::Screen; - use super::booting::Booting; +use crate::screen::Screen; pub(super) fn plugin(app: &mut App) { // Print state transitions in dev builds diff --git a/src/game/mod.rs b/src/game/mod.rs index 993202a3..62277d64 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -1,7 +1,35 @@ //! Game mechanics and content. +//! +//! The basic movement code shipped with the template is based on the +//! corresponding [Bevy example](https://github.com/janhohenheim/bevy/blob/fixed-time-movement/examples/movement/physics_in_fixed_timestep.rs). +//! See that link for an in-depth explanation of the code and the motivation +//! behind it. use bevy::prelude::*; +mod movement; +mod physics; +mod render; +pub(crate) mod spawn; + pub(super) fn plugin(app: &mut App) { - let _ = app; + app.configure_sets( + Update, + (GameSystem::UpdateTransform, GameSystem::ReadInput).chain(), + ); + app.add_plugins(( + movement::plugin, + physics::plugin, + render::plugin, + spawn::plugin, + )); +} + +#[derive(Debug, SystemSet, Clone, Copy, Eq, PartialEq, Hash)] +enum GameSystem { + /// Updates the [`Transform`] of entities based on their + /// [`physics::PhysicalTransform`]. + UpdateTransform, + /// Reads input from the player. + ReadInput, } diff --git a/src/game/movement.rs b/src/game/movement.rs new file mode 100644 index 00000000..6e2597e3 --- /dev/null +++ b/src/game/movement.rs @@ -0,0 +1,44 @@ +//! Handle player input and translate it into velocity. + +use bevy::prelude::*; + +use super::{physics::Velocity, spawn::player::Player, GameSystem}; + +pub(super) fn plugin(app: &mut App) { + app.add_systems( + Update, + handle_player_movement_input.in_set(GameSystem::ReadInput), + ); +} + +/// Handle keyboard input to move the player. +fn handle_player_movement_input( + keyboard_input: Res>, + mut query: Query<&mut Velocity, With>, +) { + /// Since Bevy's default 2D camera setup is scaled such that + /// one unit is one pixel, you can think of this as + /// "How many pixels per second should the player move?" + /// Note that physics engines may use different unit/pixel ratios. + const SPEED: f32 = 240.0; + for mut velocity in query.iter_mut() { + velocity.0 = Vec3::ZERO; + + if keyboard_input.pressed(KeyCode::KeyW) { + velocity.y += 1.0; + } + if keyboard_input.pressed(KeyCode::KeyS) { + velocity.y -= 1.0; + } + if keyboard_input.pressed(KeyCode::KeyA) { + velocity.x -= 1.0; + } + if keyboard_input.pressed(KeyCode::KeyD) { + velocity.x += 1.0; + } + + // Need to normalize and scale because otherwise + // diagonal movement would be faster than horizontal or vertical movement. + velocity.0 = velocity.normalize_or_zero() * SPEED; + } +} diff --git a/src/game/physics.rs b/src/game/physics.rs new file mode 100644 index 00000000..14f58599 --- /dev/null +++ b/src/game/physics.rs @@ -0,0 +1,70 @@ +//! Run a very simple physics simulation. + +use bevy::{ + ecs::component::{ComponentHooks, StorageType}, + prelude::*, +}; + +pub(super) fn plugin(app: &mut App) { + // `FixedUpdate` runs before `Update`, so the physics simulation is advanced + // before the player's visual representation is updated. + app.add_systems(FixedUpdate, advance_physics); +} + +/// How many units per second the player should move. +#[derive(Debug, Component, Clone, Copy, PartialEq, Default, Deref, DerefMut)] +pub(crate) struct Velocity(pub(crate) Vec3); + +/// The actual transform of the player in the physics simulation. +/// This is separate from the `Transform`, which is merely a visual +/// representation. +/// The reason for this separation is that physics simulations +/// want to run at a fixed timestep, while rendering should run +/// as fast as possible. The rendering will then interpolate between +/// the previous and current physical translation to get a smooth +/// visual representation of the player. +#[derive(Debug, Clone, Copy, PartialEq, Default, Deref, DerefMut)] +pub(crate) struct PhysicalTransform(pub(crate) Transform); + +/// The value that [`PhysicalTranslation`] had in the last fixed timestep. +/// Used for interpolation when rendering. +#[derive(Debug, Component, Clone, Copy, PartialEq, Default, Deref, DerefMut)] +pub(crate) struct PreviousPhysicalTransform(pub(crate) Transform); + +/// When adding a [`PhysicalTransform`]: +/// - make sure it is always initialized with the same value as the +/// [`Transform`] +/// - add a [`PreviousPhysicalTransform`] as well +impl Component for PhysicalTransform { + const STORAGE_TYPE: StorageType = StorageType::Table; + + fn register_component_hooks(hooks: &mut ComponentHooks) { + hooks.on_add(|mut world, entity, _component_id| { + let rendered_transform = *world.get::(entity).unwrap(); + let mut physical_transform = world.get_mut::(entity).unwrap(); + physical_transform.0 = rendered_transform; + world + .commands() + .entity(entity) + .insert(PreviousPhysicalTransform(rendered_transform)); + }); + } +} + +/// Advance the physics simulation by one fixed timestep. This may run zero or +/// multiple times per frame. +fn advance_physics( + fixed_time: Res