You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Bevy 0.14中Rapier3D碰撞检测无响应,求代码问题排查

Bevy 0.14 + Rapier3D 碰撞检测无响应问题排查

我用Bevy 0.14开发游戏,按照官方手册用Rapier3D实现碰撞检测,但始终无法触发碰撞事件,多次对照手册调整代码依然无效。以下是我的代码,麻烦帮忙排查问题:

use bevy::input::mouse::{MouseMotion, MouseWheel};
use bevy::prelude::*;
use bevy_rapier3d::prelude::*;

fn main() {
    App::new().
        add_plugins(DefaultPlugins).
        add_plugins(RapierPhysicsPlugin::<NoUserData>::default()).
        add_plugins(RapierDebugRenderPlugin::default()).
        add_systems(Startup, init_object).
        add_systems(Update, (player_movement, camera_movement)).
        add_systems(PostUpdate, landed_player_check).
        run();
}

// 玩家组件定义
#[derive(Component)]
struct Player {
    is_jumping: bool,
}

impl Default for Player {
    fn default() -> Self {
        Self {is_jumping: false}
    }
}

#[derive(Component)]
struct Camera;

#[derive(Component)]
struct CameraFollow {
    offset: Vec3,    
}

#[derive(Component)]
struct Ground;


fn init_object(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
) {
    // 地面
    commands.spawn((
        PbrBundle {
            mesh: meshes.add(Plane3d::default().mesh().size(20., 20.)),
            material: materials.add(Color::srgb_u8(255, 0, 0)),
            transform: Transform::from_xyz(0.0, 0.0, 0.0), 
            ..default()
        },
        Collider::cuboid(10.0, 0.0, 10.0), 
        Ground,
    ));


    // 玩家
    commands.spawn((
        PbrBundle {
            mesh: meshes.add(Cuboid::new(1.0, 1.0, 1.0)),
            transform: Transform::from_xyz(0.0, 1.0, 0.0), 
            material: materials.add(StandardMaterial {
                base_color: Color::WHITE,
                ..default()
            }),
            ..default()
        },
        Player::default(),
        RigidBody::Dynamic,
        Collider::cuboid(0.5, 0.5, 0.5), 
        GravityScale(10.0),
    ));

    // 光源
    commands.spawn(DirectionalLightBundle {
        transform: Transform::from_translation(Vec3::ONE).looking_at(Vec3::ZERO, Vec3::Y),
        ..default()
    });


    // 相机
    commands.spawn((
        Camera3dBundle {
            transform: Transform::from_xyz(0.0, 10.0, 10.0).looking_at(Vec3::ZERO, Vec3::Y),
            ..default()
        },
        Camera,
        CameraFollow {
            offset: Vec3::new(0.0, 10.0, 10.0), 
        },
    ));
}


fn player_movement(
    mut query: Query<(&mut Transform, &mut Player), With<Player>>, 
    keyboard_input: Res<ButtonInput<KeyCode>>,
    time: Res<Time>, 
) {
    let speed = 10.0;

    if let Ok((mut transform, mut player_state)) = query.get_single_mut() {
        if !player_state.is_jumping {
            let mut direction = Vec3::ZERO;
            if keyboard_input.pressed(KeyCode::KeyW) {
                direction.z -= 1.0;
            }
            if keyboard_input.pressed(KeyCode::KeyS) {
                direction.z += 1.0;
            }
            if keyboard_input.pressed(KeyCode::KeyA) {
                direction.x -= 1.0;
            }
            if keyboard_input.pressed(KeyCode::KeyD) {
                direction.x += 1.0;
            }
            if keyboard_input.pressed(KeyCode::Space) {
                player_state.is_jumping = true;
                direction.y += 10.0;
            }
            transform.translation += direction * speed * time.delta_seconds();
        }
    }
}


fn camera_movement(
    mut param_set: ParamSet<(
        Query<(&mut Transform, &mut Player), With<Player>>,  // 玩家查询
        Query<(&mut Transform, &mut CameraFollow), With<Camera>>,  // 相机查询
    )>,
    mouse_button: Res<ButtonInput<MouseButton>>,
    mut mouse_motion: EventReader<MouseMotion>,
    mut wheel_event: EventReader<MouseWheel>,
) {
    let zoom_speed = 0.5;


    // 按住鼠标中键时处理相机旋转
    if mouse_button.pressed(MouseButton::Middle) {
        for mouse_move_event in mouse_motion.read() {
            if let Ok((player_transform, _)) = param_set.p0().get_single() {
                let player_position = player_transform.translation;

                if let Ok((mut camera_transform, mut camera_follow)) = param_set.p1().get_single_mut() {
                    // 左右旋转以世界Y轴为基准,上下旋转以相机本地X轴为基准
                    let rotation = Quat::from_rotation_y(-mouse_move_event.delta.x * 0.005)
                        * Quat::from_axis_angle(*camera_transform.local_x(), -mouse_move_event.delta.y * 0.005);

                    camera_follow.offset = rotation * camera_follow.offset;
                    camera_transform.translation = player_position + camera_follow.offset;

                    // 保持相机朝向玩家
                    camera_transform.look_at(player_position, Vec3::Y);
                }
            }
        }
    }


    // 更新相机位置
    if let Ok((player_transform, _)) = param_set.p0().get_single() {
        let player_position = player_transform.translation;
        if let Ok((mut camera_transform, camera_follow)) = param_set.p1().get_single_mut() {
            camera_transform.translation = player_position + camera_follow.offset;
            camera_transform.look_at(player_position, Vec3::Y);
        }
    }


    // 鼠标滚轮调整相机缩放
    for v in wheel_event.read() {
        if let Ok((_, mut camera_follow)) = param_set.p1().get_single_mut() {
            let zoom_delta = -v.y * zoom_speed;
            let offset_length = camera_follow.offset.length();

            // 限制缩放范围
            let new_length = (offset_length + zoom_delta).clamp(5.0, 20.0);
            camera_follow.offset = camera_follow.offset.normalize() * new_length;
        }
    }
}


