- 教程
- 文档
- 示例工程
【免费下载链接】website-archive
Archive of the Coding Train website (first version)
本指南源自 The Coding Train「The Nature of Code」系列第 3.3 讲《Angles and Vectors》(关联文档),围绕一个高频问题展开:如何让一个用 p5.js 绘制的物体始终指向它正在移动的方向。读完本文,你将掌握两个核心工具——用
p5.Vector.heading()从速度向量反推出旋转角度,以及用静态方法p5.Vector.fromAngle()从角度直接生成单位向量,并能在引力系统、键盘操控小车、Asteroids 太空船等真实场景中直接落地使用。
一、问题背景:对象朝向与运动方向脱节
在《The Nature of Code》第 3 章「振荡运动」之前,我们处理的位置与速度都只关心「物体在哪、往哪走」,绘制时通常用圆形或矩形表示。一旦改用三角形、箭头、飞船等带方向感的形状,就会立刻遇到一个尴尬的问题:物体明明在向右移动,绘制的形状却始终朝上或朝右,视觉上完全脱节。
解决思路非常直接:把速度向量vel的「方向」换算成一个角度angle,再在绘制时rotate(angle)。这一步换算在 p5.js 中由一个内置方法完成,它正是本节的主角之一:
this.angle = this.vel.heading();heading()会返回向量与 x 轴正方向之间的夹角(弧度制,取值范围-π到π),等价于数学上的atan2(vel.y, vel.x)。关于弧度与角度单位的背景,可回顾同系列 3.1 Angles and Rotation(其中讲解了angleMode(RADIANS)/angleMode(DEGREES)的切换)。
二、数学基础:从方向向量到角度,再从角度回到向量
为什么heading()能工作?这背后是直角三角形与三角函数的关系:给定一个向量(x, y),它与 x 轴之间的夹角θ满足tan(θ) = y / x。但tan在象限判断上存在多义性,因此更稳妥的做法是使用atan2(y, x),它能根据x、y的符号返回正确的象限角。p5.Vector.heading()的底层正是这一计算。
反过来,已知角度θ与模长r,要恢复成向量,则使用极坐标转直角坐标公式:
x = r * cos(θ) y = r * sin(θ)这段三角函数背景在下一讲 3.4 Polar Coordinates 中会得到更充分的展开,那里会用sin()、cos()直接在画布上绘制螺旋与圆形轨迹。而在本节,p5.js 把「角度 → 向量」这一转换封装成了静态方法:
// 返回一个角度为 θ、模长为 1 的单位向量 let v = p5.Vector.fromAngle(theta);这里涉及一个重要的 JavaScript 概念——静态方法:fromAngle()挂在类p5.Vector上,而不是某个具体实例上,调用时直接写p5.Vector.fromAngle(...),与p5.Vector.random2D()、p5.Vector.sub()等一脉相承,详见系列 1.4 Static Functions。
三、示例一:Direction Pointing——在引力场中用 heading() 驱动朝向
仓库中的第一个变体示例完整演示了「向量 → 角度」的用法,源码位于 direction-pointing/mover.js。核心思想是:不手动维护角度,而是每帧从速度向量实时反算。
update() { this.vel.add(this.acc); this.pos.add(this.vel); this.acc.set(0, 0); } show() { stroke(255); strokeWeight(2); fill(255, 100); push(); translate(this.pos.x, this.pos.y); this.angle = this.vel.heading(); // 每帧从速度向量反算朝向 rotate(this.angle); triangle(-this.r, -this.r / 2, -this.r, this.r / 2, this.r, 0); pop(); }要点拆解:
- 三角形默认「指向右」(顶点在
(this.r, 0)),rotate(this.angle)后即指向速度方向; - 代码中注释掉了
angleA/angleV的手动积分方案,说明直接用heading()可以免去对角度、角速度的额外维护,方向完全由物理运动决定; - 整个场景搭配了一个引力吸引子 attractor.js:每个
Mover收到指向Attractor的万有引力(p5.Vector.sub→setMag(strength)→applyForce),速度方向持续改变,而三角形的朝向能实时跟随,画面立刻「活」了起来; - 入口 sketch.js 中生成 10 个质量随机的
Mover,并支持按住鼠标拖动吸引子位置来观察朝向的实时变化。
值得注意:
heading()返回的是速度方向,而不是加速度方向。如果希望形状指向「接下来会往哪走」的瞬时趋势,可以像注释中的this.angleA = this.acc.y / 50.0那样引入角加速度做平滑,但大多数情况下直接跟随速度向量已经足够自然。
四、示例二:Vehicle——用 fromAngle() 把角度变成推力
第二个变体示例回答反向问题:已知转向角度,如何让物体沿该方向运动。核心代码位于 vehicle/mover.js:
update() { // this.vel.add(this.acc); // 传统方式:速度来自加速度累加 this.vel = p5.Vector.fromAngle(this.angle); // 角度 → 单位速度向量 this.pos.add(this.vel); this.acc.set(0, 0); }而角度本身由键盘控制,见 vehicle/sketch.js:
if (keyIsDown(LEFT_ARROW)) { mover.angle -= 0.03; } else if (keyIsDown(RIGHT_ARROW)) { mover.angle += 0.03; }技术要点:
p5.Vector.fromAngle(this.angle)生成的是单位向量(模长为 1),因此每帧位移量为 1 像素/帧;如需控制速度,可对结果继续mult(speed);- 这是「角度 → 向量」的标准姿势,与示例一形成互补闭环:要么从向量求角度(
heading()),要么从角度造向量(fromAngle()); - 两处注释揭示了常见误区:
update()中直接覆盖vel会丢失加速度累积的物理效果(所以applyForce暂时闲置);show()里this.angle = this.vel.heading()与update()里vel = fromAngle(angle)同时存在,二者实际上会相互覆盖,理解这一点对读懂示例的演化过程很有帮助。
五、综合实战:Asteroids 太空船——转向 + 推进的完整闭环
第三个变体示例把两个方向的知识整合成一个经典游戏:Asteroids(小行星),源码位于 asteroids-exercise/spaceship.js。它对应的完整挑战系列是 Coding Challenge #46:Asteroids。
飞船维护一个独立的朝向角this.heading,键盘输入直接改变它:
turn(a) { this.heading += a; // 左右方向键:-0.03 / +0.03 }推进时,把「朝向角」转成「推力向量」——这正是fromAngle()背后极坐标公式的手写版:
thrust() { // 因为飞船是竖直绘制的,先偏移 90° let angle = this.heading - PI / 2; // 极坐标 → 直角坐标:x = cos(θ),y = sin(θ) let force = new createVector(cos(angle), sin(angle)); force.mult(0.1); this.applyForce(force); this.thrusting = true; }同时飞船带有完整的物理仿真:
- 阻尼
damping = 0.995(每帧速度衰减,模拟真空中的惯性滑行); - 最高速度
topspeed = 6(this.velocity.limit(topspeed)); - 边界环绕
wrapEdges()(穿过一侧后从另一侧出现); - 推进时推进器填充红色(
fill(255, 0, 0))作为视觉反馈。
入口 sketch.js 中的按键逻辑:
if (keyIsDown(LEFT_ARROW)) { ship.turn(-0.03); } else if (keyIsDown(RIGHT_ARROW)) { ship.turn(0.03); } else if (keyIsDown(90)) { // 90 是 'z' / 'Z' 的 keyCode ship.thrust(); }这个示例的价值在于:它同时展示了「方向 → 运动」与「运动 → 绘制朝向」两个方向的知识如何在一个对象上共存——heading驱动推力,而绘制时(rotate(this.heading))直接使用同一个角度,无需再从速度反算。
六、进阶应用:heading() 在后续章节中的复用
heading()并不是本节的一次性技巧,而是后续自主代理(Autonomous Agents)章节的标配。例如在 5.2 Seek 示例的 vehicle.js 中,车辆在show()里同样用:
rotate(this.vel.heading()); triangle(-this.r, -this.r / 2, -this.r, this.r / 2, this.r, 0);让寻的(seek)、追踪(pursue)、漫游(wander)等行为的载体始终指向运动方向。类似的用法还出现在 3.2 Angular Motion 的矩形抓取示例(用v.heading()计算鼠标拖拽角度)以及 5.3–5.7 各节中。
因此可以总结出一条通用模式,适用于任何需要「朝向随运动方向」的 p5.js 对象:
- 用
push()/translate(pos)/rotate(...)/pop()包裹绘制代码; - 若速度已知:
rotate(vel.heading()); - 若角度已知:
vel = p5.Vector.fromAngle(angle)生成运动向量,绘制时再rotate(angle); - 涉及重力、推力等外力时,
atan2系的计算(heading())天然兼容所有象限,无需手写象限判断。
七、延伸与练习
原文档还列出了社区贡献作品(Asteroid Game、Vector Thrust、Psychad-Angles 等 p5.js Web Editor 作品),可作为课后灵感来源。若想继续深化,建议按以下顺序实践:
- 修改 Direction Pointing 示例,把三角形换成箭头或飞船造型,观察不同
translate/rotate组合下的效果; - 给 Vehicle 示例增加加速度累积与
applyForce,让fromAngle()生成的速度参与完整物理积分; - 完成 Asteroids 练习:为飞船补充子弹发射、陨石生成与碰撞检测,将其扩展为可玩的游戏;
- 将
heading()与下一讲 3.4 Polar Coordinates 的sin/cos/tan结合起来,尝试用极坐标绘制自定义形状。
参考资料
- 本节关联文档:3.3 Angles and Vectors
- 前置基础:1.4 Static Functions(p5.Vector 静态方法)、3.1 Angles and Rotation(角度与弧度)
- 后继深化:3.4 Polar Coordinates(极坐标与三角函数)
- 配套挑战:Coding Challenge #46:Asteroids
- 示例源码目录:learning/nature-of-code/3.3-angles-and-vectors/(含 direction-pointing、vehicle、asteroids-exercise 三个变体)
- 教程
- 文档
- 示例工程
【免费下载链接】website-archive
Archive of the Coding Train website (first version)
相关推荐
运动学系列(一):基于 Processing 与 p5.js 的正向运动学(Forward Kinematics)触手系统实现
运动学系列(一):基于 Processing 与 p5.js 的正向运动学(Forward Kinematics)触手系统实现 本文是 Coding Train
教程文档示例工程A* 寻路算法(Part 2)实战:在 p5.js 网格中加入障碍物与八方向对角移动 — Coding Train CC_051 源码详解
A 寻路算法(Part 2)实战:在 p5.js 网格中加入障碍物与八方向对角移动 — Coding Train CC_051 源码详解 导读 本文基于 Cod
教程文档示例工程Coding Train 挑战 64.2:在 Processing 与 p5.js 中用逆向运动学实现跟随鼠标的蛇形触手
Coding Train 挑战 64.2:在 Processing 与 p5.js 中用逆向运动学实现跟随鼠标的蛇形触手 本篇以 Coding Train 网站
教程文档示例工程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考