Skip to content

Time 与帧率

Time.timeScale 会影响哪些时间?

一句话答案:Time.timeScale 影响的是 Unity 的缩放游戏时间,比如 Time.deltaTimeTime.timeWaitForSeconds、普通动画和物理推进;但不影响真实时间,比如 Time.unscaledDeltaTimeTime.realtimeSinceStartupWaitForSecondsRealtime

unity-timescale-affects-time

timeScale 影响的:

Time.deltaTimeTime.timeTime.fixedTimeWaitForSecondsdeltaTime 写的移动、计时、CD、Buff 时间 普通 Animator 更新 物理推进和 FixedUpdate 调用节奏

不受 timeScale 影响的:

Time.unscaledDeltaTimeTime.unscaledTimeTime.realtimeSinceStartupWaitForSecondsRealtime 用真实时间驱动的 UI 动画、暂停菜单倒计时、加载动画

timeScale = 0 会怎样?

c
Update 仍然会执行
Time.deltaTime 变成 0
依赖 deltaTime 的移动会停
WaitForSeconds 会卡住
FixedUpdate 通常不会继续调用
物理模拟暂停
UI 输入仍然可以响应
unscaledDeltaTime 仍然有值
WaitForSecondsRealtime 仍然继续

所以暂停游戏经常这么写:

c
using UnityEngine; // 引入 Unity 常用类型

public class PauseExample : MonoBehaviour // 定义暂停示例组件
{ // 类开始
    public void PauseGame() // 定义暂停游戏方法
    { // 方法开始
        Time.timeScale = 0f; // 把游戏缩放时间设为 0,暂停大部分游戏逻辑
    } // 方法结束

    public void ResumeGame() // 定义恢复游戏方法
    { // 方法开始
        Time.timeScale = 1f; // 把游戏缩放时间恢复为 1,恢复正常速度
    } // 方法结束
} // 类结束

暂停时 UI 动画要用 unscaledDeltaTime:

c
using UnityEngine; // 引入 Unity 常用类型

public class PauseMenuAnimation : MonoBehaviour // 定义暂停菜单动画组件
{ // 类开始
    [SerializeField] private float rotateSpeed = 90f; // 定义 UI 旋转速度

    private void Update() // 每帧调用一次
    { // 方法开始
        transform.Rotate(0f, 0f, rotateSpeed * Time.unscaledDeltaTime); // 使用不受 timeScale 影响的时间驱动 UI 动画
    } // 方法结束
} // 类结束

如果这里用 Time.deltaTime,当 timeScale = 0 时,暂停菜单动画也会停住。

协程区别:

c
using System.Collections; // 引入 IEnumerator 协程接口
using UnityEngine; // 引入 Unity 常用类型

public class WaitExample : MonoBehaviour // 定义等待示例组件
{ // 类开始
    private IEnumerator WaitByGameTime() // 定义受 timeScale 影响的等待协程
    { // 方法开始
        yield return new WaitForSeconds(3f); // 等待 3 秒游戏时间,timeScale 为 0 时不会继续
        Debug.Log("游戏时间等待结束"); // 输出游戏时间等待结束日志
    } // 方法结束

    private IEnumerator WaitByRealTime() // 定义不受 timeScale 影响的等待协程
    { // 方法开始
        yield return new WaitForSecondsRealtime(3f); // 等待 3 秒真实时间,timeScale 为 0 时仍然继续
        Debug.Log("真实时间等待结束"); // 输出真实时间等待结束日志
    } // 方法结束
} // 类结束

fixedDeltaTime 注意点:Time.fixedDeltaTime 这个配置值不会自动随着 timeScale 改变。 如果做慢动作,比如:

c
Time.timeScale = 0.2

为了让物理更平滑,常见做法是把 fixedDeltaTime 也按比例调小,恢复正常速度时再调回来。

CAUTION

面试高分回答:Time.timeScale 控制的是 Unity 的缩放游戏时间。它会影响 Time.deltaTimeTime.timeWaitForSeconds、普通动画和物理推进,所以用 deltaTime 驱动的角色移动、技能 CD、Buff 时间都会变慢或暂停。当 timeScale = 0 时,Update 仍然会调用,但 deltaTime 为 0,FixedUpdate 通常不再推进,WaitForSeconds 也不会继续。它不影响真实时间相关 API,比如 Time.unscaledDeltaTimeTime.unscaledTimeTime.realtimeSinceStartupWaitForSecondsRealtime。所以暂停菜单、加载动画、真实倒计时应该用 unscaled 时间,而游戏内逻辑通常用 scaled 时间。

