use bevy::prelude::*;
use bevy_map_core::CollisionData;
#[cfg(feature = "physics")]
use bevy_map_core::{CollisionShape, OneWayDirection, PhysicsBody};
#[cfg(feature = "physics")]
use avian2d::{prelude::*, schedule::PhysicsSchedulePlugin};
#[cfg(feature = "physics")]
use bevy_ecs_tilemap::prelude::*;
#[cfg(feature = "physics")]
pub struct MapCollisionPlugin;
#[cfg(feature = "physics")]
impl Plugin for MapCollisionPlugin {
fn build(&self, app: &mut App) {
if !app.is_plugin_added::<PhysicsSchedulePlugin>() {
app.add_plugins(avian2d::PhysicsPlugins::default());
}
app.add_systems(Update, spawn_tile_colliders);
}
}
#[derive(Component)]
pub struct MapCollider {
pub data: CollisionData,
}
#[cfg(feature = "physics")]
#[derive(Component)]
pub struct OneWayPlatform {
pub direction: OneWayDirection,
}
#[cfg(feature = "physics")]
pub fn spawn_tile_colliders(
mut commands: Commands,
map_query: Query<(Entity, &super::MapRoot), Added<super::MapRoot>>,
map_assets: Res<Assets<bevy_map_core::MapProject>>,
) {
for (map_entity, map_root) in map_query.iter() {
let Some(project) = map_assets.get(&map_root.handle) else {
continue;
};
let tile_size = map_root.textures.tile_size;
let level = &project.level;
let map_size = TilemapSize {
x: level.width,
y: level.height,
};
let grid_size = TilemapGridSize {
x: tile_size,
y: tile_size,
};
let tilemap_tile_size = TilemapTileSize {
x: tile_size,
y: tile_size,
};
let map_type = TilemapType::Square;
let anchor = TilemapAnchor::default();
let mut total_colliders = 0;
for layer in level.layers.iter() {
if let bevy_map_core::LayerData::Tiles {
tileset_id, tiles, ..
} = &layer.data
{
let Some(tileset) = project.get_tileset(*tileset_id) else {
continue;
};
for y in 0..level.height {
for x in 0..level.width {
let idx = (y * level.width + x) as usize;
if let Some(&Some(tile_index)) = tiles.get(idx) {
if let Some(props) = tileset.get_tile_properties(tile_index) {
if props.collision.has_collision() {
spawn_collider_for_tile(
&mut commands,
map_entity,
&props.collision,
x,
y,
tile_size,
&map_size,
&grid_size,
&tilemap_tile_size,
&map_type,
&anchor,
);
total_colliders += 1;
}
}
}
}
}
}
}
if total_colliders > 0 {
info!("Spawned {} tile colliders from map", total_colliders);
}
}
}
#[cfg(feature = "physics")]
fn spawn_collider_for_tile(
commands: &mut Commands,
map_entity: Entity,
collision: &CollisionData,
tile_x: u32,
tile_y: u32,
tile_size: f32,
map_size: &TilemapSize,
grid_size: &TilemapGridSize,
tilemap_tile_size: &TilemapTileSize,
map_type: &TilemapType,
anchor: &TilemapAnchor,
) {
let Some(collider) = shape_to_collider(&collision.shape, tile_size) else {
return;
};
let tile_pos = TilePos {
x: tile_x,
y: tile_y,
};
let center = tile_pos.center_in_world(map_size, grid_size, tilemap_tile_size, map_type, anchor);
let (offset_x, offset_y) = get_shape_offset(&collision.shape, tile_size);
let membership = 1u32 << collision.layer;
let collider_entity = commands
.spawn((
Transform::from_xyz(center.x + offset_x, center.y + offset_y, 0.0),
Visibility::default(),
body_type_to_rigid_body(collision.body_type),
collider,
CollisionLayers::from_bits(membership, collision.mask),
Restitution::new(0.0),
MapCollider {
data: collision.clone(),
},
))
.id();
if collision.one_way.is_one_way() {
commands.entity(collider_entity).insert(OneWayPlatform {
direction: collision.one_way,
});
}
commands.entity(map_entity).add_child(collider_entity);
}
#[cfg(feature = "physics")]
fn shape_to_collider(shape: &CollisionShape, tile_size: f32) -> Option<Collider> {
match shape {
CollisionShape::None => None,
CollisionShape::Full => Some(Collider::rectangle(tile_size, tile_size)),
CollisionShape::Rectangle { size, .. } => Some(Collider::rectangle(
size[0] * tile_size,
size[1] * tile_size,
)),
CollisionShape::Circle { radius, .. } => Some(Collider::circle(*radius * tile_size)),
CollisionShape::Polygon { points } => {
if points.len() < 3 {
return None;
}
let scaled: Vec<Vec2> = points
.iter()
.map(|p| Vec2::new((p[0] - 0.5) * tile_size, (0.5 - p[1]) * tile_size))
.collect();
Collider::convex_hull(scaled)
}
}
}
#[cfg(feature = "physics")]
fn get_shape_offset(shape: &CollisionShape, tile_size: f32) -> (f32, f32) {
match shape {
CollisionShape::Rectangle { offset, size } => (
(offset[0] + size[0] / 2.0 - 0.5) * tile_size,
(0.5 - offset[1] - size[1] / 2.0) * tile_size,
),
CollisionShape::Circle { offset, .. } => (
(offset[0] - 0.5) * tile_size,
(0.5 - offset[1]) * tile_size,
),
_ => (0.0, 0.0),
}
}
#[cfg(feature = "physics")]
fn body_type_to_rigid_body(body: PhysicsBody) -> RigidBody {
match body {
PhysicsBody::Static => RigidBody::Static,
PhysicsBody::Dynamic => RigidBody::Dynamic,
PhysicsBody::Kinematic => RigidBody::Kinematic,
}
}
#[cfg(not(feature = "physics"))]
pub struct MapCollisionPlugin;
#[cfg(not(feature = "physics"))]
impl Plugin for MapCollisionPlugin {
fn build(&self, _app: &mut App) {
}
}