use bevy::prelude::*;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::{HashMap, HashSet};
use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, Reflect)]
#[serde(rename_all = "lowercase")]
pub enum LoopMode {
#[default]
Loop,
Once,
PingPong,
}
impl LoopMode {
pub fn display_name(&self) -> &'static str {
match self {
LoopMode::Loop => "Loop",
LoopMode::Once => "Once",
LoopMode::PingPong => "Ping-Pong",
}
}
pub fn all() -> &'static [LoopMode] {
&[LoopMode::Loop, LoopMode::Once, LoopMode::PingPong]
}
}
fn default_volume() -> f32 {
1.0
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, Reflect, PartialEq)]
#[serde(tag = "type", content = "data")]
pub enum TriggerPayload {
#[default]
None,
Sound {
path: String,
#[serde(default = "default_volume")]
volume: f32,
},
Particle {
effect: String,
#[serde(default)]
offset: (f32, f32),
},
Custom {
event_name: String,
#[serde(default)]
#[reflect(ignore)]
params: HashMap<String, Value>,
},
}
impl TriggerPayload {
pub fn display_name(&self) -> &'static str {
match self {
TriggerPayload::None => "None",
TriggerPayload::Sound { .. } => "Sound",
TriggerPayload::Particle { .. } => "Particle",
TriggerPayload::Custom { .. } => "Custom",
}
}
pub fn all_types() -> &'static [&'static str] {
&["None", "Sound", "Particle", "Custom"]
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Reflect)]
pub struct AnimationTrigger {
#[serde(default = "Uuid::new_v4")]
pub id: Uuid,
#[serde(default)]
pub name: String,
pub time_ms: u32,
#[serde(default)]
pub payload: TriggerPayload,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub color: Option<[u8; 3]>,
}
impl AnimationTrigger {
pub fn new(name: impl Into<String>, time_ms: u32) -> Self {
Self {
id: Uuid::new_v4(),
name: name.into(),
time_ms,
payload: TriggerPayload::None,
color: None,
}
}
pub fn with_payload(name: impl Into<String>, time_ms: u32, payload: TriggerPayload) -> Self {
Self {
id: Uuid::new_v4(),
name: name.into(),
time_ms,
payload,
color: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Reflect, Default)]
#[serde(rename_all = "lowercase")]
pub enum WindowPhase {
#[default]
Begin,
Tick,
End,
}
impl WindowPhase {
pub fn display_name(&self) -> &'static str {
match self {
WindowPhase::Begin => "Begin",
WindowPhase::Tick => "Tick",
WindowPhase::End => "End",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Reflect)]
pub struct AnimationWindow {
#[serde(default = "Uuid::new_v4")]
pub id: Uuid,
#[serde(default)]
pub name: String,
pub start_ms: u32,
pub end_ms: u32,
#[serde(default)]
pub payload: TriggerPayload,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub color: Option<[u8; 3]>,
}
impl AnimationWindow {
pub fn new(name: impl Into<String>, start_ms: u32, end_ms: u32) -> Self {
Self {
id: Uuid::new_v4(),
name: name.into(),
start_ms,
end_ms,
payload: TriggerPayload::None,
color: None,
}
}
pub fn with_payload(
name: impl Into<String>,
start_ms: u32,
end_ms: u32,
payload: TriggerPayload,
) -> Self {
Self {
id: Uuid::new_v4(),
name: name.into(),
start_ms,
end_ms,
payload,
color: None,
}
}
pub fn is_active_at(&self, time_ms: u32) -> bool {
time_ms >= self.start_ms && time_ms < self.end_ms
}
pub fn duration_ms(&self) -> u32 {
self.end_ms.saturating_sub(self.start_ms)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, Reflect)]
pub struct AnimationDef {
pub frames: Vec<usize>,
#[serde(default = "default_frame_duration")]
pub frame_duration_ms: u32,
#[serde(default)]
pub loop_mode: LoopMode,
#[serde(default)]
#[reflect(ignore)]
pub triggers: Vec<AnimationTrigger>,
#[serde(default)]
#[reflect(ignore)]
pub windows: Vec<AnimationWindow>,
}
fn default_frame_duration() -> u32 {
100
}
impl AnimationDef {
pub fn new(frames: Vec<usize>, frame_duration_ms: u32, loop_mode: LoopMode) -> Self {
Self {
frames,
frame_duration_ms,
loop_mode,
triggers: Vec::new(),
windows: Vec::new(),
}
}
pub fn total_duration_ms(&self) -> u32 {
self.frames.len() as u32 * self.frame_duration_ms
}
pub fn frame_at_time(&self, time_ms: u32) -> Option<usize> {
if self.frames.is_empty() {
return None;
}
let total_duration = self.total_duration_ms();
if total_duration == 0 {
return self.frames.first().copied();
}
let loop_time = match self.loop_mode {
LoopMode::Once => time_ms.min(total_duration.saturating_sub(1)),
LoopMode::Loop => time_ms % total_duration,
LoopMode::PingPong => {
let double_duration = total_duration * 2;
let t = time_ms % double_duration;
if t < total_duration {
t
} else {
double_duration - t
}
}
};
let frame_index = (loop_time / self.frame_duration_ms) as usize;
self.frames
.get(frame_index.min(self.frames.len() - 1))
.copied()
}
pub fn triggers_in_range(&self, prev_ms: u32, current_ms: u32) -> Vec<&AnimationTrigger> {
self.triggers
.iter()
.filter(|t| t.time_ms > prev_ms && t.time_ms <= current_ms)
.collect()
}
pub fn active_windows_at(&self, time_ms: u32) -> Vec<&AnimationWindow> {
self.windows
.iter()
.filter(|w| w.is_active_at(time_ms))
.collect()
}
pub fn frame_to_time_ms(&self, frame_index: usize) -> u32 {
frame_index as u32 * self.frame_duration_ms
}
pub fn time_to_frame(&self, time_ms: u32) -> usize {
if self.frame_duration_ms == 0 {
return 0;
}
(time_ms / self.frame_duration_ms) as usize
}
pub fn add_trigger(&mut self, trigger: AnimationTrigger) {
self.triggers.push(trigger);
}
pub fn add_window(&mut self, window: AnimationWindow) {
self.windows.push(window);
}
pub fn remove_trigger(&mut self, id: Uuid) -> bool {
let len = self.triggers.len();
self.triggers.retain(|t| t.id != id);
self.triggers.len() != len
}
pub fn remove_window(&mut self, id: Uuid) -> bool {
let len = self.windows.len();
self.windows.retain(|w| w.id != id);
self.windows.len() != len
}
pub fn get_trigger(&self, id: Uuid) -> Option<&AnimationTrigger> {
self.triggers.iter().find(|t| t.id == id)
}
pub fn get_trigger_mut(&mut self, id: Uuid) -> Option<&mut AnimationTrigger> {
self.triggers.iter_mut().find(|t| t.id == id)
}
pub fn get_window(&self, id: Uuid) -> Option<&AnimationWindow> {
self.windows.iter().find(|w| w.id == id)
}
pub fn get_window_mut(&mut self, id: Uuid) -> Option<&mut AnimationWindow> {
self.windows.iter_mut().find(|w| w.id == id)
}
}
fn default_pivot() -> f32 {
0.5
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, Asset, Reflect)]
pub struct SpriteData {
#[serde(default = "Uuid::new_v4")]
pub id: Uuid,
#[serde(default)]
pub name: String,
pub sheet_path: String,
pub frame_width: u32,
pub frame_height: u32,
#[serde(default)]
pub columns: u32,
#[serde(default)]
pub rows: u32,
#[serde(default = "default_pivot")]
pub pivot_x: f32,
#[serde(default = "default_pivot")]
pub pivot_y: f32,
#[serde(default)]
#[reflect(ignore)]
pub animations: HashMap<String, AnimationDef>,
}
impl SpriteData {
pub fn new(sheet_path: impl Into<String>, frame_width: u32, frame_height: u32) -> Self {
Self {
id: Uuid::new_v4(),
name: String::new(),
sheet_path: sheet_path.into(),
frame_width,
frame_height,
columns: 0,
rows: 0,
pivot_x: 0.5,
pivot_y: 0.5,
animations: HashMap::new(),
}
}
pub fn new_named(
name: impl Into<String>,
sheet_path: impl Into<String>,
frame_width: u32,
frame_height: u32,
) -> Self {
Self {
id: Uuid::new_v4(),
name: name.into(),
sheet_path: sheet_path.into(),
frame_width,
frame_height,
columns: 0,
rows: 0,
pivot_x: 0.5,
pivot_y: 0.5,
animations: HashMap::new(),
}
}
pub fn total_frames(&self) -> usize {
(self.columns * self.rows) as usize
}
pub fn frame_to_grid(&self, frame: usize) -> (u32, u32) {
if self.columns == 0 {
return (0, 0);
}
let col = (frame as u32) % self.columns;
let row = (frame as u32) / self.columns;
(col, row)
}
pub fn grid_to_frame(&self, col: u32, row: u32) -> usize {
(row * self.columns + col) as usize
}
pub fn frame_uv(&self, frame: usize) -> (f32, f32, f32, f32) {
let (col, row) = self.frame_to_grid(frame);
let u = col as f32 / self.columns.max(1) as f32;
let v = row as f32 / self.rows.max(1) as f32;
let w = 1.0 / self.columns.max(1) as f32;
let h = 1.0 / self.rows.max(1) as f32;
(u, v, w, h)
}
pub fn add_animation(&mut self, name: impl Into<String>, animation: AnimationDef) {
self.animations.insert(name.into(), animation);
}
pub fn get_animation(&self, name: &str) -> Option<&AnimationDef> {
self.animations.get(name)
}
pub fn animation_names(&self) -> impl Iterator<Item = &str> {
self.animations.keys().map(|s| s.as_str())
}
pub fn update_from_image_size(&mut self, image_width: u32, image_height: u32) {
if self.frame_width > 0 {
self.columns = image_width / self.frame_width;
}
if self.frame_height > 0 {
self.rows = image_height / self.frame_height;
}
}
}
#[derive(Component, Debug, Clone, Default, Reflect)]
#[require(WindowTracker)]
pub struct AnimatedSprite {
#[reflect(ignore)]
pub sprite_data: Handle<SpriteData>,
pub current_animation: Option<String>,
pub elapsed_ms: u32,
pub playing: bool,
}
impl AnimatedSprite {
pub fn new(sprite_data: Handle<SpriteData>) -> Self {
Self {
sprite_data,
current_animation: None,
elapsed_ms: 0,
playing: false,
}
}
pub fn play(&mut self, animation_name: impl Into<String>) {
let name = animation_name.into();
if self.current_animation.as_ref() != Some(&name) {
self.current_animation = Some(name);
self.elapsed_ms = 0;
}
self.playing = true;
}
pub fn stop(&mut self) {
self.playing = false;
}
pub fn reset(&mut self) {
self.elapsed_ms = 0;
}
}
#[derive(Component, Debug, Clone, Default)]
pub struct WindowTracker {
pub active_windows: HashSet<Uuid>,
pub prev_elapsed_ms: u32,
}
#[derive(Message, Debug, Clone)]
pub struct AnimationTriggerEvent {
pub entity: Entity,
pub animation: String,
pub trigger_id: Uuid,
pub trigger_name: String,
pub payload: TriggerPayload,
}
#[derive(Message, Debug, Clone)]
pub struct AnimationWindowEvent {
pub entity: Entity,
pub animation: String,
pub window_id: Uuid,
pub window_name: String,
pub phase: WindowPhase,
pub payload: TriggerPayload,
pub progress: f32,
}
#[derive(Message, Debug, Clone)]
pub struct AnimationSoundEvent {
pub entity: Entity,
pub path: String,
pub volume: f32,
}
#[derive(Message, Debug, Clone)]
pub struct AnimationParticleEvent {
pub entity: Entity,
pub effect: String,
pub offset: (f32, f32),
}
#[derive(Message, Debug, Clone)]
pub struct AnimationCustomEvent {
pub entity: Entity,
pub event_name: String,
pub params: HashMap<String, Value>,
}
#[derive(EntityEvent, Debug, Clone)]
pub struct AnimationTriggered {
pub entity: Entity,
pub name: String,
pub trigger_id: Uuid,
pub animation: String,
pub time_ms: u32,
pub payload: TriggerPayload,
}
#[derive(EntityEvent, Debug, Clone)]
pub struct AnimationWindowChanged {
pub entity: Entity,
pub name: String,
pub window_id: Uuid,
pub animation: String,
pub phase: WindowPhase,
pub progress: f32,
pub payload: TriggerPayload,
}
use std::marker::PhantomData;
pub trait AnimationTriggerType: EntityEvent + Clone + Send + Sync + 'static
where
for<'a> <Self as Event>::Trigger<'a>: Default,
{
fn trigger_name() -> &'static str;
fn from_params(params: &HashMap<String, Value>) -> Option<Self>;
}
pub trait AnimationWindowType: EntityEvent + Clone + Send + Sync + 'static
where
for<'a> <Self as Event>::Trigger<'a>: Default,
{
fn window_name() -> &'static str;
fn from_params(params: &HashMap<String, Value>) -> Option<Self>;
}
trait TriggerDispatcher: Send + Sync {
fn dispatch(
&self,
commands: &mut Commands,
entity: Entity,
animation: &str,
params: &HashMap<String, Value>,
);
}
struct TypedTriggerDispatcher<T: AnimationTriggerType>
where
for<'a> <T as Event>::Trigger<'a>: Default,
{
_marker: PhantomData<T>,
}
impl<T: AnimationTriggerType> TriggerDispatcher for TypedTriggerDispatcher<T>
where
for<'a> <T as Event>::Trigger<'a>: Default,
{
fn dispatch(
&self,
commands: &mut Commands,
entity: Entity,
_animation: &str,
params: &HashMap<String, Value>,
) {
if let Some(payload) = T::from_params(params) {
commands.entity(entity).trigger(move |_| payload);
}
}
}
#[derive(Resource, Default)]
pub struct AnimationTriggerRegistry {
dispatchers: HashMap<String, Box<dyn TriggerDispatcher>>,
}
impl AnimationTriggerRegistry {
pub fn register<T: AnimationTriggerType>(&mut self)
where
for<'a> <T as Event>::Trigger<'a>: Default,
{
self.dispatchers.insert(
T::trigger_name().to_string(),
Box::new(TypedTriggerDispatcher::<T> {
_marker: PhantomData,
}),
);
}
pub fn is_registered(&self, name: &str) -> bool {
self.dispatchers.contains_key(name)
}
pub fn dispatch(
&self,
commands: &mut Commands,
entity: Entity,
animation: &str,
event_name: &str,
params: &HashMap<String, Value>,
) {
if let Some(dispatcher) = self.dispatchers.get(event_name) {
dispatcher.dispatch(commands, entity, animation, params);
}
}
}
trait WindowDispatcher: Send + Sync {
fn dispatch(
&self,
commands: &mut Commands,
entity: Entity,
animation: &str,
phase: WindowPhase,
progress: f32,
params: &HashMap<String, Value>,
);
}
struct TypedWindowDispatcher<T: AnimationWindowType>
where
for<'a> <T as Event>::Trigger<'a>: Default,
{
_marker: PhantomData<T>,
}
impl<T: AnimationWindowType> WindowDispatcher for TypedWindowDispatcher<T>
where
for<'a> <T as Event>::Trigger<'a>: Default,
{
fn dispatch(
&self,
commands: &mut Commands,
entity: Entity,
_animation: &str,
_phase: WindowPhase,
_progress: f32,
params: &HashMap<String, Value>,
) {
if let Some(payload) = T::from_params(params) {
commands.entity(entity).trigger(move |_| payload);
}
}
}
#[derive(Resource, Default)]
pub struct AnimationWindowRegistry {
dispatchers: HashMap<String, Box<dyn WindowDispatcher>>,
}
impl AnimationWindowRegistry {
pub fn register<T: AnimationWindowType>(&mut self)
where
for<'a> <T as Event>::Trigger<'a>: Default,
{
self.dispatchers.insert(
T::window_name().to_string(),
Box::new(TypedWindowDispatcher::<T> {
_marker: PhantomData,
}),
);
}
pub fn is_registered(&self, name: &str) -> bool {
self.dispatchers.contains_key(name)
}
pub fn dispatch(
&self,
commands: &mut Commands,
entity: Entity,
animation: &str,
phase: WindowPhase,
progress: f32,
event_name: &str,
params: &HashMap<String, Value>,
) {
if let Some(dispatcher) = self.dispatchers.get(event_name) {
dispatcher.dispatch(commands, entity, animation, phase, progress, params);
}
}
}
pub trait AnimationEventExt {
fn register_animation_trigger<T: AnimationTriggerType>(&mut self) -> &mut Self
where
for<'a> <T as Event>::Trigger<'a>: Default;
fn register_animation_window<T: AnimationWindowType>(&mut self) -> &mut Self
where
for<'a> <T as Event>::Trigger<'a>: Default;
}
impl AnimationEventExt for App {
fn register_animation_trigger<T: AnimationTriggerType>(&mut self) -> &mut Self
where
for<'a> <T as Event>::Trigger<'a>: Default,
{
if !self.world().contains_resource::<AnimationTriggerRegistry>() {
self.insert_resource(AnimationTriggerRegistry::default());
}
self.world_mut()
.resource_mut::<AnimationTriggerRegistry>()
.register::<T>();
self
}
fn register_animation_window<T: AnimationWindowType>(&mut self) -> &mut Self
where
for<'a> <T as Event>::Trigger<'a>: Default,
{
if !self.world().contains_resource::<AnimationWindowRegistry>() {
self.insert_resource(AnimationWindowRegistry::default());
}
self.world_mut()
.resource_mut::<AnimationWindowRegistry>()
.register::<T>();
self
}
}
pub struct SpriteAnimationPlugin;
impl Plugin for SpriteAnimationPlugin {
fn build(&self, app: &mut App) {
app.init_asset::<SpriteData>()
.register_type::<LoopMode>()
.register_type::<AnimationDef>()
.register_type::<SpriteData>()
.register_type::<AnimatedSprite>()
.register_type::<TriggerPayload>()
.register_type::<AnimationTrigger>()
.register_type::<AnimationWindow>()
.register_type::<WindowPhase>()
.add_message::<AnimationTriggerEvent>()
.add_message::<AnimationWindowEvent>()
.add_message::<AnimationSoundEvent>()
.add_message::<AnimationParticleEvent>()
.add_message::<AnimationCustomEvent>()
.init_resource::<AnimationTriggerRegistry>()
.init_resource::<AnimationWindowRegistry>()
.add_systems(Update, update_animated_sprites);
}
}
fn update_animated_sprites(
mut commands: Commands,
time: Res<Time>,
sprite_assets: Res<Assets<SpriteData>>,
trigger_registry: Res<AnimationTriggerRegistry>,
window_registry: Res<AnimationWindowRegistry>,
mut query: Query<(
Entity,
&mut AnimatedSprite,
&mut Sprite,
Option<&mut WindowTracker>,
)>,
mut trigger_events: MessageWriter<AnimationTriggerEvent>,
mut window_events: MessageWriter<AnimationWindowEvent>,
mut sound_events: MessageWriter<AnimationSoundEvent>,
mut particle_events: MessageWriter<AnimationParticleEvent>,
mut custom_events: MessageWriter<AnimationCustomEvent>,
) {
for (entity, mut animated, mut sprite, tracker_opt) in query.iter_mut() {
if !animated.playing {
continue;
}
let Some(sprite_data) = sprite_assets.get(&animated.sprite_data) else {
continue;
};
let Some(animation_name) = animated.current_animation.clone() else {
continue;
};
let Some(animation) = sprite_data.get_animation(&animation_name) else {
continue;
};
let prev_elapsed = animated.elapsed_ms;
animated.elapsed_ms += (time.delta_secs() * 1000.0) as u32;
let current_elapsed = animated.elapsed_ms;
let total_duration = animation.total_duration_ms();
if total_duration > 0 {
let (check_start, check_end, wrapped) = match animation.loop_mode {
LoopMode::Loop => {
if current_elapsed >= total_duration && prev_elapsed < total_duration {
(prev_elapsed, total_duration, true)
} else {
(
prev_elapsed % total_duration,
current_elapsed % total_duration,
false,
)
}
}
LoopMode::PingPong => {
let double_duration = total_duration * 2;
let t = current_elapsed % double_duration;
if t < total_duration {
let prev_t = prev_elapsed % double_duration;
if prev_t < total_duration {
(prev_t, t, false)
} else {
(0, t, false)
}
} else {
(0, 0, false)
}
}
LoopMode::Once => (prev_elapsed, current_elapsed.min(total_duration), false),
};
for trigger in animation.triggers_in_range(check_start, check_end) {
fire_trigger(
&mut commands,
entity,
&animation_name,
trigger,
&trigger_registry,
&mut trigger_events,
&mut sound_events,
&mut particle_events,
&mut custom_events,
);
}
if wrapped {
let wrapped_time = current_elapsed % total_duration;
for trigger in animation.triggers_in_range(0, wrapped_time) {
fire_trigger(
&mut commands,
entity,
&animation_name,
trigger,
&trigger_registry,
&mut trigger_events,
&mut sound_events,
&mut particle_events,
&mut custom_events,
);
}
}
if let Some(mut tracker) = tracker_opt {
let current_time = match animation.loop_mode {
LoopMode::Loop => current_elapsed % total_duration,
LoopMode::PingPong => {
let double_duration = total_duration * 2;
let t = current_elapsed % double_duration;
if t < total_duration {
t
} else {
double_duration - t
}
}
LoopMode::Once => current_elapsed.min(total_duration),
};
process_windows(
&mut commands,
entity,
&animation_name,
animation,
current_time,
&mut tracker,
&window_registry,
&mut window_events,
&mut sound_events,
&mut particle_events,
&mut custom_events,
);
tracker.prev_elapsed_ms = current_elapsed;
}
}
let Some(frame_index) = animation.frame_at_time(animated.elapsed_ms) else {
continue;
};
let (u, v, w, h) = sprite_data.frame_uv(frame_index);
if let Some(ref mut rect) = sprite.rect {
let pixel_x = (u * sprite_data.columns as f32 * sprite_data.frame_width as f32) as u32;
let pixel_y = (v * sprite_data.rows as f32 * sprite_data.frame_height as f32) as u32;
let pixel_w = (w * sprite_data.columns as f32 * sprite_data.frame_width as f32) as u32;
let pixel_h = (h * sprite_data.rows as f32 * sprite_data.frame_height as f32) as u32;
*rect = bevy::math::Rect::new(
pixel_x as f32,
pixel_y as f32,
(pixel_x + pixel_w) as f32,
(pixel_y + pixel_h) as f32,
);
}
if animation.loop_mode == LoopMode::Once {
if animated.elapsed_ms >= total_duration {
animated.playing = false;
}
}
}
}
fn fire_trigger(
commands: &mut Commands,
entity: Entity,
animation_name: &str,
trigger: &AnimationTrigger,
trigger_registry: &AnimationTriggerRegistry,
trigger_events: &mut MessageWriter<AnimationTriggerEvent>,
sound_events: &mut MessageWriter<AnimationSoundEvent>,
particle_events: &mut MessageWriter<AnimationParticleEvent>,
custom_events: &mut MessageWriter<AnimationCustomEvent>,
) {
trigger_events.write(AnimationTriggerEvent {
entity,
animation: animation_name.to_string(),
trigger_id: trigger.id,
trigger_name: trigger.name.clone(),
payload: trigger.payload.clone(),
});
commands.trigger(AnimationTriggered {
entity,
name: trigger.name.clone(),
trigger_id: trigger.id,
animation: animation_name.to_string(),
time_ms: trigger.time_ms,
payload: trigger.payload.clone(),
});
fire_payload_events(
commands,
entity,
animation_name,
&trigger.payload,
trigger_registry,
sound_events,
particle_events,
custom_events,
);
}
fn process_windows(
commands: &mut Commands,
entity: Entity,
animation_name: &str,
animation: &AnimationDef,
current_time: u32,
tracker: &mut WindowTracker,
window_registry: &AnimationWindowRegistry,
window_events: &mut MessageWriter<AnimationWindowEvent>,
sound_events: &mut MessageWriter<AnimationSoundEvent>,
particle_events: &mut MessageWriter<AnimationParticleEvent>,
custom_events: &mut MessageWriter<AnimationCustomEvent>,
) {
for window in &animation.windows {
let was_active = tracker.active_windows.contains(&window.id);
let is_active = window.is_active_at(current_time);
let progress = if is_active && window.duration_ms() > 0 {
(current_time.saturating_sub(window.start_ms)) as f32 / window.duration_ms() as f32
} else {
0.0
};
if !was_active && is_active {
tracker.active_windows.insert(window.id);
fire_window_event(
commands,
entity,
animation_name,
window,
WindowPhase::Begin,
0.0,
window_registry,
window_events,
sound_events,
particle_events,
custom_events,
);
} else if was_active && is_active {
fire_window_event(
commands,
entity,
animation_name,
window,
WindowPhase::Tick,
progress,
window_registry,
window_events,
sound_events,
particle_events,
custom_events,
);
} else if was_active && !is_active {
tracker.active_windows.remove(&window.id);
fire_window_event(
commands,
entity,
animation_name,
window,
WindowPhase::End,
1.0,
window_registry,
window_events,
sound_events,
particle_events,
custom_events,
);
}
}
}
fn fire_window_event(
commands: &mut Commands,
entity: Entity,
animation_name: &str,
window: &AnimationWindow,
phase: WindowPhase,
progress: f32,
window_registry: &AnimationWindowRegistry,
window_events: &mut MessageWriter<AnimationWindowEvent>,
sound_events: &mut MessageWriter<AnimationSoundEvent>,
particle_events: &mut MessageWriter<AnimationParticleEvent>,
custom_events: &mut MessageWriter<AnimationCustomEvent>,
) {
window_events.write(AnimationWindowEvent {
entity,
animation: animation_name.to_string(),
window_id: window.id,
window_name: window.name.clone(),
phase,
payload: window.payload.clone(),
progress,
});
commands.trigger(AnimationWindowChanged {
entity,
name: window.name.clone(),
window_id: window.id,
animation: animation_name.to_string(),
phase,
progress,
payload: window.payload.clone(),
});
if phase == WindowPhase::Begin {
fire_window_payload_events(
commands,
entity,
animation_name,
phase,
progress,
&window.payload,
window_registry,
sound_events,
particle_events,
custom_events,
);
}
}
fn fire_payload_events(
commands: &mut Commands,
entity: Entity,
animation_name: &str,
payload: &TriggerPayload,
trigger_registry: &AnimationTriggerRegistry,
sound_events: &mut MessageWriter<AnimationSoundEvent>,
particle_events: &mut MessageWriter<AnimationParticleEvent>,
custom_events: &mut MessageWriter<AnimationCustomEvent>,
) {
match payload {
TriggerPayload::Sound { path, volume } => {
sound_events.write(AnimationSoundEvent {
entity,
path: path.clone(),
volume: *volume,
});
}
TriggerPayload::Particle { effect, offset } => {
particle_events.write(AnimationParticleEvent {
entity,
effect: effect.clone(),
offset: *offset,
});
}
TriggerPayload::Custom { event_name, params } => {
custom_events.write(AnimationCustomEvent {
entity,
event_name: event_name.clone(),
params: params.clone(),
});
trigger_registry.dispatch(commands, entity, animation_name, event_name, params);
}
TriggerPayload::None => {}
}
}
fn fire_window_payload_events(
commands: &mut Commands,
entity: Entity,
animation_name: &str,
phase: WindowPhase,
progress: f32,
payload: &TriggerPayload,
window_registry: &AnimationWindowRegistry,
sound_events: &mut MessageWriter<AnimationSoundEvent>,
particle_events: &mut MessageWriter<AnimationParticleEvent>,
custom_events: &mut MessageWriter<AnimationCustomEvent>,
) {
match payload {
TriggerPayload::Sound { path, volume } => {
sound_events.write(AnimationSoundEvent {
entity,
path: path.clone(),
volume: *volume,
});
}
TriggerPayload::Particle { effect, offset } => {
particle_events.write(AnimationParticleEvent {
entity,
effect: effect.clone(),
offset: *offset,
});
}
TriggerPayload::Custom { event_name, params } => {
custom_events.write(AnimationCustomEvent {
entity,
event_name: event_name.clone(),
params: params.clone(),
});
window_registry.dispatch(
commands,
entity,
animation_name,
phase,
progress,
event_name,
params,
);
}
TriggerPayload::None => {}
}
}