Trouv / bevy_ecs_ldtk

ECS-friendly ldtk plugin for bevy, leveraging bevy_ecs_tilemap
Other
630 stars 74 forks source link

How to add collision to your ldtk map #220

Open Abubakar1122331 opened 11 months ago

Abubakar1122331 commented 11 months ago

I want to add collision for any these ldtk. Tell me how do you separate tiles for collision and add colliders to it.

Here's my main.rs:


 use bevy::{
    prelude::*,
    window::{PrimaryWindow, WindowTheme},
};
use bevy_ecs_ldtk::prelude::*;
use bevy_inspector_egui::prelude::*;
use bevy_inspector_egui::quick::WorldInspectorPlugin;
use std::collections::{HashMap, HashSet};

use components::Player;

mod components;
mod systems;

const PLAYER_SPEED: f32 = 500.0;
const PLAYER_SIZE: f32 = 24.0;
const GRAVITY: f32 = 1.0;
const FALL_SPEED: f32 = 10.0;

fn main() {
    App::new()
        .insert_resource(ClearColor(Color::rgb(0., 0., 0.)))
        .add_plugins(
            DefaultPlugins
                .set(ImagePlugin::default_nearest())
                .set(WindowPlugin {
                    primary_window: Some(Window {
                        title: "Bevy Fish Fight".into(),
                        resolution: (800.0, 800.0).into(),
                        resizable: false,
                        fit_canvas_to_parent: true,
                        window_theme: Some(WindowTheme::Dark),
                        ..Default::default()
                    }),
                    ..Default::default()
                }),
        )
        .add_plugins(WorldInspectorPlugin::new())
        .add_plugins(LdtkPlugin)
        .add_systems(Startup, setup)
        .insert_resource(LevelSelection::Index(1))
        .add_systems(Startup, systems::spawn_player)
        .add_systems(Update, systems::animate_sprite)
        .add_systems(Update, systems::camera_with_player_movement)
        .add_systems(Update, systems::player_movement)
        .add_systems(Update, systems::player_jump)
        // .add_systems(Update, systems::confine_player_movement)
        .run();
}

fn setup(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    windows: Query<&Window, With<PrimaryWindow>>,
) {
    commands.spawn(Camera2dBundle::default());
    let window = windows.get_single().unwrap();

    let ldtk_img = commands.spawn((
        LdtkWorldBundle {
            ldtk_handle: asset_server.load("Typical_2D_platformer_example.ldtk"),
            transform: Transform {
                scale: Vec3 {
                    x: 4.5,
                    y: 4.5,
                    z: 1.0,
                },
                translation: Vec3 {
                    x: -window.width() / 2.0,
                    y: -window.height() / 2.0,
                    z: 0.0,
                },
                ..Default::default()
            },
            ..Default::default()
        },
        Name::new("Level"),
    ));
}

This is my systems.rs:


use crate::components::*;
use bevy::prelude::*;
use bevy_ecs_ldtk::prelude::*;

use std::collections::{HashMap, HashSet};

use bevy_rapier2d::prelude::*;

use crate::*;

pub fn spawn_player(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    mut texture_atlases: ResMut<Assets<TextureAtlas>>,
) {
    let texture_handle = asset_server.load("gabe-idle-run.png");

    let texture_atlas =
        TextureAtlas::from_grid(texture_handle, Vec2::new(24.0, 24.0), 7, 1, None, None);
    let texture_atlas_handle = texture_atlases.add(texture_atlas);

    let animation_indices = AnimationIndices { first: 1, last: 6 };
    // Use only the subset of sprites in the sheet that make up the running animation
    commands.spawn((
        SpriteSheetBundle {
            texture_atlas: texture_atlas_handle,
            sprite: TextureAtlasSprite::new(3),
            transform: Transform {
                translation: Vec3 {
                    x: 0.0,
                    y: 0.0,
                    z: 4.5,
                },
                scale: Vec3::new(3.0, 3.0, 0.0),
                ..default()
            },
            ..default()
        },
        Name::new("Player"),
        Player,
        animation_indices,
        AnimationTimer(Timer::from_seconds(0.1, TimerMode::Repeating)),
    ));
}

pub fn player_movement(
    keyboard_input: Res<Input<KeyCode>>,
    mut player_query: Query<&mut Transform, With<Player>>,
    time: Res<Time>,
) {
    if let Ok(mut transform) = player_query.get_single_mut() {
        let mut direction = Vec3::ZERO;

        if keyboard_input.pressed(KeyCode::Left) || keyboard_input.pressed(KeyCode::A) {
            direction += Vec3::new(-1.0, 0.0, 0.0);
            transform.scale = Vec3::new(-3.0, 3.0, 0.0);
        }
        if keyboard_input.pressed(KeyCode::Right) || keyboard_input.pressed(KeyCode::D) {
            direction += Vec3::new(1.0, 0.0, 0.0);
            transform.scale = Vec3::new(3.0, 3.0, 0.0);
        }
        if keyboard_input.pressed(KeyCode::Up) || keyboard_input.pressed(KeyCode::W) {
            direction += Vec3::new(0.0, 1.0, 0.0);
        }
        if keyboard_input.pressed(KeyCode::Down) || keyboard_input.pressed(KeyCode::S) {
            direction += Vec3::new(0.0, -1.0, 0.0);
        }

        if direction.length() > 0.0 {
            direction = direction.normalize();
        }

        transform.translation += direction * PLAYER_SPEED * time.delta_seconds();
    }
}

