☰
p5.js 角度与向量实战:用 heading() 与 p5.Vector.fromAngle() 让物体朝向运动方向
2026/10/14 17:30:30 网站建设 项目流程
  • 教程
  • 文档
  • 示例工程

【免费下载链接】website-archive

Archive of the Coding Train website (first version)

项目地址:https://gitcode.com/gh_mirrors/we/website-archive
点击查看免费下载

本指南源自 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 对象:

  1. 用push()/translate(pos)/rotate(...)/pop()包裹绘制代码;
  2. 若速度已知:rotate(vel.heading());
  3. 若角度已知:vel = p5.Vector.fromAngle(angle)生成运动向量,绘制时再rotate(angle);
  4. 涉及重力、推力等外力时,atan2系的计算(heading())天然兼容所有象限,无需手写象限判断。

七、延伸与练习

原文档还列出了社区贡献作品(Asteroid Game、Vector Thrust、Psychad-Angles 等 p5.js Web Editor 作品),可作为课后灵感来源。若想继续深化,建议按以下顺序实践:

  1. 修改 Direction Pointing 示例,把三角形换成箭头或飞船造型,观察不同translate/rotate组合下的效果;
  2. 给 Vehicle 示例增加加速度累积与applyForce,让fromAngle()生成的速度参与完整物理积分;
  3. 完成 Asteroids 练习:为飞船补充子弹发射、陨石生成与碰撞检测,将其扩展为可玩的游戏;
  4. 将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)

项目地址:https://gitcode.com/gh_mirrors/we/website-archive
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询