deltaTimefixedDeltaTime 区别是什么?

一句话答案:Time.deltaTime上一帧到这一帧的时间间隔,跟渲染帧率走,适合 UpdateTime.fixedDeltaTime物理固定步长,跟物理模拟走,适合 FixedUpdate

unity-deltatime-vs-fixeddeltatime

先说 deltaTime:Time.deltaTime 表示上一帧到当前帧经过了多少秒。帧率越不稳定,它越会变化。

比如 60 FPS 时,一帧大约:

c
0.016

30 FPS 时,一帧大约:

c
0.033

所以在 Update 里做移动时要乘它:

c
using UnityEngine; // 引入 Unity 常用类型

public class MoveByDeltaTime : MonoBehaviour // 定义使用 deltaTime 移动的组件
{ // 类开始
    [SerializeField] private float speed = 5f; // 定义移动速度

    private void Update() // 每一帧调用一次,调用频率受帧率影响
    { // 方法开始
        Vector3 move = Vector3.forward * speed * Time.deltaTime; // 根据每帧时间计算这一帧应该移动的距离
        transform.position += move; // 修改物体位置,让移动不受帧率快慢影响
    } // 方法结束
} // 类结束

如果不乘 deltaTime,高帧率机器会移动更快,低帧率机器会移动更慢。

再说 fixedDeltaTime:Time.fixedDeltaTime 是物理系统的固定时间步长,Unity 默认常见值是 0.02 秒,也就是一秒 50 次物理步。

它主要配合 FixedUpdate 使用:

c
using UnityEngine; // 引入 Unity 常用类型

public class MoveByFixedDeltaTime : MonoBehaviour // 定义使用 fixedDeltaTime 移动物理对象的组件
{ // 类开始
    [SerializeField] private Rigidbody rb; // 定义刚体引用
    [SerializeField] private float speed = 5f; // 定义移动速度

    private void FixedUpdate() // 按物理固定步长调用,不跟渲染帧完全一致
    { // 方法开始
        Vector3 move = Vector3.forward * speed * Time.fixedDeltaTime; // 根据固定物理步长计算移动距离
        rb.MovePosition(rb.position + move); // 使用 Rigidbody 的 MovePosition 移动物理对象
    } // 方法结束
} // 类结束

Update 和 FixedUpdate 的节奏不同:

Update 跟渲染帧走。 如果帧率高,Update 调用频繁。 如果帧率低,Update 调用变少。 所以 deltaTime 是变化的。

FixedUpdate 跟物理固定步长走。 一帧里可能没有 FixedUpdate。 一帧里也可能补多个 FixedUpdate。 所以物理逻辑放这里更稳定。

常见选择:

普通角色移动,如果不用物理:

c
Update + Time.deltaTime

摄像机跟随:

c
LateUpdate + Time.deltaTime

技能 CD、Buff 时间、普通倒计时:

c
Update + Time.deltaTime

Rigidbody 移动、AddForce、物理模拟:

c
FixedUpdate + Time.fixedDeltaTime

输入要特别注意: 输入最好在 Update 里读,因为 Update 每渲染帧都会执行,按键不容易漏。 物理处理放在 FixedUpdate,使用前面记录下来的输入。

IMPORTANT

面试高分回答:deltaTime 是当前渲染帧和上一帧之间的时间差,它会随着帧率变化,主要用于 Update 中做帧率无关的移动、计时、CD 和 UI 动画。fixedDeltaTime 是 Unity 物理系统的固定时间步长,主要用于 FixedUpdate,默认常见是 0.02 秒。Update 和渲染帧同步,间隔不稳定;FixedUpdate 和物理模拟同步,可能一帧不调用,也可能一帧补多次。所以非物理逻辑通常用 deltaTime,物理相关逻辑用 fixedDeltaTime。输入一般在 Update 读取,再把结果交给 FixedUpdate 做物理移动。

卡顿时 deltaTime 变大有什么影响?

面试高分回答

卡顿时 Time.deltaTime 会变大,因为它表示“上一帧到这一帧真实经过了多少游戏时间”。正常 60 FPS 时大约是 0.016s,如果某一帧卡了 0.2s,那下一帧用 deltaTime 计算移动、计时、插值时,就会一次推进很大一步。

unity-deltatime-spike-effects

