generated from 2ndbeam/bevy-template
Compare commits
2 commits
f3b2d0313e
...
a41cf4879e
| Author | SHA1 | Date | |
|---|---|---|---|
| a41cf4879e | |||
| be36504a00 |
8 changed files with 195 additions and 21 deletions
32
src/ai/dummy.rs
Normal file
32
src/ai/dummy.rs
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
//! Dummy enemy module that does nothing
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
use crate::{GROUP_ATTACK, GROUP_ENEMY, GROUP_FRIENDLY, GROUP_INTERACTIVE, GROUP_STATIC, combat::Health, meters};
|
||||||
|
|
||||||
|
/// Dummy enemy. Has [Health] component and no AI logic
|
||||||
|
#[derive(Component, Clone, Copy, PartialEq, Eq, Debug, Default, Reflect)]
|
||||||
|
#[reflect(Component, Clone, PartialEq, Debug, Default)]
|
||||||
|
#[require(Health)]
|
||||||
|
pub struct Dummy;
|
||||||
|
|
||||||
|
impl Dummy {
|
||||||
|
/// Constructs [Dummy] bundle without sprite
|
||||||
|
pub fn bundle(position: Vec2, health: f32) -> impl Bundle {
|
||||||
|
(
|
||||||
|
// Basic
|
||||||
|
Dummy,
|
||||||
|
Health::new(health),
|
||||||
|
|
||||||
|
// World
|
||||||
|
Transform::from_xyz(position.x, position.y, 0.),
|
||||||
|
|
||||||
|
// Collision
|
||||||
|
Collider::cuboid(meters(0.5), meters(0.5)),
|
||||||
|
CollisionGroups::new(
|
||||||
|
GROUP_ENEMY,
|
||||||
|
GROUP_STATIC | GROUP_INTERACTIVE | GROUP_FRIENDLY | GROUP_ATTACK,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
6
src/ai/mod.rs
Normal file
6
src/ai/mod.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
//! Enemy AI module
|
||||||
|
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use bevy_rapier2d::prelude::*;
|
||||||
|
|
||||||
|
pub mod dummy;
|
||||||
|
|
@ -5,6 +5,8 @@ use std::{collections::HashMap, time::Duration};
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy_ecs::system::EntityEntryCommands;
|
use bevy_ecs::system::EntityEntryCommands;
|
||||||
|
|
||||||
|
type SpriteCommands<'a> = EntityEntryCommands<'a, Sprite>;
|
||||||
|
|
||||||
/// Data for [AnimatedSprite]
|
/// Data for [AnimatedSprite]
|
||||||
#[derive(Clone, PartialEq, Debug, Default, Reflect)]
|
#[derive(Clone, PartialEq, Debug, Default, Reflect)]
|
||||||
#[reflect(Clone, PartialEq, Debug, Default)]
|
#[reflect(Clone, PartialEq, Debug, Default)]
|
||||||
|
|
@ -19,6 +21,7 @@ pub struct AnimationData {
|
||||||
|
|
||||||
impl AnimationData {
|
impl AnimationData {
|
||||||
/// Construct new [AnimationData] from given [Handle<TextureAtlasLayout>]
|
/// Construct new [AnimationData] from given [Handle<TextureAtlasLayout>]
|
||||||
|
#[inline(always)]
|
||||||
pub fn new(image: Handle<Image>, frames: Handle<TextureAtlasLayout>, duration_multiplier: f32) -> Self {
|
pub fn new(image: Handle<Image>, frames: Handle<TextureAtlasLayout>, duration_multiplier: f32) -> Self {
|
||||||
Self { image, frames, duration_multiplier }
|
Self { image, frames, duration_multiplier }
|
||||||
}
|
}
|
||||||
|
|
@ -33,36 +36,41 @@ pub struct AnimatedSprite {
|
||||||
current: Option<AnimationData>,
|
current: Option<AnimationData>,
|
||||||
timer: Timer,
|
timer: Timer,
|
||||||
duration: Duration,
|
duration: Duration,
|
||||||
|
backwards: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AnimatedSprite {
|
impl AnimatedSprite {
|
||||||
/// Constructs new AnimatedSprite
|
/// Constructs new AnimatedSprite
|
||||||
|
#[inline(always)]
|
||||||
pub fn new(data: HashMap<String, AnimationData>, duration: Duration) -> Self {
|
pub fn new(data: HashMap<String, AnimationData>, duration: Duration) -> Self {
|
||||||
let timer = Timer::new(duration, TimerMode::Once);
|
let timer = Timer::new(duration, TimerMode::Once);
|
||||||
|
|
||||||
Self { data, current: None, timer, duration }
|
Self { data, current: None, timer, duration, backwards: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add new animation to sprite
|
/// Add new animation to sprite
|
||||||
|
#[inline(always)]
|
||||||
pub fn add(&mut self, data: AnimationData, label: String) {
|
pub fn add(&mut self, data: AnimationData, label: String) {
|
||||||
self.data.insert(label, data);
|
self.data.insert(label, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets internal duration
|
/// Sets internal duration
|
||||||
|
#[inline(always)]
|
||||||
pub fn set_duration(&mut self, duration: Duration) {
|
pub fn set_duration(&mut self, duration: Duration) {
|
||||||
self.duration = duration;
|
self.duration = duration;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Finds animation by its label and returns it if started playing
|
fn play_inner(
|
||||||
pub fn play(
|
|
||||||
&mut self,
|
&mut self,
|
||||||
label: &str,
|
label: &str,
|
||||||
mode: TimerMode,
|
mode: TimerMode,
|
||||||
mut sprite: EntityEntryCommands<Sprite>,
|
mut sprite: SpriteCommands,
|
||||||
|
backwards: bool,
|
||||||
) -> Option<&AnimationData> {
|
) -> Option<&AnimationData> {
|
||||||
self.data.get(label).map(|data| {
|
self.data.get(label).map(|data| {
|
||||||
self.current = Some(data.clone());
|
self.current = Some(data.clone());
|
||||||
self.timer = Timer::new(self.duration.mul_f32(data.duration_multiplier), mode);
|
self.timer = Timer::new(self.duration.mul_f32(data.duration_multiplier), mode);
|
||||||
|
self.backwards = backwards;
|
||||||
|
|
||||||
let sprite_image = data.image.clone();
|
let sprite_image = data.image.clone();
|
||||||
let layout = data.frames.clone();
|
let layout = data.frames.clone();
|
||||||
|
|
@ -75,15 +83,55 @@ impl AnimatedSprite {
|
||||||
data
|
data
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Finds animation by its label, plays it and returns it if started playing
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn play(
|
||||||
|
&mut self,
|
||||||
|
label: &str,
|
||||||
|
mode: TimerMode,
|
||||||
|
sprite: SpriteCommands,
|
||||||
|
) -> Option<&AnimationData> {
|
||||||
|
self.play_inner(label, mode, sprite, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Same as [play](AnimatedSprite::play), but animation plays from the end to the beginning
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn play_backwards(
|
||||||
|
&mut self,
|
||||||
|
label: &str,
|
||||||
|
mode: TimerMode,
|
||||||
|
sprite: SpriteCommands,
|
||||||
|
) -> Option<&AnimationData> {
|
||||||
|
self.play_inner(label, mode, sprite, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stops the animation
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn stop(&mut self) {
|
||||||
|
self.current = None;
|
||||||
|
self.timer.reset();
|
||||||
|
}
|
||||||
|
|
||||||
/// Updates animation timer
|
/// Updates animation timer
|
||||||
|
#[inline(always)]
|
||||||
pub fn tick(&mut self, dt: Duration) {
|
pub fn tick(&mut self, dt: Duration) {
|
||||||
self.timer.tick(dt);
|
self.timer.tick(dt);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns inner [Timer::fraction]
|
/// Returns inner [Timer::fraction] or [Timer::fraction_remaining] if playing backwards
|
||||||
|
#[inline(always)]
|
||||||
pub fn fraction(&self) -> f32 {
|
pub fn fraction(&self) -> f32 {
|
||||||
self.timer.fraction()
|
match self.backwards {
|
||||||
|
true => self.timer.fraction_remaining(),
|
||||||
|
false => self.timer.fraction(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns [Timer::mode]
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn mode(&self) -> TimerMode {
|
||||||
|
self.timer.mode()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -107,7 +155,7 @@ pub fn update_animated_sprites(
|
||||||
|
|
||||||
let max_frames = atlas.len() as f32;
|
let max_frames = atlas.len() as f32;
|
||||||
|
|
||||||
let current_frame = (max_frames * animation.fraction()) as usize;
|
let current_frame = (max_frames * animation.fraction()).min(max_frames - 1.) as usize;
|
||||||
|
|
||||||
if let Some(atlas) = &mut sprite.texture_atlas {
|
if let Some(atlas) = &mut sprite.texture_atlas {
|
||||||
atlas.index = current_frame;
|
atlas.index = current_frame;
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ use std::time::Duration;
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy_rapier2d::prelude::*;
|
use bevy_rapier2d::prelude::*;
|
||||||
|
|
||||||
use crate::{GROUP_ATTACK, GROUP_INTERACTIVE, GROUP_STATIC};
|
use crate::{GROUP_ATTACK, GROUP_INTERACTIVE, GROUP_STATIC, combat::Health};
|
||||||
|
|
||||||
/// Contains logic for attack-related calculations
|
/// Contains logic for attack-related calculations
|
||||||
#[derive(Component, Clone, Debug, Reflect)]
|
#[derive(Component, Clone, Debug, Reflect)]
|
||||||
|
|
@ -27,6 +27,7 @@ pub struct AttackAreaOrigin;
|
||||||
|
|
||||||
impl AttackArea {
|
impl AttackArea {
|
||||||
/// Constructs new attack area
|
/// Constructs new attack area
|
||||||
|
#[inline(always)]
|
||||||
pub fn new(accuracy: f32, damage: f32, duration: Duration, half_area: Vec2) -> Self {
|
pub fn new(accuracy: f32, damage: f32, duration: Duration, half_area: Vec2) -> Self {
|
||||||
let max_distance = ((half_area.x * 2.).powi(2) + half_area.y.powi(2)).sqrt();
|
let max_distance = ((half_area.x * 2.).powi(2) + half_area.y.powi(2)).sqrt();
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -58,6 +59,9 @@ impl AttackArea {
|
||||||
GROUP_STATIC | GROUP_INTERACTIVE | affinity,
|
GROUP_STATIC | GROUP_INTERACTIVE | affinity,
|
||||||
),
|
),
|
||||||
Collider::cuboid(half_area.x, half_area.y),
|
Collider::cuboid(half_area.x, half_area.y),
|
||||||
|
ActiveCollisionTypes::all(),
|
||||||
|
Sleeping::disabled(),
|
||||||
|
ActiveEvents::COLLISION_EVENTS,
|
||||||
|
|
||||||
// AttackAreaOrigin
|
// AttackAreaOrigin
|
||||||
Children::spawn((
|
Children::spawn((
|
||||||
|
|
@ -70,11 +74,19 @@ impl AttackArea {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates timer and returns true if attack has ended
|
/// Updates timer and returns true if attack has ended
|
||||||
|
#[inline(always)]
|
||||||
pub fn tick(&mut self, dt: Duration) -> bool {
|
pub fn tick(&mut self, dt: Duration) -> bool {
|
||||||
self.timer.tick(dt).is_finished()
|
self.timer.tick(dt).is_finished()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Calls [Timer::almost_finish]
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn almost_finish(&mut self) {
|
||||||
|
self.timer.almost_finish();
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns damage for given distance to origin
|
/// Returns damage for given distance to origin
|
||||||
|
#[inline(always)]
|
||||||
pub fn damage(&self, distance_to_origin: f32) -> f32 {
|
pub fn damage(&self, distance_to_origin: f32) -> f32 {
|
||||||
let distance_ratio = 1. - distance_to_origin.min(self.max_distance) / self.max_distance;
|
let distance_ratio = 1. - distance_to_origin.min(self.max_distance) / self.max_distance;
|
||||||
let total_multiplier = self.accuracy * distance_ratio;
|
let total_multiplier = self.accuracy * distance_ratio;
|
||||||
|
|
@ -82,11 +94,42 @@ impl AttackArea {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// System that updates AttackArea timers and despawns those who timed out
|
fn apply_attack(
|
||||||
pub fn update_attack_areas(mut commands: Commands, time: Res<Time>, areas: Query<(Entity, &mut AttackArea)>) {
|
commands: &mut Commands,
|
||||||
for (area_id, mut area) in areas {
|
areas: &mut Query<(Entity, &mut AttackArea)>,
|
||||||
|
healths: &mut Query<&mut Health>,
|
||||||
|
first: &Entity,
|
||||||
|
second: &Entity,
|
||||||
|
) -> bool {
|
||||||
|
if let Ok((area_id, mut area)) = areas.get_mut(*first) &&
|
||||||
|
let Ok(mut health) = healths.get_mut(*second) {
|
||||||
|
health.apply(area.damage(0.), commands.entity(*second));
|
||||||
|
info!("{area_id} hit {second}, health: {}", health.to_string());
|
||||||
|
area.almost_finish();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// System that applies attacks and despawns timed out [AttackArea]s
|
||||||
|
pub fn update_attack_areas(
|
||||||
|
mut commands: Commands,
|
||||||
|
time: Res<Time>,
|
||||||
|
mut areas: Query<(Entity, &mut AttackArea)>,
|
||||||
|
mut healths: Query<&mut Health>,
|
||||||
|
mut collision_events: MessageReader<CollisionEvent>,
|
||||||
|
) {
|
||||||
|
for (area_id, mut area) in areas.iter_mut() {
|
||||||
if area.tick(time.delta()) {
|
if area.tick(time.delta()) {
|
||||||
commands.entity(area_id).despawn();
|
commands.entity(area_id).despawn();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for event in collision_events.read() {
|
||||||
|
if let CollisionEvent::Started(first, second, _) = event {
|
||||||
|
if !apply_attack(&mut commands, &mut areas, &mut healths, first, second) {
|
||||||
|
apply_attack(&mut commands, &mut areas, &mut healths, second, first);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
//! This module contains core concepts of the combat system
|
//! This module contains core concepts of the combat system
|
||||||
|
|
||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
|
||||||
pub mod attack;
|
pub mod attack;
|
||||||
|
|
@ -9,17 +11,57 @@ pub mod attack;
|
||||||
#[reflect(Component, Clone, PartialEq, Debug, Default)]
|
#[reflect(Component, Clone, PartialEq, Debug, Default)]
|
||||||
pub struct Health {
|
pub struct Health {
|
||||||
/// Current health value
|
/// Current health value
|
||||||
pub current: f32,
|
current: f32,
|
||||||
/// Maximum health value
|
/// Maximum health value
|
||||||
pub max: f32,
|
max: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Event that occurs when entity health reaches zero
|
||||||
|
#[derive(EntityEvent, Clone, Copy, PartialEq, Eq, Debug, Reflect)]
|
||||||
|
#[reflect(Event, Clone, PartialEq, Debug)]
|
||||||
|
pub struct HealthDepletedEvent {
|
||||||
|
/// Event target
|
||||||
|
#[event_target] pub entity: Entity,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Health {
|
impl Health {
|
||||||
/// Constructs new health component
|
/// Constructs new health component
|
||||||
|
#[inline(always)]
|
||||||
pub fn new(max: f32) -> Self {
|
pub fn new(max: f32) -> Self {
|
||||||
Self {
|
Self {
|
||||||
current: max,
|
current: max,
|
||||||
max,
|
max,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns current health
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn get_current(&self) -> f32 {
|
||||||
|
self.current
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns max health
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn get_max(&self) -> f32 {
|
||||||
|
self.max
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Applies health change and invokes [HealthDepletedEvent] if needed
|
||||||
|
pub fn apply(&mut self, amount: f32, mut commands: EntityCommands) {
|
||||||
|
self.current = (self.current - amount).min(self.max);
|
||||||
|
if self.current <= 0. {
|
||||||
|
commands.trigger(|entity| HealthDepletedEvent { entity } );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Health {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(format!("{:.2}/{:.2}", self.current, self.max).as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Standard event observer that despawns entity whose health was depleted
|
||||||
|
pub fn despawn_on_health_depleted(event: On<HealthDepletedEvent>, mut commands: Commands) {
|
||||||
|
commands.entity(event.entity).despawn();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
14
src/lib.rs
14
src/lib.rs
|
|
@ -5,6 +5,7 @@
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
|
pub mod ai;
|
||||||
pub mod anim;
|
pub mod anim;
|
||||||
pub mod combat;
|
pub mod combat;
|
||||||
pub mod graph;
|
pub mod graph;
|
||||||
|
|
@ -18,7 +19,7 @@ use std::{collections::HashMap, time::Duration};
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy_rapier2d::prelude::*;
|
use bevy_rapier2d::prelude::*;
|
||||||
|
|
||||||
use crate::{anim::sprite::{AnimatedSprite, AnimationData}, player::Player, timer::BpmTimer};
|
use crate::{ai::dummy::Dummy, anim::sprite::{AnimatedSprite, AnimationData}, player::Player, timer::BpmTimer};
|
||||||
|
|
||||||
const PIXELS_PER_METER: f32 = 16.;
|
const PIXELS_PER_METER: f32 = 16.;
|
||||||
|
|
||||||
|
|
@ -42,11 +43,11 @@ pub fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
||||||
commands.spawn(Player::bundle(&asset_server, Vec2::ZERO));
|
commands.spawn(Player::bundle(&asset_server, Vec2::ZERO));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Test function to setup [AnimatedSprite]
|
/// Test function to setup [Dummy] with [AnimatedSprite]
|
||||||
pub fn setup_animated_sprite(
|
pub fn setup_animated_sprite_dummy(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
asset_server: Res<AssetServer>,
|
asset_server: Res<AssetServer>,
|
||||||
mut layouts: ResMut<Assets<TextureAtlasLayout>>
|
mut layouts: ResMut<Assets<TextureAtlasLayout>>,
|
||||||
) {
|
) {
|
||||||
let image = asset_server.load("sprites/animated_placeholder.png");
|
let image = asset_server.load("sprites/animated_placeholder.png");
|
||||||
|
|
||||||
|
|
@ -58,9 +59,10 @@ pub fn setup_animated_sprite(
|
||||||
data.insert("default".into(), AnimationData::new(image.clone(), layout.clone(), 5.));
|
data.insert("default".into(), AnimationData::new(image.clone(), layout.clone(), 5.));
|
||||||
|
|
||||||
let id = commands.spawn((
|
let id = commands.spawn((
|
||||||
|
Dummy::bundle(vec2(meters(4.), 0.), 10.),
|
||||||
AnimatedSprite::new(data, Duration::from_millis(500)),
|
AnimatedSprite::new(data, Duration::from_millis(500)),
|
||||||
Sprite::from_atlas_image(image, TextureAtlas { layout: layout, index: 0 }),
|
)).observe(combat::despawn_on_health_depleted)
|
||||||
)).id();
|
.id();
|
||||||
|
|
||||||
commands.run_system_cached_with(|
|
commands.run_system_cached_with(|
|
||||||
In(entity_id): In<Entity>,
|
In(entity_id): In<Entity>,
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ use leafwing_input_manager::prelude::*;
|
||||||
use seldom_state::prelude::*;
|
use seldom_state::prelude::*;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
GROUP_ATTACK, GROUP_ENEMY, GROUP_FRIENDLY, GROUP_INTERACTIVE, GROUP_STATIC, graph::action::ActionGraph, input::{
|
GROUP_ATTACK, GROUP_ENEMY, GROUP_FRIENDLY, GROUP_INTERACTIVE, GROUP_STATIC, combat::Health, graph::action::ActionGraph, input::{
|
||||||
DefaultInputMap,
|
DefaultInputMap,
|
||||||
PlayerInput,
|
PlayerInput,
|
||||||
}, meters, player::weapon::{AttackType, Weapon}
|
}, meters, player::weapon::{AttackType, Weapon}
|
||||||
|
|
@ -45,6 +45,7 @@ impl Player {
|
||||||
// Basic
|
// Basic
|
||||||
Name::new("Player"),
|
Name::new("Player"),
|
||||||
Player::default(),
|
Player::default(),
|
||||||
|
Health::new(100.),
|
||||||
|
|
||||||
// Visible
|
// Visible
|
||||||
Sprite::from_image(image),
|
Sprite::from_image(image),
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ impl Plugin for GamePlugin {
|
||||||
.insert_resource(BpmTimer::new(120.))
|
.insert_resource(BpmTimer::new(120.))
|
||||||
.add_systems(Startup, (
|
.add_systems(Startup, (
|
||||||
setup,
|
setup,
|
||||||
setup_animated_sprite,
|
setup_animated_sprite_dummy,
|
||||||
))
|
))
|
||||||
.add_systems(Update, (
|
.add_systems(Update, (
|
||||||
anim::sprite::update_animated_sprites,
|
anim::sprite::update_animated_sprites,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue