Transform:摆放、旋转与缩放
正式开机。翻开 Transform 的定义,它就是三个公开字段:
| 字段 | 类型 | 回答的问题 | 默认值 |
|---|---|---|---|
translation | Vec3 | 在哪儿 | Vec3::ZERO(原点) |
rotation | Quat | 朝向哪儿 | Quat::IDENTITY(不旋转) |
scale | Vec3 | 多大个儿 | Vec3::ONE(原始尺寸) |
三个默认值合在一起叫 Transform::IDENTITY——“原样放在原点”。第一桩订单是太阳:摆在正中央、带一点出厂倾角、整体放大两成,然后让它转起来。
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°。方向遵循右手定则:正角逆时针。
两种改法:累加与盖写
太阳要自转,还要像呼吸一样脉动。两个系统用了两种不同的修改手法:
/// 自转:每秒 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——在现有姿态上累加
/// 脉动:缩放在 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 赋值——按公式直接盖写
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 这种“出生值被悄悄覆盖”的情况。
scale是Vec3,三个轴可以不等比:Vec3::new(2.0, 1.0, 1.0)把方块拉成横放的砖。负数缩放在数学上是镜像翻转,2D 里偶尔用来“让角色面朝左”,但它会连带翻转旋转方向的视觉效果,初学阶段建议别碰。
rotate_around:绕着别人转
太阳能自转了,行星呢?公转不是绕自己转,是绕太阳转。Transform 上恰好有一个为此准备的方法:
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)
/// 公转:绕世界原点(太阳所在处)转
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——绕指定点转过给定旋转
cargo run -p ch12-transforms --example listing-12-05两颗行星各按各的速度绕太阳画圆,里圈的水星明显比外圈的地球勤快。rotate_around(point, step) 每帧做的事:把 translation 绕 point 转过 step 那么多角度。
盯着看几秒还能发现一个副产品:方块在公转的同时也在缓缓自转,始终拿同一条棱对着太阳——像被潮汐锁定的月球。这不是巧合:翻开源码,rotate_around = translate_around(位置绕点转)+ rotate(自身姿态同步转)。只想要位置画圆、姿态保持端正?换 translate_around 就是。

Figure 12-5:rotate_around 的副产品,两个时刻对照——公转转过多少度,自转同步多少度,同一条棱始终对着太阳
太阳系仪初具雏形:太阳自转,双星绕日。但订单上还有一行没动——“月亮绕着地球转”。月亮的圆心不是固定的原点,而是一颗正在公转的行星……这个难题先记下,第 5 节再收拾它。下一节先补齐旋转的另一半本领:怎么让一个东西朝向该朝的方向。