Vec:既是位置,也是箭头
动工之前,老盖得在图纸上把数算清:彗星离太阳多远?朝哪个方向飞?这些演算用的数学类型就是 Transform::translation 的类型本身——Vec3(三维向量,x、y、z 三个 f32),以及它的平面兄弟 Vec2。它们是普通的 Copy 值,不是组件、不需要 App,在 main 里就能把脾气摸清:
// 老盖摊开图纸:三个天体的位置,各用一个 Vec2 表示
let sun = Vec2::new(0.0, 0.0); // 太阳钉在图纸原点
let earth = Vec2::new(180.0, 0.0); // 地球在太阳正右方 180
let comet = Vec2::new(-120.0, 160.0); // 彗星在左上方游荡Listing 12-3(其一):三个天体的位置,各用一个 Vec2(examples/listing-12-03.rs)
同一个 Vec2 有两种解读。写成 comet 时它是位置——图纸上的一个点;而两个位置相减,得到的是箭头——从一点指向另一点的位移,有方向、有长度:
// 位置相减得到“从 B 指向 A 的箭头”,箭头的长度就是距离
let sun_to_comet = comet - sun;
println!("老盖量彗星:离太阳 {},离地球 {}", sun_to_comet.length(), comet.distance(earth));Listing 12-3(其二):位置相减得箭头,箭头的长度是距离
length() 给出箭头的长度,distance() 是“相减再取长度”的快捷写法。游戏里的“索敌半径”“拾取范围”,本质都是这一行。
性能提示:比较距离用
length_squared()(长度的平方)更省——开平方是这几个操作里唯一贵的一步,而“谁更近”用平方比较结果一样。
归一化:把箭头压成纯方向
箭头携带两份信息:方向和长度。很多场合只要方向——“朝那边走,速度我自己定”。normalize() 把箭头压成长度为 1 的单位向量,只留方向:
// 归一化:把箭头压成长度 1,只留方向
let comet_bearing = sun_to_comet.normalize();
println!("彗星的方位(单位向量):{comet_bearing:.1},长度 {:.2}", comet_bearing.length());Listing 12-3(其三):normalize 只留方向
拿到单位方向后乘上标量就是“沿这个方向走多远”:bearing * 120.0 是沿方向 120 单位的位移。第 2 章方块的运动公式、日后所有“朝目标移动”的代码,骨架都是 位置 += 方向 * 速度 * dt。
小心一个坑:零向量没有方向,Vec2::ZERO.normalize() 给出的是 NaN(非数),带着它继续算会污染一切。拿不准输入是否为零时用 normalize_or_zero(),或者用下一节的 Dir2 类型把检查交给类型系统。
点积:方向之间的问答
两个单位向量的点积(dot)是一个标量,直接回答“它们有多同向”:同向得 1,垂直得 0,反向得 −1。观测站靠它判断彗星的来意:
// 点积:两个单位向量同向得 1,垂直得 0,反向得 -1
let to_sun = (sun - comet).normalize();
let dive = Vec2::new(0.6, -0.8); // 第一晚测得的彗星速度方向
let graze = Vec2::new(0.8, 0.6); // 第二晚测得的速度方向
println!("第一晚:速度·朝日方向 = {}(全速冲着太阳来!)", dive.dot(to_sun));
println!("第二晚:速度·朝日方向 = {}(虚惊一场,正横着掠过)", graze.dot(to_sun));Listing 12-3(其四):点积判断“冲着来还是横着过”
“敌人在我身前还是身后”“子弹算不算迎面命中”,都是一次点积的事。
lerp 与 2D/3D 互转
最后两件随手用的小工具——按比例取中间点的 lerp(线性插值),和 Vec2 与 Vec3 的互转:
// lerp:在两点之间按比例取点,0.5 就是正中
println!("日地连线的中点:{}", sun.lerp(earth, 0.5));
// 2D 图纸钉进 3D 世界:补一个 z 就是 Vec3,截掉 z 又回到 Vec2
let comet_3d: Vec3 = comet.extend(5.0);
println!("彗星入册:{comet_3d},截回平面:{}", comet_3d.truncate());Listing 12-3(其五):lerp 取中点;extend 补 z、truncate 截 z
extend(z) 给平面坐标补上第三维,truncate() 把它截回去。这对组合在 2D 游戏里出场率极高:游戏逻辑在平面上用 Vec2 思考,而 translation 是 Vec3——上一节刚讲过,那个 z 是图层号,不该被平面运算搅动。“xy 归逻辑,z 归图层”的纪律,靠它们维持。
cargo run -p ch12-transforms --example listing-12-03老盖量彗星:离太阳 200,离地球 340
彗星的方位(单位向量):[-0.6, 0.8],长度 1.00
第一晚:速度·朝日方向 = 1(全速冲着太阳来!)
第二晚:速度·朝日方向 = 0(虚惊一场,正横着掠过)
日地连线的中点:[90, 0]
彗星入册:[-120, 160, 5],截回平面:[-120, 160]这页演算摊在图纸上就是 Figure 12-4:三个点是位置,太阳到彗星的实线是相减得到的箭头,虚线只报距离,lerp 的空心圈停在日地正中。
Figure 12-4:图纸演算的几何形状——位置是点,相减是箭头,归一化只留方向
图纸演算完毕。Vec 家族还有不少成员——整数版的 IVec2/UVec2、四维的 Vec4、双精度的 DVec3——API 形状全一样,用到再查。下一节正式开机:用 Transform 把太阳装上转轴。