generated from 2ndbeam/bevy-template
feat: Collision beginning
- Added system for detecting collisions with interactive objects - Added interactive doors
This commit is contained in:
parent
3fdfa4a4ad
commit
4c23a38b92
5 changed files with 524 additions and 10 deletions
142
src/layout.rs
142
src/layout.rs
|
|
@ -1 +1,143 @@
|
|||
use bevy::{ecs::query::QueryFilter, prelude::*};
|
||||
use bevy_rapier2d::prelude::*;
|
||||
|
||||
use crate::{PIXELS_PER_METER, player::Player};
|
||||
|
||||
const DOOR_OPENED_ASSET: &'static str = "sprites/interactive/door_opened.png";
|
||||
const DOOR_CLOSED_ASSET: &'static str = "sprites/interactive/door_closed.png";
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct MayInteract;
|
||||
|
||||
#[derive(Component, Default)]
|
||||
pub struct InteractiveObject;
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct Wall;
|
||||
|
||||
#[derive(Component)]
|
||||
#[require(Sprite, InteractiveObject)]
|
||||
pub struct Door;
|
||||
|
||||
#[derive(EntityEvent)]
|
||||
pub struct InteractionEvent {
|
||||
pub entity: Entity,
|
||||
}
|
||||
|
||||
fn get_interactive_id<F: QueryFilter>(
|
||||
tested_id: Entity,
|
||||
interactive_query: Query<(), F>,
|
||||
parent_query: Query<&ChildOf, With<Collider>>,
|
||||
) -> Option<Entity> {
|
||||
if interactive_query.get(tested_id).is_ok() {
|
||||
return Some(tested_id);
|
||||
}
|
||||
let Ok(parent_id) = parent_query.get(tested_id) else {
|
||||
return None;
|
||||
};
|
||||
match interactive_query.get(parent_id.0) {
|
||||
Ok(_) => Some(parent_id.0),
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn detect_interact_collisions(
|
||||
mut commands: Commands,
|
||||
mut collision_events: MessageReader<CollisionEvent>,
|
||||
player_query: Query<(), With<Player>>,
|
||||
interactive_query1: Query<(), (With<InteractiveObject>, Without<MayInteract>)>,
|
||||
interactive_query2: Query<(), (With<InteractiveObject>, With<MayInteract>)>,
|
||||
parent_query: Query<&ChildOf, With<Collider>>,
|
||||
) {
|
||||
for collision_event in collision_events.read() {
|
||||
match collision_event {
|
||||
CollisionEvent::Started(first, second, _) => {
|
||||
let Some(interactive_id) = get_interactive_id(*first, interactive_query1, parent_query) else {
|
||||
continue;
|
||||
};
|
||||
if player_query.get(*second).is_err() {
|
||||
continue;
|
||||
}
|
||||
commands.entity(interactive_id).insert(MayInteract);
|
||||
},
|
||||
CollisionEvent::Stopped(first, second, _) => {
|
||||
let Some(interactive_id) = get_interactive_id(*first, interactive_query2, parent_query) else {
|
||||
continue;
|
||||
};
|
||||
if player_query.get(*second).is_err() {
|
||||
continue;
|
||||
}
|
||||
commands.entity(interactive_id).remove::<MayInteract>();
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_door_interact(
|
||||
event: On<InteractionEvent>,
|
||||
mut commands: Commands,
|
||||
collider_query: Query<(), (With<Door>, With<Collider>)>,
|
||||
no_collider_query: Query<(), (With<Door>, Without<Collider>)>,
|
||||
mut sprite_query: Query<&mut Sprite, With<Door>>,
|
||||
asset_server: Res<AssetServer>,
|
||||
) {
|
||||
let was_opened =
|
||||
if collider_query.get(event.entity).is_ok() { false }
|
||||
else if no_collider_query.get(event.entity).is_ok() { true }
|
||||
else {
|
||||
error!("on_door_interact fired but entity {} isn't door", event.entity);
|
||||
return;
|
||||
};
|
||||
let Ok(mut sprite) = sprite_query.get_mut(event.entity) else {
|
||||
error!("on_door_interact fired but entity {} has no sprite", event.entity);
|
||||
return;
|
||||
};
|
||||
let asset_path = if was_opened { DOOR_CLOSED_ASSET } else { DOOR_OPENED_ASSET };
|
||||
sprite.image = asset_server.load(asset_path);
|
||||
|
||||
if was_opened {
|
||||
commands.entity(event.entity).insert(door_collider());
|
||||
} else {
|
||||
commands.entity(event.entity).remove::<Collider>();
|
||||
}
|
||||
}
|
||||
|
||||
fn door_collider() -> Collider {
|
||||
Collider::cuboid(PIXELS_PER_METER * 0.125, PIXELS_PER_METER)
|
||||
}
|
||||
|
||||
fn door_bundle(image: Handle<Image>, position: Vec2) -> impl Bundle {
|
||||
(
|
||||
Door,
|
||||
Sprite::from_image(image),
|
||||
Transform::from_xyz(position.x, position.y, 0.),
|
||||
Name::new(format!("Door ({}, {})", position.x, position.y)),
|
||||
door_collider(),
|
||||
Observer::new(on_door_interact),
|
||||
children![
|
||||
(
|
||||
Collider::cuboid(PIXELS_PER_METER * 0.5, PIXELS_PER_METER),
|
||||
Sensor,
|
||||
Transform::from_xyz(0., 0., 0.),
|
||||
),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
fn wall_bundle(position: Vec2) -> impl Bundle {
|
||||
(
|
||||
Wall,
|
||||
Transform::from_xyz(position.x, position.y, 0.),
|
||||
Collider::cuboid(PIXELS_PER_METER * 0.5, PIXELS_PER_METER),
|
||||
Name::new(format!("Wall ({}, {})", position.x, position.y)),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn setup_world(
|
||||
mut commands: Commands,
|
||||
asset_server: Res<AssetServer>,
|
||||
) {
|
||||
let door_image = asset_server.load(DOOR_CLOSED_ASSET);
|
||||
commands.spawn(door_bundle(door_image, vec2(128., 0.)));
|
||||
commands.spawn(wall_bundle(vec2(-128., 0.)));
|
||||
}
|
||||
|
|
|
|||
20
src/lib.rs
20
src/lib.rs
|
|
@ -7,9 +7,12 @@ pub mod ui;
|
|||
mod tests;
|
||||
|
||||
use bevy::{prelude::*, ui_widgets::ScrollbarPlugin};
|
||||
use bevy_rapier2d::{prelude::*, rapier::prelude::IntegrationParameters};
|
||||
use leafwing_input_manager::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub const PIXELS_PER_METER: f32 = 16.0;
|
||||
|
||||
pub struct ExpeditionPlugin;
|
||||
|
||||
#[derive(Actionlike, PartialEq, Eq, Hash, Debug, Clone, Reflect, Serialize, Deserialize)]
|
||||
|
|
@ -87,15 +90,30 @@ impl Plugin for ExpeditionPlugin {
|
|||
input::InputAssetPlugin::<InputAction>::default(),
|
||||
input::InputAssetPlugin::<UiAction>::default(),
|
||||
ScrollbarPlugin,
|
||||
RapierPhysicsPlugin::<()>::default()
|
||||
.with_custom_initialization(RapierContextInitialization::InitializeDefaultRapierContext {
|
||||
integration_parameters: IntegrationParameters {
|
||||
length_unit: PIXELS_PER_METER,
|
||||
..default()
|
||||
},
|
||||
rapier_configuration: RapierConfiguration {
|
||||
gravity: Vec2::ZERO,
|
||||
physics_pipeline_active: true,
|
||||
scaled_shape_subdivision: 10,
|
||||
force_update_from_transform_changes: false,
|
||||
},
|
||||
}),
|
||||
RapierDebugRenderPlugin::default(),
|
||||
))
|
||||
.init_state::<GameState>()
|
||||
.insert_resource(ui::WindowSize::default())
|
||||
.add_systems(Startup, (player::setup_player, setup_global))
|
||||
.add_systems(Startup, (player::setup_player, setup_global, layout::setup_world))
|
||||
.add_systems(Update, (
|
||||
player::handle_input,
|
||||
ui::update_window_size,
|
||||
ui::handle_input,
|
||||
insert_entity_name,
|
||||
layout::detect_interact_collisions,
|
||||
))
|
||||
.add_systems(OnEnter(GameState::Inventory), inventory::ui::setup_ui_inventory)
|
||||
.add_systems(OnExit(GameState::Inventory), inventory::ui::clear_ui_inventory)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
use bevy::prelude::*;
|
||||
use bevy_rapier2d::prelude::*;
|
||||
use leafwing_input_manager::prelude::*;
|
||||
|
||||
use crate::{GameState, InputAction as Action, inventory::{ActiveInventory, Inventory, item::Item}};
|
||||
use crate::{GameState, InputAction as Action, PIXELS_PER_METER, inventory::{ActiveInventory, Inventory, item::Item}, layout::{InteractionEvent, MayInteract}};
|
||||
|
||||
#[derive(Component, Reflect)]
|
||||
pub struct Player {
|
||||
|
|
@ -19,6 +20,11 @@ fn player_bundle(asset_server: &Res<AssetServer>) -> impl Bundle {
|
|||
Transform::from_xyz(0f32, 0f32, 1f32),
|
||||
Action::default_input_map(),
|
||||
Name::new("Player"),
|
||||
RigidBody::KinematicPositionBased,
|
||||
KinematicCharacterController::default(),
|
||||
ActiveCollisionTypes::default() | ActiveCollisionTypes::KINEMATIC_STATIC,
|
||||
Collider::cuboid(PIXELS_PER_METER * 0.5, PIXELS_PER_METER),
|
||||
ActiveEvents::COLLISION_EVENTS,
|
||||
children![
|
||||
(Inventory::new(UVec2::new(6, 2)), ActiveInventory),
|
||||
(Inventory::new(UVec2::new(5, 3)), ActiveInventory),
|
||||
|
|
@ -63,12 +69,13 @@ pub fn handle_input(
|
|||
time: Res<Time>,
|
||||
state: Res<State<GameState>>,
|
||||
mut next_state: ResMut<NextState<GameState>>,
|
||||
mut player: Query<(&Player, &mut ActionState<Action>, &mut Transform, &mut Sprite)>,
|
||||
interactables: Query<Entity, With<MayInteract>>,
|
||||
mut player: Query<(&Player, &mut ActionState<Action>, &mut KinematicCharacterController, &mut Sprite)>,
|
||||
) {
|
||||
let player = player.single_mut().expect("Player should be single");
|
||||
match state.get() {
|
||||
GameState::Running => {
|
||||
let (Player {speed}, mut action_state, mut transform, mut sprite) = player;
|
||||
let (Player {speed}, mut action_state, mut controller, mut sprite) = player;
|
||||
|
||||
if action_state.just_released(&Action::ToggleInventory) {
|
||||
next_state.set(GameState::Inventory);
|
||||
|
|
@ -77,13 +84,20 @@ pub fn handle_input(
|
|||
|
||||
let direction = action_state.clamped_value(&Action::Move);
|
||||
|
||||
transform.translation.x += direction * speed * time.delta_secs();
|
||||
controller.translation = Some(vec2(direction * speed * time.delta_secs(), 0.));
|
||||
if direction != 0f32 {
|
||||
sprite.flip_x = direction < 0f32;
|
||||
}
|
||||
|
||||
if action_state.just_pressed(&Action::Interact) {
|
||||
commands.run_system_cached(try_insert_item);
|
||||
let mut action_happened = false;
|
||||
for interactable_id in interactables {
|
||||
commands.trigger(InteractionEvent { entity: interactable_id });
|
||||
action_happened = true;
|
||||
}
|
||||
if !action_happened {
|
||||
commands.run_system_cached(try_insert_item);
|
||||
}
|
||||
}
|
||||
},
|
||||
GameState::Inventory => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue