Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add player movement code #70

Merged
merged 31 commits into from
Jul 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
a9a4d22
Move player spawning to game
janhohenheim Jul 8, 2024
eef00af
Add movement mod
janhohenheim Jul 8, 2024
9e6ef2a
Merge remote-tracking branch 'origin/main' into player-movement
janhohenheim Jul 8, 2024
f4bdddc
Add velocity
janhohenheim Jul 8, 2024
7dec08e
Use Bevy example code
janhohenheim Jul 8, 2024
5d652eb
Split stuff into files
janhohenheim Jul 8, 2024
d87c8d9
Add component hook, make translation full transform
janhohenheim Jul 8, 2024
54e79f7
Run cargo fmt
janhohenheim Jul 8, 2024
3433c36
Run stable cargo fmt
janhohenheim Jul 8, 2024
22b9802
Merge branch 'main' into player-movement
janhohenheim Jul 8, 2024
a97fea8
Remove whitespace
janhohenheim Jul 8, 2024
adf3708
Give hint for alternative hierarchy
janhohenheim Jul 8, 2024
ffb0e52
Improve hint
janhohenheim Jul 8, 2024
cd4934f
Split spawning into files
janhohenheim Jul 8, 2024
cbb6409
Add disclaimer
janhohenheim Jul 8, 2024
a1e6d25
Add alternatives
janhohenheim Jul 8, 2024
13ecc73
Merge branch 'main' into player-movement
janhohenheim Jul 8, 2024
7945346
Remove authority
janhohenheim Jul 8, 2024
bfd64af
Reorder plugins
janhohenheim Jul 8, 2024
d1b5de8
Improve phrasing
janhohenheim Jul 8, 2024
9c83090
Simplify code
janhohenheim Jul 8, 2024
a7f89b5
Fix faulty interpolation
janhohenheim Jul 8, 2024
e7be801
Add docs
janhohenheim Jul 8, 2024
a4c1565
Remove unnecessary expliciteness
janhohenheim Jul 8, 2024
d2d568a
Make ducky more glorious
janhohenheim Jul 8, 2024
7ae0325
Scope input to player entity
janhohenheim Jul 8, 2024
a03954d
Merge branch 'main' into player-movement
janhohenheim Jul 8, 2024
4899d2f
Fix duplicate on_add logic
janhohenheim Jul 8, 2024
9176856
Fix missing system set
janhohenheim Jul 8, 2024
88a15dd
Fix order
janhohenheim Jul 8, 2024
7db0c67
Run cargo fmt
janhohenheim Jul 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions src/core/dev.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 29 additions & 1 deletion src/game/mod.rs
Original file line number Diff line number Diff line change
@@ -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,
}
44 changes: 44 additions & 0 deletions src/game/movement.rs
Original file line number Diff line number Diff line change
@@ -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<ButtonInput<KeyCode>>,
mut query: Query<&mut Velocity, With<Player>>,
) {
/// 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?"
janhohenheim marked this conversation as resolved.
Show resolved Hide resolved
/// 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) {
janhohenheim marked this conversation as resolved.
Show resolved Hide resolved
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;
}
}
70 changes: 70 additions & 0 deletions src/game/physics.rs
Original file line number Diff line number Diff line change
@@ -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);
janhohenheim marked this conversation as resolved.
Show resolved Hide resolved

/// 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::<Transform>(entity).unwrap();
let mut physical_transform = world.get_mut::<PhysicalTransform>(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<Time>,
mut query: Query<(
&mut PhysicalTransform,
&mut PreviousPhysicalTransform,
&Velocity,
)>,
) {
for (mut current_physical_transform, mut previous_physical_transform, velocity) in
query.iter_mut()
{
previous_physical_transform.0 = current_physical_transform.0;
current_physical_transform.translation += velocity.0 * fixed_time.delta_seconds();
}
}
40 changes: 40 additions & 0 deletions src/game/render.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//! Handle how the physically simulated world should be rendered.

use bevy::prelude::*;

use super::{
physics::{PhysicalTransform, PreviousPhysicalTransform},
GameSystem,
};

