2025-11-30 · game_math
【游戏中的数学】游戏中的数学 (2) - 基础工具箱:三角函数
深入浅出地讲解游戏开发中的三角函数:Sin, Cos, Atan2。从单位圆原理到圆周运动、波浪动画和朝向计算的实际应用。
在打开 Unity 的 Mathf 或 Unreal 的
FMath
文档时,你可能会看到几十个函数。其中有一些函数是如此常用,以至于它们构成了游戏逻辑的基石。
本章不讨论复杂的向量或矩阵,只关注最基础的标量
(Scalar) 运算——即对单个浮点数的操作。
可以把这一篇看成 Unity Mathf / Unreal
FMath
里常用标量函数的“中文说明书”和使用场景合集。
引擎语境:本文主要以 Unity / C# 视角讲解,概念同样适用于 Unreal / Godot / 自研引擎。
Lerp (Linear Interpolation) 可能是游戏开发中最著名的函数。
\[ Lerp(a, b, t) = a + (b - a) \times t \] 或者写作: \[ Lerp(a, b, t) = (1 - t) \times a + t \times b \]
其中: - \(a\): 起始值 (当 \(t=0\)) - \(b\): 终点值 (当 \(t=1\)) - \(t\): 进度/百分比 (通常在 0 到 1 之间)
// 简单的平滑跟随
currentPos = Mathf.Lerp(currentPos, targetPos, Time.deltaTime * speed);healthColor = Color.Lerp(Color.red, Color.green, healthPercent);如果你知道起始值 \(a\)、终点值 \(b\) 和当前值 \(v\),你想知道 \(v\) 处于 \(a\) 和 \(b\) 之间的什么位置(即求 \(t\)),这就需要 InverseLerp。
\[ t = \frac{v - a}{b - a} \]
progress = InverseLerp(0, 1000, 500) ->
结果是 0.5。volume = 1.0 - InverseLerp(10, 50, currentDistance)这是 InverseLerp 和 Lerp
的组合拳。将一个数值从一个范围映射到另一个范围。
例如:将输入信号 (-1 到 1) 映射到 屏幕坐标 (0 到 1920)。
\[ t = InverseLerp(inMin, inMax, value) \] \[ result = Lerp(outMin, outMax, t) \]
public static float Remap(float value, float from1, float to1, float from2, float to2) {
return (value - from1) / (to1 - from1) * (to2 - from2) + from2;
}将数值限制在指定的范围内。
Clamp(value, min, max): 限制在 [min, max]
之间。Clamp01(value): 限制在 [0, 1]
之间。这是最常用的,特别是在处理百分比或颜色时。Lerp
是线性的,变化率是恒定的。如果你想要“起步慢,中间快,结束慢”的效果,就需要
SmoothStep。它使用一个 S 形曲线 (Sigmoid-like)
进行插值。
\[ t = Clamp01(t) \] \[ result = t \times t \times (3 - 2 \times t) \]
MoveTowards 和 Lerp
很像,但有一个关键区别:Lerp
的第三个参数是比例,而
MoveTowards 的第三个参数是最大增量
(MaxDelta)。
\[ result = value + sign(target - value) \times min(abs(target - value), maxDelta) \]
匀速移动: 如果你用
Lerp(a, b, Time.deltaTime),物体会随着接近目标而减速(芝诺悖论)。如果你想让物体以恒定速度到达目标,必须用
MoveTowards。
// 每秒移动 5 米,直到到达 target
currentPos = Vector3.MoveTowards(currentPos, target, 5.0f * Time.deltaTime);这两个函数通常用于处理时间或循环动画。
Repeat(t, length) 类似于取模运算
(Modulo),但对浮点数和负数处理得更好。它让数值永远保持在
[0, length] 之间。
\[ result = t - floor(t / length) \times length \]
PingPong(t, length) 让数值在 0
到 length
之间来回摆动。就像乒乓球一样,撞到边界就反弹。
\[ result = length - abs(Repeat(t, length \times 2) - length) \]
巡逻的敌人: 在点 A 和点 B 之间来回走动。
呼吸灯效果: 透明度在 0 到 1 之间反复渐变。
// 产生一个 0 -> 1 -> 0 -> 1 ... 的值
float alpha = Mathf.PingPong(Time.time, 1.0f);| 函数 | 作用 | 典型应用 |
|---|---|---|
| Lerp | 线性插值 (按比例) | 平滑跟随、颜色渐变 |
| InverseLerp | 求比例 | 计算进度条、归一化数据 |
| Remap | 范围转换 | 数值映射、UI适配 |
| Clamp | 限制范围 | 血量限制、角度限制 |
| SmoothStep | S形插值 | 自然动画、地形平滑 |
| MoveTowards | 匀速接近 | 匀速移动、巡逻 |
| Repeat | 循环 | 滚动背景、时间循环 |
| PingPong | 来回摆动 | 呼吸灯、往返巡逻 |
掌握这些基础函数,能让你在写游戏逻辑时少写很多
if-else,代码也会更加优雅。
在实际工程中,标量函数有一些常见的坑需要注意:
// 永远不要用 == 比较两个浮点数
if (Mathf.Abs(a - b) < 0.001f) { /* 近似相等 */ }// 错误:永远到不了目标(每帧移动剩余距离的一部分)
pos = Mathf.Lerp(pos, target, Time.deltaTime * speed);
// 正确做法之一:记录已过时间,用归一化 t
elapsed += Time.deltaTime;
float t = Mathf.Clamp01(elapsed / duration);
pos = Mathf.Lerp(startPos, target, t);第一种写法并不是”错”,它能产生自然的减速效果,但如果你需要在固定时间内到达目标,必须用第二种写法。
SmoothStep 的导数在 \(t=0\) 和 \(t=1\)
处为零,但二阶导数不为零——在某些动画场景下仍能看到”小顿挫”。如果需要更丝滑的过渡,可以用
SmootherStep(Ken Perlin 提出):
\[ t = 6t^5 - 15t^4 + 10t^3 \]
InverseLerp 和 Remap 在
a == b(分母为零)时会产生
NaN。实际项目中请务必加保护:
float SafeInverseLerp(float a, float b, float v) {
if (Mathf.Approximately(a, b)) return 0f;
return (v - a) / (b - a);
}把当前热点继续串成多页阅读,而不是停在单篇消费。
2025-11-30 · game_math
深入浅出地讲解游戏开发中的三角函数:Sin, Cos, Atan2。从单位圆原理到圆周运动、波浪动画和朝向计算的实际应用。
2025-11-30 · game_math
深入理解游戏开发中的矩阵:TRS 变换与代码实践、矩阵乘法与组合变换、MVP 管线详解、逆矩阵、法线变换,以及旋转表示方式对比。
2025-11-30 · game_math
为什么游戏引擎都用四元数来表示旋转?详解欧拉角的万向节死锁问题,以及四元数的定义、运算和 Slerp 插值。
2025-11-30 · game_math
游戏开发数学系列第一篇:向量。介绍向量的定义、加减法、标量乘法、点积与叉积及其在游戏中的实际应用。
在打开 Unity 的 Mathf 或 Unreal 的
FMath
文档时,你可能会看到几十个函数。其中有一些函数是如此常用,以至于它们构成了游戏逻辑的基石。
本章不讨论复杂的向量或矩阵,只关注最基础的标量
(Scalar) 运算——即对单个浮点数的操作。
可以把这一篇看成 Unity Mathf / Unreal
FMath
里常用标量函数的“中文说明书”和使用场景合集。
引擎语境:本文主要以 Unity / C# 视角讲解,概念同样适用于 Unreal / Godot / 自研引擎。
Lerp (Linear Interpolation) 可能是游戏开发中最著名的函数。
\[ Lerp(a, b, t) = a + (b - a) \times t \] 或者写作: \[ Lerp(a, b, t) = (1 - t) \times a + t \times b \]
其中: - \(a\): 起始值 (当 \(t=0\)) - \(b\): 终点值 (当 \(t=1\)) - \(t\): 进度/百分比 (通常在 0 到 1 之间)
// 简单的平滑跟随
currentPos = Mathf.Lerp(currentPos, targetPos, Time.deltaTime * speed);healthColor = Color.Lerp(Color.red, Color.green, healthPercent);如果你知道起始值 \(a\)、终点值 \(b\) 和当前值 \(v\),你想知道 \(v\) 处于 \(a\) 和 \(b\) 之间的什么位置(即求 \(t\)),这就需要 InverseLerp。
\[ t = \frac{v - a}{b - a} \]
progress = InverseLerp(0, 1000, 500) ->
结果是 0.5。volume = 1.0 - InverseLerp(10, 50, currentDistance)这是 InverseLerp 和 Lerp
的组合拳。将一个数值从一个范围映射到另一个范围。
例如:将输入信号 (-1 到 1) 映射到 屏幕坐标 (0 到 1920)。
\[ t = InverseLerp(inMin, inMax, value) \] \[ result = Lerp(outMin, outMax, t) \]
public static float Remap(float value, float from1, float to1, float from2, float to2) {
return (value - from1) / (to1 - from1) * (to2 - from2) + from2;
}将数值限制在指定的范围内。
Clamp(value, min, max): 限制在 [min, max]
之间。Clamp01(value): 限制在 [0, 1]
之间。这是最常用的,特别是在处理百分比或颜色时。Lerp
是线性的,变化率是恒定的。如果你想要“起步慢,中间快,结束慢”的效果,就需要
SmoothStep。它使用一个 S 形曲线 (Sigmoid-like)
进行插值。
\[ t = Clamp01(t) \] \[ result = t \times t \times (3 - 2 \times t) \]
MoveTowards 和 Lerp
很像,但有一个关键区别:Lerp
的第三个参数是比例,而
MoveTowards 的第三个参数是最大增量
(MaxDelta)。
\[ result = value + sign(target - value) \times min(abs(target - value), maxDelta) \]
匀速移动: 如果你用
Lerp(a, b, Time.deltaTime),物体会随着接近目标而减速(芝诺悖论)。如果你想让物体以恒定速度到达目标,必须用
MoveTowards。
// 每秒移动 5 米,直到到达 target
currentPos = Vector3.MoveTowards(currentPos, target, 5.0f * Time.deltaTime);这两个函数通常用于处理时间或循环动画。
Repeat(t, length) 类似于取模运算
(Modulo),但对浮点数和负数处理得更好。它让数值永远保持在
[0, length] 之间。
\[ result = t - floor(t / length) \times length \]
PingPong(t, length) 让数值在 0
到 length
之间来回摆动。就像乒乓球一样,撞到边界就反弹。
\[ result = length - abs(Repeat(t, length \times 2) - length) \]
巡逻的敌人: 在点 A 和点 B 之间来回走动。
呼吸灯效果: 透明度在 0 到 1 之间反复渐变。
// 产生一个 0 -> 1 -> 0 -> 1 ... 的值
float alpha = Mathf.PingPong(Time.time, 1.0f);| 函数 | 作用 | 典型应用 |
|---|---|---|
| Lerp | 线性插值 (按比例) | 平滑跟随、颜色渐变 |
| InverseLerp | 求比例 | 计算进度条、归一化数据 |
| Remap | 范围转换 | 数值映射、UI适配 |
| Clamp | 限制范围 | 血量限制、角度限制 |
| SmoothStep | S形插值 | 自然动画、地形平滑 |
| MoveTowards | 匀速接近 | 匀速移动、巡逻 |
| Repeat | 循环 | 滚动背景、时间循环 |
| PingPong | 来回摆动 | 呼吸灯、往返巡逻 |
掌握这些基础函数,能让你在写游戏逻辑时少写很多
if-else,代码也会更加优雅。
在实际工程中,标量函数有一些常见的坑需要注意:
// 永远不要用 == 比较两个浮点数
if (Mathf.Abs(a - b) < 0.001f) { /* 近似相等 */ }// 错误:永远到不了目标(每帧移动剩余距离的一部分)
pos = Mathf.Lerp(pos, target, Time.deltaTime * speed);
// 正确做法之一:记录已过时间,用归一化 t
elapsed += Time.deltaTime;
float t = Mathf.Clamp01(elapsed / duration);
pos = Mathf.Lerp(startPos, target, t);第一种写法并不是”错”,它能产生自然的减速效果,但如果你需要在固定时间内到达目标,必须用第二种写法。
SmoothStep 的导数在 \(t=0\) 和 \(t=1\)
处为零,但二阶导数不为零——在某些动画场景下仍能看到”小顿挫”。如果需要更丝滑的过渡,可以用
SmootherStep(Ken Perlin 提出):
\[ t = 6t^5 - 15t^4 + 10t^3 \]
InverseLerp 和 Remap 在
a == b(分母为零)时会产生
NaN。实际项目中请务必加保护:
float SafeInverseLerp(float a, float b, float v) {
if (Mathf.Approximately(a, b)) return 0f;
return (v - a) / (b - a);
}把当前热点继续串成多页阅读,而不是停在单篇消费。
2025-11-30 · game_math
深入浅出地讲解游戏开发中的三角函数:Sin, Cos, Atan2。从单位圆原理到圆周运动、波浪动画和朝向计算的实际应用。
2025-11-30 · game_math
深入理解游戏开发中的矩阵:TRS 变换与代码实践、矩阵乘法与组合变换、MVP 管线详解、逆矩阵、法线变换,以及旋转表示方式对比。
2025-11-30 · game_math
为什么游戏引擎都用四元数来表示旋转?详解欧拉角的万向节死锁问题,以及四元数的定义、运算和 Slerp 插值。
2025-11-30 · game_math
游戏开发数学系列第一篇:向量。介绍向量的定义、加减法、标量乘法、点积与叉积及其在游戏中的实际应用。