Skip to content

Transform:摆放、旋转与缩放

正式开机。翻开 Transform 的定义,它就是三个公开字段:

字段类型回答的问题默认值
translationVec3在哪儿Vec3::ZERO(原点)
rotationQuat朝向哪儿Quat::IDENTITY(不旋转)
scaleVec3多大个儿Vec3::ONE(原始尺寸)

三个默认值合在一起叫 Transform::IDENTITY——“原样放在原点”。第一桩订单是太阳:摆在正中央、带一点出厂倾角、整体放大两成,然后让它转起来。

rust
fn setup(mut commands: Commands) {
    commands.spawn(Camera2d);

    commands.spawn((
        Sun,
        Sprite::from_color(Color::srgb(1.0, 0.8, 0.2), Vec2::splat(80.0)),
        // 构造器链:从位置出发,逐项补上旋转和缩放
        Transform::from_xyz(0.0, 0.0, 0.0)
            .with_rotation(Quat::from_rotation_z(0.3))
            .with_scale(Vec3::splat(1.2)),
    ));
}

Listing 12-4(其一):构造器链——位置、旋转、缩放逐项补齐(examples/listing-12-04.rs)

这串链式写法是 Bevy 代码里的高频句式:from_xyz/from_translation/from_rotation/from_scale 任选一个起头,with_rotation/with_scale/with_translation 接着补。字段是公开的,不喜欢链式也可以直接用结构体字面量加 ..default()

Quat::from_rotation_z(0.3) 是本章第一次正面接触 Quat(四元数——表示 3D 旋转的数学对象)。它的内部构造留给下一节,眼下只需要一条用法:2D 的旋转全是“绕 z 轴转”——想象一根垂直于屏幕的轴从方块中心穿过,from_rotation_z(角度) 就是绕这根轴拧。角度的单位是弧度PI 是半圈,TAU(2π)是整圈,0.3 约 17°。方向遵循右手定则:正角逆时针

两种改法:累加与盖写

太阳要自转,还要像呼吸一样脉动。两个系统用了两种不同的修改手法:

rust
/// 自转:每秒 0.6 弧度,十秒半转完一圈——增量式修改
fn spin_sun(time: Res<Time>, mut suns: Query<&mut Transform, With<Sun>>) {
    for mut transform in &mut suns {
        transform.rotate_z(0.6 * time.delta_secs());
    }
}

Listing 12-4(其二):rotate_z——在现有姿态上累加

rust
/// 脉动:缩放在 1.0 ± 0.15 之间随正弦呼吸——赋值式修改
fn pulse_sun(time: Res<Time>, mut suns: Query<&mut Transform, With<Sun>>) {
    for mut transform in &mut suns {
        let breath = 1.0 + 0.15 * (time.elapsed_secs() * 2.0).sin();
        transform.scale = Vec3::splat(breath);
    }
}

Listing 12-4(其三):scale 赋值——按公式直接盖写

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

金色方块缓缓旋转,同时大小随正弦起伏。对照两个系统:

  • rotate_z(增量)累加式——“在现在的基础上再转这么多”。每帧的增量乘了 time.delta_secs()(上一帧到这一帧的秒数),所以无论机器跑 60 帧还是 144 帧,太阳每秒都转 0.6 弧度——这是“速度 × 时间”的老朋友,时间系统的全貌在第 18 章;
  • scale = 公式(t)盖写式——每帧根据“启动以来的总秒数”算出该有的值,直接覆盖。注意它顺手覆盖掉了出生时 with_scale(1.2) 的设定:盖写不在乎先前是什么。

两种手法没有优劣,只有适配:持续累积的运动(自转、巡航)用累加,由时间函数完全决定的状态(脉动、摆动、补间动画)用盖写。混用时小心 Listing 12-4 这种“出生值被悄悄覆盖”的情况。

scaleVec3,三个轴可以不等比:Vec3::new(2.0, 1.0, 1.0) 把方块拉成横放的砖。负数缩放在数学上是镜像翻转,2D 里偶尔用来“让角色面朝左”,但它会连带翻转旋转方向的视觉效果,初学阶段建议别碰。

rotate_around:绕着别人转

太阳能自转了,行星呢?公转不是绕自己转,是绕太阳转。Transform 上恰好有一个为此准备的方法:

rust
fn setup(mut commands: Commands) {
    commands.spawn(Camera2d);

    // 太阳:这回安分待在原点,不转不动
    commands.spawn(Sprite::from_color(Color::srgb(1.0, 0.8, 0.2), Vec2::splat(80.0)));

    // 水星:贴着太阳,跑得快
    commands.spawn((
        Orbit { speed: 1.2 },
        Sprite::from_color(Color::srgb(0.7, 0.65, 0.6), Vec2::splat(20.0)),
        Transform::from_xyz(120.0, 0.0, 0.0),
    ));

    // 地球:远一圈,慢一半
    commands.spawn((
        Orbit { speed: 0.5 },
        Sprite::from_color(Color::srgb(0.3, 0.55, 0.9), Vec2::splat(30.0)),
        Transform::from_xyz(240.0, 0.0, 0.0),
    ));
}

Listing 12-5(其一):水星与地球,各带一个公转角速度(examples/listing-12-05.rs)

rust
/// 公转:绕世界原点(太阳所在处)转
fn orbit(time: Res<Time>, mut planets: Query<(&Orbit, &mut Transform)>) {
    for (orbit, mut transform) in &mut planets {
        let step = Quat::from_rotation_z(orbit.speed * time.delta_secs());
        transform.rotate_around(Vec3::ZERO, step);
    }
}

Listing 12-5(其二):rotate_around——绕指定点转过给定旋转

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

两颗行星各按各的速度绕太阳画圆,里圈的水星明显比外圈的地球勤快。rotate_around(point, step) 每帧做的事:把 translationpoint 转过 step 那么多角度。

盯着看几秒还能发现一个副产品:方块在公转的同时也在缓缓自转,始终拿同一条棱对着太阳——像被潮汐锁定的月球。这不是巧合:翻开源码,rotate_around = translate_around(位置绕点转)+ rotate(自身姿态同步转)。只想要位置画圆、姿态保持端正?换 translate_around 就是。

两幅并排截图:左幅地球公转到 45° 处,方块斜成菱形;右幅公转到 115° 处,方块的倾角也跟到 115°;两幅里朝向太阳的那条棱都正对着太阳,居中的太阳方块始终端正

Figure 12-5:rotate_around 的副产品,两个时刻对照——公转转过多少度,自转同步多少度,同一条棱始终对着太阳

太阳系仪初具雏形:太阳自转,双星绕日。但订单上还有一行没动——“月亮绕着地球转”。月亮的圆心不是固定的原点,而是一颗正在公转的行星……这个难题先记下,第 5 节再收拾它。下一节先补齐旋转的另一半本领:怎么让一个东西朝向该朝的方向。