fn landed_player_check(
    mut collision_events: EventReader<CollisionEvent>,
    player_query: Query<Entity, With<Player>>,
    ground_query: Query<Entity, With<Ground>>,
) {
    let player_entity = player_query.get_single().ok();
    let ground_entity = ground_query.get_single().ok();

    for event in collision_events.read() {
        println!("碰撞事件触发: {:?}", event);
        match event {
            CollisionEvent::Started(entity1, entity2, _) => {
                println!("实体1: {:?}, 实体2: {:?}", entity1, entity2);

                if let (Some(player), Some(ground)) = (player_entity, ground_entity) {
                    println!("玩家实体: {:?}, 地面实体: {:?}", player, ground);

                    if (*entity1 == player && *entity2 == ground) || (*entity1 == ground && *entity2 == player) {
                        println!("玩家落地了!");
                    } else {
                        println!("触发了碰撞,但不是玩家和地面");
                    }
                }
            }
            _ => {}
        }
    }
}

问题排查与修复方案

1. 地面Collider厚度为0

代码中地面的Collider设置为Collider::cuboid(10.0, 0.0, 10.0),Y轴厚度为0的几何体在3D物理引擎中会被视为无限薄的平面,大概率会被碰撞检测忽略。
修复:将厚度改为极小的正值,比如0.1:

Collider::cuboid(10.0, 0.1, 10.0),

2. 地面未添加静态刚体组件

Rapier中,仅添加Collider而没有RigidBody的实体,不会主动参与物理碰撞事件。地面需要显式设置为固定刚体:
修复:给地面实体添加RigidBody::Fixed:

commands.spawn((
    PbrBundle { /* ... 原有代码 ... */ },
    Collider::cuboid(10.0, 0.1, 10.0),
    RigidBody::Fixed, // 添加这一行
    Ground,
));

3. 直接修改玩家Transform导致物理状态不同步

player_movement系统中直接修改transform.translation,会绕过Rapier的物理模拟,导致刚体的物理状态和视觉位置不一致,碰撞检测自然无法正常触发。
修复:改用Rapier的Velocity组件控制玩家移动:

fn player_movement(
    mut query: Query<(&mut Velocity, &mut Player), With<Player>>, 
    keyboard_input: Res<ButtonInput<KeyCode>>,
) {
    let speed = 10.0;
    let jump_force = 50.0;

    if let Ok((mut velocity, mut player_state)) = query.get_single_mut() {
        let mut direction = Vec3::ZERO;
        if keyboard_input.pressed(KeyCode::KeyW) {
            direction.z -= 1.0;
        }
        if keyboard_input.pressed(KeyCode::KeyS) {
            direction.z += 1.0;
        }
        if keyboard_input.pressed(KeyCode::KeyA) {
            direction.x -= 1.0;
        }
        if keyboard_input.pressed(KeyCode::KeyD) {
            direction.x += 1.0;
        }

        // 处理水平移动
        if direction != Vec3::ZERO {
            direction = direction.normalize();
            velocity.linvel.x = direction.x * speed;
            velocity.linvel.z = direction.z * speed;
        } else {
            velocity.linvel.x = 0.0;
            velocity.linvel.z = 0.0;
        }

        // 处理跳跃(仅在未跳跃时触发)
        if keyboard_input.just_pressed(KeyCode::Space) && !player_state.is_jumping {
            player_state.is_jumping = true;
            velocity.linvel.y = jump_force;
        }
    }
}

4. 修复语法错误

原代码中玩家PbrBundle的transform行末尾有多余的킴字符,会导致编译失败,已在上面的代码示例中移除。

额外优化:落地状态重置

当前代码中玩家is_jumping状态只会在跳跃时设为true,没有重置逻辑,需要在碰撞检测到落地时重置:

fn landed_player_check(
    mut collision_events: EventReader<CollisionEvent>,
    mut player_query: Query<&mut Player, With<Player>>,
    player_entity_query: Query<Entity, With<Player>>,
    ground_query: Query<Entity, With<Ground>>,
) {
    let player_entity = player_entity_query.get_single().ok();
    let ground_entity = ground_query.get_single().ok();
    let mut player = player_query.get_single_mut().ok();

    for event in collision_events.read() {
        match event {
            CollisionEvent::Started(entity1, entity2, _) => {
                if let (Some(player), Some(ground), Some(mut player_state)) = (player_entity, ground_entity, player) {
                    if (*entity1 == player && *entity2 == ground) || (*entity1 == ground && *entity2 == player) {
                        player_state.is_jumping = false;
                        println!("玩家落地了!");
                    }
                }
            }
            _ => {}
        }
    }
}

内容的提问来源于stack exchange,提问作者yun

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 05:24:51