会造成什么影响?

  1. 角色、子弹、怪物可能突然跳一段距离: position += speed * Time.deltaTime 本来是为了帧率无关,但卡顿后一帧 deltaTime 太大,就会单帧位移过大。
  2. 计时器、CD、Buff 可能一下扣很多时间: 如果逻辑写得不严谨,比如只判断 timer == 0,就容易跳过触发点。应该用 <= 0>= intervalwhile 补偿。
  3. 相机、动画插值可能抖动或瞬移: 比如 Lerp、跟随相机、平滑移动,如果直接吃很大的 deltaTime,表现会突然“追上去”。
  4. 物理可能更容易穿透: 尤其是你在 Update 里直接改 transform.position 移动高速物体时,单帧位移太大可能越过碰撞体。

示例代码:限制单帧 deltaTime

c
using UnityEngine; // 引入 Unity 常用类型

public class DeltaTimeClampExample : MonoBehaviour // 定义一个限制 deltaTime 的示例组件
{ // 类开始
    [SerializeField] private float speed = 5f; // 定义物体移动速度
    [SerializeField] private float maxDeltaTime = 0.05f; // 定义允许参与逻辑计算的最大 deltaTime

    private void Update() // 每帧调用一次
    { // 方法开始
        float dt = Mathf.Min(Time.deltaTime, maxDeltaTime); // 限制本帧使用的 deltaTime,避免卡顿后一帧跳太远
        Vector3 move = Vector3.forward * speed * dt; // 根据限制后的 dt 计算本帧移动距离
        transform.position += move; // 把物体向前移动
    } // 方法结束
} // 类结束

计时器更稳的写法

c
using UnityEngine; // 引入 Unity 常用类型

public class TimerExample : MonoBehaviour // 定义一个计时器示例组件
{ // 类开始
    private float timer; // 保存累计时间
    private const float Interval = 1f; // 定义每隔 1 秒触发一次

    private void Update() // 每帧调用一次
    { // 方法开始
        timer += Time.deltaTime; // 累加这一帧经过的时间
        while (timer >= Interval) // 用 while 处理大 deltaTime,避免卡顿后漏掉触发次数
        { // while 开始
            timer -= Interval; // 扣掉一次触发间隔
            Debug.Log("触发一次定时逻辑"); // 执行一次定时逻辑
        } // while 结束
    } // 方法结束
} // 类结束

一句话总结

NOTE

deltaTime 变大不是 bug,它是在补偿真实经过的时间;真正的问题是“单帧步长太大”,所以要优化卡顿根因,并对移动、计时、物理、插值这类关键逻辑做最大步长限制或分步处理。

如何实现暂停系统?

面试高分回答

暂停系统不要只理解成 Time.timeScale = 0。更完整的做法是:用一个统一的 PauseManager 管理暂停状态,同时控制游戏时间、玩家输入、暂停 UI、音频、协程、动画和一些特殊系统。

unity-pause-system

核心原理

Time.timeScale = 0 后,游戏内时间停止,Time.deltaTime 会变成 0,所以依赖 deltaTime 的移动、计时、动画、物理推进通常都会停下来。

但是要注意:Update 还是会继续执行,UI 事件也还能响应,所以暂停菜单可以继续操作。音频不一定会自动停,网络、异步加载、真实时间计时也不一定会停,需要单独处理。

基础实现代码

c
using UnityEngine; // 引入 Unity 常用类型

public class PauseManager : MonoBehaviour // 定义暂停管理器组件
{ // 类开始
    [SerializeField] private GameObject pausePanel; // 引用暂停菜单面板
    [SerializeField] private AudioSource bgmSource; // 引用背景音乐音源
    public static bool IsPaused { get; private set; } // 提供全局暂停状态,只允许内部修改

    private void Awake() // 对象初始化时调用
    { // 方法开始
        SetPaused(false); // 游戏启动时默认取消暂停
    } // 方法结束

    private void Update() // 每帧检测输入
    { // 方法开始
        if (Input.GetKeyDown(KeyCode.Escape)) // 如果玩家按下 ESC 键
        { // if 开始
            SetPaused(!IsPaused); // 在暂停和恢复之间切换
        } // if 结束
    } // 方法结束

    public void PauseGame() // 提供给 UI 按钮调用的暂停方法
    { // 方法开始
        SetPaused(true); // 设置为暂停状态
    } // 方法结束

    public void ResumeGame() // 提供给 UI 按钮调用的恢复方法
    { // 方法开始
        SetPaused(false); // 设置为恢复状态
    } // 方法结束

    private void SetPaused(bool paused) // 统一设置暂停状态
    { // 方法开始
        IsPaused = paused; // 记录当前是否暂停
        Time.timeScale = paused ? 0f : 1f; // 暂停时停止游戏时间,恢复时还原游戏时间
        if (pausePanel != null) pausePanel.SetActive(paused); // 有暂停面板时,根据状态显示或隐藏
        if (bgmSource != null && paused) bgmSource.Pause(); // 暂停时暂停背景音乐
        if (bgmSource != null && !paused) bgmSource.UnPause(); // 恢复时继续播放背景音乐
        Cursor.visible = paused; // 暂停时显示鼠标
        Cursor.lockState = paused ? CursorLockMode.None : CursorLockMode.Locked; // 暂停时释放鼠标,恢复时锁定鼠标
    } // 方法结束
} // 类结束

玩家逻辑要配合暂停状态

c
using UnityEngine; // 引入 Unity 常用类型

public class PlayerMove : MonoBehaviour // 定义玩家移动组件
{ // 类开始
    [SerializeField] private float speed = 5f; // 定义移动速度

    private void Update() // 每帧处理玩家移动
    { // 方法开始
        if (PauseManager.IsPaused) return; // 如果游戏暂停,就不处理玩家移动
        float h = Input.GetAxisRaw("Horizontal"); // 读取水平输入
        float v = Input.GetAxisRaw("Vertical"); // 读取垂直输入
        Vector3 dir = new Vector3(h, 0f, v).normalized; // 计算移动方向
        transform.position += dir * speed * Time.deltaTime; // 根据 deltaTime 移动玩家
    } // 方法结束
} // 类结束

常见坑

WaitForSeconds 会受 timeScale 影响,暂停后它也会停住;如果你希望暂停菜单里的倒计时、提示动画继续走,要用 WaitForSecondsRealtimeTime.unscaledDeltaTime

Animator 默认也受 timeScale 影响;如果暂停菜单有动画,可以把 Animator 的 Update Mode 设置为 Unscaled Time

多人游戏里,客户端暂停通常只能暂停本地表现,不能暂停服务器逻辑。比如联机战斗、倒计时、匹配、网络同步,一般不能直接靠 timeScale = 0 解决。

一句话总结

TIP

暂停系统 = timeScale 停游戏时间 + IsPaused 拦截玩法输入 + UI/音频/协程/动画单独处理。面试里这样讲,会比只说一句 Time.timeScale = 0 更完整。

如何实现不受暂停影响的 UI 动画?

面试高分回答

暂停时如果用了 Time.timeScale = 0,普通动画、计时、移动会停,因为 Time.deltaTime 变成了 0。但是暂停菜单、设置面板、Loading 圈这类 UI 动画通常不能停,所以要让它们使用“不受暂停影响的真实时间”,也就是 Time.unscaledDeltaTime

unity-unscaled-ui-animation

常用做法

手写 UI 动画时,用 Time.unscaledDeltaTime

Animator 做 UI 动画时,把 Animator 的 Update Mode 设置成 Unscaled Time

协程里等待真实时间时,用 WaitForSecondsRealtime,不要用 WaitForSeconds

如果用 DOTween,可以用 SetUpdate(true) 让 Tween 不受 timeScale 影响。

手写暂停菜单淡入动画

c
using UnityEngine; // 引入 Unity 常用类型

public class PausePanelAnimation : MonoBehaviour // 定义暂停面板动画组件
{ // 类开始
    [SerializeField] private CanvasGroup canvasGroup; // 引用 CanvasGroup,用来控制 UI 透明度
    [SerializeField] private RectTransform panel; // 引用面板 RectTransform,用来控制缩放
    [SerializeField] private float duration = 0.25f; // 定义动画持续时间
    private float timer; // 记录动画已经播放的时间
    private bool isPlaying; // 记录动画是否正在播放

    private void OnEnable() // 面板启用时调用
    { // 方法开始
        timer = 0f; // 重置动画时间
        isPlaying = true; // 标记动画开始播放
        canvasGroup.alpha = 0f; // 初始透明度设置为 0
        panel.localScale = Vector3.one * 0.9f; // 初始缩放设置为 0.9 倍
    } // 方法结束

    private void Update() // 每帧更新 UI 动画
    { // 方法开始
        if (!isPlaying) return; // 如果动画没有播放,就直接返回
        timer += Time.unscaledDeltaTime; // 使用真实时间推进动画,不受暂停影响
        float t = Mathf.Clamp01(timer / duration); // 计算 0 到 1 的动画进度
        float ease = 1f - Mathf.Pow(1f - t, 3f); // 做一个缓出效果,让动画更自然
        canvasGroup.alpha = ease; // 根据动画进度设置透明度
        panel.localScale = Vector3.LerpUnclamped(Vector3.one * 0.9f, Vector3.one, ease); // 根据动画进度设置缩放
        if (t >= 1f) isPlaying = false; // 动画播放完成后停止更新
    } // 方法结束
} // 类结束

