哪边是上:坐标系约定
老盖拧第一颗螺丝就卡住了。图纸上写着“行星装在太阳右边 240 处”——可窗口里的“右边”是哪边?“240”又是多远?第 2 章给 Transform 写坐标的时候,这些问题被一句“原点在窗口中心”带过了;现在把它彻底说清。
办法跟第 2 章一样:不背结论,让引擎自己交代。往四个位置各放一个方块,看它们落在哪儿。
//! Listing 12-1:罗盘四方块——亲眼确认坐标系往哪边长
//! 原点在窗口正中央,+X 向右,+Y 向上
use bevy::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup)
.run();
}
fn setup(mut commands: Commands) {
commands.spawn(Camera2d);
// 白色小方块:不写 Transform,落在默认位置——世界原点 (0, 0)
commands.spawn(Sprite::from_color(Color::WHITE, Vec2::splat(20.0)));
// 红色方块:x = +300,如果 +X 真朝右,它该出现在原点右边
commands.spawn((
Sprite::from_color(Color::srgb(0.9, 0.3, 0.3), Vec2::splat(40.0)),
Transform::from_xyz(300.0, 0.0, 0.0),
));
// 绿色方块:y = +200,如果 +Y 真朝上,它该出现在原点上方
commands.spawn((
Sprite::from_color(Color::srgb(0.3, 0.8, 0.4), Vec2::splat(40.0)),
Transform::from_xyz(0.0, 200.0, 0.0),
));
// 蓝色方块:两个坐标都为负,应该落在左下角
commands.spawn((
Sprite::from_color(Color::srgb(0.3, 0.5, 0.9), Vec2::splat(40.0)),
Transform::from_xyz(-300.0, -200.0, 0.0),
));
}Listing 12-1:罗盘四方块——用四个坐标试探世界的方向(examples/listing-12-01.rs)
cargo run -p ch12-transforms --example listing-12-01Figure 12-1:罗盘四方块实拍——原点在窗口正中央,+X 朝右,+Y 朝上
窗口打开:白色小方块端坐正中央,红色方块在它右边,绿色方块在它上方,蓝色方块在左下角。四个证词凑成一张罗盘:
- 原点
(0, 0)在窗口正中央——不在左上角,也不在左下角; - +X 朝右;
- +Y 朝上——注意这一条:很多 2D 框架和几乎所有窗口系统都让 y 向下增长(屏幕坐标的习惯),Bevy 的世界坐标用的是数学课上的习惯,y 向上。从别的引擎搬运代码时,这是第一个要检查的地方;
- 单位是逻辑像素——红色方块写的
300.0,在屏幕上就是约 300 个像素的距离。世界坐标的单位本身没有物理含义,是默认相机“一单位对一像素”的取景方式让它恰好等于像素——相机这台取景器的细节在第 13 章。
没写
Transform的白色方块为什么会在原点?第 3 章的 required components 在此接力:Sprite声明了“有我就必须有Transform”,缺的由引擎用Transform::default()补齐——默认值正是原点、不旋转、原始尺寸。
z 去哪儿了
Transform::from_xyz 的第三个参数一直填着 0.0。2D 世界里它不产生任何位移,但绝不是摆设——两个 Sprite 重叠时,z 裁决谁画在前面。
fn setup(mut commands: Commands) {
commands.spawn(Camera2d);
// 暗红大方块坐镇中央,z = 0.0
commands.spawn((
Sprite::from_color(Color::srgb(0.8, 0.25, 0.2), Vec2::splat(160.0)),
Transform::from_xyz(0.0, 0.0, 0.0),
));
// 金色方块往右上偏一点,z = 1.0:压在红色上面
commands.spawn((
Sprite::from_color(Color::srgb(1.0, 0.8, 0.25), Vec2::splat(100.0)),
Transform::from_xyz(60.0, 40.0, 1.0),
));
// 天蓝方块往左下偏一点,z = -1.0:被红色压住
commands.spawn((
Sprite::from_color(Color::srgb(0.35, 0.7, 0.95), Vec2::splat(100.0)),
Transform::from_xyz(-60.0, -40.0, -1.0),
));
}Listing 12-2:z 决定遮挡——金色压住红色,蓝色被红色压住(examples/listing-12-02.rs)
cargo run -p ch12-transforms --example listing-12-02三个方块叠在一起:z 为 1.0 的金色盖在红色上面,z 为 -1.0 的蓝色被红色压住。z 大的在前——可以把 z 想成“离观众的远近”,数值越大离你越近。做 2D 游戏时它就是图层编号:背景给 0、角色给 1、特效给 2,依此类推。
Figure 12-2:z 裁决遮挡——金色(z = 1)压住红色(z = 0),蓝色(z = −1)被压在最底下
再做个小实验:把金色方块的 z 从 1.0 改成 1001.0 运行——它消失了。默认的 2D 相机只看得见 z 在 −1000 到 +1000 之间的东西,出了这个范围就被裁掉。日常用 0 到 999 的图层号绰绰有余;万一某天一个 Sprite 莫名隐形,先查查它的 z 是不是飞出了这个区间。
3D 的约定,顺便记下
本章的太阳系仪是 2D 的,但坐标约定值得现在一并记住,因为 Bevy 的 2D 和 3D 用的是同一套坐标系——2D 只是“贴着 z 轴看”的特例:
- 3D 同样 +X 朝右、+Y 朝上,+Z 指向屏幕外(朝着你);
- 三轴构成右手坐标系:右手食指指 +X、中指弯向 +Y,拇指自然指向 +Z;
- 相机与所有实体的“前方”约定是 −Z 方向——这就是为什么 2D 相机站在 z 为正的高处往“下”(−Z)俯瞰图层,z 大的 Sprite 离相机近、画在前面。
Figure 12-3:同一套右手坐标系的两个视角——正对屏幕时 +Z 朝你飞来;侧看时 2D 相机站在 z 的高处,朝 −Z 俯瞰图层
“前方是 −Z”现在记个结论就行,等第 21 章进 3D 世界、用 looking_at 摆相机时再展开。
罗盘有了:哪边是右、哪边是上、z 管图层。下一节磨第二件工具——表示位置和方向的数学类型 Vec2 与 Vec3。