Bevy塔防游戏中如何区分按钮点击与普通点击实现塔放置
解决方案
你的问题核心是没区分「选塔」和「放置」两个阶段——按钮点击事件和鼠标左键点击事件会在同一帧被检测到,导致点击选塔按钮时直接触发了塔的生成逻辑。另外位置异常是因为直接用了窗口光标坐标,没转换成游戏世界坐标(Bevy默认世界坐标和窗口坐标的原点、缩放规则不一样)。
解决思路是引入一个全局状态跟踪当前选中的塔类型,把「选塔」和「放置」拆成两个独立逻辑:
- 定义全局状态资源
先创建一个存储当前选中塔类型的资源,标记是否处于待放置状态:
#[derive(Resource, Default)] struct SelectedTower(Option<TowerType>);
记得在App初始化时添加这个资源:app.insert_resource(SelectedTower::default());
- 修改按钮交互逻辑
点击选塔按钮时,只更新全局状态,不再直接生成塔:
fn tower_button_interaction( mut selected_tower: ResMut<SelectedTower>, interaction: Query<(&Interaction, &TowerType), Changed<Interaction>> ) { for (interaction, tower_type) in &interaction { if matches!(interaction, Interaction::Clicked) { info!("Selected tower type: {tower_type}"); // 设置选中的塔类型,进入待放置状态 selected_tower.0 = Some(*tower_type); } } }
- 新增塔放置逻辑
单独写一个系统,当有选中的塔且鼠标左键点击时,生成塔并清空选中状态:
fn place_selected_tower( mut commands: Commands, windows: Res<Windows>, mouse: Res<Input<MouseButton>>, assets: Res<GameAssets>, mut selected_tower: ResMut<SelectedTower>, // 引入相机用于坐标转换 q_camera: Query<(&Camera, &GlobalTransform)> ) { if mouse.just_pressed(MouseButton::Left) { if let Some(tower_type) = selected_tower.0.take() { let (camera, camera_transform) = q_camera.single(); let window = windows.get_primary().unwrap(); // 将窗口光标坐标转换为游戏世界坐标 if let Some(world_position) = window.cursor_position() .and_then(|pos| camera.viewport_to_world_2d(camera_transform, pos)) { spawn_tower(&mut commands, tower_type, &assets, world_position.extend(0.)); } } } }
- 添加塔预览跟随逻辑(可选但推荐)
当有选中的塔时,生成半透明预览精灵跟随鼠标,提升用户体验:
fn tower_preview_follow_mouse( mut commands: Commands, windows: Res<Windows>, assets: Res<GameAssets>, selected_tower: Res<SelectedTower>, q_camera: Query<(&Camera, &GlobalTransform)>, // 跟踪已生成的预览实体,避免重复创建 mut q_preview: Query<(Entity, &mut Transform, &TowerType)> ) { let (camera, camera_transform) = q_camera.single(); let window = windows.get_primary().unwrap(); match selected_tower.0 { Some(selected_type) => { // 检查是否已有预览实体 if let Some((_, mut transform, preview_type)) = q_preview.iter_mut().next() { if *preview_type != selected_type { // 选中塔类型变化时,销毁旧预览并创建新的 commands.entity(q_preview.single().0).despawn(); spawn_tower_preview(&mut commands, selected_type, &assets, Vec3::ZERO); } else { // 更新预览位置 if let Some(world_pos) = window.cursor_position() .and_then(|pos| camera.viewport_to_world_2d(camera_transform, pos)) { transform.translation = world_pos.extend(0.1); // z轴抬高避免被遮挡 } } } else { // 无预览时创建新预览 spawn_tower_preview(&mut commands, selected_type, &assets, Vec3::ZERO); } } None => { // 无选中塔时销毁预览 if let Some((entity, _, _)) = q_preview.iter().next() { commands.entity(entity).despawn(); } } } } // 生成塔预览的辅助函数 fn spawn_tower_preview(commands: &mut Commands, tower_type: TowerType, assets: &GameAssets, position: Vec3) { let texture = match tower_type { TowerType::Wizard => &assets.wizard_tower_preview, // 根据你的塔类型添加对应预览纹理 _ => &assets.default_tower_preview, }; commands.spawn(( SpriteBundle { texture: texture.clone(), transform: Transform::from_translation(position), sprite: Sprite { color: Color::rgba(1.0, 1.0, 1.0, 0.7), // 半透明区分正式塔 ..default() }, ..default() }, tower_type, )); }
最后把这些系统添加到App中,注意顺序(按钮交互先于放置逻辑):
app.add_system(tower_button_interaction) .add_system(place_selected_tower) .add_system(tower_preview_follow_mouse);
关键修复点说明
- 拆分「选塔」和「放置」逻辑,用全局状态做中间桥梁,避免同一帧触发两次操作
- 通过相机完成窗口坐标到世界坐标的转换,解决位置异常问题
- 新增预览跟随逻辑,让用户直观看到塔的放置位置,符合塔防游戏交互习惯
内容的提问来源于stack exchange,提问作者Deutrys
相关产品推荐
相关产品推荐

