Unity 输入为什么有时需要乘 Time.deltaTime,有时却不需要呢?
本文将从 mousePositionDelta 与 GetAxis 出发,理解输入数据、位移与时间的关系。
在 Unity 中处理用户输入时,我们经常看到两种不同的代码。
使用键盘控制物体移动:
csharp
float horizontal = Input.GetAxis("Horizontal");
transform.Translate(
Vector3.right * horizontal * speed * Time.deltaTime
);
使用鼠标控制摄像机旋转:
csharp
float mouseX = Input.GetAxis("Mouse X");
transform.Rotate(
Vector3.up * mouseX * sensitivity
);
第一段代码乘了 Time.deltaTime,第二段却没有。
而 Unity 还提供了另一个属性:
csharp
Vector3 delta = Input.mousePositionDelta;
它同样表示鼠标的移动量,通常也不需要乘 Time.deltaTime。
这就产生了几个问题:
mousePositionDelta是每一帧鼠标位置发生的变化吗?GetAxis()读取的不也是每一帧的输入吗?- 为什么一个需要乘
Time.deltaTime,另一个却不需要? - 有没有一条适用于所有游戏输入的通用规则?
要彻底理解这些问题,需要先区分三个概念:输入状态、运动速度与运动位移。
一、从最基础的问题开始:Time.deltaTime 到底有什么作用?
Time.deltaTime 表示上一帧到当前帧之间经过的时间,通常以秒为单位。
假设游戏以 50 FPS 运行:
Δ t = 1 50 = 0.02 秒 \Delta t=\frac{1}{50}=0.02\text{ 秒} Δt=501=0.02 秒
如果我们希望一个物体以每秒 5 个世界单位的速度向右移动,那么当前帧应该移动:
Δ x = v × Δ t \Delta x=v\times\Delta t Δx=v×Δt
代入数值:
Δ x = 5 × 0.02 = 0.1 \Delta x=5\times0.02=0.1 Δx=5×0.02=0.1
因此,在 Unity 中可以写成:
csharp
float speed = 5f;
transform.Translate(
Vector3.right * speed * Time.deltaTime
);
如果没有 Time.deltaTime,物体就会每帧移动 5 个单位。帧率越高,每秒移动次数越多,物体也就移动得越快。
Time.deltaTime 并不是用来让所有数值都与帧率无关,而是用来将"单位时间的变化率"转换为"当前时间步的变化量"。
| 已知量 | 乘以时间后 |
|---|---|
| 速度:米/秒 | 位移:米 |
| 角速度:度/秒 | 旋转角度:度 |
| 变化率:数值/秒 | 数值变化量 |
但如果某个值本身已经表示位移或角度变化,就没有必要再次乘时间。
二、mousePositionDelta:返回的已经是鼠标位移
Unity 的 Input.mousePositionDelta 是只读 Vector3 属性,表示当前输入更新中的鼠标相对位移,单位为像素,其中 z 分量始终为 0。
例如:
csharp
Vector3 delta = Input.mousePositionDelta;
假设得到:
text
delta = (20, -10, 0)
表示在这一输入更新期间,鼠标向右移动了 20 像素、向下移动了 10 像素。
这些值描述的是已经发生的移动距离,而不是每秒移动多少像素。
1. 为什么不需要乘 Time.deltaTime?
假设鼠标每水平移动 1 像素,就让摄像机旋转 0.5 度:
csharp
float sensitivity = 0.5f;
float mouseX = Input.mousePositionDelta.x;
transform.Rotate(
Vector3.up * mouseX * sensitivity
);
当鼠标移动 20 像素时:
Δ θ = 20 × 0.5 = 10 ∘ \Delta\theta=20\times0.5=10^\circ Δθ=20×0.5=10∘
旋转角度已经计算出来,不需要再次乘时间。
如果写成:
csharp
transform.Rotate(
Vector3.up * mouseX * sensitivity * Time.deltaTime
);
就会让同样的鼠标移动距离,在不同帧率下产生不同的累计旋转角度。
2. 为什么重复乘时间反而可能造成帧率依赖?
假设鼠标以相同的物理速度移动,持续 1 秒,总共产生 600 像素位移。
| 条件 | 30 FPS | 60 FPS |
|---|---|---|
| 平均每帧位移 | 20 px | 10 px |
| 直接累加的总位移 | 20 × 30 = 600 px | 10 × 60 = 600 px |
若每帧再乘 deltaTime,累计结果 |
20 × 1/30 × 30 = 20 | 10 × 1/60 × 60 = 10 |
理想化示例:假设鼠标位移均匀分布、输入无丢失;最后一行省略相同的灵敏度系数。
如果直接累加每帧鼠标位移,两个帧率下总量都是 600 像素。但如果每帧再乘一次 Time.deltaTime,累计结果就不同了。
原因在于鼠标位移本来已经随着采样时间变化:同样的移动速度下,时间间隔越长,单次采样得到的位移通常越大。再次乘时间,相当于重复引入时间因素。
结论:当鼠标位移直接用于计算旋转角度或拖拽增量时,通常不乘 Time.deltaTime。
三、GetAxis:同一个 API,可能返回两种不同含义的数据
最容易造成误解的地方是 Input.GetAxis()。
不能简单地说"GetAxis 需要乘 Time.deltaTime",也不能说"GetAxis 已经与帧率无关,所以不需要乘 Time.deltaTime"。
GetAxis() 是虚拟轴读取接口,返回值的含义取决于虚拟轴映射的输入设备及配置。
1. GetAxis("Horizontal"):返回方向控制量
csharp
float horizontal = Input.GetAxis("Horizontal");
当 Horizontal 配置为键盘方向输入时,返回值通常在 -1 到 1 之间。
| 按键状态 | 典型目标轴值 |
|---|---|
| 按住 A | -1 |
| 没有方向输入 | 0 |
| 按住 D | 1 |
使用 GetAxis() 时,轴值可能受到 Sensitivity、Gravity 等参数影响,逐渐向目标值变化。但无论如何,它仍主要表示玩家希望向哪个方向移动,以及输入强度有多大。
它不表示物体在这一帧已经移动了多少世界单位。
因此,如果 horizontal = 1 表示向右全速移动,speed = 5 表示每秒移动 5 个单位:
Δ x = horizontal × speed × Δ t \Delta x=\text{horizontal}\times\text{speed}\times\Delta t Δx=horizontal×speed×Δt
csharp
float horizontal = Input.GetAxis("Horizontal");
float speed = 5f;
transform.Translate(
Vector3.right * horizontal * speed * Time.deltaTime
);
这里乘 Time.deltaTime,是因为 horizontal * speed 得到的是目标移动速度,而不是已经发生的位移。
2. GetAxis("Mouse X"):返回经过映射处理的鼠标移动量
csharp
float mouseX = Input.GetAxis("Mouse X");
当虚拟轴配置为 Mouse Movement 时,Unity 返回的是当前鼠标位移经过该轴灵敏度处理后的数值。它不是一般意义上的 -1 到 1 方向强度,也不一定限制在这个范围内。
csharp
float mouseX = Input.GetAxis("Mouse X");
transform.Rotate(
Vector3.up * mouseX * sensitivity
);
这里的 mouseX 表示已经发生的鼠标移动所对应的控制量。如果直接将它映射为角度变化,就不需要再乘 Time.deltaTime。
Unity 官方 GetAxis() 文档也分别演示了两类做法:使用 Horizontal / Vertical 控制按速度移动时乘 Time.deltaTime;使用 Mouse X / Mouse Y 控制鼠标视角时不乘。
四、为什么 mousePositionDelta 与 GetAxis("Mouse X") 都不需要乘时间?
csharp
float a = Input.mousePositionDelta.x;
float b = Input.GetAxis("Mouse X");
两者都可以反映鼠标相对移动,但并不等价。
| 比较维度 | mousePositionDelta.x |
GetAxis("Mouse X") |
|---|---|---|
| 数据来源 | 鼠标相对位移 | Input Manager 虚拟轴 |
| 数据含义 | 像素位移 | 经过虚拟轴配置处理的移动量 |
| 单位 | 像素 | 映射后的轴值 |
| 是否受轴 Sensitivity 影响 | 否 | 是 |
| 直接用于旋转时是否乘 deltaTime | 通常不乘 | 通常不乘 |
| 是否需要配置虚拟轴 | 不需要 | 需要 |
例如,使用 mousePositionDelta 实现第一人称视角:
csharp
float mouseX = Input.mousePositionDelta.x;
float mouseY = Input.mousePositionDelta.y;
float sensitivity = 0.1f; // 度/像素
transform.Rotate(
-mouseY * sensitivity,
mouseX * sensitivity,
0f
);
使用鼠标虚拟轴:
csharp
float mouseX = Input.GetAxis("Mouse X");
float mouseY = Input.GetAxis("Mouse Y");
float sensitivity = 2f; // 针对映射后轴值的系数
transform.Rotate(
-mouseY * sensitivity,
mouseX * sensitivity,
0f
);
两段代码不能简单地共用同一个灵敏度数值,因为它们的输入量纲和映射方式不同。
五、为什么 GetAxis 与帧率无关,却仍然要乘 deltaTime?
Unity 文档说明 GetAxis() 的返回值不依赖于帧率。这很容易被误解为既然输入值已经与帧率无关,为什么还要乘时间?
实际上,输入值的帧率独立性,与游戏行为的帧率独立性,是两个不同问题。
csharp
float horizontal = Input.GetAxis("Horizontal");
不管游戏运行在 30 FPS 还是 120 FPS,玩家持续向右输入时,这个虚拟轴都可以达到接近 1 的值。这是输入状态的含义。
但如果:
csharp
transform.position += Vector3.right * horizontal * 5f;
即使 horizontal 是稳定的 1,代码每帧都让物体移动 5 个单位。
- 30 FPS 时,每秒移动约 150 个单位。
- 120 FPS 时,每秒移动约 600 个单位。
问题出在我们怎样使用输入值,而不是输入值本身。
改成:
csharp
transform.position +=
Vector3.right * horizontal * 5f * Time.deltaTime;
才把连续移动转换成按经过时间累积的运动。
反过来,mousePositionDelta 本身就是位移,即使每帧数值不同,累加后的结果仍可以对应相同的鼠标物理运动。
帧率独立性不意味着所有数值都应该乘或不乘 Time.deltaTime。必须分析整个计算过程。
六、一个重要例外:并不是只有"位移"才不需要乘时间
例如:
csharp
void FixedUpdate()
{
float horizontal = Input.GetAxis("Horizontal");
rb.AddForce(Vector3.right * horizontal * force);
}
这里 horizontal 仍然是方向控制量,但没有乘 Time.fixedDeltaTime。
原因是 Rigidbody.AddForce() 是物理系统接口。施加力、施加冲量与直接修改物体位移是不同操作;Unity 会根据力模式和物理时间步处理相应变化。
如果使用默认 ForceMode.Force,通常应提供期望的力,而不是先手动乘时间。
实践补充:对于物理运动,通常应在 Update() 中采集并缓存输入意图,在 FixedUpdate() 中调用 AddForce()。上面的代码仅用于对比时间处理机制。
同样,直接设置 Unity 6 的 Rigidbody.linearVelocity 时,需要传入速度,而不是预先计算每帧位移。
因此,更准确的判断需要同时考虑:
- 输入数据表示什么?
- 后续运算将它转换成什么?
- 最终调用的 API 需要什么单位的数据?
- 是否已经有其他系统负责处理时间?
七、通用结论:Time.deltaTime 的使用由量纲和计算目标决定
可按以下步骤判断:
- 识别输入值: 它是方向、强度、位置、位移,还是速度?
- 明确目标计算: 想得到每帧位移、旋转角度、速度、力,还是目标位置?
- 检查目标 API 的语义: 例如
Translate、Rotate、AddForce、linearVelocity。 - 决定是否乘时间: 如果要将速度或角速度转换为当前时间步的位移或角度,乘以对应时间间隔;如果已是变化量,或时间由 API / 物理系统处理,就不要重复乘。
最可靠的方法之一是检查量纲:
米 秒 × 秒 = 米 \frac{\text{米}}{\text{秒}}\times\text{秒}=\text{米} 秒米×秒=米
度 秒 × 秒 = 度 \frac{\text{度}}{\text{秒}}\times\text{秒}=\text{度} 秒度×秒=度
鼠标位移直接映射为旋转角度时:
像素 × 度 像素 = 度 \text{像素}\times\frac{\text{度}}{\text{像素}}=\text{度} 像素×像素度=度
如果再乘秒:
像素 × 度 像素 × 秒 = 度 ⋅ 秒 \text{像素}\times\frac{\text{度}}{\text{像素}}\times\text{秒}=\text{度}\cdot\text{秒} 像素×像素度×秒=度⋅秒
就不再是期望的角度单位。
量纲检查不能代替对 API 行为的理解,但能够帮助我们发现错误。
常见输入的判断总结
| 使用场景 | 数据含义 | 通常乘 deltaTime? |
|---|---|---|
GetAxis("Horizontal") 控制移动 |
方向强度 × 移动速度 | 是 |
GetAxisRaw("Vertical") 控制移动 |
方向强度 × 移动速度 | 是 |
GetAxis("Horizontal") 控制持续旋转 |
方向强度 × 角速度 | 是 |
GetAxis("Mouse X") 控制视角 |
映射后的鼠标位移 × 角度系数 | 否 |
mousePositionDelta 控制视角 |
像素位移 × 度/像素 | 否 |
mousePosition 定位对象 |
屏幕绝对位置 | 否 |
GetButtonDown() 触发跳跃 |
一次性事件 | 否 |
GetAxis("Horizontal") 控制施加力 |
输入强度 × 力 | 通常否,由物理 API 处理 |
以上是针对典型用法的判断,不是对 API 的永久限制。例如,如果先把鼠标位移转换为速度,再利用它实现按时间变化的行为,公式也要相应调整。
八、结语:不要记住 API 是否乘时间,而要理解数据如何参与计算
通过 mousePositionDelta 与 GetAxis() 的比较,可以得到三层结论。
第一层:具体 API 的使用规律。
鼠标的相对位移直接用于旋转时,通常不需要乘 Time.deltaTime。键盘方向输入用于按速度移动时,通常需要乘 Time.deltaTime。
第二层:输入系统的设计规律。
输入系统提供的数值可能表示状态、控制强度、绝对位置或变化量。同一个输入接口也可能因设备和配置不同而返回不同语义的数据。
第三层:通用编程原则。
不要根据 API 名称决定是否乘以时间,而要根据数据的物理含义、单位以及目标操作来决定。只有在需要将单位时间的变化率转换成当前时间步变化量时,才应该引入对应的时间间隔。
这不只是 Unity 输入处理的经验,也是理解游戏运动、动画、物理仿真以及实时交互程序的一条基础原则。