From 54e7c54d13a5299f824f851cb36f7264a06f4b66 Mon Sep 17 00:00:00 2001 From: Koopa1018 Date: Sun, 21 Jul 2024 19:52:57 -0400 Subject: [PATCH 1/6] Reorganize platformer example THIS. EXAMPLE. IS. A MESS! How can anyone learn from a haphazard jumble like this? It's like grabbing an armful of notes from a physicist's desk and calling it a textbook.... Anyway. I need to be able to sort through this, so I just decided to go ape. No logic was changed--it's just organized intuitively now. --- examples/platformer/camera.rs | 71 ++++ examples/platformer/climbing.rs | 82 +++++ examples/platformer/components.rs | 233 ------------- examples/platformer/enemy.rs | 117 +++++++ examples/platformer/game_flow.rs | 76 +++++ examples/platformer/inventory.rs | 32 ++ examples/platformer/main.rs | 44 +-- examples/platformer/misc_objects.rs | 29 ++ examples/platformer/physics.rs | 179 ++++++++++ examples/platformer/player.rs | 72 ++++ examples/platformer/systems.rs | 502 ---------------------------- examples/platformer/walls.rs | 184 ++++++++++ 12 files changed, 864 insertions(+), 757 deletions(-) create mode 100644 examples/platformer/camera.rs create mode 100644 examples/platformer/climbing.rs delete mode 100644 examples/platformer/components.rs create mode 100644 examples/platformer/enemy.rs create mode 100644 examples/platformer/game_flow.rs create mode 100644 examples/platformer/inventory.rs create mode 100644 examples/platformer/misc_objects.rs create mode 100644 examples/platformer/physics.rs create mode 100644 examples/platformer/player.rs delete mode 100644 examples/platformer/systems.rs create mode 100644 examples/platformer/walls.rs diff --git a/examples/platformer/camera.rs b/examples/platformer/camera.rs new file mode 100644 index 00000000..a4129e43 --- /dev/null +++ b/examples/platformer/camera.rs @@ -0,0 +1,71 @@ +use bevy::prelude::*; +use bevy_ecs_ldtk::prelude::*; + +use crate::player::Player; + +const ASPECT_RATIO: f32 = 16. / 9.; + +#[allow(clippy::type_complexity)] +pub fn camera_fit_inside_current_level( + mut camera_query: Query< + ( + &mut bevy::render::camera::OrthographicProjection, + &mut Transform, + ), + Without, + >, + player_query: Query<&Transform, With>, + level_query: Query<(&Transform, &LevelIid), (Without, Without)>, + ldtk_projects: Query<&Handle>, + level_selection: Res, + ldtk_project_assets: Res>, +) { + if let Ok(Transform { + translation: player_translation, + .. + }) = player_query.get_single() + { + let player_translation = *player_translation; + + let (mut orthographic_projection, mut camera_transform) = camera_query.single_mut(); + + for (level_transform, level_iid) in &level_query { + let ldtk_project = ldtk_project_assets + .get(ldtk_projects.single()) + .expect("Project should be loaded if level has spawned"); + + let level = ldtk_project + .get_raw_level_by_iid(&level_iid.to_string()) + .expect("Spawned level should exist in LDtk project"); + + if level_selection.is_match(&LevelIndices::default(), level) { + let level_ratio = level.px_wid as f32 / level.px_hei as f32; + orthographic_projection.viewport_origin = Vec2::ZERO; + if level_ratio > ASPECT_RATIO { + // level is wider than the screen + let height = (level.px_hei as f32 / 9.).round() * 9.; + let width = height * ASPECT_RATIO; + orthographic_projection.scaling_mode = + bevy::render::camera::ScalingMode::Fixed { width, height }; + camera_transform.translation.x = + (player_translation.x - level_transform.translation.x - width / 2.) + .clamp(0., level.px_wid as f32 - width); + camera_transform.translation.y = 0.; + } else { + // level is taller than the screen + let width = (level.px_wid as f32 / 16.).round() * 16.; + let height = width / ASPECT_RATIO; + orthographic_projection.scaling_mode = + bevy::render::camera::ScalingMode::Fixed { width, height }; + camera_transform.translation.y = + (player_translation.y - level_transform.translation.y - height / 2.) + .clamp(0., level.px_hei as f32 - height); + camera_transform.translation.x = 0.; + } + + camera_transform.translation.x += level_transform.translation.x; + camera_transform.translation.y += level_transform.translation.y; + } + } + } +} \ No newline at end of file diff --git a/examples/platformer/climbing.rs b/examples/platformer/climbing.rs new file mode 100644 index 00000000..d2b46bb9 --- /dev/null +++ b/examples/platformer/climbing.rs @@ -0,0 +1,82 @@ +use bevy::{prelude::*, utils::HashSet}; +use bevy_ecs_ldtk::prelude::*; +use bevy_rapier2d::prelude::*; + +use crate::physics::SensorBundle; + +#[derive(Clone, Eq, PartialEq, Debug, Default, Component)] +pub struct Climber { + pub climbing: bool, + pub intersecting_climbables: HashSet, +} + +#[derive(Copy, Clone, Eq, PartialEq, Debug, Default, Component)] +pub struct Climbable; + +#[derive(Clone, Default, Bundle, LdtkIntCell)] +pub struct LadderBundle { + #[from_int_grid_cell] + pub sensor_bundle: SensorBundle, + pub climbable: Climbable, +} + + +pub fn detect_climb_range( + mut climbers: Query<&mut Climber>, + climbables: Query>, + mut collisions: EventReader, +) { + for collision in collisions.read() { + match collision { + CollisionEvent::Started(collider_a, collider_b, _) => { + if let (Ok(mut climber), Ok(climbable)) = + (climbers.get_mut(*collider_a), climbables.get(*collider_b)) + { + climber.intersecting_climbables.insert(climbable); + } + if let (Ok(mut climber), Ok(climbable)) = + (climbers.get_mut(*collider_b), climbables.get(*collider_a)) + { + climber.intersecting_climbables.insert(climbable); + }; + } + CollisionEvent::Stopped(collider_a, collider_b, _) => { + if let (Ok(mut climber), Ok(climbable)) = + (climbers.get_mut(*collider_a), climbables.get(*collider_b)) + { + climber.intersecting_climbables.remove(&climbable); + } + + if let (Ok(mut climber), Ok(climbable)) = + (climbers.get_mut(*collider_b), climbables.get(*collider_a)) + { + climber.intersecting_climbables.remove(&climbable); + } + } + } + } +} + +pub fn ignore_gravity_if_climbing( + mut query: Query<(&Climber, &mut GravityScale), Changed>, +) { + for (climber, mut gravity_scale) in &mut query { + if climber.climbing { + gravity_scale.0 = 0.0; + } else { + gravity_scale.0 = 1.0; + } + } +} + + +pub struct ClimbingPlugin; + +impl Plugin for ClimbingPlugin { + fn build(&self, app: &mut App) { + app + .add_systems(Update, detect_climb_range) + .add_systems(Update, ignore_gravity_if_climbing) + ; + } +} \ No newline at end of file diff --git a/examples/platformer/components.rs b/examples/platformer/components.rs deleted file mode 100644 index cb21454e..00000000 --- a/examples/platformer/components.rs +++ /dev/null @@ -1,233 +0,0 @@ -use bevy::prelude::*; -use bevy_ecs_ldtk::{prelude::*, utils::ldtk_pixel_coords_to_translation_pivoted}; - -use std::collections::HashSet; - -use bevy_rapier2d::prelude::*; - -#[derive(Clone, Default, Bundle, LdtkIntCell)] -pub struct ColliderBundle { - pub collider: Collider, - pub rigid_body: RigidBody, - pub velocity: Velocity, - pub rotation_constraints: LockedAxes, - pub gravity_scale: GravityScale, - pub friction: Friction, - pub density: ColliderMassProperties, -} - -#[derive(Clone, Default, Bundle, LdtkIntCell)] -pub struct SensorBundle { - pub collider: Collider, - pub sensor: Sensor, - pub active_events: ActiveEvents, - pub rotation_constraints: LockedAxes, -} - -impl From<&EntityInstance> for ColliderBundle { - fn from(entity_instance: &EntityInstance) -> ColliderBundle { - let rotation_constraints = LockedAxes::ROTATION_LOCKED; - - match entity_instance.identifier.as_ref() { - "Player" => ColliderBundle { - collider: Collider::cuboid(6., 14.), - rigid_body: RigidBody::Dynamic, - friction: Friction { - coefficient: 0.0, - combine_rule: CoefficientCombineRule::Min, - }, - rotation_constraints, - ..Default::default() - }, - "Mob" => ColliderBundle { - collider: Collider::cuboid(5., 5.), - rigid_body: RigidBody::KinematicVelocityBased, - rotation_constraints, - ..Default::default() - }, - "Chest" => ColliderBundle { - collider: Collider::cuboid(8., 8.), - rigid_body: RigidBody::Dynamic, - rotation_constraints, - gravity_scale: GravityScale(1.0), - friction: Friction::new(0.5), - density: ColliderMassProperties::Density(15.0), - ..Default::default() - }, - _ => ColliderBundle::default(), - } - } -} - -impl From for SensorBundle { - fn from(int_grid_cell: IntGridCell) -> SensorBundle { - let rotation_constraints = LockedAxes::ROTATION_LOCKED; - - // ladder - if int_grid_cell.value == 2 { - SensorBundle { - collider: Collider::cuboid(8., 8.), - sensor: Sensor, - rotation_constraints, - active_events: ActiveEvents::COLLISION_EVENTS, - } - } else { - SensorBundle::default() - } - } -} - -#[derive(Clone, Component, Debug, Eq, Default, PartialEq)] -pub struct Items(Vec); - -impl From<&EntityInstance> for Items { - fn from(entity_instance: &EntityInstance) -> Self { - Items( - entity_instance - .iter_enums_field("items") - .expect("items field should be correctly typed") - .cloned() - .collect(), - ) - } -} - -#[derive(Copy, Clone, Eq, PartialEq, Debug, Default, Component)] -pub struct Player; - -#[derive(Clone, Eq, PartialEq, Debug, Default, Component)] -pub struct Climber { - pub climbing: bool, - pub intersecting_climbables: HashSet, -} - -#[derive(Clone, Default, Bundle, LdtkEntity)] -pub struct PlayerBundle { - #[sprite_bundle("player.png")] - pub sprite_bundle: SpriteBundle, - #[from_entity_instance] - pub collider_bundle: ColliderBundle, - pub player: Player, - #[worldly] - pub worldly: Worldly, - pub climber: Climber, - pub ground_detection: GroundDetection, - - // Build Items Component manually by using `impl From<&EntityInstance>` - #[from_entity_instance] - items: Items, - - // The whole EntityInstance can be stored directly as an EntityInstance component - #[from_entity_instance] - entity_instance: EntityInstance, -} - -#[derive(Copy, Clone, Eq, PartialEq, Debug, Default, Component)] -pub struct Wall; - -#[derive(Clone, Debug, Default, Bundle, LdtkIntCell)] -pub struct WallBundle { - wall: Wall, -} - -#[derive(Copy, Clone, Eq, PartialEq, Debug, Default, Component)] -pub struct Climbable; - -#[derive(Clone, Default, Bundle, LdtkIntCell)] -pub struct LadderBundle { - #[from_int_grid_cell] - pub sensor_bundle: SensorBundle, - pub climbable: Climbable, -} - -#[derive(Copy, Clone, Eq, PartialEq, Debug, Default, Component)] -pub struct Enemy; - -#[derive(Clone, PartialEq, Debug, Default, Component)] -pub struct Patrol { - pub points: Vec, - pub index: usize, - pub forward: bool, -} - -impl LdtkEntity for Patrol { - fn bundle_entity( - entity_instance: &EntityInstance, - layer_instance: &LayerInstance, - _: Option<&Handle>, - _: Option<&TilesetDefinition>, - _: &AssetServer, - _: &mut Assets, - ) -> Patrol { - let mut points = Vec::new(); - points.push(ldtk_pixel_coords_to_translation_pivoted( - entity_instance.px, - layer_instance.c_hei * layer_instance.grid_size, - IVec2::new(entity_instance.width, entity_instance.height), - entity_instance.pivot, - )); - - let ldtk_patrol_points = entity_instance - .iter_points_field("patrol") - .expect("patrol field should be correclty typed"); - - for ldtk_point in ldtk_patrol_points { - // The +1 is necessary here due to the pivot of the entities in the sample - // file. - // The patrols set up in the file look flat and grounded, - // but technically they're not if you consider the pivot, - // which is at the bottom-center for the skulls. - let pixel_coords = (ldtk_point.as_vec2() + Vec2::new(0.5, 1.)) - * Vec2::splat(layer_instance.grid_size as f32); - - points.push(ldtk_pixel_coords_to_translation_pivoted( - pixel_coords.as_ivec2(), - layer_instance.c_hei * layer_instance.grid_size, - IVec2::new(entity_instance.width, entity_instance.height), - entity_instance.pivot, - )); - } - - Patrol { - points, - index: 1, - forward: true, - } - } -} - -#[derive(Clone, Default, Bundle, LdtkEntity)] -pub struct MobBundle { - #[sprite_sheet_bundle] - pub sprite_sheet_bundle: LdtkSpriteSheetBundle, - #[from_entity_instance] - pub collider_bundle: ColliderBundle, - pub enemy: Enemy, - #[ldtk_entity] - pub patrol: Patrol, -} - -#[derive(Clone, Default, Bundle, LdtkEntity)] -pub struct ChestBundle { - #[sprite_sheet_bundle] - pub sprite_sheet_bundle: LdtkSpriteSheetBundle, - #[from_entity_instance] - pub collider_bundle: ColliderBundle, -} - -#[derive(Clone, Default, Bundle, LdtkEntity)] -pub struct PumpkinsBundle { - #[sprite_sheet_bundle(no_grid)] - pub sprite_sheet_bundle: LdtkSpriteSheetBundle, -} - -#[derive(Clone, Default, Component)] -pub struct GroundDetection { - pub on_ground: bool, -} - -#[derive(Component)] -pub struct GroundSensor { - pub ground_detection_entity: Entity, - pub intersecting_ground_entities: HashSet, -} diff --git a/examples/platformer/enemy.rs b/examples/platformer/enemy.rs new file mode 100644 index 00000000..3b32d0c4 --- /dev/null +++ b/examples/platformer/enemy.rs @@ -0,0 +1,117 @@ +use bevy::prelude::*; +use bevy_ecs_ldtk::{prelude::*, utils::ldtk_pixel_coords_to_translation_pivoted}; +use bevy_rapier2d::dynamics::Velocity; + +use crate::physics::ColliderBundle; + +#[derive(Copy, Clone, Eq, PartialEq, Debug, Default, Component)] +pub struct Enemy; + +#[derive(Clone, Default, Bundle, LdtkEntity)] +pub struct MobBundle { + #[sprite_sheet_bundle] + pub sprite_sheet_bundle: LdtkSpriteSheetBundle, + #[from_entity_instance] + pub collider_bundle: ColliderBundle, + pub enemy: Enemy, + #[ldtk_entity] + pub patrol: Patrol, +} + +#[derive(Clone, PartialEq, Debug, Default, Component)] +pub struct Patrol { + pub points: Vec, + pub index: usize, + pub forward: bool, +} + +impl LdtkEntity for Patrol { + fn bundle_entity( + entity_instance: &EntityInstance, + layer_instance: &LayerInstance, + _: Option<&Handle>, + _: Option<&TilesetDefinition>, + _: &AssetServer, + _: &mut Assets, + ) -> Patrol { + let mut points = Vec::new(); + points.push(ldtk_pixel_coords_to_translation_pivoted( + entity_instance.px, + layer_instance.c_hei * layer_instance.grid_size, + IVec2::new(entity_instance.width, entity_instance.height), + entity_instance.pivot, + )); + + let ldtk_patrol_points = entity_instance + .iter_points_field("patrol") + .expect("patrol field should be correclty typed"); + + for ldtk_point in ldtk_patrol_points { + // The +1 is necessary here due to the pivot of the entities in the sample + // file. + // The patrols set up in the file look flat and grounded, + // but technically they're not if you consider the pivot, + // which is at the bottom-center for the skulls. + let pixel_coords = (ldtk_point.as_vec2() + Vec2::new(0.5, 1.)) + * Vec2::splat(layer_instance.grid_size as f32); + + points.push(ldtk_pixel_coords_to_translation_pivoted( + pixel_coords.as_ivec2(), + layer_instance.c_hei * layer_instance.grid_size, + IVec2::new(entity_instance.width, entity_instance.height), + entity_instance.pivot, + )); + } + + Patrol { + points, + index: 1, + forward: true, + } + } +} + +pub fn patrol(mut query: Query<(&mut Transform, &mut Velocity, &mut Patrol)>) { + for (mut transform, mut velocity, mut patrol) in &mut query { + if patrol.points.len() <= 1 { + continue; + } + + let mut new_velocity = + (patrol.points[patrol.index] - transform.translation.truncate()).normalize() * 75.; + + if new_velocity.dot(velocity.linvel) < 0. { + if patrol.index == 0 { + patrol.forward = true; + } else if patrol.index == patrol.points.len() - 1 { + patrol.forward = false; + } + + transform.translation.x = patrol.points[patrol.index].x; + transform.translation.y = patrol.points[patrol.index].y; + + if patrol.forward { + patrol.index += 1; + } else { + patrol.index -= 1; + } + + new_velocity = + (patrol.points[patrol.index] - transform.translation.truncate()).normalize() * 75.; + } + + velocity.linvel = new_velocity; + } +} + + +pub struct EnemyPlugin; + +impl Plugin for EnemyPlugin { + fn build(&self, app: &mut App) { + app + .add_systems(Update, patrol) + .register_ldtk_entity::("Mob") + ; + } +} \ No newline at end of file diff --git a/examples/platformer/game_flow.rs b/examples/platformer/game_flow.rs new file mode 100644 index 00000000..ba89d00b --- /dev/null +++ b/examples/platformer/game_flow.rs @@ -0,0 +1,76 @@ +use crate::player::Player; +use bevy::prelude::*; +use bevy_ecs_ldtk::prelude::*; + +pub fn setup(mut commands: Commands, asset_server: Res) { + let camera = Camera2dBundle::default(); + commands.spawn(camera); + + let ldtk_handle = asset_server.load("Typical_2D_platformer_example.ldtk"); + commands.spawn(LdtkWorldBundle { + ldtk_handle, + ..Default::default() + }); +} + +pub fn update_level_selection( + level_query: Query<(&LevelIid, &Transform), Without>, + player_query: Query<&Transform, With>, + mut level_selection: ResMut, + ldtk_projects: Query<&Handle>, + ldtk_project_assets: Res>, +) { + for (level_iid, level_transform) in &level_query { + let ldtk_project = ldtk_project_assets + .get(ldtk_projects.single()) + .expect("Project should be loaded if level has spawned"); + + let level = ldtk_project + .get_raw_level_by_iid(&level_iid.to_string()) + .expect("Spawned level should exist in LDtk project"); + + let level_bounds = Rect { + min: Vec2::new(level_transform.translation.x, level_transform.translation.y), + max: Vec2::new( + level_transform.translation.x + level.px_wid as f32, + level_transform.translation.y + level.px_hei as f32, + ), + }; + + for player_transform in &player_query { + if player_transform.translation.x < level_bounds.max.x + && player_transform.translation.x > level_bounds.min.x + && player_transform.translation.y < level_bounds.max.y + && player_transform.translation.y > level_bounds.min.y + && !level_selection.is_match(&LevelIndices::default(), level) + { + *level_selection = LevelSelection::iid(level.iid.clone()); + } + } + } +} + +pub fn restart_level( + mut commands: Commands, + level_query: Query>, + input: Res>, +) { + if input.just_pressed(KeyCode::KeyR) { + for level_entity in &level_query { + commands.entity(level_entity).insert(Respawn); + } + } +} + + +pub struct GameFlowPlugin; + +impl Plugin for GameFlowPlugin { + fn build(&self, app: &mut App) { + app + .add_systems(Startup, setup) + .add_systems(Update, update_level_selection) + .add_systems(Update, restart_level) + ; + } +} \ No newline at end of file diff --git a/examples/platformer/inventory.rs b/examples/platformer/inventory.rs new file mode 100644 index 00000000..fc8e472d --- /dev/null +++ b/examples/platformer/inventory.rs @@ -0,0 +1,32 @@ +use bevy::prelude::*; +use bevy_ecs_ldtk::prelude::*; + +use crate::player::Player; + +#[derive(Clone, Component, Debug, Eq, Default, PartialEq)] +pub struct Inventory(Vec); + +impl From<&EntityInstance> for Inventory { + fn from(entity_instance: &EntityInstance) -> Self { + Inventory( + entity_instance + .iter_enums_field("items") + .expect("items field should be correctly typed") + .cloned() + .collect(), + ) + } +} + +/// Prints the contents of the player's inventory. +pub fn dbg_print_inventory( + input: Res>, + mut query: Query<(&Inventory, &EntityInstance), With>, +) { + for (items, entity_instance) in &mut query { + if input.just_pressed(KeyCode::KeyP) { + dbg!(&items); + dbg!(&entity_instance); + } + } +} \ No newline at end of file diff --git a/examples/platformer/main.rs b/examples/platformer/main.rs index 5f4de0af..0bf99676 100644 --- a/examples/platformer/main.rs +++ b/examples/platformer/main.rs @@ -6,8 +6,16 @@ use bevy_ecs_ldtk::prelude::*; use bevy_rapier2d::prelude::*; -mod components; -mod systems; +/// Handles initialization and switching levels +mod game_flow; +mod camera; +mod walls; +mod physics; +mod player; +mod inventory; +mod climbing; +mod enemy; +mod misc_objects; fn main() { App::new() @@ -36,25 +44,17 @@ fn main() { set_clear_color: SetClearColor::FromLevelBackground, ..Default::default() }) - .add_systems(Startup, systems::setup) - .add_systems(Update, systems::spawn_wall_collision) - .add_systems(Update, systems::movement) - .add_systems(Update, systems::detect_climb_range) - .add_systems(Update, systems::ignore_gravity_if_climbing) - .add_systems(Update, systems::patrol) - .add_systems(Update, systems::camera_fit_inside_current_level) - .add_systems(Update, systems::update_level_selection) - .add_systems(Update, systems::dbg_player_items) - .add_systems(Update, systems::spawn_ground_sensor) - .add_systems(Update, systems::ground_detection) - .add_systems(Update, systems::update_on_ground) - .add_systems(Update, systems::restart_level) - .register_ldtk_int_cell::(1) - .register_ldtk_int_cell::(2) - .register_ldtk_int_cell::(3) - .register_ldtk_entity::("Player") - .register_ldtk_entity::("Mob") - .register_ldtk_entity::("Chest") - .register_ldtk_entity::("Pumpkins") + .add_plugins(game_flow::GameFlowPlugin) + .add_systems(Update, walls::spawn_wall_collision) + .add_plugins(physics::PhysicsPlugin) + .add_plugins(climbing::ClimbingPlugin) + .add_plugins(player::PlayerPlugin) + .add_plugins(enemy::EnemyPlugin) + .add_systems(Update, inventory::dbg_print_inventory) + .add_systems(Update, camera::camera_fit_inside_current_level) + .add_plugins(misc_objects::MiscObjectsPlugin) + .register_ldtk_int_cell::(1) + .register_ldtk_int_cell::(2) + .register_ldtk_int_cell::(3) .run(); } diff --git a/examples/platformer/misc_objects.rs b/examples/platformer/misc_objects.rs new file mode 100644 index 00000000..61f1284c --- /dev/null +++ b/examples/platformer/misc_objects.rs @@ -0,0 +1,29 @@ +use bevy::prelude::*; +use bevy_ecs_ldtk::prelude::*; + +use crate::physics::ColliderBundle; + +#[derive(Clone, Default, Bundle, LdtkEntity)] +pub struct ChestBundle { + #[sprite_sheet_bundle] + pub sprite_sheet_bundle: LdtkSpriteSheetBundle, + #[from_entity_instance] + pub collider_bundle: ColliderBundle, +} + +#[derive(Clone, Default, Bundle, LdtkEntity)] +pub struct PumpkinsBundle { + #[sprite_sheet_bundle(no_grid)] + pub sprite_sheet_bundle: LdtkSpriteSheetBundle, +} + +pub struct MiscObjectsPlugin; + +impl Plugin for MiscObjectsPlugin { + fn build(&self, app: &mut App) { + app + .register_ldtk_entity::("Chest") + .register_ldtk_entity::("Pumpkins") + ; + } +} \ No newline at end of file diff --git a/examples/platformer/physics.rs b/examples/platformer/physics.rs new file mode 100644 index 00000000..2dfc1f3c --- /dev/null +++ b/examples/platformer/physics.rs @@ -0,0 +1,179 @@ +use std::collections::HashSet; + +use bevy::prelude::*; +use bevy_ecs_ldtk::prelude::*; + +use bevy_rapier2d::prelude::*; + +#[derive(Clone, Default, Bundle, LdtkIntCell)] +pub struct ColliderBundle { + pub collider: Collider, + pub rigid_body: RigidBody, + pub velocity: Velocity, + pub rotation_constraints: LockedAxes, + pub gravity_scale: GravityScale, + pub friction: Friction, + pub density: ColliderMassProperties, +} + +impl From<&EntityInstance> for ColliderBundle { + fn from(entity_instance: &EntityInstance) -> ColliderBundle { + let rotation_constraints = LockedAxes::ROTATION_LOCKED; + + match entity_instance.identifier.as_ref() { + "Player" => ColliderBundle { + collider: Collider::cuboid(6., 14.), + rigid_body: RigidBody::Dynamic, + friction: Friction { + coefficient: 0.0, + combine_rule: CoefficientCombineRule::Min, + }, + rotation_constraints, + ..Default::default() + }, + "Mob" => ColliderBundle { + collider: Collider::cuboid(5., 5.), + rigid_body: RigidBody::KinematicVelocityBased, + rotation_constraints, + ..Default::default() + }, + "Chest" => ColliderBundle { + collider: Collider::cuboid(8., 8.), + rigid_body: RigidBody::Dynamic, + rotation_constraints, + gravity_scale: GravityScale(1.0), + friction: Friction::new(0.5), + density: ColliderMassProperties::Density(15.0), + ..Default::default() + }, + _ => ColliderBundle::default(), + } + } +} + +#[derive(Clone, Default, Bundle, LdtkIntCell)] +pub struct SensorBundle { + pub collider: Collider, + pub sensor: Sensor, + pub active_events: ActiveEvents, + pub rotation_constraints: LockedAxes, +} + +impl From for SensorBundle { + fn from(int_grid_cell: IntGridCell) -> SensorBundle { + let rotation_constraints = LockedAxes::ROTATION_LOCKED; + + // ladder + if int_grid_cell.value == 2 { + SensorBundle { + collider: Collider::cuboid(8., 8.), + sensor: Sensor, + rotation_constraints, + active_events: ActiveEvents::COLLISION_EVENTS, + } + } else { + SensorBundle::default() + } + } +} + +#[derive(Component)] +pub struct GroundSensor { + pub ground_detection_entity: Entity, + pub intersecting_ground_entities: HashSet, +} + +#[derive(Clone, Default, Component)] +pub struct GroundDetection { + pub on_ground: bool, +} + +pub fn spawn_ground_sensor( + mut commands: Commands, + detect_ground_for: Query<(Entity, &Collider), Added>, +) { + for (entity, shape) in &detect_ground_for { + if let Some(cuboid) = shape.as_cuboid() { + let Vec2 { + x: half_extents_x, + y: half_extents_y, + } = cuboid.half_extents(); + + let detector_shape = Collider::cuboid(half_extents_x / 2.0, 2.); + + let sensor_translation = Vec3::new(0., -half_extents_y, 0.); + + commands.entity(entity).with_children(|builder| { + builder + .spawn_empty() + .insert(ActiveEvents::COLLISION_EVENTS) + .insert(detector_shape) + .insert(Sensor) + .insert(Transform::from_translation(sensor_translation)) + .insert(GlobalTransform::default()) + .insert(GroundSensor { + ground_detection_entity: entity, + intersecting_ground_entities: HashSet::new(), + }); + }); + } + } +} + +pub fn ground_detection( + mut ground_sensors: Query<&mut GroundSensor>, + mut collisions: EventReader, + collidables: Query, Without)>, +) { + for collision_event in collisions.read() { + match collision_event { + CollisionEvent::Started(e1, e2, _) => { + if collidables.contains(*e1) { + if let Ok(mut sensor) = ground_sensors.get_mut(*e2) { + sensor.intersecting_ground_entities.insert(*e1); + } + } else if collidables.contains(*e2) { + if let Ok(mut sensor) = ground_sensors.get_mut(*e1) { + sensor.intersecting_ground_entities.insert(*e2); + } + } + } + CollisionEvent::Stopped(e1, e2, _) => { + if collidables.contains(*e1) { + if let Ok(mut sensor) = ground_sensors.get_mut(*e2) { + sensor.intersecting_ground_entities.remove(e1); + } + } else if collidables.contains(*e2) { + if let Ok(mut sensor) = ground_sensors.get_mut(*e1) { + sensor.intersecting_ground_entities.remove(e2); + } + } + } + } + } +} + +pub fn update_on_ground( + mut ground_detectors: Query<&mut GroundDetection>, + ground_sensors: Query<&GroundSensor, Changed>, +) { + for sensor in &ground_sensors { + if let Ok(mut ground_detection) = ground_detectors.get_mut(sensor.ground_detection_entity) { + ground_detection.on_ground = !sensor.intersecting_ground_entities.is_empty(); + } + } +} + + +/// Handles platformer-specific physics operations, specifically ground detection. +pub struct PhysicsPlugin; + +impl Plugin for PhysicsPlugin { + fn build(&self, app: &mut App) { + app + .add_systems(Update, spawn_ground_sensor) + .add_systems(Update, ground_detection) + .add_systems(Update, update_on_ground) + ; + } +} \ No newline at end of file diff --git a/examples/platformer/player.rs b/examples/platformer/player.rs new file mode 100644 index 00000000..2de54795 --- /dev/null +++ b/examples/platformer/player.rs @@ -0,0 +1,72 @@ +use bevy::prelude::*; +use bevy_ecs_ldtk::prelude::*; +use bevy_rapier2d::dynamics::Velocity; + +use crate::{climbing::Climber, inventory::Inventory}; +use crate::physics::{ColliderBundle, GroundDetection}; + +#[derive(Copy, Clone, Eq, PartialEq, Debug, Default, Component)] +pub struct Player; + +#[derive(Clone, Default, Bundle, LdtkEntity)] +pub struct PlayerBundle { + #[sprite_bundle("player.png")] + pub sprite_bundle: SpriteBundle, + #[from_entity_instance] + pub collider_bundle: ColliderBundle, + pub player: Player, + #[worldly] + pub worldly: Worldly, + pub climber: Climber, + pub ground_detection: GroundDetection, + + // Build Items Component manually by using `impl From<&EntityInstance>` + #[from_entity_instance] + items: Inventory, + + // The whole EntityInstance can be stored directly as an EntityInstance component + #[from_entity_instance] + entity_instance: EntityInstance, +} + +pub fn player_movement( + input: Res>, + mut query: Query<(&mut Velocity, &mut Climber, &GroundDetection), With>, +) { + for (mut velocity, mut climber, ground_detection) in &mut query { + let right = if input.pressed(KeyCode::KeyD) { 1. } else { 0. }; + let left = if input.pressed(KeyCode::KeyA) { 1. } else { 0. }; + + velocity.linvel.x = (right - left) * 200.; + + if climber.intersecting_climbables.is_empty() { + climber.climbing = false; + } else if input.just_pressed(KeyCode::KeyW) || input.just_pressed(KeyCode::KeyS) { + climber.climbing = true; + } + + if climber.climbing { + let up = if input.pressed(KeyCode::KeyW) { 1. } else { 0. }; + let down = if input.pressed(KeyCode::KeyS) { 1. } else { 0. }; + + velocity.linvel.y = (up - down) * 200.; + } + + if input.just_pressed(KeyCode::Space) && (ground_detection.on_ground || climber.climbing) { + velocity.linvel.y = 500.; + climber.climbing = false; + } + } +} + + +pub struct PlayerPlugin; + +impl Plugin for PlayerPlugin { + fn build(&self, app: &mut App) { + app + .add_systems(Update, player_movement) + .register_ldtk_entity::("Player") + ; + } +} \ No newline at end of file diff --git a/examples/platformer/systems.rs b/examples/platformer/systems.rs deleted file mode 100644 index 76a924fb..00000000 --- a/examples/platformer/systems.rs +++ /dev/null @@ -1,502 +0,0 @@ -use crate::components::*; -use bevy::prelude::*; -use bevy_ecs_ldtk::prelude::*; - -use std::collections::{HashMap, HashSet}; - -use bevy_rapier2d::prelude::*; - -pub fn setup(mut commands: Commands, asset_server: Res) { - let camera = Camera2dBundle::default(); - commands.spawn(camera); - - let ldtk_handle = asset_server.load("Typical_2D_platformer_example.ldtk"); - commands.spawn(LdtkWorldBundle { - ldtk_handle, - ..Default::default() - }); -} - -pub fn dbg_player_items( - input: Res>, - mut query: Query<(&Items, &EntityInstance), With>, -) { - for (items, entity_instance) in &mut query { - if input.just_pressed(KeyCode::KeyP) { - dbg!(&items); - dbg!(&entity_instance); - } - } -} - -pub fn movement( - input: Res>, - mut query: Query<(&mut Velocity, &mut Climber, &GroundDetection), With>, -) { - for (mut velocity, mut climber, ground_detection) in &mut query { - let right = if input.pressed(KeyCode::KeyD) { 1. } else { 0. }; - let left = if input.pressed(KeyCode::KeyA) { 1. } else { 0. }; - - velocity.linvel.x = (right - left) * 200.; - - if climber.intersecting_climbables.is_empty() { - climber.climbing = false; - } else if input.just_pressed(KeyCode::KeyW) || input.just_pressed(KeyCode::KeyS) { - climber.climbing = true; - } - - if climber.climbing { - let up = if input.pressed(KeyCode::KeyW) { 1. } else { 0. }; - let down = if input.pressed(KeyCode::KeyS) { 1. } else { 0. }; - - velocity.linvel.y = (up - down) * 200.; - } - - if input.just_pressed(KeyCode::Space) && (ground_detection.on_ground || climber.climbing) { - velocity.linvel.y = 500.; - climber.climbing = false; - } - } -} - -/// Spawns heron collisions for the walls of a level -/// -/// You could just insert a ColliderBundle in to the WallBundle, -/// but this spawns a different collider for EVERY wall tile. -/// This approach leads to bad performance. -/// -/// Instead, by flagging the wall tiles and spawning the collisions later, -/// we can minimize the amount of colliding entities. -/// -/// The algorithm used here is a nice compromise between simplicity, speed, -/// and a small number of rectangle colliders. -/// In basic terms, it will: -/// 1. consider where the walls are -/// 2. combine wall tiles into flat "plates" in each individual row -/// 3. combine the plates into rectangles across multiple rows wherever possible -/// 4. spawn colliders for each rectangle -pub fn spawn_wall_collision( - mut commands: Commands, - wall_query: Query<(&GridCoords, &Parent), Added>, - parent_query: Query<&Parent, Without>, - level_query: Query<(Entity, &LevelIid)>, - ldtk_projects: Query<&Handle>, - ldtk_project_assets: Res>, -) { - /// Represents a wide wall that is 1 tile tall - /// Used to spawn wall collisions - #[derive(Clone, Eq, PartialEq, Debug, Default, Hash)] - struct Plate { - left: i32, - right: i32, - } - - /// A simple rectangle type representing a wall of any size - struct Rect { - left: i32, - right: i32, - top: i32, - bottom: i32, - } - - // Consider where the walls are - // storing them as GridCoords in a HashSet for quick, easy lookup - // - // The key of this map will be the entity of the level the wall belongs to. - // This has two consequences in the resulting collision entities: - // 1. it forces the walls to be split along level boundaries - // 2. it lets us easily add the collision entities as children of the appropriate level entity - let mut level_to_wall_locations: HashMap> = HashMap::new(); - - wall_query.iter().for_each(|(&grid_coords, parent)| { - // An intgrid tile's direct parent will be a layer entity, not the level entity - // To get the level entity, you need the tile's grandparent. - // This is where parent_query comes in. - if let Ok(grandparent) = parent_query.get(parent.get()) { - level_to_wall_locations - .entry(grandparent.get()) - .or_default() - .insert(grid_coords); - } - }); - - if !wall_query.is_empty() { - level_query.iter().for_each(|(level_entity, level_iid)| { - if let Some(level_walls) = level_to_wall_locations.get(&level_entity) { - let ldtk_project = ldtk_project_assets - .get(ldtk_projects.single()) - .expect("Project should be loaded if level has spawned"); - - let level = ldtk_project - .as_standalone() - .get_loaded_level_by_iid(&level_iid.to_string()) - .expect("Spawned level should exist in LDtk project"); - - let LayerInstance { - c_wid: width, - c_hei: height, - grid_size, - .. - } = level.layer_instances()[0]; - - // combine wall tiles into flat "plates" in each individual row - let mut plate_stack: Vec> = Vec::new(); - - for y in 0..height { - let mut row_plates: Vec = Vec::new(); - let mut plate_start = None; - - // + 1 to the width so the algorithm "terminates" plates that touch the right edge - for x in 0..width + 1 { - match (plate_start, level_walls.contains(&GridCoords { x, y })) { - (Some(s), false) => { - row_plates.push(Plate { - left: s, - right: x - 1, - }); - plate_start = None; - } - (None, true) => plate_start = Some(x), - _ => (), - } - } - - plate_stack.push(row_plates); - } - - // combine "plates" into rectangles across multiple rows - let mut rect_builder: HashMap = HashMap::new(); - let mut prev_row: Vec = Vec::new(); - let mut wall_rects: Vec = Vec::new(); - - // an extra empty row so the algorithm "finishes" the rects that touch the top edge - plate_stack.push(Vec::new()); - - for (y, current_row) in plate_stack.into_iter().enumerate() { - for prev_plate in &prev_row { - if !current_row.contains(prev_plate) { - // remove the finished rect so that the same plate in the future starts a new rect - if let Some(rect) = rect_builder.remove(prev_plate) { - wall_rects.push(rect); - } - } - } - for plate in ¤t_row { - rect_builder - .entry(plate.clone()) - .and_modify(|e| e.top += 1) - .or_insert(Rect { - bottom: y as i32, - top: y as i32, - left: plate.left, - right: plate.right, - }); - } - prev_row = current_row; - } - - commands.entity(level_entity).with_children(|level| { - // Spawn colliders for every rectangle.. - // Making the collider a child of the level serves two purposes: - // 1. Adjusts the transforms to be relative to the level for free - // 2. the colliders will be despawned automatically when levels unload - for wall_rect in wall_rects { - level - .spawn_empty() - .insert(Collider::cuboid( - (wall_rect.right as f32 - wall_rect.left as f32 + 1.) - * grid_size as f32 - / 2., - (wall_rect.top as f32 - wall_rect.bottom as f32 + 1.) - * grid_size as f32 - / 2., - )) - .insert(RigidBody::Fixed) - .insert(Friction::new(1.0)) - .insert(Transform::from_xyz( - (wall_rect.left + wall_rect.right + 1) as f32 * grid_size as f32 - / 2., - (wall_rect.bottom + wall_rect.top + 1) as f32 * grid_size as f32 - / 2., - 0., - )) - .insert(GlobalTransform::default()); - } - }); - } - }); - } -} - -pub fn detect_climb_range( - mut climbers: Query<&mut Climber>, - climbables: Query>, - mut collisions: EventReader, -) { - for collision in collisions.read() { - match collision { - CollisionEvent::Started(collider_a, collider_b, _) => { - if let (Ok(mut climber), Ok(climbable)) = - (climbers.get_mut(*collider_a), climbables.get(*collider_b)) - { - climber.intersecting_climbables.insert(climbable); - } - if let (Ok(mut climber), Ok(climbable)) = - (climbers.get_mut(*collider_b), climbables.get(*collider_a)) - { - climber.intersecting_climbables.insert(climbable); - }; - } - CollisionEvent::Stopped(collider_a, collider_b, _) => { - if let (Ok(mut climber), Ok(climbable)) = - (climbers.get_mut(*collider_a), climbables.get(*collider_b)) - { - climber.intersecting_climbables.remove(&climbable); - } - - if let (Ok(mut climber), Ok(climbable)) = - (climbers.get_mut(*collider_b), climbables.get(*collider_a)) - { - climber.intersecting_climbables.remove(&climbable); - } - } - } - } -} - -pub fn ignore_gravity_if_climbing( - mut query: Query<(&Climber, &mut GravityScale), Changed>, -) { - for (climber, mut gravity_scale) in &mut query { - if climber.climbing { - gravity_scale.0 = 0.0; - } else { - gravity_scale.0 = 1.0; - } - } -} - -pub fn patrol(mut query: Query<(&mut Transform, &mut Velocity, &mut Patrol)>) { - for (mut transform, mut velocity, mut patrol) in &mut query { - if patrol.points.len() <= 1 { - continue; - } - - let mut new_velocity = - (patrol.points[patrol.index] - transform.translation.truncate()).normalize() * 75.; - - if new_velocity.dot(velocity.linvel) < 0. { - if patrol.index == 0 { - patrol.forward = true; - } else if patrol.index == patrol.points.len() - 1 { - patrol.forward = false; - } - - transform.translation.x = patrol.points[patrol.index].x; - transform.translation.y = patrol.points[patrol.index].y; - - if patrol.forward { - patrol.index += 1; - } else { - patrol.index -= 1; - } - - new_velocity = - (patrol.points[patrol.index] - transform.translation.truncate()).normalize() * 75.; - } - - velocity.linvel = new_velocity; - } -} - -const ASPECT_RATIO: f32 = 16. / 9.; - -#[allow(clippy::type_complexity)] -pub fn camera_fit_inside_current_level( - mut camera_query: Query< - ( - &mut bevy::render::camera::OrthographicProjection, - &mut Transform, - ), - Without, - >, - player_query: Query<&Transform, With>, - level_query: Query<(&Transform, &LevelIid), (Without, Without)>, - ldtk_projects: Query<&Handle>, - level_selection: Res, - ldtk_project_assets: Res>, -) { - if let Ok(Transform { - translation: player_translation, - .. - }) = player_query.get_single() - { - let player_translation = *player_translation; - - let (mut orthographic_projection, mut camera_transform) = camera_query.single_mut(); - - for (level_transform, level_iid) in &level_query { - let ldtk_project = ldtk_project_assets - .get(ldtk_projects.single()) - .expect("Project should be loaded if level has spawned"); - - let level = ldtk_project - .get_raw_level_by_iid(&level_iid.to_string()) - .expect("Spawned level should exist in LDtk project"); - - if level_selection.is_match(&LevelIndices::default(), level) { - let level_ratio = level.px_wid as f32 / level.px_hei as f32; - orthographic_projection.viewport_origin = Vec2::ZERO; - if level_ratio > ASPECT_RATIO { - // level is wider than the screen - let height = (level.px_hei as f32 / 9.).round() * 9.; - let width = height * ASPECT_RATIO; - orthographic_projection.scaling_mode = - bevy::render::camera::ScalingMode::Fixed { width, height }; - camera_transform.translation.x = - (player_translation.x - level_transform.translation.x - width / 2.) - .clamp(0., level.px_wid as f32 - width); - camera_transform.translation.y = 0.; - } else { - // level is taller than the screen - let width = (level.px_wid as f32 / 16.).round() * 16.; - let height = width / ASPECT_RATIO; - orthographic_projection.scaling_mode = - bevy::render::camera::ScalingMode::Fixed { width, height }; - camera_transform.translation.y = - (player_translation.y - level_transform.translation.y - height / 2.) - .clamp(0., level.px_hei as f32 - height); - camera_transform.translation.x = 0.; - } - - camera_transform.translation.x += level_transform.translation.x; - camera_transform.translation.y += level_transform.translation.y; - } - } - } -} - -pub fn update_level_selection( - level_query: Query<(&LevelIid, &Transform), Without>, - player_query: Query<&Transform, With>, - mut level_selection: ResMut, - ldtk_projects: Query<&Handle>, - ldtk_project_assets: Res>, -) { - for (level_iid, level_transform) in &level_query { - let ldtk_project = ldtk_project_assets - .get(ldtk_projects.single()) - .expect("Project should be loaded if level has spawned"); - - let level = ldtk_project - .get_raw_level_by_iid(&level_iid.to_string()) - .expect("Spawned level should exist in LDtk project"); - - let level_bounds = Rect { - min: Vec2::new(level_transform.translation.x, level_transform.translation.y), - max: Vec2::new( - level_transform.translation.x + level.px_wid as f32, - level_transform.translation.y + level.px_hei as f32, - ), - }; - - for player_transform in &player_query { - if player_transform.translation.x < level_bounds.max.x - && player_transform.translation.x > level_bounds.min.x - && player_transform.translation.y < level_bounds.max.y - && player_transform.translation.y > level_bounds.min.y - && !level_selection.is_match(&LevelIndices::default(), level) - { - *level_selection = LevelSelection::iid(level.iid.clone()); - } - } - } -} - -pub fn spawn_ground_sensor( - mut commands: Commands, - detect_ground_for: Query<(Entity, &Collider), Added>, -) { - for (entity, shape) in &detect_ground_for { - if let Some(cuboid) = shape.as_cuboid() { - let Vec2 { - x: half_extents_x, - y: half_extents_y, - } = cuboid.half_extents(); - - let detector_shape = Collider::cuboid(half_extents_x / 2.0, 2.); - - let sensor_translation = Vec3::new(0., -half_extents_y, 0.); - - commands.entity(entity).with_children(|builder| { - builder - .spawn_empty() - .insert(ActiveEvents::COLLISION_EVENTS) - .insert(detector_shape) - .insert(Sensor) - .insert(Transform::from_translation(sensor_translation)) - .insert(GlobalTransform::default()) - .insert(GroundSensor { - ground_detection_entity: entity, - intersecting_ground_entities: HashSet::new(), - }); - }); - } - } -} - -pub fn ground_detection( - mut ground_sensors: Query<&mut GroundSensor>, - mut collisions: EventReader, - collidables: Query, Without)>, -) { - for collision_event in collisions.read() { - match collision_event { - CollisionEvent::Started(e1, e2, _) => { - if collidables.contains(*e1) { - if let Ok(mut sensor) = ground_sensors.get_mut(*e2) { - sensor.intersecting_ground_entities.insert(*e1); - } - } else if collidables.contains(*e2) { - if let Ok(mut sensor) = ground_sensors.get_mut(*e1) { - sensor.intersecting_ground_entities.insert(*e2); - } - } - } - CollisionEvent::Stopped(e1, e2, _) => { - if collidables.contains(*e1) { - if let Ok(mut sensor) = ground_sensors.get_mut(*e2) { - sensor.intersecting_ground_entities.remove(e1); - } - } else if collidables.contains(*e2) { - if let Ok(mut sensor) = ground_sensors.get_mut(*e1) { - sensor.intersecting_ground_entities.remove(e2); - } - } - } - } - } -} - -pub fn update_on_ground( - mut ground_detectors: Query<&mut GroundDetection>, - ground_sensors: Query<&GroundSensor, Changed>, -) { - for sensor in &ground_sensors { - if let Ok(mut ground_detection) = ground_detectors.get_mut(sensor.ground_detection_entity) { - ground_detection.on_ground = !sensor.intersecting_ground_entities.is_empty(); - } - } -} - -pub fn restart_level( - mut commands: Commands, - level_query: Query>, - input: Res>, -) { - if input.just_pressed(KeyCode::KeyR) { - for level_entity in &level_query { - commands.entity(level_entity).insert(Respawn); - } - } -} diff --git a/examples/platformer/walls.rs b/examples/platformer/walls.rs new file mode 100644 index 00000000..76ed49f9 --- /dev/null +++ b/examples/platformer/walls.rs @@ -0,0 +1,184 @@ +use std::collections::HashSet; + +use bevy::{prelude::*, utils::HashMap}; +use bevy_ecs_ldtk::prelude::*; + +use bevy_rapier2d::prelude::*; + + +#[derive(Copy, Clone, Eq, PartialEq, Debug, Default, Component)] +pub struct Wall; + +#[derive(Clone, Debug, Default, Bundle, LdtkIntCell)] +pub struct WallBundle { + wall: Wall, +} + +/// Spawns heron collisions for the walls of a level +/// +/// You could just insert a ColliderBundle into the WallBundle, +/// but this spawns a different collider for EVERY wall tile. +/// This approach leads to bad performance. +/// +/// Instead, by flagging the wall tiles and spawning the collisions later, +/// we can minimize the amount of colliding entities. +/// +/// The algorithm used here is a nice compromise between simplicity, speed, +/// and a small number of rectangle colliders. +/// In basic terms, it will: +/// 1. consider where the walls are +/// 2. combine wall tiles into flat "plates" in each individual row +/// 3. combine the plates into rectangles across multiple rows wherever possible +/// 4. spawn colliders for each rectangle +pub fn spawn_wall_collision( + mut commands: Commands, + wall_query: Query<(&GridCoords, &Parent), Added>, + parent_query: Query<&Parent, Without>, + level_query: Query<(Entity, &LevelIid)>, + ldtk_projects: Query<&Handle>, + ldtk_project_assets: Res>, +) { + /// Represents a wide wall that is 1 tile tall + /// Used to spawn wall collisions + #[derive(Clone, Eq, PartialEq, Debug, Default, Hash)] + struct Plate { + left: i32, + right: i32, + } + + /// A simple rectangle type representing a wall of any size + struct Rect { + left: i32, + right: i32, + top: i32, + bottom: i32, + } + + // Consider where the walls are + // storing them as GridCoords in a HashSet for quick, easy lookup + // + // The key of this map will be the entity of the level the wall belongs to. + // This has two consequences in the resulting collision entities: + // 1. it forces the walls to be split along level boundaries + // 2. it lets us easily add the collision entities as children of the appropriate level entity + let mut level_to_wall_locations: HashMap> = HashMap::new(); + + wall_query.iter().for_each(|(&grid_coords, parent)| { + // An intgrid tile's direct parent will be a layer entity, not the level entity + // To get the level entity, you need the tile's grandparent. + // This is where parent_query comes in. + if let Ok(grandparent) = parent_query.get(parent.get()) { + level_to_wall_locations + .entry(grandparent.get()) + .or_default() + .insert(grid_coords); + } + }); + + if !wall_query.is_empty() { + level_query.iter().for_each(|(level_entity, level_iid)| { + if let Some(level_walls) = level_to_wall_locations.get(&level_entity) { + let ldtk_project = ldtk_project_assets + .get(ldtk_projects.single()) + .expect("Project should be loaded if level has spawned"); + + let level = ldtk_project + .as_standalone() + .get_loaded_level_by_iid(&level_iid.to_string()) + .expect("Spawned level should exist in LDtk project"); + + let LayerInstance { + c_wid: width, + c_hei: height, + grid_size, + .. + } = level.layer_instances()[0]; + + // combine wall tiles into flat "plates" in each individual row + let mut plate_stack: Vec> = Vec::new(); + + for y in 0..height { + let mut row_plates: Vec = Vec::new(); + let mut plate_start = None; + + // + 1 to the width so the algorithm "terminates" plates that touch the right edge + for x in 0..width + 1 { + match (plate_start, level_walls.contains(&GridCoords { x, y })) { + (Some(s), false) => { + row_plates.push(Plate { + left: s, + right: x - 1, + }); + plate_start = None; + } + (None, true) => plate_start = Some(x), + _ => (), + } + } + + plate_stack.push(row_plates); + } + + // combine "plates" into rectangles across multiple rows + let mut rect_builder: HashMap = HashMap::new(); + let mut prev_row: Vec = Vec::new(); + let mut wall_rects: Vec = Vec::new(); + + // an extra empty row so the algorithm "finishes" the rects that touch the top edge + plate_stack.push(Vec::new()); + + for (y, current_row) in plate_stack.into_iter().enumerate() { + for prev_plate in &prev_row { + if !current_row.contains(prev_plate) { + // remove the finished rect so that the same plate in the future starts a new rect + if let Some(rect) = rect_builder.remove(prev_plate) { + wall_rects.push(rect); + } + } + } + for plate in ¤t_row { + rect_builder + .entry(plate.clone()) + .and_modify(|e| e.top += 1) + .or_insert(Rect { + bottom: y as i32, + top: y as i32, + left: plate.left, + right: plate.right, + }); + } + prev_row = current_row; + } + + commands.entity(level_entity).with_children(|level| { + // Spawn colliders for every rectangle.. + // Making the collider a child of the level serves two purposes: + // 1. Adjusts the transforms to be relative to the level for free + // 2. the colliders will be despawned automatically when levels unload + for wall_rect in wall_rects { + level + .spawn_empty() + .insert(Collider::cuboid( + (wall_rect.right as f32 - wall_rect.left as f32 + 1.) + * grid_size as f32 + / 2., + (wall_rect.top as f32 - wall_rect.bottom as f32 + 1.) + * grid_size as f32 + / 2., + )) + .insert(RigidBody::Fixed) + .insert(Friction::new(1.0)) + .insert(Transform::from_xyz( + (wall_rect.left + wall_rect.right + 1) as f32 * grid_size as f32 + / 2., + (wall_rect.bottom + wall_rect.top + 1) as f32 * grid_size as f32 + / 2., + 0., + )) + .insert(GlobalTransform::default()); + } + }); + } + }); + } +} \ No newline at end of file From 2cd4d2a3ccba57e4856a6bc72c11b89b846d7186 Mon Sep 17 00:00:00 2001 From: Koopa1018 Date: Sun, 21 Jul 2024 20:19:55 -0400 Subject: [PATCH 2/6] Rustfmt reorganized platformer example --- examples/platformer/camera.rs | 2 +- examples/platformer/climbing.rs | 14 +++++--------- examples/platformer/enemy.rs | 9 +++------ examples/platformer/game_flow.rs | 11 ++++------- examples/platformer/inventory.rs | 2 +- examples/platformer/main.rs | 12 ++++++------ examples/platformer/misc_objects.rs | 8 +++----- examples/platformer/physics.rs | 15 ++++++--------- examples/platformer/player.rs | 11 ++++------- examples/platformer/walls.rs | 3 +-- 10 files changed, 34 insertions(+), 53 deletions(-) diff --git a/examples/platformer/camera.rs b/examples/platformer/camera.rs index a4129e43..e3a52d5f 100644 --- a/examples/platformer/camera.rs +++ b/examples/platformer/camera.rs @@ -68,4 +68,4 @@ pub fn camera_fit_inside_current_level( } } } -} \ No newline at end of file +} diff --git a/examples/platformer/climbing.rs b/examples/platformer/climbing.rs index d2b46bb9..20aa10e2 100644 --- a/examples/platformer/climbing.rs +++ b/examples/platformer/climbing.rs @@ -20,7 +20,6 @@ pub struct LadderBundle { pub climbable: Climbable, } - pub fn detect_climb_range( mut climbers: Query<&mut Climber>, climbables: Query>, @@ -69,14 +68,11 @@ pub fn ignore_gravity_if_climbing( } } - pub struct ClimbingPlugin; impl Plugin for ClimbingPlugin { - fn build(&self, app: &mut App) { - app - .add_systems(Update, detect_climb_range) - .add_systems(Update, ignore_gravity_if_climbing) - ; - } -} \ No newline at end of file + fn build(&self, app: &mut App) { + app.add_systems(Update, detect_climb_range) + .add_systems(Update, ignore_gravity_if_climbing); + } +} diff --git a/examples/platformer/enemy.rs b/examples/platformer/enemy.rs index 3b32d0c4..005ce047 100644 --- a/examples/platformer/enemy.rs +++ b/examples/platformer/enemy.rs @@ -104,14 +104,11 @@ pub fn patrol(mut query: Query<(&mut Transform, &mut Velocity, &mut Patrol)>) { } } - pub struct EnemyPlugin; impl Plugin for EnemyPlugin { fn build(&self, app: &mut App) { - app - .add_systems(Update, patrol) - .register_ldtk_entity::("Mob") - ; + app.add_systems(Update, patrol) + .register_ldtk_entity::("Mob"); } -} \ No newline at end of file +} diff --git a/examples/platformer/game_flow.rs b/examples/platformer/game_flow.rs index ba89d00b..dc6fbdc8 100644 --- a/examples/platformer/game_flow.rs +++ b/examples/platformer/game_flow.rs @@ -62,15 +62,12 @@ pub fn restart_level( } } - pub struct GameFlowPlugin; impl Plugin for GameFlowPlugin { fn build(&self, app: &mut App) { - app - .add_systems(Startup, setup) - .add_systems(Update, update_level_selection) - .add_systems(Update, restart_level) - ; + app.add_systems(Startup, setup) + .add_systems(Update, update_level_selection) + .add_systems(Update, restart_level); } -} \ No newline at end of file +} diff --git a/examples/platformer/inventory.rs b/examples/platformer/inventory.rs index fc8e472d..99ab4c58 100644 --- a/examples/platformer/inventory.rs +++ b/examples/platformer/inventory.rs @@ -29,4 +29,4 @@ pub fn dbg_print_inventory( dbg!(&entity_instance); } } -} \ No newline at end of file +} diff --git a/examples/platformer/main.rs b/examples/platformer/main.rs index 0bf99676..e4d80019 100644 --- a/examples/platformer/main.rs +++ b/examples/platformer/main.rs @@ -6,16 +6,16 @@ use bevy_ecs_ldtk::prelude::*; use bevy_rapier2d::prelude::*; -/// Handles initialization and switching levels -mod game_flow; mod camera; -mod walls; -mod physics; -mod player; -mod inventory; mod climbing; mod enemy; +/// Handles initialization and switching levels +mod game_flow; +mod inventory; mod misc_objects; +mod physics; +mod player; +mod walls; fn main() { App::new() diff --git a/examples/platformer/misc_objects.rs b/examples/platformer/misc_objects.rs index 61f1284c..14208320 100644 --- a/examples/platformer/misc_objects.rs +++ b/examples/platformer/misc_objects.rs @@ -21,9 +21,7 @@ pub struct MiscObjectsPlugin; impl Plugin for MiscObjectsPlugin { fn build(&self, app: &mut App) { - app - .register_ldtk_entity::("Chest") - .register_ldtk_entity::("Pumpkins") - ; + app.register_ldtk_entity::("Chest") + .register_ldtk_entity::("Pumpkins"); } -} \ No newline at end of file +} diff --git a/examples/platformer/physics.rs b/examples/platformer/physics.rs index 2dfc1f3c..12cd3698 100644 --- a/examples/platformer/physics.rs +++ b/examples/platformer/physics.rs @@ -164,16 +164,13 @@ pub fn update_on_ground( } } - /// Handles platformer-specific physics operations, specifically ground detection. pub struct PhysicsPlugin; impl Plugin for PhysicsPlugin { - fn build(&self, app: &mut App) { - app - .add_systems(Update, spawn_ground_sensor) - .add_systems(Update, ground_detection) - .add_systems(Update, update_on_ground) - ; - } -} \ No newline at end of file + fn build(&self, app: &mut App) { + app.add_systems(Update, spawn_ground_sensor) + .add_systems(Update, ground_detection) + .add_systems(Update, update_on_ground); + } +} diff --git a/examples/platformer/player.rs b/examples/platformer/player.rs index 2de54795..73bf637b 100644 --- a/examples/platformer/player.rs +++ b/examples/platformer/player.rs @@ -2,8 +2,8 @@ use bevy::prelude::*; use bevy_ecs_ldtk::prelude::*; use bevy_rapier2d::dynamics::Velocity; -use crate::{climbing::Climber, inventory::Inventory}; use crate::physics::{ColliderBundle, GroundDetection}; +use crate::{climbing::Climber, inventory::Inventory}; #[derive(Copy, Clone, Eq, PartialEq, Debug, Default, Component)] pub struct Player; @@ -59,14 +59,11 @@ pub fn player_movement( } } - pub struct PlayerPlugin; impl Plugin for PlayerPlugin { fn build(&self, app: &mut App) { - app - .add_systems(Update, player_movement) - .register_ldtk_entity::("Player") - ; + app.add_systems(Update, player_movement) + .register_ldtk_entity::("Player"); } -} \ No newline at end of file +} diff --git a/examples/platformer/walls.rs b/examples/platformer/walls.rs index 76ed49f9..10c8a192 100644 --- a/examples/platformer/walls.rs +++ b/examples/platformer/walls.rs @@ -5,7 +5,6 @@ use bevy_ecs_ldtk::prelude::*; use bevy_rapier2d::prelude::*; - #[derive(Copy, Clone, Eq, PartialEq, Debug, Default, Component)] pub struct Wall; @@ -181,4 +180,4 @@ pub fn spawn_wall_collision( } }); } -} \ No newline at end of file +} From eadab68fe6decd16e04c7955ad32ea846ce65b8c Mon Sep 17 00:00:00 2001 From: Koopa1018 Date: Sat, 10 Aug 2024 05:52:55 -0400 Subject: [PATCH 3/6] Move wall system + cell registry to a WallPlugin If we're consolidating related logic, it really does make sense to put all wall-related logic in a plugin. --- examples/platformer/main.rs | 4 +--- examples/platformer/walls.rs | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/examples/platformer/main.rs b/examples/platformer/main.rs index e4d80019..6cb6c441 100644 --- a/examples/platformer/main.rs +++ b/examples/platformer/main.rs @@ -45,7 +45,7 @@ fn main() { ..Default::default() }) .add_plugins(game_flow::GameFlowPlugin) - .add_systems(Update, walls::spawn_wall_collision) + .add_plugins(walls::WallPlugin) .add_plugins(physics::PhysicsPlugin) .add_plugins(climbing::ClimbingPlugin) .add_plugins(player::PlayerPlugin) @@ -53,8 +53,6 @@ fn main() { .add_systems(Update, inventory::dbg_print_inventory) .add_systems(Update, camera::camera_fit_inside_current_level) .add_plugins(misc_objects::MiscObjectsPlugin) - .register_ldtk_int_cell::(1) .register_ldtk_int_cell::(2) - .register_ldtk_int_cell::(3) .run(); } diff --git a/examples/platformer/walls.rs b/examples/platformer/walls.rs index 10c8a192..6f0bc570 100644 --- a/examples/platformer/walls.rs +++ b/examples/platformer/walls.rs @@ -181,3 +181,17 @@ pub fn spawn_wall_collision( }); } } + +/// Plugin which spawns walls on appropriate LDtk int cells, +/// then merges them together to reduce physics load. +/// +/// Walls are hardcoded as int cell values 1 (dirt) and 3 (stone). +pub struct WallPlugin; + +impl Plugin for WallPlugin { + fn build(&self, app: &mut App) { + app.add_systems(Update, spawn_wall_collision) + .register_ldtk_int_cell::(1) //dirt + .register_ldtk_int_cell::(3); //stone + } +} From fbabd8813436c4eb0ae1cd7d4ffa59e4073b45c4 Mon Sep 17 00:00:00 2001 From: Koopa1018 Date: Sat, 10 Aug 2024 05:58:48 -0400 Subject: [PATCH 4/6] Move ladder int-cell reg to plugin also Only there's an existing plugin for the climbing system. --- examples/platformer/climbing.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/platformer/climbing.rs b/examples/platformer/climbing.rs index 20aa10e2..a66ef8f3 100644 --- a/examples/platformer/climbing.rs +++ b/examples/platformer/climbing.rs @@ -73,6 +73,7 @@ pub struct ClimbingPlugin; impl Plugin for ClimbingPlugin { fn build(&self, app: &mut App) { app.add_systems(Update, detect_climb_range) - .add_systems(Update, ignore_gravity_if_climbing); + .add_systems(Update, ignore_gravity_if_climbing) + .register_ldtk_int_cell::(2); } } From 1297827dcf3dc9516c18e2c77d20f7b18e4ece3a Mon Sep 17 00:00:00 2001 From: Koopa1018 Date: Sat, 10 Aug 2024 06:04:11 -0400 Subject: [PATCH 5/6] Move ground detection into own plugin+module --- examples/platformer/ground_detection.rs | 103 ++++++++++++++++++++++++ examples/platformer/main.rs | 4 +- examples/platformer/physics.rs | 100 ----------------------- examples/platformer/player.rs | 2 +- 4 files changed, 106 insertions(+), 103 deletions(-) create mode 100644 examples/platformer/ground_detection.rs diff --git a/examples/platformer/ground_detection.rs b/examples/platformer/ground_detection.rs new file mode 100644 index 00000000..391f60fd --- /dev/null +++ b/examples/platformer/ground_detection.rs @@ -0,0 +1,103 @@ +use std::collections::HashSet; + +use bevy::prelude::*; + +use bevy_rapier2d::prelude::*; + +#[derive(Component)] +pub struct GroundSensor { + pub ground_detection_entity: Entity, + pub intersecting_ground_entities: HashSet, +} + +#[derive(Clone, Default, Component)] +pub struct GroundDetection { + pub on_ground: bool, +} + +pub fn spawn_ground_sensor( + mut commands: Commands, + detect_ground_for: Query<(Entity, &Collider), Added>, +) { + for (entity, shape) in &detect_ground_for { + if let Some(cuboid) = shape.as_cuboid() { + let Vec2 { + x: half_extents_x, + y: half_extents_y, + } = cuboid.half_extents(); + + let detector_shape = Collider::cuboid(half_extents_x / 2.0, 2.); + + let sensor_translation = Vec3::new(0., -half_extents_y, 0.); + + commands.entity(entity).with_children(|builder| { + builder + .spawn_empty() + .insert(ActiveEvents::COLLISION_EVENTS) + .insert(detector_shape) + .insert(Sensor) + .insert(Transform::from_translation(sensor_translation)) + .insert(GlobalTransform::default()) + .insert(GroundSensor { + ground_detection_entity: entity, + intersecting_ground_entities: HashSet::new(), + }); + }); + } + } +} + +pub fn ground_detection( + mut ground_sensors: Query<&mut GroundSensor>, + mut collisions: EventReader, + collidables: Query, Without)>, +) { + for collision_event in collisions.read() { + match collision_event { + CollisionEvent::Started(e1, e2, _) => { + if collidables.contains(*e1) { + if let Ok(mut sensor) = ground_sensors.get_mut(*e2) { + sensor.intersecting_ground_entities.insert(*e1); + } + } else if collidables.contains(*e2) { + if let Ok(mut sensor) = ground_sensors.get_mut(*e1) { + sensor.intersecting_ground_entities.insert(*e2); + } + } + } + CollisionEvent::Stopped(e1, e2, _) => { + if collidables.contains(*e1) { + if let Ok(mut sensor) = ground_sensors.get_mut(*e2) { + sensor.intersecting_ground_entities.remove(e1); + } + } else if collidables.contains(*e2) { + if let Ok(mut sensor) = ground_sensors.get_mut(*e1) { + sensor.intersecting_ground_entities.remove(e2); + } + } + } + } + } +} + +pub fn update_on_ground( + mut ground_detectors: Query<&mut GroundDetection>, + ground_sensors: Query<&GroundSensor, Changed>, +) { + for sensor in &ground_sensors { + if let Ok(mut ground_detection) = ground_detectors.get_mut(sensor.ground_detection_entity) { + ground_detection.on_ground = !sensor.intersecting_ground_entities.is_empty(); + } + } +} + +/// Handles platformer-specific physics operations, specifically ground detection. +pub struct GroundDetectionPlugin; + +impl Plugin for GroundDetectionPlugin { + fn build(&self, app: &mut App) { + app.add_systems(Update, spawn_ground_sensor) + .add_systems(Update, ground_detection) + .add_systems(Update, update_on_ground); + } +} diff --git a/examples/platformer/main.rs b/examples/platformer/main.rs index 6cb6c441..f8ac479d 100644 --- a/examples/platformer/main.rs +++ b/examples/platformer/main.rs @@ -11,6 +11,7 @@ mod climbing; mod enemy; /// Handles initialization and switching levels mod game_flow; +mod ground_detection; mod inventory; mod misc_objects; mod physics; @@ -46,13 +47,12 @@ fn main() { }) .add_plugins(game_flow::GameFlowPlugin) .add_plugins(walls::WallPlugin) - .add_plugins(physics::PhysicsPlugin) + .add_plugins(ground_detection::GroundDetectionPlugin) .add_plugins(climbing::ClimbingPlugin) .add_plugins(player::PlayerPlugin) .add_plugins(enemy::EnemyPlugin) .add_systems(Update, inventory::dbg_print_inventory) .add_systems(Update, camera::camera_fit_inside_current_level) .add_plugins(misc_objects::MiscObjectsPlugin) - .register_ldtk_int_cell::(2) .run(); } diff --git a/examples/platformer/physics.rs b/examples/platformer/physics.rs index 12cd3698..e426fb8d 100644 --- a/examples/platformer/physics.rs +++ b/examples/platformer/physics.rs @@ -1,5 +1,3 @@ -use std::collections::HashSet; - use bevy::prelude::*; use bevy_ecs_ldtk::prelude::*; @@ -76,101 +74,3 @@ impl From for SensorBundle { } } } - -#[derive(Component)] -pub struct GroundSensor { - pub ground_detection_entity: Entity, - pub intersecting_ground_entities: HashSet, -} - -#[derive(Clone, Default, Component)] -pub struct GroundDetection { - pub on_ground: bool, -} - -pub fn spawn_ground_sensor( - mut commands: Commands, - detect_ground_for: Query<(Entity, &Collider), Added>, -) { - for (entity, shape) in &detect_ground_for { - if let Some(cuboid) = shape.as_cuboid() { - let Vec2 { - x: half_extents_x, - y: half_extents_y, - } = cuboid.half_extents(); - - let detector_shape = Collider::cuboid(half_extents_x / 2.0, 2.); - - let sensor_translation = Vec3::new(0., -half_extents_y, 0.); - - commands.entity(entity).with_children(|builder| { - builder - .spawn_empty() - .insert(ActiveEvents::COLLISION_EVENTS) - .insert(detector_shape) - .insert(Sensor) - .insert(Transform::from_translation(sensor_translation)) - .insert(GlobalTransform::default()) - .insert(GroundSensor { - ground_detection_entity: entity, - intersecting_ground_entities: HashSet::new(), - }); - }); - } - } -} - -pub fn ground_detection( - mut ground_sensors: Query<&mut GroundSensor>, - mut collisions: EventReader, - collidables: Query, Without)>, -) { - for collision_event in collisions.read() { - match collision_event { - CollisionEvent::Started(e1, e2, _) => { - if collidables.contains(*e1) { - if let Ok(mut sensor) = ground_sensors.get_mut(*e2) { - sensor.intersecting_ground_entities.insert(*e1); - } - } else if collidables.contains(*e2) { - if let Ok(mut sensor) = ground_sensors.get_mut(*e1) { - sensor.intersecting_ground_entities.insert(*e2); - } - } - } - CollisionEvent::Stopped(e1, e2, _) => { - if collidables.contains(*e1) { - if let Ok(mut sensor) = ground_sensors.get_mut(*e2) { - sensor.intersecting_ground_entities.remove(e1); - } - } else if collidables.contains(*e2) { - if let Ok(mut sensor) = ground_sensors.get_mut(*e1) { - sensor.intersecting_ground_entities.remove(e2); - } - } - } - } - } -} - -pub fn update_on_ground( - mut ground_detectors: Query<&mut GroundDetection>, - ground_sensors: Query<&GroundSensor, Changed>, -) { - for sensor in &ground_sensors { - if let Ok(mut ground_detection) = ground_detectors.get_mut(sensor.ground_detection_entity) { - ground_detection.on_ground = !sensor.intersecting_ground_entities.is_empty(); - } - } -} - -/// Handles platformer-specific physics operations, specifically ground detection. -pub struct PhysicsPlugin; - -impl Plugin for PhysicsPlugin { - fn build(&self, app: &mut App) { - app.add_systems(Update, spawn_ground_sensor) - .add_systems(Update, ground_detection) - .add_systems(Update, update_on_ground); - } -} diff --git a/examples/platformer/player.rs b/examples/platformer/player.rs index 73bf637b..b79346ac 100644 --- a/examples/platformer/player.rs +++ b/examples/platformer/player.rs @@ -2,7 +2,7 @@ use bevy::prelude::*; use bevy_ecs_ldtk::prelude::*; use bevy_rapier2d::dynamics::Velocity; -use crate::physics::{ColliderBundle, GroundDetection}; +use crate::{ground_detection::GroundDetection, physics::ColliderBundle}; use crate::{climbing::Climber, inventory::Inventory}; #[derive(Copy, Clone, Eq, PartialEq, Debug, Default, Component)] From 9f004ad88f4793b5b0c1aad15e0f63ace21ebb6a Mon Sep 17 00:00:00 2001 From: Koopa1018 Date: Sat, 10 Aug 2024 06:15:51 -0400 Subject: [PATCH 6/6] Rename "physics" to "colliders" Because that's all that's left after "ground_detection" is split off. (Also technically there's no physics calculations being done here.) --- examples/platformer/climbing.rs | 2 +- examples/platformer/{physics.rs => colliders.rs} | 0 examples/platformer/enemy.rs | 2 +- examples/platformer/main.rs | 3 ++- examples/platformer/misc_objects.rs | 2 +- examples/platformer/player.rs | 2 +- 6 files changed, 6 insertions(+), 5 deletions(-) rename examples/platformer/{physics.rs => colliders.rs} (100%) diff --git a/examples/platformer/climbing.rs b/examples/platformer/climbing.rs index a66ef8f3..20796c15 100644 --- a/examples/platformer/climbing.rs +++ b/examples/platformer/climbing.rs @@ -2,7 +2,7 @@ use bevy::{prelude::*, utils::HashSet}; use bevy_ecs_ldtk::prelude::*; use bevy_rapier2d::prelude::*; -use crate::physics::SensorBundle; +use crate::colliders::SensorBundle; #[derive(Clone, Eq, PartialEq, Debug, Default, Component)] pub struct Climber { diff --git a/examples/platformer/physics.rs b/examples/platformer/colliders.rs similarity index 100% rename from examples/platformer/physics.rs rename to examples/platformer/colliders.rs diff --git a/examples/platformer/enemy.rs b/examples/platformer/enemy.rs index 005ce047..cf4e5768 100644 --- a/examples/platformer/enemy.rs +++ b/examples/platformer/enemy.rs @@ -2,7 +2,7 @@ use bevy::prelude::*; use bevy_ecs_ldtk::{prelude::*, utils::ldtk_pixel_coords_to_translation_pivoted}; use bevy_rapier2d::dynamics::Velocity; -use crate::physics::ColliderBundle; +use crate::colliders::ColliderBundle; #[derive(Copy, Clone, Eq, PartialEq, Debug, Default, Component)] pub struct Enemy; diff --git a/examples/platformer/main.rs b/examples/platformer/main.rs index f8ac479d..97118d60 100644 --- a/examples/platformer/main.rs +++ b/examples/platformer/main.rs @@ -8,13 +8,14 @@ use bevy_rapier2d::prelude::*; mod camera; mod climbing; +/// Bundles for auto-loading Rapier colliders as part of the level +mod colliders; mod enemy; /// Handles initialization and switching levels mod game_flow; mod ground_detection; mod inventory; mod misc_objects; -mod physics; mod player; mod walls; diff --git a/examples/platformer/misc_objects.rs b/examples/platformer/misc_objects.rs index 14208320..c6263a98 100644 --- a/examples/platformer/misc_objects.rs +++ b/examples/platformer/misc_objects.rs @@ -1,7 +1,7 @@ use bevy::prelude::*; use bevy_ecs_ldtk::prelude::*; -use crate::physics::ColliderBundle; +use crate::colliders::ColliderBundle; #[derive(Clone, Default, Bundle, LdtkEntity)] pub struct ChestBundle { diff --git a/examples/platformer/player.rs b/examples/platformer/player.rs index b79346ac..527736b5 100644 --- a/examples/platformer/player.rs +++ b/examples/platformer/player.rs @@ -2,8 +2,8 @@ use bevy::prelude::*; use bevy_ecs_ldtk::prelude::*; use bevy_rapier2d::dynamics::Velocity; -use crate::{ground_detection::GroundDetection, physics::ColliderBundle}; use crate::{climbing::Climber, inventory::Inventory}; +use crate::{colliders::ColliderBundle, ground_detection::GroundDetection}; #[derive(Copy, Clone, Eq, PartialEq, Debug, Default, Component)] pub struct Player;