generated from 2ndbeam/bevy-template
discontinued
This commit is contained in:
parent
a41cf4879e
commit
e8b1499bba
10 changed files with 370 additions and 12 deletions
106
src/ai/jumper/mod.rs
Normal file
106
src/ai/jumper/mod.rs
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
//! Jumper enemy module
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use states::*;
|
||||||
|
use triggers::*;
|
||||||
|
|
||||||
|
use crate::{GROUP_ATTACK, GROUP_ENEMY, GROUP_FRIENDLY, GROUP_INTERACTIVE, GROUP_STATIC, ai::jumper::states::Approaching, combat::Health, curve::BallisticCurve, meters, timer::WantsTickEvent};
|
||||||
|
|
||||||
|
pub mod states;
|
||||||
|
pub mod systems;
|
||||||
|
pub mod triggers;
|
||||||
|
|
||||||
|
/// Applies impulse to entity using its direction to [AiTarget]
|
||||||
|
#[derive(Clone, Copy, PartialEq, Debug, Default, Reflect)]
|
||||||
|
#[reflect(Clone, PartialEq, Debug, Default)]
|
||||||
|
pub struct ApplyDirectedImpulse {
|
||||||
|
impulse: Vec2,
|
||||||
|
torque_impulse: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EntityCommand for ApplyDirectedImpulse {
|
||||||
|
fn apply(self, mut entity: EntityWorldMut) -> () {
|
||||||
|
let Some(entity_t) = entity.get::<Transform>() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(AiTarget(target_id)) = entity.get::<AiTarget>() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(target_t) = entity.world().get::<Transform>(*target_id) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let direction = (target_t.translation.x - entity_t.translation.x).signum();
|
||||||
|
let impulse = ExternalImpulse {
|
||||||
|
impulse: self.impulse.with_x(self.impulse.x * direction),
|
||||||
|
torque_impulse: self.torque_impulse,
|
||||||
|
};
|
||||||
|
info!("Sent {impulse:#?}");
|
||||||
|
entity.insert(impulse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Jumper enemy
|
||||||
|
#[derive(Component, Clone, Copy, PartialEq, Eq, Debug, Default, Reflect)]
|
||||||
|
#[reflect(Component, Clone, PartialEq, Debug, Default)]
|
||||||
|
#[require(Health, StateMachine, Transform, WantsTickEvent)]
|
||||||
|
pub struct Jumper;
|
||||||
|
|
||||||
|
impl Jumper {
|
||||||
|
/// Constructs [Jumper] bundle
|
||||||
|
pub fn bundle(position: Vec2, health: f32) -> impl Bundle {
|
||||||
|
(
|
||||||
|
// Basic
|
||||||
|
Jumper,
|
||||||
|
Health::new(health),
|
||||||
|
|
||||||
|
// World
|
||||||
|
Transform::from_xyz(position.x, position.y, 0.),
|
||||||
|
|
||||||
|
// Collision
|
||||||
|
RigidBody::Dynamic,
|
||||||
|
LockedAxes::ROTATION_LOCKED,
|
||||||
|
Collider::cuboid(meters(0.5), meters(0.5)),
|
||||||
|
ActiveEvents::COLLISION_EVENTS,
|
||||||
|
ActiveCollisionTypes::all(),
|
||||||
|
CollisionGroups::new(
|
||||||
|
GROUP_ENEMY,
|
||||||
|
GROUP_STATIC | GROUP_INTERACTIVE | GROUP_FRIENDLY | GROUP_ATTACK,
|
||||||
|
),
|
||||||
|
Velocity::zero(),
|
||||||
|
Friction {
|
||||||
|
coefficient: 0.,
|
||||||
|
combine_rule: CoefficientCombineRule::Min,
|
||||||
|
},
|
||||||
|
|
||||||
|
// StateMachine
|
||||||
|
states::Idle::default(),
|
||||||
|
StateMachine::default()
|
||||||
|
// On enter Idle
|
||||||
|
.on_enter::<Idle>(|e| {
|
||||||
|
e.insert(Velocity::zero());
|
||||||
|
})
|
||||||
|
// Idle -> Approaching
|
||||||
|
.trans::<Idle, _>(target_in_range(Idle::MIN_DISTANCE), Approaching)
|
||||||
|
// Approaching -> Attacking
|
||||||
|
.trans::<Approaching, _>(
|
||||||
|
ticked
|
||||||
|
.ignore_and(target_in_range(Approaching::MIN_DISTANCE).not())
|
||||||
|
.ignore_and(target_in_range(Approaching::MAX_DISTANCE)),
|
||||||
|
Attacking::new(),
|
||||||
|
)
|
||||||
|
// On enter Attacking
|
||||||
|
.on_enter::<Attacking>(|e| {
|
||||||
|
e.insert(Velocity::zero());
|
||||||
|
e.queue(ApplyDirectedImpulse {
|
||||||
|
impulse: vec2(12000., 9000.),
|
||||||
|
..default()
|
||||||
|
});
|
||||||
|
})
|
||||||
|
// Approaching -> Idle
|
||||||
|
.trans::<Approaching, _>(target_in_range(Approaching::MAX_RANGE).not(), Idle)
|
||||||
|
// Attacking -> Approaching
|
||||||
|
.trans::<Attacking, _>(done(None), Approaching)
|
||||||
|
.set_trans_logging(true),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
51
src/ai/jumper/states.rs
Normal file
51
src/ai/jumper/states.rs
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
//! Jumper states
|
||||||
|
|
||||||
|
use bevy::time::Stopwatch;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// Entity awaits until [AiTarget] comes closer, then transits to [Approaching]
|
||||||
|
#[derive(Component, Clone, Copy, PartialEq, Debug, Default, Reflect)]
|
||||||
|
#[reflect(Component, Clone, PartialEq, Debug, Default)]
|
||||||
|
pub struct Idle;
|
||||||
|
|
||||||
|
impl Idle {
|
||||||
|
/// Minimal distance required for next state in pixels
|
||||||
|
pub const MIN_DISTANCE: f32 = meters(4.);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Approaches distance, required to jump, awaits for next [BpmTimer](crate::timer::BpmTimer) tick, then transits to [Attacking]
|
||||||
|
/// If target distances from entity very far, transits to [Idle]
|
||||||
|
#[derive(Component, Clone, Copy, PartialEq, Debug, Reflect, Default)]
|
||||||
|
#[reflect(Component, Clone, PartialEq, Debug, Default)]
|
||||||
|
pub struct Approaching;
|
||||||
|
|
||||||
|
impl Approaching {
|
||||||
|
/// Minimal distance in pixels, when entity should transit to [Attacking]
|
||||||
|
pub const MIN_DISTANCE: f32 = meters(2.);
|
||||||
|
/// Maximal distance in pixels, when entity should transit to [Attacking]
|
||||||
|
pub const MAX_DISTANCE: f32 = meters(3.);
|
||||||
|
/// Distance in pixels, when entity should transit to [Idle]
|
||||||
|
pub const MAX_RANGE: f32 = meters(6.);
|
||||||
|
/// Approaching speed, px/s
|
||||||
|
pub const SPEED: f32 = meters(0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Jumps at the [AiTarget]
|
||||||
|
/// If target was reached and jumper was not hit, it deals damage
|
||||||
|
/// Transits to [Approaching] when reaches the floor
|
||||||
|
#[derive(Component, Clone, Debug, Reflect)]
|
||||||
|
#[reflect(Component, Clone, Debug)]
|
||||||
|
pub struct Attacking {
|
||||||
|
/// May deal damage to [AiTarget]
|
||||||
|
pub may_attack: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Attacking {
|
||||||
|
/// Constructs new [Attacking] state
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self { may_attack: true }
|
||||||
|
}
|
||||||
|
/// Damage applied to the [AiTarget]
|
||||||
|
pub const DAMAGE: f32 = 20.;
|
||||||
|
}
|
||||||
78
src/ai/jumper/systems.rs
Normal file
78
src/ai/jumper/systems.rs
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
//! Jumper systems
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use crate::combat::attack::AttackArea;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// Update in [Approaching] or [Attacking] state
|
||||||
|
pub fn update(
|
||||||
|
mut commands: Commands,
|
||||||
|
time: Res<Time>,
|
||||||
|
jumpers: Query<(
|
||||||
|
Entity,
|
||||||
|
&Transform,
|
||||||
|
&mut Sprite,
|
||||||
|
&AiTarget,
|
||||||
|
&mut Velocity,
|
||||||
|
Option<&Approaching>,
|
||||||
|
Option<&mut Attacking>,
|
||||||
|
), With<Jumper>>,
|
||||||
|
targets: Query<&Transform, Without<Jumper>>,
|
||||||
|
mut collision_events: MessageReader<CollisionEvent>,
|
||||||
|
) {
|
||||||
|
for (
|
||||||
|
jumper_id,
|
||||||
|
transform,
|
||||||
|
mut sprite,
|
||||||
|
AiTarget(target_id),
|
||||||
|
mut velocity,
|
||||||
|
maybe_approaching,
|
||||||
|
maybe_attacking
|
||||||
|
) in jumpers {
|
||||||
|
if maybe_approaching.is_some() {
|
||||||
|
let Ok(target_transform) = targets.get(*target_id) else {
|
||||||
|
error!("Entity {target_id} does not exist, but is target for jumper {jumper_id}");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let distance = transform.translation.x - target_transform.translation.x;
|
||||||
|
|
||||||
|
let (direction_sign, flip_x) = if distance.abs() <= Approaching::MIN_DISTANCE + 1. { (1., false) }
|
||||||
|
else { (-1., true) };
|
||||||
|
|
||||||
|
let x_velocity = Approaching::SPEED * direction_sign;
|
||||||
|
|
||||||
|
velocity.linvel.x = x_velocity;
|
||||||
|
|
||||||
|
sprite.flip_x = flip_x;
|
||||||
|
} else if let Some(mut attacking) = maybe_attacking {
|
||||||
|
for event in collision_events.read() {
|
||||||
|
if let CollisionEvent::Started(first, second, _) = event {
|
||||||
|
info!("{first} hit {second}");
|
||||||
|
if first == &jumper_id && second == target_id ||
|
||||||
|
first == target_id && second == &jumper_id {
|
||||||
|
info!("jumper hit target");
|
||||||
|
if attacking.may_attack {
|
||||||
|
attacking.may_attack = false;
|
||||||
|
let attack_area = AttackArea::new(
|
||||||
|
1.,
|
||||||
|
Attacking::DAMAGE,
|
||||||
|
Duration::from_secs(1),
|
||||||
|
vec2(meters(0.6), 1.)
|
||||||
|
);
|
||||||
|
commands.entity(jumper_id).with_child(attack_area.into_bundle(
|
||||||
|
vec2(0., 0.),
|
||||||
|
false,
|
||||||
|
GROUP_FRIENDLY,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
commands.entity(jumper_id).insert(Done::Success);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/ai/jumper/triggers.rs
Normal file
26
src/ai/jumper/triggers.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
//! Jumper state machine triggers
|
||||||
|
|
||||||
|
use crate::timer::{BpmTimer, TickEvent};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// Returns true if entity currently has [TickEvent]
|
||||||
|
pub fn ticked(In(entity): In<Entity>, query: Query<Has<TickEvent>>) -> bool {
|
||||||
|
query.get(entity).map_or(false, |has_event| has_event)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns signed difference in position.x to target if it is in the given range
|
||||||
|
pub fn target_in_range(min_distance: f32) -> impl EntityTrigger<Out = Option<f32>> {
|
||||||
|
(move |In(my_id): In<Entity>, transforms: Query<&Transform>, targets: Query<&AiTarget>| {
|
||||||
|
let Ok(AiTarget(target)) = targets.get(my_id) else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
transforms.get(*target).map_or(None, |t1| {
|
||||||
|
transforms.get(my_id).map_or(None, |t2| {
|
||||||
|
let result = t1.translation.x - t2.translation.x;
|
||||||
|
if result.abs() <= min_distance { Some(result) } else { None }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}).into_trigger()
|
||||||
|
}
|
||||||
|
|
@ -2,5 +2,12 @@
|
||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy_rapier2d::prelude::*;
|
use bevy_rapier2d::prelude::*;
|
||||||
|
use seldom_state::prelude::*;
|
||||||
|
|
||||||
pub mod dummy;
|
pub mod dummy;
|
||||||
|
pub mod jumper;
|
||||||
|
|
||||||
|
/// Component for marking target entity for enemies
|
||||||
|
#[derive(Component, Clone, Copy, PartialEq, Eq, Deref, DerefMut, Debug, Reflect)]
|
||||||
|
#[reflect(Component, Clone, PartialEq, Debug)]
|
||||||
|
pub struct AiTarget(pub Entity);
|
||||||
|
|
|
||||||
39
src/curve.rs
Normal file
39
src/curve.rs
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
//! Specialized curves module
|
||||||
|
|
||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
/// Curve that calculates [Vec2] velocity based on simple ballistics
|
||||||
|
#[derive(Clone, Copy, Debug, Reflect)]
|
||||||
|
#[reflect(Clone, Debug)]
|
||||||
|
pub struct BallisticCurve {
|
||||||
|
x_velocity: f32,
|
||||||
|
y_velocity: f32,
|
||||||
|
gravity: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BallisticCurve {
|
||||||
|
/// Constructs new [BallisticCurve] using velocity and angle
|
||||||
|
pub fn new(velocity: f32, angle: f32, gravity: f32) -> Self {
|
||||||
|
let x_velocity = velocity * angle.cos();
|
||||||
|
let y_velocity = velocity * angle.sin();
|
||||||
|
Self { x_velocity, y_velocity, gravity }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constructs new [BallisticCurve] using distance to highest point and time to reach it
|
||||||
|
pub fn from_point(point: Vec2, time: f32, gravity: f32) -> Self {
|
||||||
|
let x_velocity = point.x / time;
|
||||||
|
let y_velocity = point.y / time - gravity * time / 2.;
|
||||||
|
Self { x_velocity, y_velocity, gravity }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Curve<Vec2> for BallisticCurve {
|
||||||
|
fn domain(&self) -> Interval {
|
||||||
|
Interval::new(0., f32::INFINITY).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sample_unchecked(&self, t: f32) -> Vec2 {
|
||||||
|
let y_velocity = self.y_velocity * t + self.gravity * t.powi(2) / 2.;
|
||||||
|
vec2(self.x_velocity, y_velocity)
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/lib.rs
22
src/lib.rs
|
|
@ -8,6 +8,7 @@ mod tests;
|
||||||
pub mod ai;
|
pub mod ai;
|
||||||
pub mod anim;
|
pub mod anim;
|
||||||
pub mod combat;
|
pub mod combat;
|
||||||
|
pub mod curve;
|
||||||
pub mod graph;
|
pub mod graph;
|
||||||
pub mod input;
|
pub mod input;
|
||||||
pub mod player;
|
pub mod player;
|
||||||
|
|
@ -19,7 +20,7 @@ use std::{collections::HashMap, time::Duration};
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy_rapier2d::prelude::*;
|
use bevy_rapier2d::prelude::*;
|
||||||
|
|
||||||
use crate::{ai::dummy::Dummy, anim::sprite::{AnimatedSprite, AnimationData}, player::Player, timer::BpmTimer};
|
use crate::{ai::{AiTarget, jumper::Jumper}, anim::sprite::{AnimatedSprite, AnimationData}, player::Player, timer::BpmTimer};
|
||||||
|
|
||||||
const PIXELS_PER_METER: f32 = 16.;
|
const PIXELS_PER_METER: f32 = 16.;
|
||||||
|
|
||||||
|
|
@ -41,9 +42,17 @@ pub const GROUP_ATTACK: Group = Group::GROUP_5;
|
||||||
/// Temporary function to setup world
|
/// Temporary function to setup world
|
||||||
pub fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
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));
|
||||||
|
commands.spawn((
|
||||||
|
Collider::cuboid(meters(8.), meters(0.5)),
|
||||||
|
Friction {
|
||||||
|
coefficient: 0.,
|
||||||
|
combine_rule: CoefficientCombineRule::Min,
|
||||||
|
},
|
||||||
|
Transform::from_xyz(0., meters(-1.5), 0.),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Test function to setup [Dummy] with [AnimatedSprite]
|
/// Test function to setup [Jumper] with [AnimatedSprite]
|
||||||
pub fn setup_animated_sprite_dummy(
|
pub fn setup_animated_sprite_dummy(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
asset_server: Res<AssetServer>,
|
asset_server: Res<AssetServer>,
|
||||||
|
|
@ -59,7 +68,7 @@ pub fn setup_animated_sprite_dummy(
|
||||||
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.),
|
Jumper::bundle(vec2(meters(6.), meters(-0.65625)), 10.),
|
||||||
AnimatedSprite::new(data, Duration::from_millis(500)),
|
AnimatedSprite::new(data, Duration::from_millis(500)),
|
||||||
)).observe(combat::despawn_on_health_depleted)
|
)).observe(combat::despawn_on_health_depleted)
|
||||||
.id();
|
.id();
|
||||||
|
|
@ -67,10 +76,13 @@ pub fn setup_animated_sprite_dummy(
|
||||||
commands.run_system_cached_with(|
|
commands.run_system_cached_with(|
|
||||||
In(entity_id): In<Entity>,
|
In(entity_id): In<Entity>,
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
mut query: Query<&mut AnimatedSprite>,
|
mut anim_query: Query<(&mut AnimatedSprite, &mut Transform)>,
|
||||||
|
player: Single<Entity, With<Player>>,
|
||||||
| {
|
| {
|
||||||
let mut animation = query.get_mut(entity_id).unwrap();
|
let (mut animation, mut transform) = anim_query.get_mut(entity_id).unwrap();
|
||||||
|
|
||||||
animation.play("default", TimerMode::Repeating, commands.entity(entity_id).entry::<Sprite>());
|
animation.play("default", TimerMode::Repeating, commands.entity(entity_id).entry::<Sprite>());
|
||||||
|
transform.scale.y = 0.5;
|
||||||
|
commands.entity(entity_id).insert(AiTarget(*player));
|
||||||
}, id);
|
}, id);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -104,8 +104,7 @@ impl Player {
|
||||||
PlayerInput::LightAttack,
|
PlayerInput::LightAttack,
|
||||||
]),
|
]),
|
||||||
triggers::from_awaiting_to_choosing,
|
triggers::from_awaiting_to_choosing,
|
||||||
)
|
),
|
||||||
.set_trans_logging(true),
|
|
||||||
|
|
||||||
// Weapon
|
// Weapon
|
||||||
weapon::knife::Knife::default(),
|
weapon::knife::Knife::default(),
|
||||||
|
|
|
||||||
|
|
@ -26,14 +26,20 @@ 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_dummy,
|
setup_animated_sprite_dummy.after(setup),
|
||||||
|
))
|
||||||
|
.add_systems(PreUpdate, (
|
||||||
|
combat::attack::update_attack_areas,
|
||||||
))
|
))
|
||||||
.add_systems(Update, (
|
.add_systems(Update, (
|
||||||
|
ai::jumper::systems::update,
|
||||||
anim::sprite::update_animated_sprites,
|
anim::sprite::update_animated_sprites,
|
||||||
combat::attack::update_attack_areas,
|
|
||||||
player::systems::handle_input,
|
player::systems::handle_input,
|
||||||
timer::update_bpm_timer,
|
timer::update_bpm_timer,
|
||||||
))
|
))
|
||||||
|
.add_systems(PostUpdate, (
|
||||||
|
timer::remove_tick_events,
|
||||||
|
))
|
||||||
.register_component_as::<dyn Weapon, knife::Knife>();
|
.register_component_as::<dyn Weapon, knife::Knife>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
40
src/timer.rs
40
src/timer.rs
|
|
@ -35,6 +35,7 @@ impl BpmTimer {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates internal timer and returns true if it just ticked
|
/// Updates internal timer and returns true if it just ticked
|
||||||
|
#[inline(always)]
|
||||||
pub fn tick(&mut self, dt: Duration) -> bool {
|
pub fn tick(&mut self, dt: Duration) -> bool {
|
||||||
let ticked = self.timer.tick(dt).just_finished();
|
let ticked = self.timer.tick(dt).just_finished();
|
||||||
if ticked { self.elapsed += self.timer.times_finished_this_tick() as f32; }
|
if ticked { self.elapsed += self.timer.times_finished_this_tick() as f32; }
|
||||||
|
|
@ -46,6 +47,12 @@ impl BpmTimer {
|
||||||
pub fn beats_elapsed(&self) -> f32 {
|
pub fn beats_elapsed(&self) -> f32 {
|
||||||
self.elapsed + self.timer.fraction()
|
self.elapsed + self.timer.fraction()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns internal timer duration
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn get_duration(&self) -> Duration {
|
||||||
|
self.timer.duration()
|
||||||
|
}
|
||||||
|
|
||||||
/// Get beats per minute value of timer
|
/// Get beats per minute value of timer
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
|
|
@ -54,23 +61,50 @@ impl BpmTimer {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set new BPM value
|
/// Set new BPM value
|
||||||
|
#[inline(always)]
|
||||||
pub fn set_bpm(&mut self, new_bpm: f32) {
|
pub fn set_bpm(&mut self, new_bpm: f32) {
|
||||||
self.timer.set_duration(duration_from_bpm(new_bpm));
|
self.timer.set_duration(duration_from_bpm(new_bpm));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resets elapsed beats
|
/// Resets elapsed beats
|
||||||
|
#[inline(always)]
|
||||||
pub fn reset(&mut self) {
|
pub fn reset(&mut self) {
|
||||||
self.timer.reset();
|
self.timer.reset();
|
||||||
self.elapsed = 0.;
|
self.elapsed = 0.;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns how close current state is to rhythm
|
/// Returns how close current state is to rhythm
|
||||||
|
#[inline(always)]
|
||||||
pub fn accuracy(&self) -> f32 {
|
pub fn accuracy(&self) -> f32 {
|
||||||
(self.timer.fraction() - 0.5).abs() * 2.
|
(self.timer.fraction() - 0.5).abs() * 2.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// System that ticks [BpmTimer]
|
/// Indicates that [BpmTimer] just ticked
|
||||||
pub fn update_bpm_timer(time: Res<Time>, mut timer: ResMut<BpmTimer>) {
|
#[derive(Component, Event, Clone, Copy, PartialEq, Eq, Debug, Default, Reflect)]
|
||||||
timer.tick(time.delta());
|
#[reflect(Component, Event, Clone, PartialEq, Debug, Default)]
|
||||||
|
pub struct TickEvent;
|
||||||
|
|
||||||
|
/// Indicated that entity will receive [TickEvent] components
|
||||||
|
#[derive(Component, Clone, Copy, PartialEq, Eq, Debug, Default, Reflect)]
|
||||||
|
#[reflect(Component, Clone, PartialEq, Debug, Default)]
|
||||||
|
pub struct WantsTickEvent;
|
||||||
|
|
||||||
|
/// System that ticks [BpmTimer] and inserts [TickEvent] for entities with [WantsTickEvent]
|
||||||
|
pub fn update_bpm_timer(
|
||||||
|
mut commands: Commands,
|
||||||
|
time: Res<Time>,
|
||||||
|
mut timer: ResMut<BpmTimer>,
|
||||||
|
query: Query<Entity, With<WantsTickEvent>>,
|
||||||
|
) {
|
||||||
|
if timer.tick(time.delta()) {
|
||||||
|
commands.trigger(TickEvent);
|
||||||
|
info!("Tick");
|
||||||
|
query.iter().for_each(|e| { commands.entity(e).insert(TickEvent); } );
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove [TickEvent] from each entity. Runs in [PostUpdate]
|
||||||
|
pub fn remove_tick_events(mut commands: Commands, query: Query<Entity, With<TickEvent>>) {
|
||||||
|
query.iter().for_each(|e| { commands.entity(e).remove::<TickEvent>(); } );
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue