working state
3
.cargo/config.toml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
[target.x86_64-unknown-linux-gnu]
|
||||
linker = "clang"
|
||||
rustflags = ["-C", "link-arg=-fuse-ld=/usr/bin/mold"]
|
||||
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/target
|
||||
4879
Cargo.lock
generated
Normal file
34
Cargo.toml
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
[package]
|
||||
name = "brackeys-game"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[profile.dev]
|
||||
opt-level = 1
|
||||
|
||||
[profile.dev.package."*"]
|
||||
opt-level = 3
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
opt-level = 3
|
||||
codegen-units = 1
|
||||
incremental = false
|
||||
debug = false
|
||||
|
||||
[dependencies]
|
||||
bevy = "0.13.2"
|
||||
bevy_asset_loader = "0.20.2"
|
||||
bevy_editor_pls = "0.8.1"
|
||||
leafwing-input-manager = "0.13.3"
|
||||
# Use unstable version for Bevy 0.13 support
|
||||
bevy_ecs_ldtk = { git = "https://github.com/Trouv/bevy_ecs_ldtk.git" }
|
||||
bevy_xpbd_2d = "0.4.2"
|
||||
|
||||
[patch.crates-io]
|
||||
# Patch unstable version to resolve conflicting dependencies from bevy_ecs_ldtk
|
||||
bevy_ecs_tilemap = { git = "https://github.com/StarArawn/bevy_ecs_tilemap" }
|
||||
|
||||
[dev-dependencies]
|
||||
bevy = { version = "0.13.2", features = ["dynamic_linking"] }
|
||||
|
||||
BIN
assets/brackeys_platformer_assets.zip
Normal file
26
assets/brackeys_platformer_assets/LICENSE & CREDITS.txt
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
All assets in the pack have been repackaged and many have been modified by Brackeys.
|
||||
|
||||
|
||||
LICENSE for all assets:
|
||||
|
||||
Creative Commons Zero (CC0)
|
||||
|
||||
|
||||
CREDIT:
|
||||
|
||||
SPRITES by analogStudios_:
|
||||
knight (https://analogstudios.itch.io/camelot)
|
||||
slime (https://analogstudios.itch.io/dungeonsprites)
|
||||
platforms and coin (https://analogstudios.itch.io/four-seasons-platformer-sprites)
|
||||
|
||||
SPRITES by RottingPixels:
|
||||
world_tileset and fruit (https://rottingpixels.itch.io/four-seasons-platformer-tileset-16x16free)
|
||||
|
||||
|
||||
WORLD TILESET originally
|
||||
|
||||
SOUNDS by Brackeys, Asbjørn Thirslund
|
||||
|
||||
MUSIC by Brackeys, Sofia Thirslund
|
||||
|
||||
FONTS by Jayvee Enaguas - HarvettFox96 - (https://www.dafont.com/pixel-operator.font?l[]=10&l[]=1)
|
||||
BIN
assets/brackeys_platformer_assets/fonts/PixelOperator8-Bold.ttf
Normal file
BIN
assets/brackeys_platformer_assets/fonts/PixelOperator8.ttf
Normal file
BIN
assets/brackeys_platformer_assets/music/time_for_adventure.mp3
Normal file
BIN
assets/brackeys_platformer_assets/sounds/coin.wav
Normal file
BIN
assets/brackeys_platformer_assets/sounds/explosion.wav
Normal file
BIN
assets/brackeys_platformer_assets/sounds/hurt.wav
Normal file
BIN
assets/brackeys_platformer_assets/sounds/jump.wav
Normal file
BIN
assets/brackeys_platformer_assets/sounds/power_up.wav
Normal file
BIN
assets/brackeys_platformer_assets/sounds/tap.wav
Normal file
BIN
assets/brackeys_platformer_assets/sprites/coin.png
Normal file
|
After Width: | Height: | Size: 500 B |
BIN
assets/brackeys_platformer_assets/sprites/fruit.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
assets/brackeys_platformer_assets/sprites/knight.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
assets/brackeys_platformer_assets/sprites/platforms.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
assets/brackeys_platformer_assets/sprites/slime_green.png
Normal file
|
After Width: | Height: | Size: 908 B |
BIN
assets/brackeys_platformer_assets/sprites/slime_purple.png
Normal file
|
After Width: | Height: | Size: 1 KiB |
BIN
assets/brackeys_platformer_assets/sprites/world_tileset.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
786
assets/level.ldtk
Normal file
2
rust-toolchain.toml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
[toolchain]
|
||||
channel = "nightly"
|
||||
27
src/assets/ldtk.rs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
use bevy::prelude::*;
|
||||
use bevy_ecs_ldtk::prelude::*;
|
||||
|
||||
use crate::GameState;
|
||||
|
||||
pub struct LdtkPlugin;
|
||||
|
||||
#[derive(Default, Resource)]
|
||||
pub struct WorldConfig {
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
impl Plugin for LdtkPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.init_resource::<WorldConfig>()
|
||||
.add_plugins(bevy_ecs_ldtk::LdtkPlugin)
|
||||
.add_systems(OnEnter(GameState::Loading), spawn_world)
|
||||
.insert_resource(LevelSelection::Uid(0));
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_world(mut commands: Commands, asset_server: Res<AssetServer>, config: Res<WorldConfig>) {
|
||||
commands.spawn(LdtkWorldBundle {
|
||||
ldtk_handle: asset_server.load(&config.path),
|
||||
..default()
|
||||
});
|
||||
}
|
||||
13
src/assets/mod.rs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
use bevy::app::Plugin;
|
||||
|
||||
use self::ldtk::LdtkPlugin;
|
||||
|
||||
pub mod ldtk;
|
||||
|
||||
pub struct AssetPlugin;
|
||||
impl Plugin for AssetPlugin {
|
||||
fn build(&self, app: &mut bevy::prelude::App) {
|
||||
app.add_plugins(LdtkPlugin);
|
||||
}
|
||||
}
|
||||
|
||||
92
src/camera/mod.rs
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
use bevy::prelude::*;
|
||||
use bevy_ecs_ldtk::prelude::*;
|
||||
|
||||
#[derive(Component, Debug)]
|
||||
pub struct Follow {
|
||||
pub target: Entity,
|
||||
pub offset: Vec3,
|
||||
}
|
||||
|
||||
impl Follow {
|
||||
pub fn new(target: Entity) -> Self {
|
||||
Self {
|
||||
target,
|
||||
offset: Vec3::ZERO,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_offset(self, offset: Vec3) -> Self {
|
||||
Self { offset, ..self }
|
||||
}
|
||||
}
|
||||
|
||||
// link this to the camera's target
|
||||
// https://trouv.github.io/bevy_ecs_ldtk/latest/how-to-guides/create-bevy-relations-from-ldtk-entity-references.html
|
||||
#[derive(Debug, Default, Deref, DerefMut, Component)]
|
||||
pub struct UnresolvedTargetRef(Option<EntityIid>);
|
||||
impl UnresolvedTargetRef {
|
||||
pub fn from_field(instance: &EntityInstance) -> UnresolvedTargetRef {
|
||||
Self(
|
||||
instance
|
||||
.get_maybe_entity_ref_field("Target")
|
||||
.expect("expected entity to have mother entity ref field")
|
||||
.as_ref()
|
||||
.map(|entity_ref| EntityIid::new(entity_ref.entity_iid.clone())),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Bundle, Default, LdtkEntity)]
|
||||
pub struct CameraEntity {
|
||||
#[with(UnresolvedTargetRef::from_field)]
|
||||
unresolved_target_ref: UnresolvedTargetRef,
|
||||
camera_2d_bundle: Camera2dBundle
|
||||
}
|
||||
|
||||
pub struct CameraPlugin;
|
||||
impl Plugin for CameraPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_systems(Update, (follow_target, resolve_target_references, dynamic_scale)).register_ldtk_entity::<CameraEntity>("Camera");
|
||||
}
|
||||
}
|
||||
|
||||
fn follow_target(
|
||||
mut follower_q: Query<(&mut Transform, &Follow)>,
|
||||
target_q: Query<&GlobalTransform, Without<Follow>>,
|
||||
) {
|
||||
for (mut transform, follow) in follower_q.iter_mut() {
|
||||
if let Ok(target) = target_q.get(follow.target) {
|
||||
transform.translation = target.transform_point(follow.offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_target_references(
|
||||
mut commands: Commands,
|
||||
unresolved_targets: Query<(Entity, &UnresolvedTargetRef), Added<UnresolvedTargetRef>>,
|
||||
ldtk_entities: Query<(Entity, &EntityIid)>,
|
||||
) {
|
||||
for (child_entity, unresolved_target_ref) in unresolved_targets.iter() {
|
||||
if let Some(target_iid) = unresolved_target_ref.0.as_ref() {
|
||||
let (target_entity, _) = ldtk_entities
|
||||
.iter()
|
||||
.find(|(_, iid)| *iid == target_iid)
|
||||
.expect("enemy's target entity should exist");
|
||||
|
||||
commands
|
||||
.entity(child_entity)
|
||||
.remove::<UnresolvedTargetRef>()
|
||||
.insert(Follow::new(target_entity));
|
||||
} else {
|
||||
commands
|
||||
.entity(child_entity)
|
||||
.remove::<UnresolvedTargetRef>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dynamic_scale(mut query: Query<&mut OrthographicProjection>) {
|
||||
for mut projection in query.iter_mut() {
|
||||
projection.scale = 0.2;
|
||||
}
|
||||
}
|
||||
11
src/debug/mod.rs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
use bevy::app::Plugin;
|
||||
use bevy_editor_pls::EditorPlugin;
|
||||
use bevy_xpbd_2d::plugins::PhysicsDebugPlugin;
|
||||
|
||||
pub struct DebugPlugin;
|
||||
impl Plugin for DebugPlugin {
|
||||
fn build(&self, app: &mut bevy::prelude::App) {
|
||||
app.add_plugins((EditorPlugin::default(), PhysicsDebugPlugin::default()));
|
||||
}
|
||||
}
|
||||
|
||||
35
src/level/mod.rs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
use bevy::{input::InputPlugin, prelude::*};
|
||||
use bevy_ecs_ldtk::prelude::*;
|
||||
use bevy_xpbd_2d::prelude::*;
|
||||
|
||||
#[derive(Component, Default)]
|
||||
pub struct Tile;
|
||||
|
||||
#[derive(Bundle)]
|
||||
pub struct TileBundle {
|
||||
tile: Tile,
|
||||
collider: Collider,
|
||||
rigidbody: RigidBody,
|
||||
}
|
||||
|
||||
impl Default for TileBundle {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
tile: Tile,
|
||||
collider: Collider::rectangle(16., 16.),
|
||||
rigidbody: RigidBody::Static,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Bundle, LdtkIntCell)]
|
||||
pub struct TileIntCell {
|
||||
tile_bundle: TileBundle,
|
||||
}
|
||||
|
||||
pub struct LevelPlugin;
|
||||
impl Plugin for LevelPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.register_default_ldtk_int_cell_for_layer::<TileIntCell>("Ground");
|
||||
}
|
||||
}
|
||||
55
src/lib.rs
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
use assets::{ldtk::WorldConfig, AssetPlugin};
|
||||
use bevy::prelude::*;
|
||||
use bevy_xpbd_2d::math::{Scalar, Vector};
|
||||
use camera::CameraPlugin;
|
||||
use debug::DebugPlugin;
|
||||
use level::LevelPlugin;
|
||||
use physics::PhysicsPlugin;
|
||||
use player::PlayerPlugin;
|
||||
|
||||
mod assets;
|
||||
mod camera;
|
||||
mod debug;
|
||||
mod level;
|
||||
mod physics;
|
||||
mod player;
|
||||
|
||||
#[derive(States, Default, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum GameState {
|
||||
MainMenu,
|
||||
#[default]
|
||||
Loading,
|
||||
Playing,
|
||||
}
|
||||
|
||||
pub const GRAVITY: Scalar = 9.81;
|
||||
pub const GRAVITY_VECTOR: Vector = Vector { x: 0., y: -GRAVITY };
|
||||
|
||||
pub struct GamePlugin;
|
||||
impl Plugin for GamePlugin {
|
||||
fn build(&self, app: &mut bevy::prelude::App) {
|
||||
app.add_plugins(DefaultPlugins.set(ImagePlugin::default_nearest()))
|
||||
.add_plugins((
|
||||
AssetPlugin,
|
||||
PhysicsPlugin,
|
||||
LevelPlugin,
|
||||
PlayerPlugin,
|
||||
CameraPlugin,
|
||||
))
|
||||
.init_state::<GameState>()
|
||||
.insert_resource(WorldConfig {
|
||||
path: "level.ldtk".to_string(),
|
||||
});
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
app.add_plugins(DebugPlugin);
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_camera(mut commands: Commands) {
|
||||
let mut camera = Camera2dBundle::default();
|
||||
camera.projection.scale = 0.5;
|
||||
camera.transform.translation.x += 1280.0 / 4.0;
|
||||
camera.transform.translation.y += 720.0 / 4.0;
|
||||
commands.spawn(camera);
|
||||
}
|
||||
6
src/main.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
use bevy::prelude::*;
|
||||
use brackeys_game::GamePlugin;
|
||||
|
||||
fn main() {
|
||||
App::new().add_plugins(GamePlugin).run();
|
||||
}
|
||||
8
src/physics/mod.rs
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
use bevy::app::Plugin;
|
||||
|
||||
pub struct PhysicsPlugin;
|
||||
impl Plugin for PhysicsPlugin {
|
||||
fn build(&self, app: &mut bevy::prelude::App) {
|
||||
app.add_plugins(bevy_xpbd_2d::prelude::PhysicsPlugins::default());
|
||||
}
|
||||
}
|
||||
292
src/player/controller.rs
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
use bevy::prelude::*;
|
||||
use bevy_xpbd_2d::{
|
||||
math::{Scalar, Vector, PI},
|
||||
prelude::*,
|
||||
SubstepSchedule, SubstepSet,
|
||||
};
|
||||
|
||||
use crate::GRAVITY_VECTOR;
|
||||
|
||||
#[derive(Component, Reflect)]
|
||||
#[component(storage = "SparseSet")]
|
||||
pub struct Grounded;
|
||||
|
||||
#[derive(Component, Reflect)]
|
||||
pub struct JumpImpulse(Scalar);
|
||||
|
||||
impl Default for JumpImpulse {
|
||||
fn default() -> Self {
|
||||
JumpImpulse(7.)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component, Reflect)]
|
||||
pub struct MaxJumpDuration(Scalar);
|
||||
|
||||
impl Default for MaxJumpDuration {
|
||||
fn default() -> Self {
|
||||
MaxJumpDuration(0.1)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component, Reflect)]
|
||||
pub struct MovementAcceleration(Scalar);
|
||||
|
||||
impl Default for MovementAcceleration {
|
||||
fn default() -> Self {
|
||||
MovementAcceleration(75.)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component, Reflect)]
|
||||
pub struct MovementDampingFactor(Scalar);
|
||||
|
||||
impl Default for MovementDampingFactor {
|
||||
fn default() -> Self {
|
||||
MovementDampingFactor(0.9)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component, Reflect)]
|
||||
pub struct MaxSlopeAngle(Scalar);
|
||||
|
||||
impl Default for MaxSlopeAngle {
|
||||
fn default() -> Self {
|
||||
MaxSlopeAngle(PI * 0.45)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component, Default, Reflect)]
|
||||
pub struct CharacterController;
|
||||
|
||||
#[derive(Component, Reflect)]
|
||||
pub struct ControllerGravity(Vector);
|
||||
|
||||
impl Default for ControllerGravity {
|
||||
fn default() -> Self {
|
||||
ControllerGravity(GRAVITY_VECTOR)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Bundle, Default)]
|
||||
pub struct MovementBundle {
|
||||
acceleration: MovementAcceleration,
|
||||
damping: MovementDampingFactor,
|
||||
jump_impulse: JumpImpulse,
|
||||
max_slope_angle: MaxSlopeAngle,
|
||||
direction: MovementDirection,
|
||||
}
|
||||
|
||||
#[derive(Bundle)]
|
||||
pub struct CharacterControllerBundle {
|
||||
character_controller: CharacterController,
|
||||
rigidbody: RigidBody,
|
||||
collider: Collider,
|
||||
ground_caster: ShapeCaster,
|
||||
gravity: ControllerGravity,
|
||||
movement: MovementBundle,
|
||||
locked_axes: LockedAxes,
|
||||
}
|
||||
|
||||
impl Default for CharacterControllerBundle {
|
||||
fn default() -> Self {
|
||||
let collider = Collider::rectangle(12., 24.);
|
||||
let mut caster_shape = collider.clone();
|
||||
caster_shape.set_scale(Vector::ONE * 0.5, 1);
|
||||
|
||||
Self {
|
||||
locked_axes: LockedAxes::ROTATION_LOCKED,
|
||||
character_controller: CharacterController,
|
||||
rigidbody: RigidBody::Kinematic,
|
||||
collider,
|
||||
ground_caster: ShapeCaster::new(caster_shape, Vector::ZERO, 0.0, Direction2d::NEG_Y)
|
||||
.with_max_time_of_impact(1.0),
|
||||
gravity: ControllerGravity(GRAVITY_VECTOR * 5.),
|
||||
movement: MovementBundle {
|
||||
acceleration: MovementAcceleration(420.),
|
||||
jump_impulse: JumpImpulse(46.),
|
||||
..default()
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component, Reflect)]
|
||||
#[component(storage = "SparseSet")]
|
||||
pub struct Jumping;
|
||||
|
||||
#[derive(Component, Default, Reflect)]
|
||||
pub struct MovementDirection(pub Scalar);
|
||||
|
||||
pub struct CharacterControllerPlugin;
|
||||
impl Plugin for CharacterControllerPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.register_type::<Grounded>()
|
||||
.register_type::<MovementAcceleration>()
|
||||
.register_type::<MovementDampingFactor>()
|
||||
.register_type::<JumpImpulse>()
|
||||
.register_type::<MaxSlopeAngle>()
|
||||
.register_type::<MovementDirection>()
|
||||
.register_type::<CharacterController>()
|
||||
.register_type::<ControllerGravity>()
|
||||
.add_systems(
|
||||
Update,
|
||||
(
|
||||
update_grounded,
|
||||
apply_gravity,
|
||||
handle_movement,
|
||||
handle_jump,
|
||||
apply_movement_damping,
|
||||
)
|
||||
.chain(),
|
||||
)
|
||||
.add_systems(
|
||||
// Run collision handling in substep schedule
|
||||
SubstepSchedule,
|
||||
kinematic_controller_collisions.in_set(SubstepSet::SolveUserConstraints),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_movement(
|
||||
time: Res<Time>,
|
||||
mut query: Query<
|
||||
(
|
||||
&mut LinearVelocity,
|
||||
&MovementAcceleration,
|
||||
&MovementDirection,
|
||||
),
|
||||
With<CharacterController>,
|
||||
>,
|
||||
) {
|
||||
let delta = time.delta_seconds();
|
||||
for (mut velocity, MovementAcceleration(acceleration), MovementDirection(direction)) in
|
||||
query.iter_mut()
|
||||
{
|
||||
velocity.x += direction * acceleration * delta;
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_jump(
|
||||
mut query: Query<
|
||||
(&mut LinearVelocity, &JumpImpulse),
|
||||
(With<CharacterController>, With<Jumping>, With<Grounded>),
|
||||
>,
|
||||
) {
|
||||
for (mut velocity, JumpImpulse(impulse)) in query.iter_mut() {
|
||||
velocity.y = *impulse;
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_movement_damping(mut query: Query<(&MovementDampingFactor, &mut LinearVelocity)>) {
|
||||
for (damping_factor, mut linear_velocity) in &mut query {
|
||||
// We could use `LinearDamping`, but we don't want to dampen movement along the Y axis
|
||||
linear_velocity.x *= damping_factor.0;
|
||||
}
|
||||
}
|
||||
|
||||
fn update_grounded(
|
||||
mut commands: Commands,
|
||||
mut query: Query<
|
||||
(Entity, &ShapeHits, &Rotation, Option<&MaxSlopeAngle>),
|
||||
With<CharacterController>,
|
||||
>,
|
||||
) {
|
||||
for (entity, hits, rotation, max_slope_angle) in &mut query {
|
||||
// The character is grounded if the shape caster has a hit with a normal
|
||||
// that isn't too steep.
|
||||
let is_grounded = hits.iter().any(|hit| {
|
||||
if let Some(angle) = max_slope_angle {
|
||||
rotation.rotate(-hit.normal2).angle_between(Vector::Y).abs() <= angle.0
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
|
||||
if is_grounded {
|
||||
commands.entity(entity).insert(Grounded);
|
||||
} else {
|
||||
commands.entity(entity).remove::<Grounded>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_gravity(time: Res<Time>, mut query: Query<(&mut LinearVelocity, &ControllerGravity)>) {
|
||||
let delta = time.delta_seconds();
|
||||
for (mut velocity, ControllerGravity(gravity)) in query.iter_mut() {
|
||||
velocity.0 += *gravity * delta;
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn kinematic_controller_collisions(
|
||||
collisions: Res<Collisions>,
|
||||
collider_parents: Query<&ColliderParent, Without<Sensor>>,
|
||||
mut character_controllers: Query<
|
||||
(
|
||||
&RigidBody,
|
||||
&mut Position,
|
||||
&Rotation,
|
||||
&mut LinearVelocity,
|
||||
Option<&MaxSlopeAngle>,
|
||||
),
|
||||
With<CharacterController>,
|
||||
>,
|
||||
) {
|
||||
// Iterate through collisions and move the kinematic body to resolve penetration
|
||||
for contacts in collisions.iter() {
|
||||
// If the collision didn't happen during this substep, skip the collision
|
||||
if !contacts.during_current_substep {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the rigid body entities of the colliders (colliders could be children)
|
||||
let Ok([collider_parent1, collider_parent2]) =
|
||||
collider_parents.get_many([contacts.entity1, contacts.entity2])
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Get the body of the character controller and whether it is the first
|
||||
// or second entity in the collision.
|
||||
let is_first: bool;
|
||||
let (rb, mut position, rotation, mut linear_velocity, max_slope_angle) =
|
||||
if let Ok(character) = character_controllers.get_mut(collider_parent1.get()) {
|
||||
is_first = true;
|
||||
character
|
||||
} else if let Ok(character) = character_controllers.get_mut(collider_parent2.get()) {
|
||||
is_first = false;
|
||||
character
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// This system only handles collision response for kinematic character controllers
|
||||
if !rb.is_kinematic() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Iterate through contact manifolds and their contacts.
|
||||
// Each contact in a single manifold shares the same contact normal.
|
||||
for manifold in contacts.manifolds.iter() {
|
||||
let normal = if is_first {
|
||||
-manifold.global_normal1(rotation)
|
||||
} else {
|
||||
-manifold.global_normal2(rotation)
|
||||
};
|
||||
|
||||
// Solve each penetrating contact in the manifold
|
||||
for contact in manifold.contacts.iter().filter(|c| c.penetration > 0.0) {
|
||||
position.0 += normal * contact.penetration;
|
||||
}
|
||||
|
||||
// If the slope isn't too steep to walk on but the character
|
||||
// is falling, reset vertical velocity.
|
||||
if max_slope_angle.is_some_and(|angle| normal.angle_between(Vector::Y).abs() <= angle.0)
|
||||
&& linear_velocity.y < 0.0
|
||||
{
|
||||
linear_velocity.y = linear_velocity.y.max(0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
31
src/player/input.rs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
use bevy::prelude::*;
|
||||
use leafwing_input_manager::prelude::*;
|
||||
|
||||
#[derive(Actionlike, PartialEq, Eq, Clone, Copy, Hash, Debug, Reflect)]
|
||||
pub enum PlayerAction {
|
||||
Move,
|
||||
Jump,
|
||||
}
|
||||
|
||||
impl PlayerAction {
|
||||
pub fn default_input_map() -> InputMap<Self> {
|
||||
let mut input_map = InputMap::default();
|
||||
|
||||
// Default gamepad input bindings
|
||||
input_map.insert(Self::Move, DualAxis::left_stick());
|
||||
input_map.insert(Self::Jump, GamepadButtonType::South);
|
||||
|
||||
// Default kbm input bindings
|
||||
input_map.insert(Self::Move, VirtualDPad::wasd());
|
||||
input_map.insert(Self::Jump, KeyCode::Space);
|
||||
|
||||
input_map
|
||||
}
|
||||
}
|
||||
|
||||
pub struct InputPlugin;
|
||||
impl Plugin for InputPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_plugins(InputManagerPlugin::<PlayerAction>::default());
|
||||
}
|
||||
}
|
||||
71
src/player/mod.rs
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
mod controller;
|
||||
mod input;
|
||||
|
||||
use bevy::prelude::*;
|
||||
use bevy_ecs_ldtk::prelude::*;
|
||||
use leafwing_input_manager::{action_state::ActionState, input_map::InputMap, InputManagerBundle};
|
||||
|
||||
use self::{
|
||||
controller::{
|
||||
CharacterControllerBundle, CharacterControllerPlugin, Jumping, MovementDirection,
|
||||
},
|
||||
input::{InputPlugin, PlayerAction},
|
||||
};
|
||||
|
||||
#[derive(Component, Default)]
|
||||
pub struct Player;
|
||||
|
||||
#[derive(Bundle)]
|
||||
pub struct PlayerBundle {
|
||||
player: Player,
|
||||
input_manager_bundle: InputManagerBundle<PlayerAction>,
|
||||
character_controller_bundle: CharacterControllerBundle,
|
||||
}
|
||||
|
||||
impl Default for PlayerBundle {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
player: Player,
|
||||
input_manager_bundle: InputManagerBundle::with_map(PlayerAction::default_input_map()),
|
||||
character_controller_bundle: CharacterControllerBundle::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Bundle, LdtkEntity)]
|
||||
pub struct PlayerEntity {
|
||||
player_bundle: PlayerBundle,
|
||||
#[sprite_sheet_bundle]
|
||||
sprite_sheet_bundle: SpriteSheetBundle,
|
||||
}
|
||||
|
||||
pub struct PlayerPlugin;
|
||||
impl Plugin for PlayerPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_plugins((InputPlugin, CharacterControllerPlugin))
|
||||
.add_systems(Update, (apply_movement_input, apply_jumping_input))
|
||||
.register_ldtk_entity::<PlayerEntity>("Player");
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_movement_input(mut query: Query<(&ActionState<PlayerAction>, &mut MovementDirection)>) {
|
||||
for (action_state, mut direction) in query.iter_mut() {
|
||||
direction.0 = action_state
|
||||
.clamped_axis_pair(&PlayerAction::Move)
|
||||
.map_or(0., |v| v.x())
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_jumping_input(
|
||||
mut commands: Commands,
|
||||
mut query: Query<(Entity, &ActionState<PlayerAction>, Has<Jumping>)>,
|
||||
) {
|
||||
for (entity, action_state, is_jumping) in query.iter_mut() {
|
||||
let wants_to_jump = action_state.pressed(&PlayerAction::Jump);
|
||||
if wants_to_jump && !is_jumping {
|
||||
commands.entity(entity).insert(Jumping);
|
||||
} else if !wants_to_jump && is_jumping {
|
||||
commands.entity(entity).remove::<Jumping>();
|
||||
}
|
||||
}
|
||||
}
|
||||