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_rapier2d::prelude::*;
|
||||
use seldom_state::prelude::*;
|
||||
|
||||
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 anim;
|
||||
pub mod combat;
|
||||
pub mod curve;
|
||||
pub mod graph;
|
||||
pub mod input;
|
||||
pub mod player;
|
||||
|
|
@ -19,7 +20,7 @@ use std::{collections::HashMap, time::Duration};
|
|||
use bevy::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.;
|
||||
|
||||
|
|
@ -41,9 +42,17 @@ pub const GROUP_ATTACK: Group = Group::GROUP_5;
|
|||
/// Temporary function to setup world
|
||||
pub fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
||||
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(
|
||||
mut commands: Commands,
|
||||
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.));
|
||||
|
||||
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)),
|
||||
)).observe(combat::despawn_on_health_depleted)
|
||||
.id();
|
||||
|
|
@ -67,10 +76,13 @@ pub fn setup_animated_sprite_dummy(
|
|||
commands.run_system_cached_with(|
|
||||
In(entity_id): In<Entity>,
|
||||
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>());
|
||||
transform.scale.y = 0.5;
|
||||
commands.entity(entity_id).insert(AiTarget(*player));
|
||||
}, id);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -104,8 +104,7 @@ impl Player {
|
|||
PlayerInput::LightAttack,
|
||||
]),
|
||||
triggers::from_awaiting_to_choosing,
|
||||
)
|
||||
.set_trans_logging(true),
|
||||
),
|
||||
|
||||
// Weapon
|
||||
weapon::knife::Knife::default(),
|
||||
|
|
|
|||
|
|
@ -26,14 +26,20 @@ impl Plugin for GamePlugin {
|
|||
.insert_resource(BpmTimer::new(120.))
|
||||
.add_systems(Startup, (
|
||||
setup,
|
||||
setup_animated_sprite_dummy,
|
||||
setup_animated_sprite_dummy.after(setup),
|
||||
))
|
||||
.add_systems(PreUpdate, (
|
||||
combat::attack::update_attack_areas,
|
||||
))
|
||||
.add_systems(Update, (
|
||||
ai::jumper::systems::update,
|
||||
anim::sprite::update_animated_sprites,
|
||||
combat::attack::update_attack_areas,
|
||||
player::systems::handle_input,
|
||||
timer::update_bpm_timer,
|
||||
))
|
||||
.add_systems(PostUpdate, (
|
||||
timer::remove_tick_events,
|
||||
))
|
||||
.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
|
||||
#[inline(always)]
|
||||
pub fn tick(&mut self, dt: Duration) -> bool {
|
||||
let ticked = self.timer.tick(dt).just_finished();
|
||||
if ticked { self.elapsed += self.timer.times_finished_this_tick() as f32; }
|
||||
|
|
@ -46,6 +47,12 @@ impl BpmTimer {
|
|||
pub fn beats_elapsed(&self) -> f32 {
|
||||
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
|
||||
#[inline(always)]
|
||||
|
|
@ -54,23 +61,50 @@ impl BpmTimer {
|
|||
}
|
||||
|
||||
/// Set new BPM value
|
||||
#[inline(always)]
|
||||
pub fn set_bpm(&mut self, new_bpm: f32) {
|
||||
self.timer.set_duration(duration_from_bpm(new_bpm));
|
||||
}
|
||||
|
||||
/// Resets elapsed beats
|
||||
#[inline(always)]
|
||||
pub fn reset(&mut self) {
|
||||
self.timer.reset();
|
||||
self.elapsed = 0.;
|
||||
}
|
||||
|
||||
/// Returns how close current state is to rhythm
|
||||
#[inline(always)]
|
||||
pub fn accuracy(&self) -> f32 {
|
||||
(self.timer.fraction() - 0.5).abs() * 2.
|
||||
}
|
||||
}
|
||||
|
||||
/// System that ticks [BpmTimer]
|
||||
pub fn update_bpm_timer(time: Res<Time>, mut timer: ResMut<BpmTimer>) {
|
||||
timer.tick(time.delta());
|
||||
/// Indicates that [BpmTimer] just ticked
|
||||
#[derive(Component, Event, Clone, Copy, PartialEq, Eq, Debug, Default, Reflect)]
|
||||
#[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