Skip to content

哪边是上:坐标系约定

老盖拧第一颗螺丝就卡住了。图纸上写着“行星装在太阳右边 240 处”——可窗口里的“右边”是哪边?“240”又是多远?第 2 章给 Transform 写坐标的时候,这些问题被一句“原点在窗口中心”带过了;现在把它彻底说清。

办法跟第 2 章一样:不背结论,让引擎自己交代。往四个位置各放一个方块,看它们落在哪儿。

rust
//! 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)

console
cargo run -p ch12-transforms --example listing-12-01

深色窗口里四个方块:白色小方块在正中央,红色方块在右侧,绿色方块在上方,蓝色方块在左下角

Figure 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 裁决谁画在前面

rust
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)

console
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 离相机近、画在前面。

示意图:左幅正对屏幕,原点处 +X 朝右、+Y 朝上、+Z 用圆点记号表示指向屏幕外;右幅把 z 轴横放侧看,三块图层立在 z = 0、1、2 处,2D 相机站在 z 轴高处、镜头沿 −Z 指向图层,z 大的离相机近

Figure 12-3:同一套右手坐标系的两个视角——正对屏幕时 +Z 朝你飞来;侧看时 2D 相机站在 z 的高处,朝 −Z 俯瞰图层

“前方是 −Z”现在记个结论就行,等第 21 章进 3D 世界、用 looking_at 摆相机时再展开。

罗盘有了:哪边是右、哪边是上、z 管图层。下一节磨第二件工具——表示位置和方向的数学类型 Vec2Vec3