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

Bevy塔防游戏中如何区分按钮点击与普通点击实现塔放置

解决方案

你的问题核心是没区分「选塔」和「放置」两个阶段——按钮点击事件和鼠标左键点击事件会在同一帧被检测到,导致点击选塔按钮时直接触发了塔的生成逻辑。另外位置异常是因为直接用了窗口光标坐标,没转换成游戏世界坐标(Bevy默认世界坐标和窗口坐标的原点、缩放规则不一样)。

解决思路是引入一个全局状态跟踪当前选中的塔类型,把「选塔」和「放置」拆成两个独立逻辑:

  1. 定义全局状态资源
    先创建一个存储当前选中塔类型的资源,标记是否处于待放置状态:
#[derive(Resource, Default)]
struct SelectedTower(Option<TowerType>);

记得在App初始化时添加这个资源:app.insert_resource(SelectedTower::default());

  1. 修改按钮交互逻辑
    点击选塔按钮时,只更新全局状态,不再直接生成塔:
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);
    }
  }
}
  1. 新增塔放置逻辑
    单独写一个系统,当有选中的塔且鼠标左键点击时,生成塔并清空选中状态:
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.));
      }
    }
  }
}
  1. 添加塔预览跟随逻辑(可选但推荐)
    当有选中的塔时,生成半透明预览精灵跟随鼠标,提升用户体验:
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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.01 18:50:26