基于 CocosCreator v3.8.7 炮台旋转研究延伸,系统梳理方向向量的七大核心用途。
一、什么是方向向量
方向向量就是从A点指向B点的箭头,表示”从A出发,水平方向走多少、垂直方向走多少能到达B”。
1 2
| dx = B.x - A.x dy = B.y - A.y
|
示例
假设炮台在 (2, 3),敌人在 (5, 7):
1 2 3 4 5 6 7
| 7 · · · · · 敌人(5,7) 6 · · · · · ↑ 5 · · · · · | dy = 4 4 · · · · · ↑ 3 · 炮台(2,3)----→ 1 2 3 4 5 dx = 3
|
1 2 3
| dx = 5 - 2 = 3 (向右走 3 格) dy = 7 - 3 = 4 (向上走 4 格) 方向向量 = (3, 4)
|
方向向量是游戏开发中最基础的数学工具,下面所有操作都从它出发。
二、七大核心应用
2.1 求角度(旋转/瞄准)
原理
atan2(dy, dx) 返回方向向量与X轴正方向的夹角(弧度),范围 (-π, π],即 (-180°, 180°]。
它和普通 atan(y/x) 的区别是:atan2 能根据 dx 和 dy 的正负号判断象限,返回完整的 360 度范围角度,不会出现象限歧义。
计算过程
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 炮台在 (100, 100),敌人在 (200, 300)
第一步:求方向向量 dx = 200 - 100 = 100 dy = 300 - 100 = 200
第二步:求弧度 radian = atan2(200, 100) = 1.107 弧度
第三步:弧度转角度 angle = 1.107 × 180 / π ≈ 63.43°
第四步:赋值 tower.angle = 63.43° 炮台就对准敌人了
|
弧度与角度的换算公式:
1 2 3 4
| angle = radian × 180 / π radian = angle × π / 180
1 rad = 180° / π ≈ 57.2958°
|
四象限情况
1 2 3 4 5 6 7 8 9
| 90° | | 敌人在右上方 → dx>0, dy>0 → 角度 0°~90° | 180° ----炮台---- 0° | | 敌人在右下方 → dx>0, dy<0 → 角度 0°~-90° | -90°
|
| 敌人位置 |
dx |
dy |
atan2 结果 |
| 右上 (3, 4) |
+3 |
+4 |
53° |
| 左上 (-3, 4) |
-3 |
+4 |
127° |
| 左下 (-3, -4) |
-3 |
-4 |
-127° |
| 右下 (3, -4) |
+3 |
-4 |
-53° |
代码示例
1 2 3 4 5 6 7
| update(deltaTime: number) { const dx = this.enemy.position.x - this.tower.position.x const dy = this.enemy.position.y - this.tower.position.y const radian = Math.atan2(dy, dx) const angle = (radian * 180) / Math.PI this.tower.angle = angle }
|
应用场景
- 炮台瞄准敌人
- 角色面朝鼠标方向
- NPC 看向玩家
- 箭头指向目标点
2.2 求距离(射程检测)
原理
勾股定理。方向向量的 dx 和 dy 构成直角三角形的两条直角边,斜边就是两点之间的直线距离。
1 2 3 4 5 6 7 8 9
| 敌人 /| / | 距离 / | dy / | / ____| 炮台 dx
distance = √(dx² + dy²)
|
计算过程
1 2 3 4 5 6 7 8 9 10 11
| 炮台在 (100, 100),敌人在 (400, 500)
第一步:求方向向量 dx = 400 - 100 = 300 dy = 500 - 100 = 400
第二步:求距离 distance = √(300² + 400²) = √(90000 + 160000) = √250000 = 500
|
代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| update(dt: number) { const dx = enemy.position.x - tower.position.x const dy = enemy.position.y - tower.position.y const distance = Math.sqrt(dx * dx + dy * dy)
if (distance < 300) { this.attack(enemy) } else if (distance < 500) { this.lookAt(enemy) } else { this.idle() } }
|
优化技巧:距离平方比较
Math.sqrt 开方运算比较慢,如果只是比较远近,可以跳过开方:
1 2 3 4 5 6
| const distSq = dx * dx + dy * dy
if (distSq < 300 * 300) { this.attack(enemy) }
|
因为如果 a < b,那么 a² < b²(都是正数时),结果一样但省了一次开方。当场上有几百个敌人需要逐帧判断距离时,这个优化很有意义。
应用场景
- 攻击范围检测
- 拾取道具的距离判断
- 技能释放范围
- 仇恨系统(优先攻击最近的敌人)
- 碰撞检测(两个圆形碰撞体距离 < 半径之和)
2.3 归一化(求单位向量)
原理
归一化就是保留方向、把长度缩放为 1。
方向向量 (dx, dy) 同时包含两个信息:方向和距离。有时候我们只需要方向不需要距离,就要归一化。
1 2
| 原始向量: (300, 400) 长度 = 500 归一化后: (0.6, 0.8) 长度 = 1
|
计算过程
1 2 3 4 5 6 7 8 9 10
| 方向向量 (dx, dy) = (300, 400)
第一步:求长度 length = √(300² + 400²) = 500
第二步:每个分量除以长度 nx = 300 / 500 = 0.6 ny = 400 / 500 = 0.8
归一化结果:(0.6, 0.8)
|
验证长度确实是 1:
1
| √(0.6² + 0.8²) = √(0.36 + 0.64) = √1 = 1 ✓
|
为什么需要归一化
没有归一化的问题:
1 2 3 4 5 6 7 8 9
| 假设子弹速度 speed = 5
敌人A距离100:方向向量 (60, 80) bullet.x += 60 * 5 = 300 每帧飞300!太快了
敌人B距离10:方向向量 (6, 8) bullet.x += 6 * 5 = 30 每帧飞30,正常
同样的 speed,敌人越远子弹越快,这不合理
|
归一化后:
1 2 3 4 5 6 7 8 9
| 不管敌人多远,归一化后长度都是 1
敌人A归一化:(0.6, 0.8) bullet.x += 0.6 * 5 = 3
敌人B归一化:(0.6, 0.8) 方向相同的话结果一样 bullet.x += 0.6 * 5 = 3
速度一致了!
|
代码示例
1 2 3 4 5 6 7 8 9 10
| const dx = enemy.position.x - bullet.position.x const dy = enemy.position.y - bullet.position.y const length = Math.sqrt(dx * dx + dy * dy)
const nx = dx / length const ny = dy / length
bullet.position.x += nx * bulletSpeed * dt bullet.position.y += ny * bulletSpeed * dt
|
应用场景
- 子弹匀速飞行
- 角色匀速移动到目标点
- 力的方向(施加一个固定大小的力)
- 法线方向(用于光照计算、反弹计算)
2.4 移动/追踪
原理
让一个物体朝另一个物体移动,每帧沿着方向向量走一小步。
1 2 3 4
| 第1帧: 敌人·············→玩家 第2帧: 敌人··········→玩家 第3帧: 敌人······→玩家 第4帧: 敌人··→玩家
|
计算过程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 敌人在 (100, 100),玩家在 (400, 500),敌人速度 speed = 3
第一步:求方向向量 dx = 400 - 100 = 300 dy = 500 - 100 = 400
第二步:归一化 length = √(300² + 400²) = 500 nx = 300 / 500 = 0.6 ny = 400 / 500 = 0.8
第三步:按速度移动 enemy.x += 0.6 * 3 = 1.8 enemy.y += 0.8 * 3 = 2.4
敌人新位置:(101.8, 102.4)
下一帧重新算方向,继续追…
|
代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| update(dt: number) { const dx = player.position.x - enemy.position.x const dy = player.position.y - enemy.position.y const length = Math.sqrt(dx * dx + dy * dy)
if (length < 5) return
const nx = dx / length const ny = dy / length
const speed = 200 enemy.position.x += nx * speed * dt enemy.position.y += ny * speed * dt }
|
直线追踪 vs 预判追踪
上面是最简单的直线追踪,敌人永远朝玩家当前位置跑。如果玩家在动,敌人会一直拐弯跟着。
更高级的做法是预判追踪:预测玩家会到哪,直接朝预测点飞,导弹就是这个原理。
1 2 3 4 5 6 7 8 9 10
| 直线追踪:弯弯曲曲跟着跑 ~~~→ / / 玩家→→→→→→→ 敌人
预判追踪:直接截击 敌人--------→ 交汇点 ↑ 玩家→→→→→→→→↗
|
应用场景
- 怪物追玩家
- 导弹追踪目标
- 宠物跟随主人
- 自动寻路中的每一步
- 摄像机平滑跟随角色
2.5 击退/反弹
原理
方向向量取反 (-dx, -dy) 就是反方向。击退就是把目标沿着反方向推出去。
1 2
| 攻击方向: 敌人 ----→ 玩家 击退方向: 玩家 ----→ (被推走)
|
计算过程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 敌人在 (100, 100),玩家在 (400, 100),击退力 = 50
第一步:求从敌人到玩家的方向 dx = 400 - 100 = 300 dy = 100 - 100 = 0 方向向量:(300, 0)
第二步:归一化 length = 300 nx = 300 / 300 = 1 ny = 0 / 300 = 0 单位向量:(1, 0) → 水平向右
第三步:沿这个方向击退玩家 player.x += 1 * 50 = 50 player.y += 0 * 50 = 0
玩家从 (400, 100) 被推到 (450, 100)
|
带衰减的击退效果
实际游戏中击退不会一瞬间完成,而是快速推出然后逐渐减速:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| onHit(attacker: Node) { const dx = this.node.position.x - attacker.position.x const dy = this.node.position.y - attacker.position.y const length = Math.sqrt(dx * dx + dy * dy)
this.knockbackVx = (dx / length) * 500 this.knockbackVy = (dy / length) * 500 }
update(dt: number) { this.node.position.x += this.knockbackVx * dt this.node.position.y += this.knockbackVy * dt
this.knockbackVx *= 0.95 this.knockbackVy *= 0.95 }
|
效果就是被打一下往后滑一段距离然后停下来。
反弹(弹球/弹幕)
球碰到墙壁时,反转撞墙方向的分量即可:
1 2 3 4 5 6 7
| 撞到左右墙壁 → vx = -vx (水平反转) 撞到上下墙壁 → vy = -vy (垂直反转)
| ↗ | ↗ / | / 球 ----→墙 → vx取反 → 球往回飞
|
应用场景
- 被攻击时的击退效果
- 弹球反弹
- 爆炸冲击波(从爆炸中心向外推)
- 弹幕反弹
- 碰撞后的弹开效果
2.6 判断前后关系(点积)
原理
点积(Dot Product) 公式:
把两个向量的对应分量相乘再相加。结果的含义:
1 2 3
| dot > 0 → 两个向量方向大致相同(夹角 < 90°)→ 在前方 dot = 0 → 两个向量互相垂直(夹角 = 90°) → 在正侧面 dot < 0 → 两个向量方向大致相反(夹角 > 90°)→ 在后方
|
直观理解
1 2 3 4 5 6 7 8 9
| 想象你站着面朝右方(朝向向量 = (1, 0))
B(在前方) ↗ 你→→→→→→ 朝向 ↘ C(在前方)
A(在后方)
|
你的朝向是 (1, 0),某个目标相对于你的方向是 (dx, dy):
1 2 3 4
| dot = 1 * dx + 0 * dy = dx
如果 dx > 0 → 目标在你前面(和你朝向同一边) 如果 dx < 0 → 目标在你后面(和你朝向相反)
|
详细例子
1 2 3 4 5 6 7 8 9 10 11 12 13
| 玩家在 (200, 200),朝向右上方,朝向向量 face = (0.7, 0.7)
敌人A在 (300, 300) → 方向 = (100, 100) → 归一化 (0.7, 0.7) dot = 0.7*0.7 + 0.7*0.7 = 0.49 + 0.49 = 0.98 结果 > 0 → 在前方 ✓(正前方)
敌人B在 (100, 100) → 方向 = (-100, -100) → 归一化 (-0.7, -0.7) dot = 0.7*(-0.7) + 0.7*(-0.7) = -0.49 + (-0.49) = -0.98 结果 < 0 → 在后方 ✓(正后方)
敌人C在 (300, 100) → 方向 = (100, -100) → 归一化 (0.7, -0.7) dot = 0.7*0.7 + 0.7*(-0.7) = 0.49 + (-0.49) = 0 结果 = 0 → 在正侧面 ✓
|
视野锥检测
点积还可以判断目标是否在一定角度的视野范围内:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| const FOV_COS = Math.cos(60 * Math.PI / 180)
update() { const dx = enemy.x - player.x const dy = enemy.y - player.y const len = Math.sqrt(dx * dx + dy * dy)
const dot = faceDx * (dx / len) + faceDy * (dy / len)
if (dot > FOV_COS) { this.canSeeEnemy = true } else { this.canSeeEnemy = false } }
|
1 2 3 4 5 6 7 8
| 可见区域 / 60° \ / | \ / | \ / ____|____ \ 玩家朝向方向
dot > 0.5 表示夹角 < 60°,在扇形内
|
应用场景
- 视野/视锥检测(敌人能否看到玩家)
- 背刺判定(攻击者在目标背后才触发)
- 扇形技能范围
- AI 感知系统
- 判断是迎面碰撞还是追尾碰撞
2.7 判断左右关系(叉积)
原理
二维叉积(Cross Product) 公式:
1
| cross = ax * by - ay * bx
|
结果的含义:
1 2 3
| cross > 0 → b 在 a 的左边(逆时针方向) cross = 0 → b 和 a 共线(平行) cross < 0 → b 在 a 的右边(顺时针方向)
|
直观理解
1 2 3 4 5 6 7 8 9
| 想象你面朝右方(朝向 = (1, 0))
左边 (cross > 0) B ↑ 你→→→→→→ 朝向 ↓ C 右边 (cross < 0)
|
详细例子
1 2 3 4 5 6 7 8 9 10 11 12 13
| 玩家朝向 face = (1, 0)(面朝右)
敌人A在上方:方向 = (1, 2) cross = 1*2 - 0*1 = 2 结果 > 0 → 在左边 ✓(面朝右时,上方就是左边)
敌人B在下方:方向 = (1, -2) cross = 1*(-2) - 0*1 = -2 结果 < 0 → 在右边 ✓(面朝右时,下方就是右边)
敌人C在正前方:方向 = (3, 0) cross = 1*0 - 0*3 = 0 结果 = 0 → 共线 ✓(正前方,不偏左也不偏右)
|
实际应用:最短转向决策
炮台要追踪敌人,应该顺时针转还是逆时针转?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| update(dt: number) { const rad = tower.angle * Math.PI / 180 const faceDx = Math.cos(rad) const faceDy = Math.sin(rad)
const dx = enemy.x - tower.x const dy = enemy.y - tower.y
const cross = faceDx * dy - faceDy * dx const rotateSpeed = 90
if (cross > 0) { tower.angle += rotateSpeed * dt } else if (cross < 0) { tower.angle -= rotateSpeed * dt } }
|
1 2 3 4 5 6 7 8 9
| 情况1:敌人在左边 情况2:敌人在右边
敌人 ↑ ↓ | 逆时针转 顺时针转 | | | 炮台→→朝向 炮台→→朝向 | 敌人
|
这比直接用 atan2 赋值的好处是:炮台有转速限制,更真实。不会一下子对准,而是慢慢转过去,叉积告诉它每帧该往哪边转。
点积 + 叉积组合使用
单独用一个只能得到部分信息,组合起来可以精确判断全方位:
1 2 3 4 5 6 7 8 9 10 11
| 前左方 前右方 dot>0, cross>0 dot>0, cross<0 \ / \ 前方 / \ dot>0 / 正左方 -----+---→---+------ 正右方 cross>0 \ / cross<0 \ 后方/ dot<0 后左方 后右方 dot<0, cross>0 dot<0, cross<0
|
1 2 3 4
| if (dot > 0 && cross > 0) if (dot > 0 && cross < 0) if (dot < 0 && cross > 0) if (dot < 0 && cross < 0)
|
应用场景
- 转向决策(左转还是右转)
- 环绕 AI(绕着玩家转圈的敌人)
- 判断碰撞发生在左侧还是右侧
- 路径规划中的拐弯方向
- 格斗游戏中判断攻击从哪一侧来
三、炮台旋转中的角度环绕问题
问题描述
直接用 atan2 赋值角度没问题,但加上插值(lerp)做平滑旋转后,在角度越过临界值(180° / -180°)时,插值会走”远路”(反向旋转一大圈)。
1 2 3 4
| 当前角度 170°,目标角度 -170°
错误的插值路径:170° → 0° → -170° (转了 340°) 正确的插值路径:170° → 180° → -170° (只转了 20°)
|
解决方案
计算角度差,如果差值超过 180 度就 ±360 度修正,确保始终走最短路径:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| update(deltaTime: number) { const dx = this.enemy.position.x - this.tower.position.x const dy = this.enemy.position.y - this.tower.position.y const radian = Math.atan2(dy, dx) let angle = (radian * 180) / Math.PI
let currentAngle = this.tower.angle let angleDiff = angle - currentAngle
if (angleDiff > 180) { angle -= 360 } else if (angleDiff < -180) { angle += 360 }
const interpolatedAngle = lerp(this.tower.angle, angle, deltaTime * 2) this.tower.angle = interpolatedAngle }
|
四、三维旋转方案:四元数
为什么不能在三维中用欧拉角插值
在三维空间中,如果分别对 X、Y、Z 三个角度单独计算差值再插值,旋转路径会变得复杂跳脱,不够平滑。还会出现万向节锁问题:当中间轴旋转 90 度时,另外两个轴会对齐,丧失一个自由度。
四元数方案
四元数(Quaternion)是一种用于表示三维旋转的高效数学工具,能避免万向节锁,插值更加平滑。
CocosCreator 中的 Rotation 就是四元数,使用球面线性插值(SLERP)实现平滑旋转:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| update(deltaTime: number) { const dV3 = this.enemy.getPosition().subtract(this.tower.getPosition()) const normalizedDir = dV3.normalize() const targetQuat = quat() Quat.rotationTo(targetQuat, v3(1, 0, 0), normalizedDir) const currentQuat = this.tower.getRotation() const slerpedQuat = quat() Quat.slerp(slerpedQuat, currentQuat, targetQuat, deltaTime * 2) this.tower.setRotation(slerpedQuat) }
|
五、总结对比
| 工具 |
公式 |
得到什么 |
典型用途 |
| 方向向量 |
(dx, dy) |
怎么走 |
一切的基础 |
| atan2 |
atan2(dy, dx) |
几度角 |
旋转瞄准 |
| 勾股定理 |
√(dx² + dy²) |
多远 |
射程检测 |
| 归一化 |
(dx/len, dy/len) |
纯方向 |
匀速移动 |
| 取反 |
(-dx, -dy) |
反方向 |
击退反弹 |
| 点积 |
ax*bx + ay*by |
前后关系 |
视野检测 |
| 叉积 |
ax*by - ay*bx |
左右关系 |
转向决策 |
1 2 3 4 5 6
| 方向向量 (dx, dy) / | \ \ atan2 距离 归一化 点积/叉积 | | | | 旋转 射程 匀速移动 前后左右 瞄准 检测 追踪击退 视野判定
|
这七个操作全都从最基础的方向向量 (dx, dy) 出发,掌握了它们基本就能处理游戏开发中绝大多数与”方向、位置、运动”相关的问题。