Anchor:钉子钉在哪
走马灯一搭好,老雷立刻要镜头感:“近景!把阿燕放大。”小棠把 custom_size 调大——人是大了,脚也离地了。再调小,人缩回去,又浮在半空。台板没动、Transform 没动,怎么人就不踩地了?
因为放大缩小是绕着钉子进行的。Transform 的平移只说“钉子钉在世界的哪一点”,而画的哪个点对准钉子,由 Anchor(锚点)说了算——默认是 CENTER,腰眼。腰眼钉死了,个子一长,脚自然往下窜;个子一缩,脚就吊起来。
Anchor 在 0.18 里是个独立组件,也是 Sprite 的 required component(第 3 章的规矩:挂 Sprite 时引擎自动补一个默认值)。它内里只是一个 Vec2,单位是自身宽高的比例:(0, 0) 是中心,(-0.5, -0.5) 是左下角,九个常用位置都有现成常量:
Figure 15-7:Anchor 的坐标语义——钉子(Transform)不动,换锚点等于换“画的哪个点对准钉子”
九种钉法一次看全:
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn(Camera2d);
let still = asset_server.load("actors/ayan-still.png");
let anchors = [
Anchor::TOP_LEFT,
Anchor::TOP_CENTER,
Anchor::TOP_RIGHT,
Anchor::CENTER_LEFT,
Anchor::CENTER,
Anchor::CENTER_RIGHT,
Anchor::BOTTOM_LEFT,
Anchor::BOTTOM_CENTER,
Anchor::BOTTOM_RIGHT,
];
for (i, anchor) in anchors.into_iter().enumerate() {
let pin = Vec3::new(
(i % 3) as f32 * 330.0 - 330.0,
110.0 - (i / 3) as f32 * 220.0,
0.0,
);
// 金十字标出钉子的位置(即这一组的 Transform 平移量)
commands.spawn((
Sprite::from_color(Color::srgb(0.93, 0.74, 0.29), Vec2::new(26.0, 4.0)),
Transform::from_translation(pin.with_z(1.0)),
));
commands.spawn((
Sprite::from_color(Color::srgb(0.93, 0.74, 0.29), Vec2::new(4.0, 26.0)),
Transform::from_translation(pin.with_z(1.0)),
));
// 同一张定位照,钉法各不相同
commands.spawn((
Sprite {
image: still.clone(),
custom_size: Some(Vec2::new(32.0, 40.0) * 3.0),
color: Color::srgba(1.0, 1.0, 1.0, 0.88),
..default()
},
anchor,
Transform::from_translation(pin),
));
}
println!("小棠:九张定位照,九种钉法,钉子全在金十字上——画各自让开。");
}Listing 15-6:锚点九宫——九张定位照钉在九枚金十字上(examples/listing-15-06.rs)
cargo run -p ch15-sprites --example listing-15-06小棠:九张定位照,九种钉法,钉子全在金十字上——画各自让开。
Figure 15-8:同一张画、九种锚点——注意每个十字相对画的位置
读这张图的窍门:金十字永远是 Transform 给的那一点,锚点名说的是画身上的点。TOP_LEFT 的意思是“画的左上角对准钉子”,于是画整体垂到钉子的右下方;BOTTOM_CENTER 是“鞋底中点对准钉子”,画整体立在钉子上方。
脚踩实地
回到老雷的近景问题。正解是给演员换一种钉法——把锚点钉到鞋底:
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn(Camera2d);
let still = asset_server.load("actors/ayan-still.png");
// 台板
commands.spawn((
Sprite::from_color(Color::srgb(0.30, 0.24, 0.18), Vec2::new(1100.0, 10.0)),
Transform::from_xyz(0.0, FLOOR_TOP - 5.0, 0.0),
));
// 左:默认锚点(CENTER)。先按 3 倍身高把腰挪到脚上方 60 像素处,脚正好落线
commands.spawn((
Zoomed,
Sprite {
image: still.clone(),
custom_size: Some(Vec2::new(32.0, 40.0) * 3.0),
..default()
},
Transform::from_xyz(-220.0, FLOOR_TOP + 60.0, 1.0),
));
// 右:锚点改到鞋底(BOTTOM_CENTER),钉子直接钉在台板线上
commands.spawn((
Zoomed,
Sprite {
image: still,
custom_size: Some(Vec2::new(32.0, 40.0) * 3.0),
..default()
},
Anchor::BOTTOM_CENTER,
Transform::from_xyz(220.0, FLOOR_TOP, 1.0),
));
println!("老雷:推近景再拉回来,看哪位的脚不老实。");
}Listing 15-7(节选一):左边默认 CENTER 钉腰,右边 BOTTOM_CENTER 钉鞋底、钉子直接给到台板线(examples/listing-15-07.rs)
/// 近景远景来回推拉:只改 custom_size,不动 Transform
fn push_and_pull(time: Res<Time>, mut query: Query<&mut Sprite, With<Zoomed>>) {
let zoom = 3.0 + 1.4 * (time.elapsed_secs() * 1.2).sin();
for mut sprite in &mut query {
sprite.custom_size = Some(Vec2::new(32.0, 40.0) * zoom);
}
}Listing 15-7(节选二):推拉镜头——只改 custom_size,Transform 一动不动(examples/listing-15-07.rs)
cargo run -p ch15-sprites --example listing-15-07老雷:推近景再拉回来,看哪位的脚不老实。
Figure 15-9:尺寸来回变,钉腰的脚一会儿悬空一会儿入地,钉鞋底的纹丝不动
道理一句话:人物该把“立足点”当原点,而不是几何中心。同理可推开去——吊灯笼用 TOP_CENTER(挂钩在顶上)、贴墙的旗用 CENTER_LEFT(旗杆贴墙)、下一章的伤害飘字也要选锚点决定数字从哪儿冒出来。Anchor 还接受任意比例:Anchor(Vec2::new(0.0, -0.4)) 把原点放在鞋底略偏上,适合脚边带几像素阴影的画稿。
两个容易混的边界划清楚:
- 锚点只挪画面,不挪实体本身。实体的原点永远是
Transform那枚钉子:第 12 章层级里的子实体照旧挂在父实体的钉子上,不会跟着父亲的锚点跑——锚点是纯渲染层面的对齐; - 锚点以可见矩形为准,不管像素内容。我们的画稿鞋底正好踩在图的最后一行像素上,
BOTTOM_CENTER才严丝合缝;如果画稿四周留了透明边,锚点钉的是透明边的边缘——画稿裁不裁边,是美术与程序要对齐的口径。