mirror of
https://github.com/Xevion/Pac-Man.git
synced 2025-12-17 20:12:37 -06:00
refactor: move animation components into new systems/animation submodule
This commit is contained in:
82
src/systems/animation/blinking.rs
Normal file
82
src/systems/animation/blinking.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
use bevy_ecs::{
|
||||
component::Component,
|
||||
entity::Entity,
|
||||
query::{Has, With},
|
||||
system::{Commands, Query, Res},
|
||||
};
|
||||
|
||||
use crate::systems::{DeltaTime, Frozen, Hidden, Renderable};
|
||||
|
||||
#[derive(Component, Debug)]
|
||||
pub struct Blinking {
|
||||
pub tick_timer: u32,
|
||||
pub interval_ticks: u32,
|
||||
}
|
||||
|
||||
impl Blinking {
|
||||
pub fn new(interval_ticks: u32) -> Self {
|
||||
Self {
|
||||
tick_timer: 0,
|
||||
interval_ticks,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates blinking entities by toggling their visibility at regular intervals.
|
||||
///
|
||||
/// This system manages entities that have both `Blinking` and `Renderable` components,
|
||||
/// accumulating ticks and toggling visibility when the specified interval is reached.
|
||||
/// Uses integer arithmetic for deterministic behavior.
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn blinking_system(
|
||||
mut commands: Commands,
|
||||
time: Res<DeltaTime>,
|
||||
mut query: Query<(Entity, &mut Blinking, Has<Hidden>, Has<Frozen>), With<Renderable>>,
|
||||
) {
|
||||
for (entity, mut blinking, hidden, frozen) in query.iter_mut() {
|
||||
// If the entity is frozen, blinking is disabled and the entity is unhidden (if it was hidden)
|
||||
if frozen {
|
||||
if hidden {
|
||||
commands.entity(entity).remove::<Hidden>();
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Increase the timer by the delta ticks
|
||||
blinking.tick_timer += time.ticks;
|
||||
|
||||
// Handle zero interval case (immediate toggling)
|
||||
if blinking.interval_ticks == 0 {
|
||||
if time.ticks > 0 {
|
||||
if hidden {
|
||||
commands.entity(entity).remove::<Hidden>();
|
||||
} else {
|
||||
commands.entity(entity).insert(Hidden);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Calculate how many complete intervals have passed
|
||||
let complete_intervals = blinking.tick_timer / blinking.interval_ticks;
|
||||
|
||||
// If no complete intervals have passed, there's nothing to do yet
|
||||
if complete_intervals == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Update the timer to the remainder after complete intervals
|
||||
blinking.tick_timer %= blinking.interval_ticks;
|
||||
|
||||
// Toggle the Hidden component for each complete interval
|
||||
// Since toggling twice is a no-op, we only need to toggle if the count is odd
|
||||
if complete_intervals % 2 == 1 {
|
||||
if hidden {
|
||||
commands.entity(entity).remove::<Hidden>();
|
||||
} else {
|
||||
commands.entity(entity).insert(Hidden);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
104
src/systems/animation/directional.rs
Normal file
104
src/systems/animation/directional.rs
Normal file
@@ -0,0 +1,104 @@
|
||||
use bevy_ecs::{
|
||||
component::Component,
|
||||
query::{Has, Or, With, Without},
|
||||
system::{Query, Res},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
systems::{DeltaTime, Dying, Frozen, LinearAnimation, Looping, Position, Renderable, Velocity},
|
||||
texture::animated::DirectionalTiles,
|
||||
};
|
||||
|
||||
/// Directional animation component with shared timing across all directions
|
||||
#[derive(Component, Clone)]
|
||||
pub struct DirectionalAnimation {
|
||||
pub moving_tiles: DirectionalTiles,
|
||||
pub stopped_tiles: DirectionalTiles,
|
||||
pub current_frame: usize,
|
||||
pub time_bank: u16,
|
||||
pub frame_duration: u16,
|
||||
}
|
||||
|
||||
impl DirectionalAnimation {
|
||||
/// Creates a new directional animation with the given tiles and frame duration
|
||||
pub fn new(moving_tiles: DirectionalTiles, stopped_tiles: DirectionalTiles, frame_duration: u16) -> Self {
|
||||
Self {
|
||||
moving_tiles,
|
||||
stopped_tiles,
|
||||
current_frame: 0,
|
||||
time_bank: 0,
|
||||
frame_duration,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates directional animated entities with synchronized timing across directions.
|
||||
///
|
||||
/// This runs before the render system to update sprites based on current direction and movement state.
|
||||
/// All directions share the same frame timing to ensure perfect synchronization.
|
||||
pub fn directional_render_system(
|
||||
dt: Res<DeltaTime>,
|
||||
mut query: Query<(&Position, &Velocity, &mut DirectionalAnimation, &mut Renderable), Without<Frozen>>,
|
||||
) {
|
||||
let ticks = (dt.seconds * 60.0).round() as u16; // Convert from seconds to ticks at 60 ticks/sec
|
||||
|
||||
for (position, velocity, mut anim, mut renderable) in query.iter_mut() {
|
||||
let stopped = matches!(position, Position::Stopped { .. });
|
||||
|
||||
// Only tick animation when moving to preserve stopped frame
|
||||
if !stopped {
|
||||
// Tick shared animation state
|
||||
anim.time_bank += ticks;
|
||||
while anim.time_bank >= anim.frame_duration {
|
||||
anim.time_bank -= anim.frame_duration;
|
||||
anim.current_frame += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Get tiles for current direction and movement state
|
||||
let tiles = if stopped {
|
||||
anim.stopped_tiles.get(velocity.direction)
|
||||
} else {
|
||||
anim.moving_tiles.get(velocity.direction)
|
||||
};
|
||||
|
||||
if !tiles.is_empty() {
|
||||
let new_tile = tiles.get_tile(anim.current_frame);
|
||||
if renderable.sprite != new_tile {
|
||||
renderable.sprite = new_tile;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// System that updates `Renderable` sprites for entities with `LinearAnimation`.
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn linear_render_system(
|
||||
dt: Res<DeltaTime>,
|
||||
mut query: Query<(&mut LinearAnimation, &mut Renderable, Has<Looping>), Or<(Without<Frozen>, With<Dying>)>>,
|
||||
) {
|
||||
for (mut anim, mut renderable, looping) in query.iter_mut() {
|
||||
if anim.finished {
|
||||
continue;
|
||||
}
|
||||
|
||||
anim.time_bank += dt.ticks as u16;
|
||||
let frames_to_advance = (anim.time_bank / anim.frame_duration) as usize;
|
||||
|
||||
if frames_to_advance == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let total_frames = anim.tiles.len();
|
||||
|
||||
if !looping && anim.current_frame + frames_to_advance >= total_frames {
|
||||
anim.finished = true;
|
||||
anim.current_frame = total_frames - 1;
|
||||
} else {
|
||||
anim.current_frame += frames_to_advance;
|
||||
}
|
||||
|
||||
anim.time_bank %= anim.frame_duration;
|
||||
renderable.sprite = anim.tiles.get_tile(anim.current_frame);
|
||||
}
|
||||
}
|
||||
30
src/systems/animation/linear.rs
Normal file
30
src/systems/animation/linear.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
use crate::texture::animated::TileSequence;
|
||||
use bevy_ecs::component::Component;
|
||||
use bevy_ecs::resource::Resource;
|
||||
|
||||
/// Tag component to mark animations that should loop when they reach the end
|
||||
#[derive(Component, Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct Looping;
|
||||
|
||||
/// Linear animation component for non-directional animations (frightened ghosts)
|
||||
#[derive(Component, Resource, Clone)]
|
||||
pub struct LinearAnimation {
|
||||
pub tiles: TileSequence,
|
||||
pub current_frame: usize,
|
||||
pub time_bank: u16,
|
||||
pub frame_duration: u16,
|
||||
pub finished: bool,
|
||||
}
|
||||
|
||||
impl LinearAnimation {
|
||||
/// Creates a new linear animation with the given tiles and frame duration
|
||||
pub fn new(tiles: TileSequence, frame_duration: u16) -> Self {
|
||||
Self {
|
||||
tiles,
|
||||
current_frame: 0,
|
||||
time_bank: 0,
|
||||
frame_duration,
|
||||
finished: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
7
src/systems/animation/mod.rs
Normal file
7
src/systems/animation/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
mod blinking;
|
||||
mod directional;
|
||||
mod linear;
|
||||
|
||||
pub use self::blinking::*;
|
||||
pub use self::directional::*;
|
||||
pub use self::linear::*;
|
||||
Reference in New Issue
Block a user