Animator 方案

c
using UnityEngine; // 引入 Unity 常用类型

public class PauseAnimatorUnscaled : MonoBehaviour // 定义暂停 UI Animator 设置组件
{ // 类开始
    [SerializeField] private Animator animator; // 引用 UI 上的 Animator

    private void Awake() // 初始化时调用
    { // 方法开始
        animator.updateMode = AnimatorUpdateMode.UnscaledTime; // 让 Animator 使用真实时间更新,不受 timeScale 影响
    } // 方法结束
} // 类结束

协程方案

c
using UnityEngine; // 引入 Unity 常用类型
using System.Collections; // 引入 IEnumerator 所在命名空间

public class PauseCoroutineExample : MonoBehaviour // 定义暂停协程示例组件
{ // 类开始
    private IEnumerator ShowTip() // 定义显示提示的协程
    { // 协程开始
        Debug.Log("显示暂停提示"); // 打印提示出现
        yield return new WaitForSecondsRealtime(1f); // 等待真实世界 1 秒,不受 timeScale 影响
        Debug.Log("隐藏暂停提示"); // 打印提示隐藏
    } // 协程结束
} // 类结束

一句话总结

NOTE

暂停菜单 UI 动画不要依赖 Time.deltaTime,要用 Time.unscaledDeltaTimeWaitForSecondsRealtimeAnimatorUpdateMode.UnscaledTime,这样即使 timeScale = 0,UI 也能继续播放。

FixedUpdate 频率过高有什么问题?

面试高分回答

FixedUpdate 频率过高,主要问题是 CPU 压力变大。因为 FixedUpdate 是按照固定物理步长执行的,步长越小,1 秒内执行次数越多。默认 Time.fixedDeltaTime = 0.02f,大约每秒 50 次;如果改成 0.005f,就会变成每秒 200 次,物理模拟、碰撞检测、刚体更新、脚本逻辑都会更频繁。

unity-fixedupdate-high-frequency

为什么会卡?

FixedUpdate 频率越高,Unity 每秒要跑的物理步数越多。每一次物理步里,可能会执行:

碰撞检测、刚体积分、关节模拟、触发器检测、OnCollisionOnTrigger、你写在 FixedUpdate 里的逻辑。

所以如果你把很多角色移动、AI、射线检测、寻路判断都放进 FixedUpdate,频率一高,成本会被成倍放大。

还有一个连锁问题

如果某一帧卡住了,Unity 为了追上固定物理时间,可能会在下一帧连续执行多次 FixedUpdate。这就会出现一种情况:本来已经卡了,还要补跑很多物理帧,导致这一帧更重。

所以 FixedUpdate 频率过高,有时会让卡顿更明显。

它是不是能让物理更准?

一定程度上是的。fixedDeltaTime 越小,物理步长越细,高速物体、刚体运动、碰撞模拟可能更稳定。

但它不是免费的。更高频率换来的是更高 CPU 消耗,尤其在移动端、低端机、大量刚体、大量碰撞体、大量怪物时,很容易掉帧、发热、耗电。

输入手感不一定更好

很多新手会以为 FixedUpdate 越频繁,角色操作越丝滑。其实玩家输入一般应该在 Update 里采样,因为 Update 跟渲染帧同步,更接近玩家实际按键时机。

比较常见的写法是:Update 读取输入,FixedUpdate 执行物理移动。

c
using UnityEngine; // 引入 Unity 常用类型

public class PlayerPhysicsMove : MonoBehaviour // 定义玩家物理移动组件
{ // 类开始
    [SerializeField] private Rigidbody rb; // 引用玩家刚体
    [SerializeField] private float speed = 5f; // 定义玩家移动速度
    private Vector2 input; // 保存玩家输入方向

    private void Update() // 每帧读取输入
    { // 方法开始
        input.x = Input.GetAxisRaw("Horizontal"); // 读取水平输入
        input.y = Input.GetAxisRaw("Vertical"); // 读取垂直输入
    } // 方法结束

