Initial commit
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
# for Linux
|
||||
[target.x86_64-unknown-linux-gnu]
|
||||
linker = "clang"
|
||||
rustflags = ["-C", "link-arg=-fuse-ld=/usr/bin/mold", "-Zshare-generics=y", ]
|
||||
|
||||
[unstable]
|
||||
codegen-backend = true
|
||||
|
||||
[profile.dev]
|
||||
codegen-backend = "cranelift"
|
||||
|
||||
[profile.dev.package."*"]
|
||||
codegen-backend = "llvm"
|
||||
@@ -0,0 +1,2 @@
|
||||
/target
|
||||
/.jackdaw
|
||||
Generated
+7856
File diff suppressed because it is too large
Load Diff
+34
@@ -0,0 +1,34 @@
|
||||
[package]
|
||||
name = "game-test"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
authors = [""]
|
||||
|
||||
# A normal Bevy project. jackdaw builds its own editor artifacts in
|
||||
# .jackdaw/ and never changes this manifest; `cargo run` works with or
|
||||
# without the editor installed.
|
||||
|
||||
# Stand this project on its own even when it is created inside another
|
||||
# cargo workspace, which would otherwise adopt it and refuse to build
|
||||
# ("current package believes it's in a workspace when it's not").
|
||||
# Delete this to make it a member of the surrounding workspace instead.
|
||||
[workspace]
|
||||
|
||||
[dependencies]
|
||||
bevy = { version = "0.19", features = ["dynamic_linking", "free_camera"] }
|
||||
# Loads the scenes you author in the editor (assets/*.bsn) and builds
|
||||
# physics colliders from them. An ordinary dependency; remove it if you
|
||||
# stop using authored scenes.
|
||||
jackdaw_runtime = { version = "0.19", features = ["physics"], path="../jackdaw/crates/jackdaw_runtime" }
|
||||
avian3d = "0.7"
|
||||
arrayvec = "0.7.8"
|
||||
bevy-inspector-egui = "0.37.0"
|
||||
bevy_egui = "0.40.0"
|
||||
|
||||
# Enable a small amount of optimization in the dev profile.
|
||||
[profile.dev]
|
||||
opt-level = 1
|
||||
|
||||
# Enable a large amount of optimization in the dev profile for dependencies.
|
||||
[profile.dev.package."*"]
|
||||
opt-level = 3
|
||||
@@ -0,0 +1,12 @@
|
||||
# Game Test
|
||||
|
||||
A Bevy game, editable in the [jackdaw](https://github.com/jbuehler23/jackdaw) editor.
|
||||
|
||||
- `cargo run` plays the game standalone; no editor required.
|
||||
- Open the folder in jackdaw to author scenes, then hit Play. The
|
||||
editor builds its own artifacts into `.jackdaw/` (gitignored) and
|
||||
never modifies your manifest or lockfile.
|
||||
- Gameplay lives in `src/lib.rs` (`GamePlugin`); authored scenes live
|
||||
in `assets/*.bsn`.
|
||||
- Components you derive `Reflect` on appear in the editor's inspector
|
||||
automatically; no registration code is needed.
|
||||
@@ -0,0 +1,22 @@
|
||||
# jackdaw project settings. Everything here has a working default;
|
||||
# delete any line you are not using.
|
||||
|
||||
# The game plugin type inside your lib crate. Uncomment to override
|
||||
# source detection.
|
||||
# plugin = "GamePlugin"
|
||||
|
||||
# In a cargo workspace, the member jackdaw builds as the game.
|
||||
# package = "my-game"
|
||||
|
||||
# Recorded when this project was set up. Jackdaw compares these on
|
||||
# open so an editor upgrade is reported instead of surfacing as a
|
||||
# build failure.
|
||||
[jackdaw]
|
||||
version = "0.19.0"
|
||||
bevy = "0.19"
|
||||
|
||||
[[run]]
|
||||
name = "Play"
|
||||
# instances = 2
|
||||
# env = { SERVER_ADDR = "127.0.0.1:5000" }
|
||||
# args = []
|
||||
@@ -0,0 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
use bevy::prelude::*;
|
||||
|
||||
pub struct FighterSettingsCommon {
|
||||
pub walk_speed_ease: f32,
|
||||
pub ground_max_horizontal_velocity: f32,
|
||||
}
|
||||
pub struct InputSettingsCommon {
|
||||
pub stick_deadzone: f32,
|
||||
pub trigger_deadzone: f32,
|
||||
pub smash_input_reset_axis_threshold: f32,
|
||||
pub smash_input_axis_threshold: f32,
|
||||
pub smash_input_frame_threshold: u32
|
||||
}
|
||||
|
||||
impl Default for InputSettingsCommon {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
stick_deadzone: 0.28f32,
|
||||
trigger_deadzone: 0.3f32,
|
||||
smash_input_reset_axis_threshold: 0.25,
|
||||
smash_input_axis_threshold: 0.8,
|
||||
smash_input_frame_threshold: 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
pub struct GameSettings {
|
||||
pub figher_common: FighterSettingsCommon,
|
||||
pub input_common: InputSettingsCommon
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
use bevy::prelude::*;
|
||||
|
||||
use crate::input::player::InputFrame;
|
||||
|
||||
use super::player::{FighterInput, BaseInputMap, InputActionState};
|
||||
|
||||
pub fn postprocess_input(input_state: Query<&mut FighterInput>) {
|
||||
for mut player_input in input_state {
|
||||
}
|
||||
}
|
||||
pub fn preprocess_keyboard_input(key: Res<ButtonInput<KeyCode>>, input_map: Res<BaseInputMap>, mut input_state: Query<&mut FighterInput>) {
|
||||
// Keyboard input is only for player 0
|
||||
for mut keeb_input in &mut input_state.iter_mut().take(1) {
|
||||
let mut input_frame = InputFrame::default();
|
||||
for keyboard_elment in &input_map.keyboard {
|
||||
let action_state = if key.just_pressed(keyboard_elment.key) {
|
||||
InputActionState::JustPressed
|
||||
} else if key.just_released(keyboard_elment.key) {
|
||||
InputActionState::JustReleased
|
||||
} else if key.pressed(keyboard_elment.key) {
|
||||
InputActionState::Pressed
|
||||
} else {
|
||||
InputActionState::Released
|
||||
};
|
||||
|
||||
let action_strength = match action_state {
|
||||
InputActionState::JustPressed | InputActionState::Pressed => 1.0f32,
|
||||
InputActionState::JustReleased | InputActionState::Released => 0.0f32,
|
||||
};
|
||||
|
||||
match keyboard_elment.action {
|
||||
super::player::InputMapAction::MovementXDir(sign) => input_frame.movement.x += (sign as f32) * action_strength,
|
||||
super::player::InputMapAction::MovementYDir(sign) => input_frame.movement.y += (sign as f32) * action_strength,
|
||||
super::player::InputMapAction::Jump => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
input_frame.movement = input_frame.movement.normalize_or_zero();
|
||||
input_frame.directional_attack = input_frame.directional_attack.normalize_or_zero();
|
||||
keeb_input.push_input_frame(input_frame);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod player;
|
||||
pub mod logic;
|
||||
pub mod plugin;
|
||||
@@ -0,0 +1,93 @@
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use bevy::prelude::*;
|
||||
use arrayvec::ArrayVec;
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct InputFrame {
|
||||
pub movement: Vec2,
|
||||
pub directional_attack: Vec2
|
||||
}
|
||||
|
||||
#[derive(Component, Default)]
|
||||
pub struct FighterInput {
|
||||
input_buffer: VecDeque<InputFrame>
|
||||
}
|
||||
|
||||
impl FighterInput {
|
||||
const INPUT_BUFFER_SIZE: usize = 5;
|
||||
pub fn get_last_frame(&self) -> Option<&InputFrame> {
|
||||
self.input_buffer.back()
|
||||
}
|
||||
|
||||
pub fn push_input_frame(&mut self, frame: InputFrame) {
|
||||
while self.input_buffer.len() >= Self::INPUT_BUFFER_SIZE {
|
||||
self.input_buffer.pop_front();
|
||||
}
|
||||
self.input_buffer.push_back(frame);
|
||||
}
|
||||
|
||||
pub fn clear_buffer(&mut self) {
|
||||
while self.input_buffer.len() > 1 {
|
||||
self.input_buffer.pop_front();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_smash_x_movement(&self, reset_deadzone: f32, threshold: f32, frame_threshold: u32) -> Option<f32> {
|
||||
let iter = self.input_buffer.iter().zip(self.input_buffer.iter().skip(1));
|
||||
|
||||
let mut frames_in_deadzone = 0xFE;
|
||||
for (prev, current) in iter {
|
||||
let x_abs = current.movement.x.abs();
|
||||
if x_abs < reset_deadzone {
|
||||
frames_in_deadzone = 0xFE;
|
||||
} else {
|
||||
let prev_x_abs = prev.movement.x.abs();
|
||||
if prev_x_abs < reset_deadzone || prev.movement.x.signum() != current.movement.x.signum() {
|
||||
frames_in_deadzone = 0;
|
||||
} else {
|
||||
frames_in_deadzone += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if x_abs >= threshold && frames_in_deadzone <= frame_threshold {
|
||||
return Some(x_abs.signum());
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum InputActionState {
|
||||
JustPressed,
|
||||
Pressed,
|
||||
JustReleased,
|
||||
Released
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum InputMapAction {
|
||||
MovementXDir(i32),
|
||||
MovementYDir(i32),
|
||||
Jump
|
||||
}
|
||||
#[derive(Debug)]
|
||||
pub struct KeyboardInputMapElement {
|
||||
pub key: KeyCode,
|
||||
pub action: InputMapAction
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
pub struct BaseInputMap {
|
||||
pub keyboard: Vec<KeyboardInputMapElement>
|
||||
}
|
||||
|
||||
#[derive(Component, Debug)]
|
||||
#[relationship(relationship_target = ControlledBy)]
|
||||
pub struct Controls(pub Entity);
|
||||
|
||||
#[derive(Component, Debug)]
|
||||
#[relationship_target(relationship = Controls)]
|
||||
pub struct ControlledBy(Entity);
|
||||
@@ -0,0 +1,29 @@
|
||||
use bevy::prelude::*;
|
||||
use crate::fighter::*;
|
||||
use crate::input::player::{ControlledBy, Controls, FighterInput};
|
||||
use crate::input::{
|
||||
logic::{postprocess_input, preprocess_keyboard_input}, player::{BaseInputMap, InputMapAction, KeyboardInputMapElement},
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FighterInputPlugin;
|
||||
|
||||
impl Plugin for FighterInputPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.insert_resource(BaseInputMap {
|
||||
keyboard: vec![
|
||||
// right
|
||||
KeyboardInputMapElement {
|
||||
key: KeyCode::KeyD,
|
||||
action: InputMapAction::MovementXDir(1),
|
||||
},
|
||||
// left
|
||||
KeyboardInputMapElement {
|
||||
key: KeyCode::KeyA,
|
||||
action: InputMapAction::MovementXDir(-1),
|
||||
},
|
||||
],
|
||||
})
|
||||
.add_systems(FixedPreUpdate, (preprocess_keyboard_input, postprocess_input).chain());
|
||||
}
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
//! game-test: gameplay shared between `cargo run` and the
|
||||
//! editor's Play button.
|
||||
//!
|
||||
//! Scene content (entities, components, positions) lives in
|
||||
//! `assets/*.bsn` files authored in the editor. Game behaviour
|
||||
//! (systems, observers, resources) lives in [`GamePlugin`].
|
||||
//!
|
||||
//! # Try it
|
||||
//!
|
||||
//! 1. Open this project in jackdaw.
|
||||
//! 2. Hierarchy: right-click, `Add > Cube`.
|
||||
//! 3. Inspector: `Add Component > SpinningCube`. Set `speed` to `1.5`.
|
||||
//! 4. `File > Save`, then click Play. The cube spins.
|
||||
//! 5. `cargo run` launches the same game without the editor.
|
||||
//!
|
||||
//! # Adding your own components
|
||||
//!
|
||||
//! Write a component anywhere in this library (any module, not
|
||||
//! `main.rs`), deriving `Component, Reflect, Default` with
|
||||
//! `#[reflect(Component, Default)]`, like `SpinningCube` below. After you
|
||||
//! save, click Rebuild in jackdaw (or run `jd build`) and it
|
||||
//! appears in `Add Component`. No registration code is needed.
|
||||
|
||||
use bevy::prelude::*;
|
||||
use jackdaw_runtime::prelude::*;
|
||||
|
||||
use crate::{fighter::{states::wait::WaitState, *}, game_settings::{FighterSettingsCommon, GameSettings, InputSettingsCommon}, stage::debug_draw_scene};
|
||||
|
||||
pub mod fighter;
|
||||
pub mod input;
|
||||
pub mod stage;
|
||||
pub mod game_settings;
|
||||
|
||||
/// Your game's Bevy plugin. The editor finds it by this name (override
|
||||
/// with `plugin = "..."` in jackdaw.toml) and runs it on Play; the
|
||||
/// standalone binary adds it in `main.rs`.
|
||||
#[derive(Default)]
|
||||
pub struct GamePlugin;
|
||||
|
||||
pub fn setup(mut commands: Commands) {
|
||||
let fighter = commands
|
||||
.spawn((
|
||||
Name("Fighter".into()),
|
||||
FighterECB {
|
||||
vertical_half: 0.5,
|
||||
horizontal_half: 0.25,
|
||||
},
|
||||
FighterVelocity::default(),
|
||||
FighterTranslation(Vec2::new(0.5, 1.25)),
|
||||
FighterAttributes {
|
||||
base_walk_accel: 0.01,
|
||||
stick_walk_accel: 0.02,
|
||||
max_walk_vel: 0.16,
|
||||
ground_friction: 0.008,
|
||||
dash_duration: 15
|
||||
},
|
||||
WaitState {},
|
||||
input::player::FighterInput::default()
|
||||
))
|
||||
.id();
|
||||
}
|
||||
|
||||
impl Plugin for GamePlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_systems(Startup, stage::test_scene.spawn())
|
||||
.add_systems(Startup, setup)
|
||||
.add_systems(Update, spin_cubes)
|
||||
.add_systems(Update, debug_draw_scene)
|
||||
.add_plugins(input::plugin::FighterInputPlugin)
|
||||
.add_plugins(fighter::FighterPlugin)
|
||||
.insert_resource(GameSettings {
|
||||
figher_common: FighterSettingsCommon {
|
||||
walk_speed_ease: 0.5,
|
||||
ground_max_horizontal_velocity: 0.3
|
||||
},
|
||||
input_common: InputSettingsCommon::default(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Spin-rate in radians per second. Attach it in the inspector; the
|
||||
/// editor sees this component with no registration code because Bevy's
|
||||
/// `reflect_auto_register` picks up the `Reflect` derive. `Default` +
|
||||
/// `#[reflect(Default)]` let the editor add it and the game reconstruct
|
||||
/// it with sensible starting values.
|
||||
#[derive(Component, Reflect, Default)]
|
||||
#[reflect(Component, Default, @EditorCategory::new("Actor"))]
|
||||
pub struct SpinningCube {
|
||||
pub speed: f32,
|
||||
}
|
||||
|
||||
fn spin_cubes(time: Res<Time>, mut cubes: Query<(&SpinningCube, &mut Transform)>) {
|
||||
let dt = time.delta_secs();
|
||||
for (cube, mut transform) in &mut cubes {
|
||||
transform.rotate_y(cube.speed * dt);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component, Reflect, Default)]
|
||||
#[reflect(Component, Default, @EditorCategory::new("Actor"))]
|
||||
pub struct TestCube {
|
||||
pub frog: f32,
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
//! Standalone binary for game-test: `cargo run` plays the game
|
||||
//! without the editor. The editor's Play button runs the same
|
||||
//! [`game_test::GamePlugin`] through its own game runner instead.
|
||||
|
||||
use bevy::camera_controller::free_camera::FreeCameraPlugin;
|
||||
use bevy::image::{ImageAddressMode, ImagePlugin, ImageSamplerDescriptor};
|
||||
use bevy::prelude::*;
|
||||
use bevy::remote::http::RemoteHttpPlugin;
|
||||
use bevy::remote::{RemotePlugin};
|
||||
use bevy_egui::EguiPlugin;
|
||||
use bevy_inspector_egui::{
|
||||
quick::WorldInspectorPlugin
|
||||
};
|
||||
|
||||
fn main() -> AppExit {
|
||||
// A `Repeat` sampler so the tileable materials authored in the
|
||||
// editor render the same way at runtime; Bevy's default
|
||||
// `ClampToEdge` smears texture edges on brush geometry.
|
||||
let default_plugins = DefaultPlugins.set(ImagePlugin {
|
||||
default_sampler: ImageSamplerDescriptor {
|
||||
address_mode_u: ImageAddressMode::Repeat,
|
||||
address_mode_v: ImageAddressMode::Repeat,
|
||||
address_mode_w: ImageAddressMode::Repeat,
|
||||
..ImageSamplerDescriptor::linear()
|
||||
},
|
||||
});
|
||||
|
||||
App::new()
|
||||
.add_plugins(default_plugins)
|
||||
.add_plugins(avian3d::prelude::PhysicsPlugins::default())
|
||||
.add_plugins(jackdaw_runtime::JackdawPlugin)
|
||||
.add_plugins(game_test::GamePlugin)
|
||||
.add_plugins(FreeCameraPlugin)
|
||||
.add_plugins(RemotePlugin::default())
|
||||
.add_plugins(RemoteHttpPlugin::default())
|
||||
.add_plugins(EguiPlugin::default())
|
||||
.add_plugins(WorldInspectorPlugin::default()) // adds default options and `InspectorEguiImpl`s
|
||||
.insert_resource(Time::<Fixed>::from_hz(60.0))
|
||||
.run()
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
use bevy::{camera_controller::free_camera::FreeCamera, color::palettes::css::{GREEN, RED}, prelude::*};
|
||||
|
||||
#[derive(Component, Reflect, Clone, Default)]
|
||||
#[reflect(Component)]
|
||||
pub struct StagePlane {
|
||||
pub plane: Plane2d,
|
||||
pub position: Vec2,
|
||||
half_size: f32,
|
||||
basis_x: Vec2,
|
||||
}
|
||||
|
||||
pub struct StagePlaneIntersectResult {
|
||||
pub position: Vec2
|
||||
}
|
||||
|
||||
impl StagePlane {
|
||||
pub fn new(normal: Dir2, half_size: f32, position: Vec2) -> Self {
|
||||
let orthogonal = normal.perp();
|
||||
let basis_x = orthogonal * half_size;
|
||||
|
||||
Self {
|
||||
plane: Plane2d::new(*normal),
|
||||
position,
|
||||
half_size,
|
||||
basis_x,
|
||||
}
|
||||
}
|
||||
pub fn intersect_ray(&self, ray: Ray2d) -> Option<StagePlaneIntersectResult> {
|
||||
if let Some(result) = ray.plane_intersection_point(self.position, self.plane) {
|
||||
return Some(StagePlaneIntersectResult {
|
||||
position: result
|
||||
});
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn project(&self, vec: Vec2) -> Vec2 {
|
||||
vec - vec.project_onto_normalized(self.plane.normal.as_vec2())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn debug_draw_scene(mut gizmos: Gizmos, planes: Query<&StagePlane>) {
|
||||
for plane in planes {
|
||||
let plane_pos_3d = Vec3::new(plane.position.x, plane.position.y, 0.0);
|
||||
let plane_normal_3d = Vec3::new(plane.plane.normal.x, plane.plane.normal.y, 0.0);
|
||||
let plane_basis_x_3d = Vec3::new(plane.basis_x.x, plane.basis_x.y, 0.0);
|
||||
gizmos.rect(
|
||||
Transform::from_translation(plane_pos_3d).looking_to(plane_normal_3d, Dir3::Z).to_isometry(),
|
||||
Vec2::new(plane.half_size * 2.0, 1.0),
|
||||
GREEN,
|
||||
);
|
||||
gizmos.line(plane_pos_3d, plane_pos_3d + plane_basis_x_3d, RED);
|
||||
}
|
||||
}
|
||||
|
||||
/// set up a simple 3D scene
|
||||
pub fn test_scene() -> impl SceneList {
|
||||
let up = Dir2::Y;
|
||||
bsn_list! [
|
||||
(
|
||||
#Cube
|
||||
Mesh3d(asset_value(Cuboid::new(5.0, 1.0, 1.0)))
|
||||
MeshMaterial3d::<StandardMaterial>(asset_value(Color::srgb_u8(124, 144, 255)))
|
||||
Transform::from_xyz(0.0, 0.5, 0.0)
|
||||
),
|
||||
(
|
||||
#StagePlane
|
||||
template_value(StagePlane::new(up, 2.5, Vec2::new(0.0, 1.0)))
|
||||
),
|
||||
(
|
||||
PointLight {
|
||||
shadow_maps_enabled: true,
|
||||
}
|
||||
Transform::from_xyz(4.0, 8.0, 4.0)
|
||||
),
|
||||
(
|
||||
Camera3d
|
||||
template_value(Transform::from_xyz(0.0, 1.5, 9.0))
|
||||
/*FreeCamera {
|
||||
sensitivity: 0.2,
|
||||
friction: 25.0,
|
||||
walk_speed: 3.0,
|
||||
run_speed: 9.0,
|
||||
}*/
|
||||
)
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user