指哪打哪:鼠标按键与光标
鼠标看客上桌,老雷的要求升了级:“点哪儿,阿燕去哪儿。”这比键盘多出一个维度——键盘问“按没按”,鼠标还得问“指着哪”。两件事分开解决。
按没按是老熟人:快照资源 ButtonInput<MouseButton>,三问与键盘一字不差。MouseButton(鼠标按键枚举)一共六种:Left、Right、Middle、侧键 Back/Forward,再加兜底的 Other(u16)——电竞鼠标侧面那排编号键都从这儿报上来。
指着哪要走三步。光标位置挂在 Window 组件上:window.cursor_position() 给出窗口坐标——原点在左上角、y 朝下、单位是逻辑像素,第 12 章讲过的屏幕坐标系。而阿燕活在世界坐标里——原点居中、y 朝上。两套坐标的换算,第 13 章已经备好了工具:viewport_to_world_2d,当时用它反算取景框的四角,现在兑现承诺,反算光标:
/// 左键安旗:把光标的窗口坐标反算成世界坐标,旗插到点的位置
fn point_and_call(
mouse: Res<ButtonInput<MouseButton>>,
window: Single<&Window>,
camera: Single<(&Camera, &GlobalTransform)>,
mut flag: Single<(&mut Transform, &mut Visibility), With<Flag>>,
mut target: ResMut<DashTarget>,
mut was_inside: Local<bool>,
) {
// 光标在不在窗口里?不在就拿不到坐标——这正是 Option 的用处
let Some(cursor) = window.cursor_position() else {
if *was_inside {
*was_inside = false;
println!("场记:看客的手出了台口,光标没影了。");
}
return;
};
*was_inside = true;
if mouse.just_pressed(MouseButton::Left) {
// 窗口坐标(原点左上、y 朝下)→ 世界坐标(原点居中、y 朝上)
let (camera, camera_transform) = *camera;
let Ok(point) = camera.viewport_to_world_2d(camera_transform, cursor) else {
return;
};
let (flag_transform, flag_visibility) = &mut *flag;
flag_transform.translation = point.extend(0.5);
**flag_visibility = Visibility::Visible;
target.0 = Some(point.x.clamp(-STAGE_HALF, STAGE_HALF));
println!("场记:令旗插在 ({:.0}, {:.0})。", point.x, point.y);
}
if mouse.just_pressed(MouseButton::Right) && target.0.take().is_some() {
println!("阿燕:叫停就停。");
}
}Listing 17-4(其一):左键安旗——窗口坐标反算世界坐标(examples/listing-17-04.rs)
/// 朝令旗跑:到站收旗
fn dash(
time: Res<Time>,
mut target: ResMut<DashTarget>,
mut ayan: Single<(&mut Transform, &mut Sprite), With<Ayan>>,
mut flag_visibility: Single<&mut Visibility, (With<Flag>, Without<Ayan>)>,
) {
let Some(goal_x) = target.0 else { return };
let (transform, sprite) = &mut *ayan;
let step = RUN_SPEED * time.delta_secs();
let distance = goal_x - transform.translation.x;
sprite.flip_x = distance < 0.0;
if distance.abs() <= step {
transform.translation.x = goal_x;
target.0 = None;
**flag_visibility = Visibility::Hidden;
println!("阿燕:到位。下一处?");
} else {
transform.translation.x += step * distance.signum();
}
}Listing 17-4(其二):朝旗跑——到站收旗
cargo run -p ch17-input --example listing-17-04老雷:换鼠标看客。左键把令旗插在台上,阿燕跑过去;右键叫停。
场记:令旗插在 (448, 0)。
阿燕:到位。下一处?
场记:令旗插在 (-512, 0)。
阿燕:叫停就停。
场记:看客的手出了台口,光标没影了。
Figure 17-4:点哪儿走哪儿——窗口坐标到世界坐标的一条龙
值得停一停的有三处:
cursor_position()返回Option<Vec2>。光标滑出窗口,它就是None——上面的输出最后一行就是把鼠标甩出窗外的现场。let-else早退是标准姿势;别unwrap,看客的手什么时候出台口你管不着。窗口还有一条CursorMoved消息流报告光标移动,带position与delta,适合“光标划过之处留痕”这类需求;- 反算要相机和它的
GlobalTransform一起上。viewport_to_world_2d的几何意义是“把视口上的一个点投回世界”,所以镜头摆哪、推拉到什么档位都会影响结果(第 13 章的账)。它返回Result,相机这帧还没就绪之类的角况会给Err,照例早退; - 意图与执行分开。点击系统只负责把目标写进
DashTarget资源、把旗插上;真正挪人的是另一个系统dash。这个拆分眼下像是讲究,到 17.7 节它会长成本章的主角。
把 (448, 0) 这行输出和点击位置对一对:1280 宽的窗口,点在约 85% 处——窗口坐标 x ≈ 1088,减去半宽 640,正是世界坐标 448。三步走没有魔法,就是一次坐标平移加 y 翻转。
试一把:把令旗插到台口外——往天上点。旗会忠实地插在半空(它用的是完整的
point),阿燕却只跑到旗的正下方就停(dash 只认point.x,还被clamp拦在台板上)。一次点击,两种用法,看清“反算出的世界点”和“游戏怎么用它”是两层事。
鼠标的“指哪”解决了。但有一类需求恰恰不该用光标位置——转镜头、调准星这种“手上的动作”。光标会撞上屏幕边缘,到了边上就再也读不出变化。下一节给导播配一条不受边框限制的摇臂。