    private void FixedUpdate() // 按固定物理步长执行
    { // 方法开始
        Vector3 dir = new Vector3(input.x, 0f, input.y).normalized; // 把二维输入转换成三维移动方向
        Vector3 targetVelocity = dir * speed; // 根据方向和速度计算目标速度
        rb.velocity = new Vector3(targetVelocity.x, rb.velocity.y, targetVelocity.z); // 修改刚体速度并保留原来的 Y 轴速度
    } // 方法结束
} // 类结束

什么时候需要提高 FixedUpdate 频率?

比如高速物体、精密物理、赛车、弹球、格斗判定、对物理稳定性要求很高的玩法,可能会适当降低 fixedDeltaTime

但一般不会盲目调得很高,而是优先考虑:

开启连续碰撞检测、减少刚体数量、简化碰撞体、减少 FixedUpdate 里的逻辑、把非物理逻辑移到 Update 或定时器里。

一句话总结

NOTE

FixedUpdate 频率过高会让物理更细,但会显著增加 CPU 消耗,还可能导致卡顿后连续补跑多个物理帧。面试里可以说:它不是越高越好,要根据物理精度和性能预算平衡。

如何限制最高帧率?

面试高分回答

Unity 里限制最高帧率,最常用的是设置 Application.targetFrameRate。但要注意:在 PC 端如果开启了垂直同步 QualitySettings.vSyncCount,它可能会优先控制帧率,导致 targetFrameRate 不按你预期生效。

unity-limit-max-framerate

最常用写法

c
using UnityEngine; // 引入 Unity 常用类型

public class FrameRateLimiter : MonoBehaviour // 定义帧率限制组件
{ // 类开始
    [SerializeField] private int targetFrameRate = 60; // 设置目标最高帧率

    private void Awake() // 游戏对象初始化时调用
    { // 方法开始
        QualitySettings.vSyncCount = 0; // 关闭垂直同步,让 targetFrameRate 生效
        Application.targetFrameRate = targetFrameRate; // 设置游戏最高帧率
    } // 方法结束
} // 类结束

几个常见设置

Application.targetFrameRate = 30:省电、降发热,适合低端机或非强操作游戏。

Application.targetFrameRate = 60:最常见,流畅度和性能比较平衡。

Application.targetFrameRate = 120:适合高刷设备,但更耗电、更容易发热。

Application.targetFrameRate = -1:使用平台默认帧率策略。

VSync 是什么关系?

QualitySettings.vSyncCount = 1 表示等待显示器垂直同步。比如显示器是 60Hz,游戏通常会被限制在 60 FPS;如果是 120Hz,就可能是 120 FPS。

QualitySettings.vSyncCount = 2 表示每隔两个垂直同步刷新一次。比如 60Hz 显示器,大约就是 30 FPS。

QualitySettings.vSyncCount = 0 表示不使用 VSync,这时 Application.targetFrameRate 更容易按你设置的数值工作。

移动端怎么说?

移动端一般更常用 Application.targetFrameRate,因为限制帧率可以明显降低功耗、发热和电池消耗。比如项目性能压力大时,可以提供画质档位:

c
using UnityEngine; // 引入 Unity 常用类型

public class GraphicsFrameRateSetting : MonoBehaviour // 定义画质帧率设置组件
{ // 类开始
    public void SetLowPowerMode() // 设置省电模式
    { // 方法开始
        Application.targetFrameRate = 30; // 把最高帧率限制为 30
    } // 方法结束

    public void SetBalancedMode() // 设置平衡模式
    { // 方法开始
        Application.targetFrameRate = 60; // 把最高帧率限制为 60
    } // 方法结束

    public void SetHighFrameRateMode() // 设置高帧率模式
    { // 方法开始
        Application.targetFrameRate = 120; // 把最高帧率限制为 120
    } // 方法结束
} // 类结束

一句话总结

WARNING

限制最高帧率用 Application.targetFrameRate,PC 端要注意 vSyncCount 是否覆盖它;移动端常用 30、60、120 档位来平衡流畅度、发热和耗电。

VSync 是什么?

面试高分回答

VSync 全称是 Vertical Synchronization,中文叫垂直同步。它的作用是:让 GPU 提交画面的时机,和显示器刷新的节奏对齐,从而减少画面撕裂。

unity-vsync-explained

先理解显示器刷新

比如 60Hz 显示器,意思是它每秒刷新 60 次画面,也就是大约每 16.67ms 刷新一次。

如果 GPU 渲染得很快,在显示器刷新到一半的时候就把新画面塞进去,屏幕上就可能出现:上半部分是旧画面,下半部分是新画面。这个现象叫画面撕裂

