游戏数学基础
概述
游戏开发本质上就是"用数学描述世界,用代码驱动变化"。位置是向量、旋转是矩阵、移动是向量加法、边界检测是碰撞公式。本文挑选游戏开发中最核心的数学知识,以"看得懂、用得着"为原则,配套 JavaScript 代码示例,让数学从公式变成可运行的代码。
向量运算
向量(Vector)是游戏数学的基石,它同时包含方向和大小(模长)。游戏中位置、速度、加速度、朝向都可以用向量表示。
向量的表示与基本运算
// 2D 向量常用对象字面量表示,或用 [x, y] 数组
const v = { x: 3, y: 4 }
// 模长(长度):sqrt(x² + y²)
function length(v) {
return Math.hypot(v.x, v.y)
}
console.log(length({ x: 3, y: 4 })) // 5
// 归一化(单位向量):方向不变,长度为 1
function normalize(v) {
const len = length(v)
if (len === 0) return { x: 0, y: 0 }
return { x: v.x / len, y: v.y / len }
}
// 向量加减:对应分量相加/相减
function add(a, b) { return { x: a.x + b.x, y: a.y + b.y } }
function sub(a, b) { return { x: a.x - b.x, y: a.y - b.y } }
// 数乘:缩放向量
function scale(v, s) { return { x: v.x * s, y: v.y * s } }
// 点积:a·b = |a||b|cosθ,用于判断方向关系
function dot(a, b) { return a.x * b.x + a.y * b.y }
// 叉积(2D 标量形式):a×b = |a||b|sinθ,用于判断旋转方向
function cross(a, b) { return a.x * b.y - a.y * b.x }向量的典型游戏应用
应用 1:朝向某点的单位方向
// 让子弹朝鼠标方向射出
const player = { x: 100, y: 100 }
const mouse = { x: 300, y: 200 }
const direction = normalize(sub(mouse, player))
// direction ≈ { x: 0.894, y: 0.447 }
bullet.velocity = scale(direction, 300) // 速度 300 的方向向量应用 2:点积判断前后
// 敌人是否在玩家正前方(点积 > 0 表示方向一致)
const forward = normalize(player.velocity) // 玩家朝向
const toEnemy = normalize(sub(enemy.pos, player.pos))
const isAhead = dot(forward, toEnemy) > 0应用 3:叉积判断左右
// 敌人是在玩家左边还是右边
const dir = sub(enemy.pos, player.pos)
const side = cross(forward, dir) // > 0 左侧,< 0 右侧矩阵变换
矩阵(Matrix)用于描述变换:平移、旋转、缩放。游戏引擎中的位置、旋转、缩放最终都合并为一个变换矩阵。
2D 变换矩阵
在 2D 中,点 (x, y) 用齐次坐标表示为 (x, y, 1),变换矩阵是 3×3:
| a c tx | a: 水平缩放/错切 tx: 水平平移
| b d ty | d: 垂直缩放/错切 ty: 垂直平移
| 0 0 1 |用矩阵平移一个点
// 平移矩阵:把点 (x, y) 平移 (tx, ty)
// 结果 = (x + tx, y + ty)
function translate(point, tx, ty) {
return { x: point.x + tx, y: point.y + ty }
}用矩阵旋转一个点(绕原点)
// 旋转 θ 弧度(逆时针):
// x' = x*cosθ - y*sinθ
// y' = x*sinθ + y*cosθ
function rotate(point, angle) {
const c = Math.cos(angle)
const s = Math.sin(angle)
return {
x: point.x * c - point.y * s,
y: point.x * s + point.y * c
}
}Canvas 内置变换矩阵
Canvas API 的 transform() 与矩阵一一对应,这在实际开发中最常用:
ctx.save()
ctx.translate(player.x, player.y) // 平移到玩家位置
ctx.rotate(player.angle) // 绕玩家中心旋转
ctx.drawImage(img, -w / 2, -h / 2) // 在原点绘制,图片居中
ctx.restore()矩阵乘法顺序
矩阵不满足交换律。先缩放 → 再旋转 → 后平移 是标准顺序(TRS:Translate × Rotate × Scale)。改变顺序结果完全不同,例如先旋转再平移与先平移再旋转得到的最终位置不同。
碰撞检测算法
碰撞检测是游戏最频繁的计算。根据精度要求,选择不同层级的算法。
1. AABB(轴对齐包围盒)
矩形各边与坐标轴平行,仅比较坐标范围。最快速的碰撞检测,常用于粗略判断。
function aabbCollide(a, b) {
// a, b 为 { x, y, w, h }(x, y 为左上角)
return (
a.x < b.x + b.w &&
a.x + a.w > b.x &&
a.y < b.y + b.h &&
a.y + a.h > b.y
)
}2. 圆与圆
比较两圆心的距离与半径之和:
function circleCollide(a, b) {
const dx = a.x - b.x
const dy = a.y - b.y
const distSq = dx * dx + dy * dy // 平方距离避免开方
const rSum = a.r + b.r
return distSq <= rSum * rSum
}3. 圆与矩形
把矩形按四边外扩圆半径,先判断圆心到矩形的最近点,再比较距离:
function circleRectCollide(circle, rect) {
// 找到矩形上离圆心最近的点
const closestX = clamp(circle.x, rect.x, rect.x + rect.w)
const closestY = clamp(circle.y, rect.y, rect.y + rect.h)
const dx = circle.x - closestX
const dy = circle.y - closestY
return dx * dx + dy * dy <= circle.r * circle.r
}
function clamp(v, min, max) {
return Math.max(min, Math.min(max, v))
}4. 点与矩形 / 点与圆
function pointInRect(px, py, rect) {
return (
px >= rect.x && px <= rect.x + rect.w &&
py >= rect.y && py <= rect.y + rect.h
)
}
function pointInCircle(px, py, circle) {
const dx = px - circle.x
const dy = py - circle.y
return dx * dx + dy * dy <= circle.r * circle.r
}5. 分离轴定理(SAT)
对任意凸多边形(含旋转矩形),只要找到一条轴,两形状在其上的投影不重叠,则两形状不相交:
function satCollide(polyA, polyB) {
// 获取所有可能的分离轴(两多边形的所有法线)
const axes = getAxes(polyA).concat(getAxes(polyB))
for (const axis of axes) {
const pA = project(polyA, axis) // 投影到轴上的 [min, max]
const pB = project(polyB, axis)
if (pA[1] < pB[0] || pB[1] < pA[0]) {
return false // 存在分离轴 → 不相交
}
}
return true // 所有轴都重叠 → 相交
}
function project(poly, axis) {
let min = Infinity, max = -Infinity
for (const p of poly) {
const d = p.x * axis.x + p.y * axis.y
min = Math.min(min, d)
max = Math.max(max, d)
}
return [min, max]
}空间分割
当场景中物体数量较多时,逐个两两检测的复杂度是 O(n²)。空间分割通过把空间划分成区域,只检测同一区域内的物体,把复杂度降到 O(n log n) 甚至更低。
网格(Uniform Grid)
把空间划分成等大小的格子,物体登记到其所在格子,只检测同格及相邻格:
const CELL_SIZE = 64
const grid = new Map() // key: "col,row" → 物体数组
function gridKey(x, y) {
const col = Math.floor(x / CELL_SIZE)
const row = Math.floor(y / CELL_SIZE)
return `${col},${row}`
}
function insert(obj) {
const key = gridKey(obj.x, obj.y)
if (!grid.has(key)) grid.set(key, [])
grid.get(key).push(obj)
}
function getNeighbors(obj) {
const result = []
// 检测以 obj 所在格为中心的 3×3 邻域
const cx = Math.floor(obj.x / CELL_SIZE)
const cy = Math.floor(obj.y / CELL_SIZE)
for (let dx = -1; dx <= 1; dx++) {
for (let dy = -1; dy <= 1; dy++) {
const list = grid.get(`${cx + dx},${cy + dy}`)
if (list) result.push(...list)
}
}
return result
}适用场景:物体分布均匀的游戏(如大量子弹、消除类)。
四叉树(Quadtree)
四叉树将空间递归四等分,每个节点最多容纳 K 个物体,超过则分裂。对分布不均的场景(如大地图大量静态物 + 少数移动物)效果显著。完整实现见 游戏物理引擎原理——四叉树优化。
碰撞检测分层
生产级游戏通常组合多层检测:
Broad Phase(粗检测) Narrow Phase(精检测)
空间分割减少候选对 → 精确算法判定
AABB / 四叉树 → SAT / 三角形细分三角函数与角度
弧度 vs 角度
游戏中统一使用弧度(Radians),Math.sin/cos 的参数都是弧度:
const degToRad = deg => deg * Math.PI / 180
const radToDeg = rad => rad * 180 / Math.PI
// 计算两点间的角度(弧度)
function angleBetween(from, to) {
return Math.atan2(to.y - from.y, to.x - from.x)
}
// 平滑转向:限制角度差在 maxDelta 内
function turnToward(current, target, maxDelta) {
let diff = target - current
// 归一化到 [-π, π],避免转大圈
diff = Math.atan2(Math.sin(diff), Math.cos(diff))
return current + clamp(diff, -maxDelta, maxDelta)
}角度插值
// 在 start 和 end 之间按 t (0~1) 插值,正确处理跨越 ±π 的情况
function lerpAngle(start, end, t) {
let diff = end - start
diff = Math.atan2(Math.sin(diff), Math.cos(diff))
return start + diff * t
}综合示例:瞄准与预测
把向量、角度、点积组合成实际场景——敌人向移动中的玩家射击,子弹带提前量预测(虚线为预测命中点):
本章小结
- 向量:表示位置/速度/方向,加减乘与点积叉积是基础运算
- 矩阵:描述平移/旋转/缩放,Canvas
transform()是实际应用 - 碰撞检测:AABB 最快、圆简单、SAT 精确;粗检测 + 精检测分层使用
- 空间分割:网格适合均匀分布,四叉树适合分布不均
- 角度:统一弧度,
atan2计算方向,lerpAngle平滑转向
下一篇文章将讲解游戏物理引擎原理,看看刚体、碰撞响应和力与约束如何构建起真实感的物理世界。