pub fn camera_with_player_movement(
    keyboard_input: Res<Input<KeyCode>>,
    mut camera_query: Query<&mut Transform, With<Camera>>,
    time: Res<Time>,
) {
    if let Ok(mut transform) = camera_query.get_single_mut() {
        let mut direction = Vec3::ZERO;

        if keyboard_input.pressed(KeyCode::Left) || keyboard_input.pressed(KeyCode::A) {
            direction += Vec3::new(-1.0, 0.0, 0.0);
        }
        if keyboard_input.pressed(KeyCode::Right) || keyboard_input.pressed(KeyCode::D) {
            direction += Vec3::new(1.0, 0.0, 0.0);
        }
        if keyboard_input.pressed(KeyCode::Up) || keyboard_input.pressed(KeyCode::W) {
            direction += Vec3::new(0.0, 1.0, 0.0);
        }
        if keyboard_input.pressed(KeyCode::Down) || keyboard_input.pressed(KeyCode::S) {
            direction += Vec3::new(0.0, -1.0, 0.0);
        }

        if direction.length() > 0.0 {
            direction = direction.normalize();
        }

        transform.translation += direction * PLAYER_SPEED * time.delta_seconds();
    }
}

pub fn confine_player_movement(
    mut player_query: Query<&mut Transform, With<Player>>,
    window_query: Query<&Window, With<PrimaryWindow>>,
) {
    if let Ok(mut player_transform) = player_query.get_single_mut() {
        let window = window_query.get_single().unwrap();

        let half_player_size = PLAYER_SIZE;
        let x_min = -window.width() / 2.0 + half_player_size - 2.8;
        let x_max = window.width() / 2.0 - half_player_size;

        let mut translation = player_transform.translation;

        // Bound the player X position
        if translation.x < x_min {
            translation.x = x_min
        } else if translation.x > x_max {
            translation.x = x_max
        }

        player_transform.translation = translation;
    }
}

pub fn animate_sprite(
    keyboard_input: Res<Input<KeyCode>>,
    time: Res<Time>,
    mut query: Query<(
        &AnimationIndices,
        &mut AnimationTimer,
        &mut TextureAtlasSprite,
    )>,
) {
    for (indices, mut timer, mut sprite) in &mut query {
        timer.tick(time.delta());
        if keyboard_input.pressed(KeyCode::Right)
            || keyboard_input.pressed(KeyCode::D)
            || keyboard_input.pressed(KeyCode::Left)
            || keyboard_input.pressed(KeyCode::A)
        {
            if timer.just_finished() {
                sprite.index = if sprite.index == indices.last {
                    indices.first
                } else {
                    sprite.index + 1
                };
            }
        } else {
            sprite.index = 0
        }
    }
}

pub fn player_jump(
    mut commands: Commands,
    keyboard_input: Res<Input<KeyCode>>,
    mut player: Query<(Entity, &mut Transform, &mut Jump), With<Player>>,
    time: Res<Time>,
) {
    let Ok((player, mut transform, mut jump)) = player.get_single_mut() else {return;};
    let jump_power = (time.delta_seconds() * FALL_SPEED * 2.).min(jump.0);
    jump.0 -= jump_power;
    transform.translation.y += jump_power;
    if jump.0 == 0. {
        commands.entity(player).remove::<Jump>();
    }
}

components.rs:


use bevy::prelude::*;
use bevy_inspector_egui::prelude::*;
use bevy_inspector_egui::quick::WorldInspectorPlugin;

#[derive(Component, InspectorOptions)]
pub struct Player;

#[derive(Copy, Clone, Eq, PartialEq, Debug, Default, Component)]
pub struct Wall;

#[derive(Component, Deref, DerefMut)]
pub struct AnimationTimer(pub Timer);

#[derive(Component, InspectorOptions)]
pub struct AnimationIndices {
    pub first: usize,
    pub last: usize,
}

#[derive(Component)]
pub struct Jump(pub f32);
Trouv commented 11 months ago

There's a system in the platformer example that does this sort of thing, but fyi the platformer example uses rapier for physics/collision. Naively, you could spawn a collider for every Wall tile, and even do so through LdtkIntCell registration, but this leaves you with a lot of colliders in a level which slows things down a lot.

Instead, the platformer example uses the Wall component as a marker, then has a separate system that "merges" wall tiles into new, fewer, larger rectangle collider entities: https://github.com/Trouv/bevy_ecs_ldtk/blob/v0.8.0/examples/platformer/systems.rs#L78

Though, it doesn't look like you're spawning these components in any way. Be sure to do so either through the registering LdtkEntity/LdtkIntCell bundles to the app, or by fleshing them out in a system that queries for Added<EntityInstance> or Added<IntGridCell>, or some combination of the two: https://docs.rs/bevy_ecs_ldtk/latest/bevy_ecs_ldtk/#entity-and-intgrid-layers