VSync 做了什么

开启 VSync 后,GPU 渲染完一帧不会马上显示,而是等显示器下一次刷新时机,再把完整的一帧交给显示器。

这样一整屏看到的是同一帧画面,撕裂会明显减少。

代价是什么

VSync 的优点是画面更稳定,不容易撕裂。

缺点是可能增加输入延迟。因为 GPU 渲染完后要等显示器刷新点,玩家按下按键到画面反应之间可能多等一点时间。

还有一个问题是,如果性能达不到显示器刷新率,帧率可能会掉得比较明显。比如 60Hz 下开 VSync,游戏稳定不了 60 FPS,就可能掉到更低档位,具体表现会看平台和缓冲策略。

Unity 里怎么设置

c
using UnityEngine; // 引入 Unity 常用类型

public class VSyncSetting : MonoBehaviour // 定义垂直同步设置组件
{ // 类开始
    private void Awake() // 游戏对象初始化时调用
    { // 方法开始
        QualitySettings.vSyncCount = 1; // 开启垂直同步,每次显示器刷新提交一次画面
        Application.targetFrameRate = -1; // 使用平台默认帧率策略,避免和 VSync 设置互相干扰
    } // 方法结束
} // 类结束

几个值怎么理解

QualitySettings.vSyncCount = 0:关闭 VSync,这时可以用 Application.targetFrameRate 控制目标帧率。

QualitySettings.vSyncCount = 1:每次显示器刷新同步一次。60Hz 显示器上通常接近 60 FPS,120Hz 显示器上可能接近 120 FPS。

QualitySettings.vSyncCount = 2:每两次显示器刷新同步一次。60Hz 显示器上大约是 30 FPS。

面试一句话

IMPORTANT

VSync 是让 GPU 出帧和显示器刷新同步的机制,可以减少画面撕裂,但可能带来输入延迟和帧率受刷新率限制的问题。在 Unity 里主要通过 QualitySettings.vSyncCount 控制。

游戏逻辑帧和渲染帧可以分离吗?

面试高分回答

可以分离,而且很多游戏都会这么做。核心思想是:游戏逻辑按固定 Tick 更新,渲染按设备帧率绘制。逻辑负责规则,渲染负责表现,中间用插值让画面看起来顺滑。

game-logic-render-frame-separation

为什么要分离?

假设游戏逻辑固定 30 Tick/s,那么每次逻辑只推进 1 / 30 秒。无论玩家设备是 30 FPS、60 FPS 还是 120 FPS,移动、技能 CD、AI 判断、战斗判定都按同样的节奏走。

渲染帧则可以根据设备性能变化。性能好就 120 FPS,性能差就 45 FPS,但不应该让角色因为帧率不同而跑得更快或更慢。

Unity 里的对应关系

Update 更接近渲染帧,每渲染一帧通常调用一次,适合处理输入、相机、UI、表现层逻辑。

FixedUpdate 是固定时间步长,适合处理物理、刚体、固定 Tick 逻辑。

但项目里如果要做更严格的战斗帧、服务器同步、录像回放,也可以自己写一个 Tick 系统,而不是完全依赖 FixedUpdate

简单 Tick 系统示例

c
using UnityEngine; // 引入 Unity 常用类型

public class LogicTickRunner : MonoBehaviour // 定义逻辑 Tick 驱动器
{ // 类开始
    [SerializeField] private int tickRate = 30; // 定义每秒逻辑 Tick 次数
    private float fixedStep; // 保存每个 Tick 的固定时间步长
    private float accumulator; // 保存累计但还没执行完的时间
    private int tickIndex; // 保存当前逻辑 Tick 编号

    private void Awake() // 初始化时调用
    { // 方法开始
        fixedStep = 1f / tickRate; // 根据 Tick 频率计算固定步长
    } // 方法结束

    private void Update() // 每个渲染帧调用一次
    { // 方法开始
        accumulator += Time.deltaTime; // 把这一帧经过的时间加入累计器
        while (accumulator >= fixedStep) // 如果累计时间足够执行一次逻辑 Tick
        { // while 开始
            TickLogic(fixedStep); // 执行一次固定步长的游戏逻辑
            accumulator -= fixedStep; // 从累计器中扣掉已经执行的时间
            tickIndex++; // 逻辑 Tick 编号加一
        } // while 结束
        float renderAlpha = accumulator / fixedStep; // 计算当前渲染帧位于两个逻辑帧之间的比例
        RenderInterpolation(renderAlpha); // 根据比例做渲染插值,让画面更平滑
    } // 方法结束

