Initial commit
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
use bevy::prelude::*;
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct FighterAttributes {
|
||||
pub base_walk_accel: f32,
|
||||
pub stick_walk_accel: f32,
|
||||
pub max_walk_vel: f32,
|
||||
pub ground_friction: f32,
|
||||
pub dash_duration: u32
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
use bevy::{
|
||||
color::palettes::css::{GREEN, HOT_PINK, RED, YELLOW},
|
||||
ecs::relationship::Relationship,
|
||||
prelude::*,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
game_settings::GameSettings,
|
||||
input::player::{ControlledBy, FighterInput},
|
||||
stage::StagePlane,
|
||||
};
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct Grounded;
|
||||
|
||||
#[derive(Component, Deref, DerefMut, Default, Debug)]
|
||||
pub struct FighterVelocity(pub Vec2);
|
||||
|
||||
#[derive(Component, Deref, DerefMut, Default)]
|
||||
pub struct FighterPreviousTranslation(pub Vec2);
|
||||
|
||||
#[derive(Component, Deref, DerefMut, Default)]
|
||||
#[require(FighterPreviousTranslation)]
|
||||
pub struct FighterTranslation(pub Vec2);
|
||||
|
||||
#[derive(Component, Debug)]
|
||||
#[require(Transform)]
|
||||
pub struct FighterECB {
|
||||
pub vertical_half: f32,
|
||||
pub horizontal_half: f32,
|
||||
}
|
||||
|
||||
impl FighterECB {
|
||||
pub fn to_3d_lineloop(&self, trf: Transform) -> [Vec3; 4] {
|
||||
let rel_up = trf.transform_point(Vec3::new(0.0, self.vertical_half, 0.0));
|
||||
let rel_down = trf.transform_point(Vec3::new(0.0, -self.vertical_half, 0.0));
|
||||
let rel_left = trf.transform_point(Vec3::new(-self.horizontal_half, 0.0, 0.0));
|
||||
let rel_right = trf.transform_point(Vec3::new(self.horizontal_half, 0.0, 0.0));
|
||||
[rel_up, rel_right, rel_down, rel_left]
|
||||
}
|
||||
}
|
||||
|
||||
pub fn debug_draw_ecb(ecbs: Query<(&FighterECB, &FighterTranslation)>, mut gizmos: Gizmos) {
|
||||
for (ecb, trf) in ecbs {
|
||||
let transformed = ecb.to_3d_lineloop(Transform::from_xyz(trf.x, trf.y, 0.0));
|
||||
gizmos.lineloop(transformed, YELLOW);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn integrate_gravity(
|
||||
fighters: Query<(&mut FighterVelocity)>,
|
||||
time: Res<Time<Fixed>>,
|
||||
) {
|
||||
/*for (movement, mut velocity) in fighters {
|
||||
//velocity.y -= movement.gravity;
|
||||
}*/
|
||||
}
|
||||
|
||||
pub fn apply_motion(
|
||||
fighters: Query<(
|
||||
&FighterVelocity,
|
||||
&mut FighterTranslation,
|
||||
&mut FighterPreviousTranslation,
|
||||
)>,
|
||||
time: Res<Time<Fixed>>,
|
||||
) {
|
||||
for (velocity, mut translation, mut prev_translation) in fighters {
|
||||
prev_translation.0 = translation.0;
|
||||
translation.0 += velocity.0;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn collide_fighter_with_scene(
|
||||
fighters: Query<(&FighterECB, &mut FighterVelocity, &mut FighterTranslation)>,
|
||||
stage_planes: Query<&StagePlane>,
|
||||
mut gizmos: Gizmos,
|
||||
) {
|
||||
for (ecb, mut velocity, mut translation) in fighters {
|
||||
for plane in stage_planes {
|
||||
let dist_sq = ecb.vertical_half * ecb.vertical_half;
|
||||
let ray = Ray2d::new(translation.0, Dir2::NEG_Y);
|
||||
|
||||
let intersection_result = plane
|
||||
.intersect_ray(ray)
|
||||
.filter(|result| result.position.distance_squared(translation.0) <= dist_sq);
|
||||
|
||||
if let Some(result) = intersection_result {
|
||||
gizmos.sphere(
|
||||
Isometry3d::from(Vec3::new(result.position.x, result.position.y, 0.0)),
|
||||
0.1f32,
|
||||
HOT_PINK,
|
||||
);
|
||||
velocity.0 = plane.project(velocity.0);
|
||||
translation.0 = result.position + Vec2::new(0.0, ecb.vertical_half);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fighter_movement() {}
|
||||
@@ -0,0 +1,8 @@
|
||||
pub mod plugin;
|
||||
pub mod fighter;
|
||||
pub mod data;
|
||||
pub mod state;
|
||||
pub mod states;
|
||||
pub use fighter::*;
|
||||
pub use plugin::*;
|
||||
pub use data::*;
|
||||
@@ -0,0 +1,29 @@
|
||||
use bevy::prelude::*;
|
||||
use crate::fighter::states;
|
||||
use crate::fighter::{
|
||||
FighterECB, FighterTranslation, FighterVelocity, apply_motion, collide_fighter_with_scene, integrate_gravity, state::state_interrupt_system,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FighterPlugin;
|
||||
|
||||
impl Plugin for FighterPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_systems(FixedPostUpdate, super::debug_draw_ecb)
|
||||
.add_systems(
|
||||
FixedUpdate,
|
||||
(
|
||||
state_interrupt_system::<states::wait::WaitState>,
|
||||
state_interrupt_system::<states::walk::WalkState>,
|
||||
state_interrupt_system::<states::dash::DashState>,
|
||||
states::walk::walk_update,
|
||||
states::dash::dash_update,
|
||||
integrate_gravity,
|
||||
apply_motion,
|
||||
collide_fighter_with_scene,
|
||||
)
|
||||
.chain(),
|
||||
)
|
||||
.add_observer(states::dash::on_insert_dash);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
use bevy::{ecs::relationship::RelationshipSourceCollection, prelude::*};
|
||||
|
||||
use crate::{fighter::{FighterAttributes, states}, game_settings::GameSettings, input::player::FighterInput};
|
||||
|
||||
pub enum FighterStateTransition {
|
||||
Wait,
|
||||
Walk,
|
||||
Dash
|
||||
}
|
||||
|
||||
pub trait FighterState : Component + Sized {
|
||||
fn remove(&self,
|
||||
state_context: &FighterStateContext,
|
||||
commands: &mut Commands) {
|
||||
commands.entity(state_context.entity).remove::<Self>();
|
||||
}
|
||||
fn check_interrupt(
|
||||
&self,
|
||||
state_context: &FighterStateContext,
|
||||
) -> Option<FighterStateTransition>;
|
||||
}
|
||||
|
||||
pub struct FighterStateContext<'a> {
|
||||
pub input: &'a mut FighterInput,
|
||||
pub fighter_attribs: &'a FighterAttributes,
|
||||
pub game_settings: &'a Res<'a, GameSettings>,
|
||||
pub entity: Entity,
|
||||
}
|
||||
|
||||
fn create_state(transition: FighterStateTransition, state_context: FighterStateContext, commands: &mut Commands) {
|
||||
let mut ent_cmd = commands.entity(state_context.entity);
|
||||
match transition {
|
||||
FighterStateTransition::Wait => ent_cmd.insert(states::wait::WaitState),
|
||||
FighterStateTransition::Walk => ent_cmd.insert(states::walk::WalkState),
|
||||
FighterStateTransition::Dash => ent_cmd.insert(states::dash::DashState::default()),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn state_interrupt_system<T: FighterState + Component + std::fmt::Debug>(
|
||||
query: Query<(Entity, &T, &mut FighterInput, &FighterAttributes)>,
|
||||
game_settings: Res<GameSettings>,
|
||||
mut commands: Commands,
|
||||
) {
|
||||
for (ent, state, mut input, attribs) in query {
|
||||
info!("{:?}", state);
|
||||
let state_context = FighterStateContext {
|
||||
input: &mut input,
|
||||
fighter_attribs: &attribs,
|
||||
game_settings: &game_settings,
|
||||
entity: ent,
|
||||
};
|
||||
if let Some(new_state) = state.check_interrupt(&state_context) {
|
||||
commands.entity(ent).remove::<T>();
|
||||
create_state(new_state, state_context, &mut commands);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
use crate::{
|
||||
fighter::state::{FighterState, FighterStateContext, FighterStateTransition},
|
||||
input::player::FighterInput,
|
||||
};
|
||||
use bevy::prelude::*;
|
||||
|
||||
pub struct DashStateCreator {}
|
||||
|
||||
#[derive(Component, Debug)]
|
||||
pub struct DashState {
|
||||
frames_in_dash: u32,
|
||||
}
|
||||
|
||||
impl Default for DashState {
|
||||
fn default() -> Self {
|
||||
Self { frames_in_dash: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
impl FighterState for DashState {
|
||||
fn check_interrupt(
|
||||
&self,
|
||||
state_context: &FighterStateContext,
|
||||
) -> Option<FighterStateTransition> {
|
||||
if self.frames_in_dash > state_context.fighter_attribs.dash_duration {
|
||||
// Dash finish shenanigans
|
||||
super::walk::check_input(state_context)
|
||||
.or_else(|| super::wait::check_input(state_context))
|
||||
} else if let Some(last_frame) = state_context.input.get_last_frame() {
|
||||
check_smash_input_with_dir(state_context)
|
||||
.filter(|dir| *dir != last_frame.movement.x.signum())
|
||||
.and_then(|_| super::dash::check_input(state_context))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start(state_context: &mut FighterStateContext) {
|
||||
state_context.input.clear_buffer();
|
||||
}
|
||||
|
||||
fn check_smash_input_with_dir(state_context: &FighterStateContext) -> Option<f32> {
|
||||
state_context.input.has_smash_x_movement(
|
||||
state_context
|
||||
.game_settings
|
||||
.input_common
|
||||
.smash_input_reset_axis_threshold,
|
||||
state_context
|
||||
.game_settings
|
||||
.input_common
|
||||
.smash_input_axis_threshold,
|
||||
state_context
|
||||
.game_settings
|
||||
.input_common
|
||||
.smash_input_frame_threshold,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn dash_update(dash_query: Query<&mut DashState>) {
|
||||
for mut dash in dash_query {
|
||||
dash.frames_in_dash += 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_input(state_context: &FighterStateContext) -> Option<FighterStateTransition> {
|
||||
check_smash_input_with_dir(state_context).map(|_| FighterStateTransition::Dash)
|
||||
}
|
||||
|
||||
pub fn on_insert_dash(
|
||||
add: On<Insert, DashState>,
|
||||
query: Query<(Entity, &mut FighterInput), With<DashState>>,
|
||||
) {
|
||||
for (entity, mut input) in query {
|
||||
if entity == add.entity {
|
||||
input.clear_buffer();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod wait;
|
||||
pub mod walk;
|
||||
pub mod dash;
|
||||
@@ -0,0 +1,23 @@
|
||||
use bevy::prelude::*;
|
||||
use crate::fighter::{state::{FighterState, FighterStateContext, FighterStateTransition}, states::walk};
|
||||
|
||||
#[derive(Component, Debug)]
|
||||
pub struct WaitState;
|
||||
|
||||
impl FighterState for WaitState {
|
||||
fn check_interrupt(
|
||||
&self,
|
||||
state_context: &FighterStateContext,
|
||||
) -> Option<FighterStateTransition> {
|
||||
walk::check_input(state_context)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_input(state_context: &FighterStateContext) -> Option<FighterStateTransition> {
|
||||
if let Some(input_frame) = state_context.input.get_last_frame() {
|
||||
if input_frame.movement.x.abs() < state_context.game_settings.input_common.stick_deadzone {
|
||||
return Some(FighterStateTransition::Wait);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
use crate::{
|
||||
fighter::{
|
||||
FighterAttributes, FighterVelocity,
|
||||
state::{FighterState, FighterStateContext, FighterStateTransition},
|
||||
states::{dash, wait},
|
||||
},
|
||||
game_settings::GameSettings,
|
||||
input::player::FighterInput,
|
||||
};
|
||||
use bevy::prelude::*;
|
||||
|
||||
#[derive(Component, Debug)]
|
||||
pub struct WalkState;
|
||||
|
||||
impl FighterState for WalkState {
|
||||
fn check_interrupt(
|
||||
&self,
|
||||
state_context: &FighterStateContext,
|
||||
) -> Option<FighterStateTransition> {
|
||||
wait::check_input(state_context).or_else(|| dash::check_input(state_context))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn walk_update(
|
||||
fighters: Query<(
|
||||
&mut FighterVelocity,
|
||||
&FighterAttributes,
|
||||
&FighterInput,
|
||||
), With<WalkState>>,
|
||||
game_settings: Res<GameSettings>,
|
||||
) {
|
||||
for (mut velocity, attributes, input) in fighters {
|
||||
if let Some(last_frame) = input.get_last_frame() {
|
||||
let accel_mul = 1.0;
|
||||
let mut accel = last_frame.movement.x * attributes.stick_walk_accel * accel_mul; // stick-proportional
|
||||
accel += (last_frame.movement.x.signum() * attributes.base_walk_accel) * accel_mul; // constant
|
||||
let target_vel = last_frame.movement.x * attributes.max_walk_vel * accel_mul;
|
||||
|
||||
if target_vel != 0.0 {
|
||||
let mult = velocity.x / target_vel;
|
||||
if mult > 0.0 && mult < 1.0 {
|
||||
accel *= (1.0 - mult) * game_settings.figher_common.walk_speed_ease; // 0.5 — linear ease-out toward target
|
||||
}
|
||||
}
|
||||
|
||||
// Friction mode
|
||||
if target_vel == 0.0 {
|
||||
let mut out_friction = velocity.x.signum() * (-attributes.ground_friction);
|
||||
// apply friction
|
||||
if attributes.ground_friction.abs() > velocity.x.abs() {
|
||||
out_friction = -velocity.x;
|
||||
}
|
||||
|
||||
accel = out_friction;
|
||||
} else {
|
||||
let ground_max_horizontal_velocity =
|
||||
game_settings.figher_common.ground_max_horizontal_velocity;
|
||||
if !(velocity.x * accel < 0.0) {
|
||||
// accelerating, not reversing
|
||||
if accel > 0.0 {
|
||||
if velocity.x + accel > target_vel {
|
||||
accel = -attributes.ground_friction;
|
||||
if velocity.x + accel < target_vel {
|
||||
accel = target_vel - velocity.x;
|
||||
}
|
||||
if velocity.x + accel > ground_max_horizontal_velocity {
|
||||
accel = ground_max_horizontal_velocity - velocity.x;
|
||||
}
|
||||
}
|
||||
} else if velocity.x + accel < target_vel {
|
||||
accel = attributes.ground_friction;
|
||||
if velocity.x + accel > target_vel {
|
||||
accel = target_vel - velocity.x;
|
||||
}
|
||||
if velocity.x + accel < -ground_max_horizontal_velocity {
|
||||
accel = -ground_max_horizontal_velocity - velocity.x;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
velocity.x += accel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_input(state_context: &FighterStateContext) -> Option<FighterStateTransition> {
|
||||
if let Some(input_frame) = state_context.input.get_last_frame() {
|
||||
if input_frame.movement.x.abs() >= state_context.game_settings.input_common.stick_deadzone {
|
||||
return Some(FighterStateTransition::Walk);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
Reference in New Issue
Block a user