如何在Bevy中结合Tiled编辑器与Rapier实现多边形碰撞?
1. 旋转轴错误(核心问题)
代码中使用Quat::from_rotation_x(rot)处理碰撞体旋转,但2D场景下Rapier和Bevy的旋转都是围绕Z轴进行的。绕X轴旋转会导致碰撞体在3D空间中倾斜,直接造成多边形形状和位置错乱。
修复:将旋转替换为Quat::from_rotation_z(rot)。
2. 坐标系转换错误
Tiled的坐标系是Y轴向下,而Bevy/Rapier是Y轴向上,且碰撞体的位置和多边形点需要从Tiled的Tile左上角原点转换为Bevy的Tile中心原点。
处理碰撞体位置(object_data.x/y)
单个Tile的半尺寸为grid_size / 2.0,将Tiled中相对于Tile左上角的偏移转换为相对于Tile中心的Bevy坐标:
let tile_half = grid_size / 2.0; let pos = Vect::new( object_data.x - tile_half, tile_half - object_data.y, // Y轴翻转,从向下转为向上 );
处理多边形点坐标
Tiled中多边形的点是相对于碰撞体原点(object_data.x/y)的局部坐标,且Y轴向下,需要转换为Bevy的Y轴向上的局部坐标:
tiled::ObjectShape::Polygon { points } => { let poly_points: Vec<Vect> = points .iter() .map(|(x, y)| Vect::new(*x, -*y)) // 翻转Y轴 .collect(); Collider::convex_hull(&poly_points)? }
3. 凹多边形的形状问题
Rapier的convex_hull会自动生成输入点的凸包,如果Tiled中的多边形是凹形,这会导致碰撞形状被错误简化。如果你的Tile是静态碰撞体(不会移动),可以改用Collider::trimesh来支持凹多边形:
tiled::ObjectShape::Polygon { points } => { let poly_points: Vec<Vect> = points .iter() .map(|(x, y)| Vect::new(*x, -*y)) .collect(); // 注意:trimesh仅适用于静态碰撞体,动态物体不能使用 Some(Collider::trimesh(poly_points, None)) }
注意:使用trimesh时,确保点的顺序是逆时针,且没有自交,否则Rapier可能无法正确处理。
4. 变换组合的验证
确认get_tilemap_center_transform和tile_pos.center_in_world的计算逻辑是否正确:
tile_pos.center_in_world应返回Tile中心的世界坐标(基于Tilemap的坐标系)get_tilemap_center_transform通常用于将Tilemap的中心对齐到世界原点,确保后续的偏移计算正确
完整修复后的关键代码片段
if let Some(object_layer) = layer_tile.collision.clone() { let tile_half = grid_size / 2.0; // 假设grid_size是单个Tile的边长,比如32.0 let shapes = object_layer .object_data() .iter() .filter_map(|object_data| { // 转换碰撞体位置:Tile左上角 → Tile中心(Bevy坐标系) let pos = Vect::new( object_data.x - tile_half, tile_half - object_data.y, ); let rot = object_data.rotation; let shape = match &object_data.shape { tiled::ObjectShape::Rect { width, height } => { Collider::cuboid(width / 2., height / 2.) } tiled::ObjectShape::Ellipse { width, height } => { Collider::capsule_x(width / 2., height / 2.) } tiled::ObjectShape::Polyline { points } => { let line_points: Vec<Vect> = points .iter() .map(|(x, y)| Vect::new(*x, -*y)) .collect(); Collider::polyline(line_points, None) } tiled::ObjectShape::Polygon { points } => { let poly_points: Vec<Vect> = points .iter() .map(|(x, y)| Vect::new(*x, -*y)) .collect(); // 若为静态Tile,可替换为Collider::trimesh(poly_points, None) Collider::convex_hull(&poly_points)? } _ => return None, }; Some((pos, rot, shape)) }) .collect::<Vec<_>>(); let world_pos = tile_pos.center_in_world(&grid_size, &map_type); let tile_transform = get_tilemap_center_transform(&map_size, &grid_size, &map_type, 0.) * Transform::from_xyz( world_pos.x + offset_x, world_pos.y - offset_y, 0., ); if shapes.len() == 1 { let (pos, rot, collider) = shapes[0].clone(); let local_transform = Transform { translation: Vec3::new(pos.x, pos.y, 0.), rotation: Quat::from_rotation_z(rot), // 改为绕Z轴旋转 ..default() }; let final_transform = tile_transform * local_transform; commands .entity(tile_entity) .insert((collider, TransformBundle::from_transform(final_transform))); } else if shapes.len() > 1 { // 组合碰撞体时,确保每个形状的位置和旋转已正确转换 let compound_shapes = shapes .into_iter() .map(|(pos, rot, shape)| { ( Isometry::new(pos, rot), // Rapier的Isometry,直接使用转换后的pos和rot shape, ) }) .collect(); commands.entity(tile_entity).insert(( Collider::compound(compound_shapes), TransformBundle::from_transform(tile_transform), )); } }
工具组合的合理性
你使用的工具链(Bevy 0.10.1 + bevy_ecs_tilemap + bevy_rapier2d + Tiled)是完全合理的,这是当前Bevy 2D Tilemap游戏开发的标准组合之一,问题出在坐标系转换和旋转轴的细节处理上。
内容的提问来源于stack exchange,提问作者Eva