pub(super) fn plugin(app: &mut App) {
app.add_systems(
Update,
update_rendered_transform.in_set(GameSystem::UpdateTransform),
);
}

/// Interpolate between the previous and current physical translation to get a
/// smooth visual representation of the player. This is a tradeoff, as it will
/// also make visual representation lag slightly behind the actual physics
/// simulation.
fn update_rendered_transform(
fixed_time: Res<Time<Fixed>>,
mut query: Query<(
&mut Transform,
&PhysicalTransform,
&PreviousPhysicalTransform,
)>,
) {
for (mut transform, current_physical_transform, previous_physical_transform) in query.iter_mut()
{
let previous = previous_physical_transform.translation;
let current = current_physical_transform.translation;
// The overstep fraction is a value between 0 and 1 that tells us how far we are
// between two fixed timesteps.
let alpha = fixed_time.overstep_fraction();

let rendered_translation = previous.lerp(current, alpha);
transform.translation = rendered_translation;
}
}
18 changes: 18 additions & 0 deletions src/game/spawn/level.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//! Spawn the main level by triggering other observers.

use bevy::prelude::*;

use super::player::SpawnPlayer;

pub(super) fn plugin(app: &mut App) {
app.observe(spawn_level);
}

#[derive(Debug, Event)]
pub(crate) struct SpawnLevel;

fn spawn_level(_trigger: Trigger<SpawnLevel>, mut commands: Commands) {
// The only thing we have in our level is a player,
// but add things like walls etc. here.
commands.trigger(SpawnPlayer);
}
12 changes: 12 additions & 0 deletions src/game/spawn/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//! Handles spawning of entities. Here, we are using
//! [observers](https://docs.rs/bevy/latest/bevy/ecs/prelude/struct.Observer.html)
//! for this, but you could also use `Events<E>` or `Commands`.

use bevy::prelude::*;

pub(crate) mod level;
pub(crate) mod player;

pub(super) fn plugin(app: &mut App) {
app.add_plugins((level::plugin, player::plugin));
}
40 changes: 40 additions & 0 deletions src/game/spawn/player.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//! Spawn the player.

use bevy::prelude::*;

use crate::{
game::physics::{PhysicalTransform, Velocity},
screen::Screen,
};

pub(super) fn plugin(app: &mut App) {
app.observe(spawn_player);
}

#[derive(Debug, Event)]
benfrankel marked this conversation as resolved.
Show resolved Hide resolved
pub(crate) struct SpawnPlayer;

#[derive(Debug, Component, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) struct Player;

fn spawn_player(
_trigger: Trigger<SpawnPlayer>,
mut commands: Commands,
asset_server: Res<AssetServer>,
) {
commands.spawn((
Name::new("Player"),
Player,
SpriteBundle {
texture: asset_server.load("ducky.png"),
transform: Transform::from_scale(Vec3::splat(0.5)),
..Default::default()
},
Velocity::default(),
// If your physics engine of choice uses `Transform` directly,
// a good hierarchy to follow instead is to have a `Player` root entity
// with the physical transform and the rendered transform as individual children.
PhysicalTransform::default(),
StateScoped(Screen::Playing),
benfrankel marked this conversation as resolved.
Show resolved Hide resolved
));
}
11 changes: 3 additions & 8 deletions src/screen/playing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
use bevy::{input::common_conditions::input_just_pressed, prelude::*};

use super::Screen;
use crate::game::spawn::level::SpawnLevel;

pub(super) fn plugin(app: &mut App) {
app.add_systems(OnEnter(Screen::Playing), enter_playing)
Expand All @@ -15,14 +16,8 @@ pub(super) fn plugin(app: &mut App) {
);
}

fn enter_playing(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn((
SpriteBundle {
texture: asset_server.load("ducky.png"),
..Default::default()
},
StateScoped(Screen::Playing),
));
fn enter_playing(mut commands: Commands) {
commands.trigger(SpawnLevel);
}

// TODO: Reset camera transform here.
Expand Down