引言

在打开 Unity 的 Mathf 或 Unreal 的 FMath 文档时,你可能会看到几十个函数。其中有一些函数是如此常用,以至于它们构成了游戏逻辑的基石。 本章不讨论复杂的向量或矩阵,只关注最基础的标量 (Scalar) 运算——即对单个浮点数的操作。 可以把这一篇看成 Unity Mathf / Unreal FMath 里常用标量函数的“中文说明书”和使用场景合集。

引擎语境:本文主要以 Unity / C# 视角讲解,概念同样适用于 Unreal / Godot / 自研引擎。

1. 线性插值 (Lerp)

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 之间)

Lerp Visual

游戏中的应用

  1. 平滑移动: 每帧将物体的位置向目标位置移动一点点。
// 简单的平滑跟随
currentPos = Mathf.Lerp(currentPos, targetPos, Time.deltaTime * speed);
  1. 颜色渐变: 随着血量降低,血条颜色从绿色变为红色。
healthColor = Color.Lerp(Color.red, Color.green, healthPercent);
  1. 动画混合: 在两个动画状态之间过渡。

2. 反向插值 (InverseLerp)

如果你知道起始值 \(a\)、终点值 \(b\) 和当前值 \(v\),你想知道 \(v\) 处于 \(a\)\(b\) 之间的什么位置(即求 \(t\)),这就需要 InverseLerp

公式

\[ t = \frac{v - a}{b - a} \]

游戏中的应用

  1. 计算进度条: 玩家经验值是 500,当前等级是从 0 到 1000 经验。 progress = InverseLerp(0, 1000, 500) -> 结果是 0.5。
  2. 根据距离计算音量: 声音在 10米处最大,50米处消失。 volume = 1.0 - InverseLerp(10, 50, currentDistance)

3. 范围映射 (Remap)

这是 InverseLerpLerp 的组合拳。将一个数值从一个范围映射到另一个范围。 例如:将输入信号 (-1 到 1) 映射到 屏幕坐标 (0 到 1920)。

公式

\[ t = InverseLerp(inMin, inMax, value) \] \[ result = Lerp(outMin, outMax, t) \]

Remap Visual

代码实现 (Unity Extension)

public static float Remap(float value, float from1, float to1, float from2, float to2) {
  return (value - from1) / (to1 - from1) * (to2 - from2) + from2;
}

游戏中的应用

  • UI 布局: 将 HP (0-100) 映射到 UI 宽度 (0-300 像素)。
  • 难度调整: 将 玩家等级 (1-60) 映射到 怪物攻击力 (10-5000)。

4. 限制 (Clamp)

将数值限制在指定的范围内。

变体

  • Clamp(value, min, max): 限制在 [min, max] 之间。
  • Clamp01(value): 限制在 [0, 1] 之间。这是最常用的,特别是在处理百分比或颜色时。

游戏中的应用

  • 防止数值越界: 玩家血量不能超过上限,也不能低于 0。
  • 摄像机角度限制: 俯仰角限制在 -80度 到 80度之间,防止脖子折断。

5. 平滑步进 (SmoothStep)

Lerp 是线性的,变化率是恒定的。如果你想要“起步慢,中间快,结束慢”的效果,就需要 SmoothStep。它使用一个 S 形曲线 (Sigmoid-like) 进行插值。

公式 (Hermite 插值)

\[ t = Clamp01(t) \] \[ result = t \times t \times (3 - 2 \times t) \]

SmoothStep Curve

游戏中的应用

  • 更自然的动画: 比如 UI 窗口弹出的动画,或者相机的移动,使用 SmoothStep 会比 Lerp 看起来更有“重量感”。
  • 地形生成: 在生成高度图时,平滑噪声值。

6. 接近 (MoveTowards)

MoveTowardsLerp 很像,但有一个关键区别: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);

7. 循环与摆动 (Repeat & PingPong)

这两个函数通常用于处理时间或循环动画。

重复 (Repeat)

Repeat(t, length) 类似于取模运算 (Modulo),但对浮点数和负数处理得更好。它让数值永远保持在 [0, length] 之间。

公式

\[ result = t - floor(t / length) \times length \]

游戏中的应用

  • 无限滚动的背景: 纹理坐标 UV 的循环。
  • 循环动画: 让时间 \(t\) 永远在 0 到 1 之间循环。

乒乓 (PingPong)

PingPong(t, length) 让数值在 0length 之间来回摆动。就像乒乓球一样,撞到边界就反弹。

公式

\[ 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,代码也会更加优雅。

8. 易错点与边界值

在实际工程中,标量函数有一些常见的坑需要注意:

浮点精度

// 永远不要用 == 比较两个浮点数
if (Mathf.Abs(a - b) < 0.001f) { /* 近似相等 */ }

Lerp 的 “芝诺悖论” 误用

// 错误:永远到不了目标(每帧移动剩余距离的一部分)
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 vs SmootherStep

SmoothStep 的导数在 \(t=0\)\(t=1\) 处为零,但二阶导数不为零——在某些动画场景下仍能看到”小顿挫”。如果需要更丝滑的过渡,可以用 SmootherStep(Ken Perlin 提出):

\[ t = 6t^5 - 15t^4 + 10t^3 \]

除零防御

InverseLerpRemapa == b(分母为零)时会产生 NaN。实际项目中请务必加保护:

float SafeInverseLerp(float a, float b, float v) {
    if (Mathf.Approximately(a, b)) return 0f;
    return (v - a) / (b - a);
}

回到目录 | 下一篇: 三角函数入门 >

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。