    private void TickLogic(float dt) // 执行固定 Tick 的游戏逻辑
    { // 方法开始
        Debug.Log("逻辑 Tick:" + tickIndex + ",步长:" + dt); // 输出当前逻辑 Tick 信息
    } // 方法结束

    private void RenderInterpolation(float alpha) // 执行渲染插值逻辑
    { // 方法开始
        Debug.Log("渲染插值比例:" + alpha); // 输出当前渲染插值比例
    } // 方法结束
} // 类结束

分离后会出现什么情况?

一帧渲染时间很短时,可能这一帧不执行逻辑 Tick,只画一次画面。

一帧渲染时间刚好时,可能执行一次逻辑 Tick。

一帧卡顿很久时,可能连续执行多次逻辑 Tick 来追赶时间。

所以真实项目里通常会限制单帧最大补跑次数,避免卡顿后疯狂补 Tick,把游戏拖得更卡。

一句话总结

WARNING

游戏逻辑帧和渲染帧可以分离。逻辑帧固定,保证规则稳定;渲染帧灵活,保证画面尽量流畅;中间用插值解决视觉抖动。面试里能说出“固定 Tick、变帧率渲染、插值、补步上限”,就很加分。

帧率不稳定会影响手感吗?

面试高分回答

会,而且影响很明显。手感不只看平均 FPS,更要看每一帧耗时是否稳定,也就是 Frame Time。稳定的 45 FPS,有时候会比忽高忽低的 60 FPS 更舒服。

unity-unstable-framerate-feel

为什么会影响手感?

如果帧率稳定,比如每帧都是 16ms 左右,玩家输入、角色移动、镜头跟随、动画播放都会比较均匀。

但如果帧率不稳定,比如有的帧 8ms,有的帧 40ms,画面节奏就会忽快忽慢。玩家会感觉角色“不跟手”、镜头“抖”、攻击判定“飘”、操作反馈“粘”。

具体影响

输入延迟会波动。 玩家按下攻击键,如果刚好遇到一帧很长的卡顿,这次输入到画面反馈就会变慢。

移动会不均匀。 即使用了 Time.deltaTime 保证平均速度正确,单帧 deltaTime 忽大忽小,画面上仍然可能出现一顿一顿的感觉。

相机跟随会抖。 相机如果在 LateUpdate 里跟随角色,角色帧时间不稳定,相机插值也可能忽快忽慢。

物理和判定可能不稳定。 比如攻击窗口、闪避窗口、碰撞检测,如果帧时间波动太大,玩家会感觉判定不可靠。

减少影响的常见做法

输入在 Update 里采样,然后缓存输入,不要漏掉玩家按键。

物理移动放在 FixedUpdate,刚体开启插值 Rigidbody Interpolation,减少画面抖动。

角色表现层、相机跟随、动画过渡要做平滑处理。

避免一帧里出现 GC、同步加载、大量 Instantiate、复杂寻路、大量 UI 重建。

必要时限制最高帧率,让帧时间更稳定,比如稳定 60 FPS 或稳定 30 FPS。

输入缓存示例

c
using UnityEngine; // 引入 Unity 常用类型

public class InputBufferExample : MonoBehaviour // 定义输入缓存示例组件
{ // 类开始
    private bool jumpPressed; // 记录玩家是否按下跳跃键

    private void Update() // 每个渲染帧调用一次
    { // 方法开始
        if (Input.GetKeyDown(KeyCode.Space)) // 如果这一帧检测到空格键按下
        { // if 开始
            jumpPressed = true; // 缓存跳跃输入,避免后续逻辑因为帧率波动漏掉输入
        } // if 结束
    } // 方法结束

    private void FixedUpdate() // 每个固定物理帧调用一次
    { // 方法开始
        if (jumpPressed) // 如果之前缓存过跳跃输入
        { // if 开始
            Debug.Log("执行跳跃逻辑"); // 在固定物理帧中执行跳跃逻辑
            jumpPressed = false; // 消耗这次跳跃输入
        } // if 结束
    } // 方法结束
} // 类结束

一句话总结

TIP

帧率不稳定一定会影响手感,因为玩家感受到的是“输入到画面反馈的节奏”。面试里可以说:平均 FPS 只是结果,真正决定手感的是稳定的 Frame Time、低输入延迟和一致的逻辑更新节奏。

文章评价

读完这篇,留下你的看法

暂无审核通过的评价。

登录账号后才能评价。

本站访客数0总站访问量0本页访问量0