Skip to content

Commit

Permalink
Duck animation (#110)
Browse files Browse the repository at this point in the history
Fixes: #94

---------

Co-authored-by: Ben Frankel <[email protected]>
Co-authored-by: Jan Hohenheim <[email protected]>
  • Loading branch information
3 people committed Jul 12, 2024
1 parent a7a594a commit 8c7cd5d
Show file tree
Hide file tree
Showing 7 changed files with 209 additions and 71 deletions.
Binary file modified assets/images/ducky.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
161 changes: 161 additions & 0 deletions src/game/animation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
//! Player sprite animation.
//! This is based on multiple examples and may be very different for your game.
//! - [Sprite flipping](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_flipping.rs)
//! - [Sprite animation](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_animation.rs)
//! - [Timers](https://github.com/bevyengine/bevy/blob/latest/examples/time/timers.rs)

use std::time::Duration;

use bevy::prelude::*;

use super::{audio::sfx::Sfx, movement::MovementController};
use crate::AppSet;

pub(super) fn plugin(app: &mut App) {
// Animate and play sound effects based on controls.
app.register_type::<PlayerAnimation>();
app.add_systems(
Update,
(
update_animation_timer.in_set(AppSet::TickTimers),
(
update_animation_movement,
update_animation_atlas,
trigger_step_sfx,
)
.chain()
.in_set(AppSet::Update),
),
);
}

/// Update the sprite direction and animation state (idling/walking).
fn update_animation_movement(
mut player_query: Query<(&MovementController, &mut Sprite, &mut PlayerAnimation)>,
) {
for (controller, mut sprite, mut animation) in &mut player_query {
let dx = controller.0.x;
if dx != 0.0 {
sprite.flip_x = dx < 0.0;
}

let animation_state = if controller.0 == Vec2::ZERO {
PlayerAnimationState::Idling
} else {
PlayerAnimationState::Walking
};
animation.update_state(animation_state);
}
}

/// Update the animation timer.
fn update_animation_timer(time: Res<Time>, mut query: Query<&mut PlayerAnimation>) {
for mut animation in &mut query {
animation.update_timer(time.delta());
}
}

/// Update the texture atlas to reflect changes in the animation.
fn update_animation_atlas(mut query: Query<(&PlayerAnimation, &mut TextureAtlas)>) {
for (animation, mut atlas) in &mut query {
if animation.changed() {
atlas.index = animation.get_atlas_index();
}
}
}

/// If the player is moving, play a step sound effect synchronized with the animation.
fn trigger_step_sfx(mut commands: Commands, mut step_query: Query<&PlayerAnimation>) {
for animation in &mut step_query {
if animation.state == PlayerAnimationState::Walking
&& animation.changed()
&& (animation.frame == 2 || animation.frame == 5)
{
commands.trigger(Sfx::Step);
}
}
}

/// Component that tracks player's animation state.
/// It is tightly bound to the texture atlas we use.
#[derive(Component, Reflect)]
#[reflect(Component)]
pub struct PlayerAnimation {
timer: Timer,
frame: usize,
state: PlayerAnimationState,
}

#[derive(Reflect, PartialEq)]
pub enum PlayerAnimationState {
Idling,
Walking,
}

impl PlayerAnimation {
/// The number of idle frames.
const IDLE_FRAMES: usize = 2;
/// The duration of each idle frame.
const IDLE_INTERVAL: Duration = Duration::from_millis(500);

fn idling() -> Self {
Self {
timer: Timer::new(Self::IDLE_INTERVAL, TimerMode::Repeating),
frame: 0,
state: PlayerAnimationState::Idling,
}
}

/// The number of walking frames.
const WALKING_FRAMES: usize = 6;
/// The duration of each walking frame.
const WALKING_INTERVAL: Duration = Duration::from_millis(50);

fn walking() -> Self {
Self {
timer: Timer::new(Self::WALKING_INTERVAL, TimerMode::Repeating),
frame: 0,
state: PlayerAnimationState::Walking,
}
}

pub fn new() -> Self {
Self::idling()
}

/// Update animation timers.
pub fn update_timer(&mut self, delta: Duration) {
self.timer.tick(delta);
if !self.timer.finished() {
return;
}
self.frame = (self.frame + 1)
% match self.state {
PlayerAnimationState::Idling => Self::IDLE_FRAMES,
PlayerAnimationState::Walking => Self::WALKING_FRAMES,
};
}

/// Update animation state if it changes.
pub fn update_state(&mut self, state: PlayerAnimationState) {
if self.state != state {
match state {
PlayerAnimationState::Idling => *self = Self::idling(),
PlayerAnimationState::Walking => *self = Self::walking(),
}
}
}

/// Whether animation changed this tick.
pub fn changed(&self) -> bool {
self.timer.finished()
}

/// Return sprite index in the atlas.
pub fn get_atlas_index(&self) -> usize {
match self.state {
PlayerAnimationState::Idling => self.frame,
PlayerAnimationState::Walking => 6 + self.frame,
}
}
}
16 changes: 14 additions & 2 deletions src/game/assets.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
use bevy::{prelude::*, utils::HashMap};
use bevy::{
prelude::*,
render::texture::{ImageLoaderSettings, ImageSampler},
utils::HashMap,
};

#[derive(PartialEq, Eq, Hash, Reflect)]
pub enum ImageAsset {
Expand All @@ -12,7 +16,15 @@ impl ImageAssets {
pub fn new(asset_server: &AssetServer) -> Self {
let mut assets = HashMap::new();

assets.insert(ImageAsset::Ducky, asset_server.load("images/ducky.png"));
assets.insert(
ImageAsset::Ducky,
asset_server.load_with_settings(
"images/ducky.png",
|settings: &mut ImageLoaderSettings| {
settings.sampler = ImageSampler::nearest();
},
),
);

Self(assets)
}
Expand Down
8 changes: 7 additions & 1 deletion src/game/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,17 @@

use bevy::prelude::*;

mod animation;
pub mod assets;
pub mod audio;
mod movement;
pub mod spawn;

pub(super) fn plugin(app: &mut App) {
app.add_plugins((audio::plugin, movement::plugin, spawn::plugin));
app.add_plugins((
animation::plugin,
audio::plugin,
movement::plugin,
spawn::plugin,
));
}
61 changes: 1 addition & 60 deletions src/game/movement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,8 @@
//! If you want to move the player in a smoother way,
//! consider using a [fixed timestep](https://github.com/bevyengine/bevy/blob/latest/examples/movement/physics_in_fixed_timestep.rs).

use std::time::Duration;

use bevy::{prelude::*, window::PrimaryWindow};

use super::audio::sfx::Sfx;
use crate::AppSet;

pub(super) fn plugin(app: &mut App) {
Expand All @@ -26,19 +23,6 @@ pub(super) fn plugin(app: &mut App) {
.chain()
.in_set(AppSet::Update),
);

// Update facing based on controls.
app.add_systems(Update, update_facing.in_set(AppSet::Update));

// Trigger step sound effects based on controls.
app.register_type::<StepSfx>();
app.add_systems(
Update,
(
tick_step_sfx.in_set(AppSet::TickTimers),
trigger_step_sfx.in_set(AppSet::Update),
),
);
}

#[derive(Component, Reflect, Default)]
Expand Down Expand Up @@ -102,54 +86,11 @@ fn wrap_within_window(
window_query: Query<&Window, With<PrimaryWindow>>,
mut wrap_query: Query<&mut Transform, With<WrapWithinWindow>>,
) {
let size = window_query.single().size() + 50.0;
let size = window_query.single().size() + 256.0;
let half_size = size / 2.0;
for mut transform in &mut wrap_query {
let position = transform.translation.xy();
let wrapped = (position + half_size).rem_euclid(size) - half_size;
transform.translation = wrapped.extend(transform.translation.z);
}
}

fn update_facing(mut player_query: Query<(&MovementController, &mut Sprite)>) {
for (controller, mut sprite) in &mut player_query {
let dx = controller.0.x;
if dx != 0.0 {
sprite.flip_x = dx < 0.0;
}
}
}

/// Time between walk sound effects.
#[derive(Component, Reflect)]
#[reflect(Component)]
pub struct StepSfx {
pub cooldown_timer: Timer,
}

impl StepSfx {
pub fn new(cooldown: Duration) -> Self {
let mut cooldown_timer = Timer::new(cooldown, TimerMode::Once);
cooldown_timer.set_elapsed(cooldown);
Self { cooldown_timer }
}
}

fn tick_step_sfx(time: Res<Time>, mut step_query: Query<&mut StepSfx>) {
for mut step in &mut step_query {
step.cooldown_timer.tick(time.delta());
}
}

/// If the player is moving, play a step sound effect.
fn trigger_step_sfx(
mut commands: Commands,
mut step_query: Query<(&MovementController, &mut StepSfx)>,
) {
for (controller, mut step) in &mut step_query {
if step.cooldown_timer.finished() && controller.0 != Vec2::ZERO {
step.cooldown_timer.reset();
commands.trigger(Sfx::Step);
}
}
}
28 changes: 22 additions & 6 deletions src/game/spawn/player.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
//! Spawn the player.

use std::time::Duration;

use bevy::prelude::*;

use crate::{
game::{
animation::PlayerAnimation,
assets::{ImageAsset, ImageAssets},
movement::{Movement, MovementController, StepSfx, WrapWithinWindow},
movement::{Movement, MovementController, WrapWithinWindow},
},
screen::Screen,
};
Expand All @@ -24,19 +23,36 @@ pub struct SpawnPlayer;
#[reflect(Component)]
pub struct Player;

fn spawn_player(_trigger: Trigger<SpawnPlayer>, mut commands: Commands, images: Res<ImageAssets>) {
fn spawn_player(
_trigger: Trigger<SpawnPlayer>,
mut commands: Commands,
images: Res<ImageAssets>,
mut texture_atlas_layouts: ResMut<Assets<TextureAtlasLayout>>,
) {
// A texture atlas is a way to split one image with a grid into multiple sprites.
// By attaching it to a [`SpriteBundle`] and providing an index, we can specify which section of the image we want to see.
// We will use this to animate our player character. You can learn more about texture atlases in this example:
// https://github.com/bevyengine/bevy/blob/latest/examples/2d/texture_atlas.rs
let layout = TextureAtlasLayout::from_grid(UVec2::splat(32), 6, 2, Some(UVec2::splat(1)), None);
let texture_atlas_layout = texture_atlas_layouts.add(layout);
let player_animation = PlayerAnimation::new();

commands.spawn((
Name::new("Player"),
Player,
SpriteBundle {
texture: images[&ImageAsset::Ducky].clone_weak(),
transform: Transform::from_scale(Vec3::splat(0.5)),
transform: Transform::from_scale(Vec2::splat(8.0).extend(1.0)),
..Default::default()
},
TextureAtlas {
layout: texture_atlas_layout.clone(),
index: player_animation.get_atlas_index(),
},
MovementController::default(),
Movement { speed: 420.0 },
WrapWithinWindow,
StepSfx::new(Duration::from_millis(250)),
player_animation,
StateScoped(Screen::Playing),
));
}
6 changes: 4 additions & 2 deletions src/screen/splash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ pub(super) fn plugin(app: &mut App) {
// Animate splash screen.
app.add_systems(
Update,
(tick_fade_in_out, apply_fade_in_out)
.chain()
(
tick_fade_in_out.in_set(AppSet::TickTimers),
apply_fade_in_out.in_set(AppSet::Update),
)
.run_if(in_state(Screen::Splash)),
);

Expand Down

0 comments on commit 8c7cd5d

Please sign in to comment.