Unity 常用 API 与核心类全解:从生命周期到协程,附性能避坑清单(Unity 6 适配)

Unity 常用 API 与核心类全解:从生命周期到协程,附性能避坑清单(Unity 6 适配)

适用引擎版本:Unity 6(6000.x)/ Unity 6.3 LTS,同时向下兼容 2021/2022 LTS

语言:C# | 覆盖范围:内置渲染管线、URP 均适用

更新时间:2026-09-18

这篇做了四件事:

  1. 每个 API 都给出「签名 → 最小可用示例 → 常见坑 → 性能代价」,拒绝只列不解释。
  2. 补齐并修正了常见资料里缺失或写错的部分 ,例如 Random.Range 整数版与浮点版的边界语义不同、Mathf.Round 采用的是"银行家舍入"、协程在 MonoBehaviour 禁用时并不会停止等。
  3. 适配 Unity 6 的 API 变更 ,例如 Rigidbody.velocity 已更名为 linearVelocityFindObjectOfType 已弃用。
  4. 增加横向对比表和一张性能避坑清单,让这篇文章可以当中速手册用,也可以当代码 Review 的检查表用。

阅读建议:初学可以按顺序读第 2~9 节;已经会写代码的,直接跳到第 6、12、16、18、20 节和末尾的面试速答。


1. 全文知识地图

分类 涉及类型 你主要用它做什么
调试输出 Debug 打印日志、警告、错误,定位问题
生命周期 MonoBehaviour 事件函数 在正确的时间点执行正确的事
对象与组件 GameObjectComponentObject 查找、激活、增删组件、实例化与销毁
空间变换 TransformVector2/3Quaternion 位置、旋转、缩放、方向与距离计算
输入交互 InputOnMouseXXX 键盘、鼠标、触摸、手柄输入
通信 SendMessage 系列、UnityEvent 脚本之间传递消息
动画 Animator 状态机切换、参数驱动、动画事件
时间 Time 帧无关的移动、暂停、倒计时
数学与随机 MathfRandom 插值、取整、三角函数、随机数
异步 CoroutineInvokeAwaitable 延时、定时、分帧执行
物理 RigidbodyCollider 力的施加、碰撞与触发器事件

一句话总纲:Unity 脚本 = 在生命周期函数的正确时间点,用 Transform / Vector 计算空间,用 Component 拿到底层能力,用 Time 抹平帧率差异,用协程或 Invoke 处理时间维度。


2. 调试输出:Debug

三个方法的使用边界要分清,否则控制台会被淹没,真正的问题反而看不见。

方法 控制台颜色 使用场景
Debug.Log() 白色 常规流程、变量值跟踪
Debug.LogWarning() 黄色 可继续运行但需要关注的情况
Debug.LogError() 红色 异常、必须修复的错误,会带上堆栈

2.1 最小示例

csharp 复制代码
public class PlayerController : MonoBehaviour
{
    [SerializeField] private int playerHealth = 100;

    private void Start()
    {
        // 推荐用 $ 插值,可读性远好于字符串拼接
        Debug.Log($"游戏开始,角色生命值为:{playerHealth}");
    }

    private void TakeDamage(int damage)
    {
        playerHealth -= damage;
        if (playerHealth <= 20 && playerHealth > 0)
            Debug.LogWarning($"生命值偏低:{playerHealth}");
        if (playerHealth <= 0)
            Debug.LogError("玩家死亡!请检查死亡流程", this); // 第二个参数把报错定位到指定对象
    }
}

2.2 坑与优化

  • 正式包里残留大量 Debug.Log 会明显拖慢帧率。 字符串拼接 + 堆栈采集本身就是开销。
  • 不要用 "a" + b + "c" 拼日志,用 $"..." 插值(编译器会走 DefaultInterpolatedStringHandler,减少临时字符串)。

生产环境的正确做法是包一层可条件编译的日志门面:

csharp 复制代码
public static class Log
{
    // 只在编辑器 / Development Build 中保留,正式包会被编译期剥离
    [System.Diagnostics.Conditional("UNITY_EDITOR")]
    [System.Diagnostics.Conditional("DEVELOPMENT_BUILD")]
    public static void Info(object msg) => UnityEngine.Debug.Log(msg);
}

[Conditional] 的妙处是:调用点连同参数求值一起被编译器删除 ,比在方法内部写 if (debug) 更彻底(后者仍会执行字符串拼接)。


3. 生命周期函数:Unity 脚本的核心骨架

生命周期函数由引擎自动调用,写错时间点是新手 80% 的 Bug 来源

3.1 完整执行顺序

顺序 函数 触发时机 调用次数
1 Reset() 编辑器中组件被添加或被 Reset 时 仅编辑器
2 Awake() 对象被实例化 / 场景加载时 一次
3 OnEnable() 每次 enabled = true 或对象被激活 多次
4 Start() 首次 Update() 之前 一次
5 FixedUpdate() 固定时间步长(默认 0.02s) 多次
6 OnTriggerXXX() 触发器进出、停留(物理步内) 多次
7 OnCollisionXXX() 碰撞进出、停留(物理步内) 多次
8 Update() 每帧一次 多次
9 协程 yield return null 之后 每帧 Update 之后恢复 多次
10 LateUpdate() 所有 Update() 之后 多次
11 OnGUI() GUI 事件渲染,每帧通常两次 多次
12 OnDrawGizmos() 编辑器 Scene 视图绘制 仅编辑器
13 OnDisable() enabled = false 或对象被销毁 多次
14 OnDestroy() 对象被真正销毁 一次
15 OnApplicationQuit() 应用退出 一次

移动端还需要关注:OnApplicationPause(bool)(切后台)、OnApplicationFocus(bool)(切焦点)。

关键结论:同一个对象内是 Awake → OnEnable → Start;而场景中所有对象的 AwakeOnEnable 都执行完毕后,才会执行第一个 Start 这就是"在 Awake 里初始化,在 Start 里访问别人"这条惯例的由来。

但要注意:不同脚本之间 Awake 的执行顺序是不保证的。

3.2 Awake vs Start

对比项 Awake() Start()
调用时机 对象实例化后立即 首次 Update 前,且晚于所有 Awake
对象被禁用时 仍然调用(只要对象被创建) 不调用,直到对象被激活
依赖其他对象 不安全 相对安全
典型用途 缓存自身组件引用、初始化字段 注册事件、开启协程、访问外部对象
csharp 复制代码
public class Enemy : MonoBehaviour
{
    [SerializeField] private float speed = 5f;
    private Rigidbody rb;
    private Transform target;

    private void Awake()
    {
        // 只碰自己身上的东西,最安全
        if (!TryGetComponent(out rb))
            Debug.LogError("Enemy 缺少 Rigidbody 组件", this);
    }

    private void Start()
    {
        // 现在可以安全地找外部对象了
        target = GameObject.FindWithTag("Player")?.transform;
    }
}

3.3 Update / FixedUpdate / LateUpdate 对比

这是面试必问、实战必踩的一组函数。

对比项 Update() FixedUpdate() LateUpdate()
调用频率 每帧一次,随帧率变化 固定间隔(由 Fixed Timestep 决定) 每帧一次,晚于所有 Update
时间字段 Time.deltaTime Time.fixedDeltaTime(此处读 deltaTime 返回的也是它) Time.deltaTime
Time.timeScale = 0 仍然执行 停止执行 仍然执行
主要用途 输入检测、UI、非物理逻辑 刚体受力、物理相关运动 相机跟随、IK、依赖他人最终位置的逻辑
csharp 复制代码
public class PlayerMovement : MonoBehaviour
{
    [SerializeField] private float moveSpeed = 5f;
    private Vector3 moveInput;

    private void Update()
    {
        // 1) 在 Update 里读输入
        float h = Input.GetAxisRaw("Horizontal");
        float v = Input.GetAxisRaw("Vertical");
        // ClampMagnitude 修复斜向移动速度 1.414 倍的经典 Bug
        moveInput = Vector3.ClampMagnitude(new Vector3(h, 0f, v), 1f);
    }

    private void FixedUpdate()
    {
        // 2) 在 FixedUpdate 里用输入(如果用 Rigidbody 驱动,详见第 18 节)
        transform.Translate(moveInput * (moveSpeed * Time.fixedDeltaTime), Space.World);
    }
}

"读输入用 Update,用输入驱动物理用 FixedUpdate" 这条分工是必须记住的。

LateUpdate 的经典用法是相机跟随,保证相机在角色移动之后再计算位置,否则画面会抖动:

csharp 复制代码
public class CameraFollow : MonoBehaviour
{
    [SerializeField] private Transform target;
    [SerializeField] private Vector3 offset = new Vector3(0f, 2f, -5f);
    [SerializeField] private float smoothSpeed = 8f;

    private void LateUpdate()
    {
        Vector3 desired = target.position + offset;
        // 用 deltaTime 归一化,避免帧率影响跟随速度
        transform.position = Vector3.Lerp(transform.position, desired, smoothSpeed * Time.deltaTime);
    }
}

注意原文示例中直接写 Lerp(..., smoothSpeed)smoothSpeed = 0.125f 时在高帧率下会明显偏慢、低帧率下又过快。任何跟时间相关的插值都应该乘 Time.deltaTime

3.4 OnDestroy 与资源释放

csharp 复制代码
public class EventSubscriber : MonoBehaviour
{
    private void OnEnable()  => GameEvents.OnPlayerDied += HandlePlayerDied;
    private void OnDisable() => GameEvents.OnPlayerDied -= HandlePlayerDied;
    // 注意:OnDisable 是解绑的最佳位置,OnDestroy 有时会来不及
}

OnDestroyOnDisable 之后调用;如果对象在销毁前先被 SetActive(false)OnDisable 会先跑一次。事件订阅一定要成对解绑,否则会内存泄漏并拿到已销毁对象。


4. GameObject:场景中的基本单位

GameObject 本身只是一个容器,功能全部来自挂载的组件

4.1 查找对象

方法 查找范围 性能 建议
GameObject.Find("Name") 全场景激活对象,按名字 差,O(n) 只在初始化时用一次
GameObject.FindWithTag("Tag") 全场景按标签 标签需在 Tag Manager 预定义
GameObject.FindGameObjectsWithTag("Tag") 返回全部同标签对象 中,每次分配数组 初始化时用,勿每帧调用
transform.Find("Child/Sub") 直接子节点的相对路径 层级内查找首选
FindAnyObjectByType<T>() 任意一个 T 类型对象 较好,找到即停 Unity 2023.1+ 推荐
FindFirstObjectByType<T>() 第一个 T 类型对象(有序) 需要确定性结果时用
FindObjectsByType<T>(FindObjectsSortMode.None) 全部 T 类型对象 较好 替代已弃用的 FindObjectsOfType
csharp 复制代码
// 已弃用(Unity 2023.1 起)
// var gm = FindObjectOfType<GameManager>();
// 推荐写法
var gm = FindFirstObjectByType<GameManager>();

为什么旧方法被弃用? 因为 FindObjectOfType 在返回前会按 InstanceID 排序,这个排序占了绝大部分耗时(部分测试中超过 90%),而绝大多数场景根本不需要这个顺序。

更好的做法 :能拖引用就拖引用,能单例就单例,能事件就事件,不要用字符串查找作为主要手段

csharp 复制代码
public class UIManager : MonoBehaviour
{
    // 首选:Inspector 拖引用,零运行时查找开销
    [SerializeField] private GameObject pausePanel;

    public static UIManager Instance { get; private set; }

    private void Awake()
    {
        if (Instance != null && Instance != this) { Destroy(gameObject); return; }
        Instance = this;
    }

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Escape))
            pausePanel.SetActive(!pausePanel.activeSelf);
    }
}

4.2 激活状态

csharp 复制代码
gameObject.SetActive(false);   // 关闭自身及所有子物体
bool self = gameObject.activeSelf;        // 只看自己
bool inHierarchy = gameObject.activeInHierarchy; // 自己 + 所有父级都激活才为 true

注意点

  • 父物体关闭后,子物体 activeSelf 仍可能为 true,但 activeInHierarchyfalse。判断"实际是否显示"要用后者。
  • 对象被 SetActive(false) 后,其上的 Update、协程都会停止(详见第 16 节)。
  • 频繁 SetActive 会触发大量 OnEnable/OnDisable 与 Canvas 重建;纯 UI 显隐建议改用 CanvasGroup.alpha,或只关闭 Renderer

4.3 动态增删组件

csharp 复制代码
var rb = gameObject.AddComponent<Rigidbody>();   // 同类型组件重复添加会抛异常
Destroy(GetComponent<BoxCollider>());            // 删除组件

需要保证组件存在时,用特性声明依赖,避免运行时才发现问题:

csharp 复制代码
[RequireComponent(typeof(Rigidbody))]
[DisallowMultipleComponent]
public class PhysicsObject : MonoBehaviour { }

4.4 标签比较

csharp 复制代码
// 差:== 会分配字符串并做完整比较
if (other.gameObject.tag == "Player") { }

// 好:CompareTag 只比较标签索引,零 GC
if (other.CompareTag("Player")) { }

5. MonoBehaviour 基类

继承 MonoBehaviour 才具备"被引擎驱动"的能力:生命周期回调、协程、Inspector 序列化。

5.1 常用内置成员

成员 说明
gameObject 当前脚本所在的对象
transform 快捷访问 gameObject.transform
enabled 是否启用该脚本(不影响对象上的其他组件)
name 对象名(组件名即对象名)
Invoke / InvokeRepeating / CancelInvoke 延时调用
StartCoroutine / StopCoroutine / StopAllCoroutines 协程
print() 等价于 Debug.Log,可读性一般,不推荐

5.2 Inspector 暴露字段的正确姿势

csharp 复制代码
public class Weapon : MonoBehaviour
{
    // 不推荐:public 字段对全工程可见,容易被外部随意修改
    // public float damage = 10f;

    [Header("伤害参数")]
    [SerializeField, Tooltip("单次伤害值"), Range(1f, 100f)]
    private float damage = 10f;

    [SerializeField, HideInInspector] private int debugId;

    public float Damage => damage; // 需要外部读取时再开放只读属性
}

6. Component:组件查询与性能

组件查询是新手代码里最大的性能黑洞 ------因为随手就写在 Update 里了。

6.1 查询方法族

方法 作用范围 说明
GetComponent<T>() 自身 最常用,找不到返回 null
TryGetComponent<T>(out T comp) 自身 无 GC 分配,2019.2+ 推荐
GetComponentInChildren<T>() 自身 + 子物体(深度优先) 包含自身
GetComponentInParent<T>() 自身 + 父物体 包含自身
GetComponents<T>() 自身(可多个) 返回数组,必有分配
GetComponentsInChildren<T>() 自身 + 全部子物体 层级深时有开销
GetComponentsInChildren<T>(true) 同上,含未激活对象 默认不含未激活

6.2 示例

csharp 复制代码
// 差:空引用风险,且每帧都在查找
void Update()
{
    GetComponent<Rigidbody>().AddForce(Vector3.up);
}

// 好:Awake 缓存一次,并用 TryGetComponent 保证安全
private Rigidbody rb;

private void Awake()
{
    if (!TryGetComponent(out rb))
    {
        Debug.LogError($"{name} 缺少 Rigidbody,脚本已禁用", this);
        enabled = false;
    }
}

private void FixedUpdate() => rb.AddForce(Vector3.up, ForceMode.Acceleration);

6.3 性能排行(从好到差)

  1. Inspector 拖引用 / 单例 / 事件 ------ 无运行时开销,首选
  2. TryGetComponent(一次)并缓存 ------ 几乎无开销
  3. transform.FindGetComponentInChildren ------ 层级内可控
  4. GetComponentsInChildren ------ 递归 + 数组分配
  5. GameObject.Find / FindObjectsByType ------ 全场景遍历,永远不要放在 Update

经验法则:任何 GetComponentFindCamera.mainGetComponents 出现的位置,只允许在 Awake / Start 里。


7. Transform:位置、旋转、缩放

Transform 是每个 GameObject 必带的组件,也是所有空间逻辑的入口。

7.1 属性一览

属性 坐标系 类型 说明
position 世界 Vector3 世界坐标位置
rotation 世界 Quaternion 世界旋转(四元数)
eulerAngles 世界 Vector3 世界欧拉角,读写有换算开销
localPosition 父级局部 Vector3 相对父物体的位置
localRotation 父级局部 Quaternion 相对父物体的旋转
localScale 父级局部 Vector3 相对父物体的缩放
lossyScale 世界 Vector3 世界缩放,只读
forward / right / up 世界 Vector3 自身朝向单位向量
childCount / parent --- --- 层级信息
csharp 复制代码
// 世界坐标与局部坐标的相互转换,做 UI/挂点计算时非常有用
Vector3 worldPos = transform.TransformPoint(localPoint);
Vector3 localPos = transform.InverseTransformPoint(worldPos);

7.2 移动:Translate

Translate 默认使用 Space.Self(自身坐标系),这是最容易搞错的一点。

csharp 复制代码
// 沿自身前方移动(默认 Space.Self)
transform.Translate(Vector3.forward * speed * Time.deltaTime);

// 沿世界前方移动
transform.Translate(Vector3.forward * speed * Time.deltaTime, Space.World);

Translate 会直接修改 position / localPosition。如果对象带 Rigidbody 且非 Kinematic,不要用 Translate 移动------你会绕过物理求解,导致穿模、抖动、碰撞丢失。正确做法见第 18 节。

7.3 旋转:Rotate / RotateAround / LookAt

csharp 复制代码
// 绕自身 Y 轴旋转
transform.Rotate(Vector3.up * rotateSpeed * Time.deltaTime);

// 绕世界 Y 轴旋转
transform.Rotate(Vector3.up * rotateSpeed * Time.deltaTime, Space.World);

// 绕某个中心点公转(行星绕恒星、敌人环绕玩家)
transform.RotateAround(target.position, Vector3.up, orbitSpeed * Time.deltaTime);

// 让 Z 轴朝向目标(炮塔、追踪)
transform.LookAt(target);

// 平滑朝向目标,避免瞬间转头
Quaternion targetRot = Quaternion.LookRotation(target.position - transform.position);
transform.rotation = Quaternion.Slerp(transform.rotation, targetRot, turnSpeed * Time.deltaTime);

  • 旋转角正负遵循右手螺旋定则:拇指指向轴的正方向,其余四指方向为正角。
  • 四元数不能直接改 x/y/z ,那四个分量互相耦合。要么整体赋值 Quaternion.Euler(...),要么用 Quaternion.Slerp/LookRotation 组合。
  • 欧拉角存在万向节死锁(Gimbal Lock),连续旋转建议始终使用四元数。

7.4 缩放

csharp 复制代码
transform.localScale = new Vector3(2f, 1f, 1f);

// 运行时渐变放大(特效常用)
private void Update()
{
    transform.localScale += Vector3.one * (growthSpeed * Time.deltaTime);
}

lossyScale 由所有父级的 localScale 累乘得到,因此无法直接设置 。若需要世界缩放固定为某值,必须反向推算 localScale


8. Vector2 / Vector3:一切空间计算的基础

Vector2 用于 2D 与 UI 坐标,Vector3 用于 3D;两者 API 几乎对称。

8.1 运算与常用静态方法

成员 说明 典型用途
magnitude 向量长度,含开方 需要真实距离时
sqrMagnitude 长度平方,无开方 距离比较,性能更优
normalized 单位向量 只要方向不要长度
Vector3.Distance(a, b) 两点距离 判断是否进入范围
Vector3.Dot(a, b) 点积 判断朝向关系(前 / 后 / 垂直)
Vector3.Cross(a, b) 叉积 求法线、判断左右
Vector3.Lerp / Slerp 线性 / 球面插值 平滑移动、转向
Vector3.MoveTowards 匀速靠近 有上限的追踪
Vector3.ClampMagnitude 限制长度 限制移动速度上限
Vector3.Angle(a, b) 夹角(度) 视野判断、攻击角度

8.2 关键优化:用 sqrMagnitude 代替 Distance

csharp 复制代码
// 有开方,开销大
if (Vector3.Distance(a, b) < detectRange) { }

// 无开方,等价的性能友好写法
if ((a - b).sqrMagnitude < detectRange * detectRange) { }

8.3 点积的实战意义

csharp 复制代码
Vector3 toTarget = (target.position - transform.position).normalized;

if (Vector3.Dot(transform.forward, toTarget) > 0.7f)
    Debug.Log("目标在正前方约 45 度扇形内"); // 0.7 ≈ cos45°

用点积做视野扇形判断,比 Angle 更便宜(无三角函数)。

8.4 坑

  • 零向量归一化会返回零向量 ,不报错但要小心后续逻辑出现除零或方向为 (0,0,0) 的异常:
csharp 复制代码
if (dir.sqrMagnitude > 0.0001f) { dir = dir.normalized; }
  • Vector2Vector3 之间存在隐式转换 ,但 Vector2 → Vector3 会把 z 置 0,容易在混用 2D/3D API 时埋雷。
  • 斜向输入记得 ClampMagnitude,否则对角线速度是直线的 √2 倍。

9. Input:用户输入接口

重要前提Input 属于旧版 Input Manager 。若项目只启用了新 Input System(Project Settings → Player → Active Input Handling 设为 Input System Package (New) ),InputOnMouseXXX 系列全部失效 。需要兼容时请改为 Both,或统一迁移到新输入系统。

9.1 键盘

csharp 复制代码
if (Input.GetKey(KeyCode.Space))      { } // 按住期间持续为 true
if (Input.GetKeyDown(KeyCode.Space))  { } // 仅按下那一帧
if (Input.GetKeyUp(KeyCode.Space))    { } // 仅松开那一帧

9.2 鼠标

csharp 复制代码
if (Input.GetMouseButtonDown(0)) { }  // 0 左键 / 1 右键 / 2 中键
if (Input.GetMouseButton(1))     { }
if (Input.GetMouseButtonUp(2))   { }

Vector3 screenPos = Input.mousePosition;                 // z 恒为 0
Vector3 worldPos = Camera.main.ScreenToWorldPoint(new Vector3(screenPos.x, screenPos.y, 10f));

Camera.main 在 2020.2+ 内部做了缓存,但每帧调用仍有开销 ,请在 Awake 中缓存 Camera 引用。

9.3 轴输入(推荐用于移动)

csharp 复制代码
float smooth = Input.GetAxis("Horizontal");    // -1 ~ 1,带平滑,适合角色移动
float raw    = Input.GetAxisRaw("Horizontal"); // -1 / 0 / 1,无平滑,适合 UI 导航

9.4 触摸

csharp 复制代码
if (Input.touchCount > 0)
{
    Touch touch = Input.GetTouch(0);
    if (touch.phase == TouchPhase.Began) { /* ... */ }
}

9.5 移动端与跨平台注意事项

  • 移动平台应完全避免依赖 Input.GetKey,改用虚拟摇杆或 Input.touchCount
  • OnMouseXXX 在移动端不可用。
  • 新输入系统下要读取手柄/多设备,建议直接使用 Input Actions 资源而非轮询。

10. 消息系统:SendMessage / BroadcastMessage / SendMessageUpwards

csharp 复制代码
gameObject.SendMessage("TakeDamage", 10);        // 自身所有脚本
gameObject.BroadcastMessage("OnEnemySpawn");     // 自身 + 所有子物体
gameObject.SendMessageUpwards("OnChildEvent");   // 自身 + 所有父物体

10.1 三者的差异

方法 传播方向 适用场景
SendMessage 仅自身挂载的脚本 同一对象内组件通信
BroadcastMessage 自身 → 所有子孙 通知整棵子树(如全体敌人进入战斗)
SendMessageUpwards 自身 → 所有祖先 子物体向父级汇报

10.2 坑(这也是它被诟病的原因)

  • 基于反射 + 字符串匹配 ,性能比直接调用差一个数量级,且方法名写错不会有任何提示,静默失败。
  • 不支持返回值,参数最多一个。
  • 传播顺序依赖脚本挂载顺序与层级顺序,不可控。
  • 递归查找子物体/父物体,层级深时开销明显。

10.3 替代方案

方案 性能 类型安全 适用
直接引用调用 最高 关系明确的组件
UnityEvent(Inspector 可视化) 策划配置驱动的交互
C# event / Action 全局广播、系统解耦
ScriptableObject 事件通道 跨场景、模块化架构
SendMessage 系列 仅用于极简原型
csharp 复制代码
// 更现代的事件写法
public static class GameEvents
{
    public static event System.Action<int> OnPlayerDamaged;

    public static void RaisePlayerDamaged(int amount) => OnPlayerDamaged?.Invoke(amount);
}

11. Animator:动画状态机

Animator 的核心是动画状态机:多个状态 + 过渡条件 + 参数。

11.1 参数驱动状态切换

csharp 复制代码
public class PlayerAnimatorController : MonoBehaviour
{
    private static readonly int SpeedHash = Animator.StringToHash("Speed");
    private static readonly int IsGroundedHash = Animator.StringToHash("IsGrounded");
    private static readonly int AttackHash = Animator.StringToHash("Attack");

    private Animator animator;

    private void Awake() => animator = GetComponent<Animator>();

    private void Update()
    {
        float speed = Input.GetAxisRaw("Horizontal");
        // 用 Hash 代替字符串,省去每次的字符串哈希计算
        animator.SetFloat(SpeedHash, Mathf.Abs(speed), 0.1f, Time.deltaTime);
        animator.SetBool(IsGroundedHash, true);

        if (Input.GetKeyDown(KeyCode.J))
            animator.SetTrigger(AttackHash);
    }
}

11.2 直接播放与过渡

csharp 复制代码
animator.Play("ExplosionAnimation");     // 立即切换,会打断当前过渡
animator.CrossFade("Run", 0.15f);        // 带混合时长的平滑过渡,更自然
animator.Play("Attack", 0, 0f);          // 第三参数为 normalizedTime,可重播

11.3 动画事件

在 Animation 窗口的关键帧上添加事件,动画播放到该帧时回调:

csharp 复制代码
// 挂在带 Animator 的物体上,名字必须与动画事件中的函数名一致
private void OnAttackHitFrame()
{
    // 触发伤害判定,保证"打到人的那一刻"与表现一致
    HitDetector.Detect(attackPoint.position, attackRadius);
}

11.4 注意事项

  • 状态机复杂度要克制,超过 10 个状态的单层机建议分层(Layer)+ 子状态机(Sub-State Machine)。
  • 参数名统一管理,用 Animator.StringToHash 缓存,避免字符串散落各处。
  • SetTrigger 会自行复位,SetBool 不会;忘记复位是"动画卡在攻击状态"的常见原因。
  • Animator 的 Update Mode 可设为 Animate Physics,与物理同步。

12. Time:抹平帧率差异的关键

属性 含义 timeScale 影响
Time.time 游戏开始至今的缩放时间 是(暂停时不变)
Time.deltaTime 上一帧到当前帧的耗时
Time.unscaledDeltaTime 真实帧间隔
Time.fixedDeltaTime 物理固定步长(默认 0.02)
Time.timeScale 时间缩放系数(默认 1) ---
Time.realtimeSinceStartup 真实运行时间
Time.frameCount 累计帧数

12.1 为什么一切都要乘 deltaTime

csharp 复制代码
// 错:帧率越高移动越快
transform.Translate(Vector3.forward * 5f);

// 对:每秒移动 5 个单位,与帧率无关
transform.Translate(Vector3.forward * (5f * Time.deltaTime));

12.2 暂停的正确姿势

csharp 复制代码
public class PauseManager : MonoBehaviour
{
    [SerializeField] private GameObject pauseMenu;
    private bool isPaused;

    private void Update()
    {
        if (!Input.GetKeyDown(KeyCode.Escape)) return;

        isPaused = !isPaused;
        Time.timeScale = isPaused ? 0f : 1f;
        pauseMenu.SetActive(isPaused);
    }
}

Time.timeScale = 0 的影响范围,必须清楚:

会停 不会停
FixedUpdate Update / LateUpdate
WaitForSeconds 协程 yield return null 协程
Invoke / InvokeRepeating 计时 WaitForSecondsRealtimeWaitForEndOfFrame
依赖 deltaTime 的自定义计时 基于帧数的自定义计时
--- AudioSource 播放、UI EventSystem 点击

由此推出两个高频 Bug:

  1. 暂停菜单弹出后,背景 UI 按钮依然可点击 ------ 因为 EventSystem 走的是 Update。
  2. WaitForSeconds 做的技能冷却,在暂停时不会走。如果希望"暂停也计时",改用 WaitForSecondsRealtime

13. Mathf:常用数学函数

13.1 取整三兄弟

csharp 复制代码
Mathf.Floor(3.8f);  // 3  向下取整(返回 float)
Mathf.Ceil(3.2f);   // 4  向上取整(返回 float)
Mathf.Round(3.8f);  // 4  四舍六入五取偶!
Mathf.RoundToInt(3.5f); // 4

必须知道的坑:Mathf.Round 采用"银行家舍入"(round half to even) ,遇到 .5 时返回偶数

csharp 复制代码
Debug.Log(Mathf.Round(10.5f)); // 10
Debug.Log(Mathf.Round(11.5f)); // 12
Debug.Log(Mathf.Round(-10.5f)); // -10

如果你的业务需要"传统四舍五入",要自己写:

csharp 复制代码
int RoundHalfUp(float v) => Mathf.FloorToInt(v + 0.5f);

13.2 限制与插值

csharp 复制代码
Mathf.Clamp(hp, 0f, maxHp);                  // 夹紧到区间
Mathf.Clamp01(t);                            // 夹紧到 0~1
Mathf.Lerp(0f, 100f, 0.5f);                  // 50,线性插值
Mathf.InverseLerp(0f, 100f, 25f);            // 0.25,反向求比例
Mathf.MoveTowards(cur, target, speed * Time.deltaTime); // 匀速逼近
Mathf.SmoothStep(0f, 1f, t);                 // 带缓入缓出

Lerp 的经典误用Lerp(a, b, 0.1f) 每帧调用并不会"匀速接近",而是距离越近越慢的指数逼近 。想要严格匀速请用 MoveTowards,想要与帧率无关请用 1 - Mathf.Exp(-k * Time.deltaTime) 或直接把 k*deltaTime 作为 t。

13.3 三角函数与角度

csharp 复制代码
float rad = 30f * Mathf.Deg2Rad;   // 度 → 弧度
float deg = 0.5f * Mathf.Rad2Deg;  // 弧度 → 度

Mathf.Sin(rad); Mathf.Cos(rad); Mathf.Tan(rad); // 参数必须是弧度
Mathf.Atan2(dy, dx) * Mathf.Rad2Deg;            // 由方向求角度,比 Atan 更稳

正弦波的典型用法(漂浮、摆动、呼吸缩放):

csharp 复制代码
// 物体上下漂浮
float y = baseY + Mathf.Sin(Time.time * frequency) * amplitude;
transform.position = new Vector3(transform.position.x, y, transform.position.z);

13.4 其他

csharp 复制代码
Mathf.Abs(-5f);          // 5
Mathf.Min(5f, 10f);      // 5
Mathf.Max(5f, 10f);      // 10
Mathf.Pow(2f, 3f);       // 8   幂运算
Mathf.Sqrt(16f);         // 4   平方根,参数必须非负
Mathf.Sign(-3f);         // -1
Mathf.Approximately(a, b); // 浮点近似比较,替代 ==
Mathf.Repeat(t, 1f);     // 循环取值,做循环背景
Mathf.PingPong(t, 1f);   // 往返取值,做往复运动

浮点数不要用 == 比较 ,用 Mathf.Approximately 或自行设置容差 Mathf.Abs(a - b) < 1e-5f


14. Random:随机数

14.1 两个重载的边界语义不同(高频错误点)

重载 返回范围 是否包含最大值
Random.Range(int min, int max) [min, max) 不包含 max
Random.Range(float min, float max) [min, max] 包含 max
csharp 复制代码
int level = Random.Range(5, 11);      // 5~10,想取到 10 必须写 11
float f = Random.Range(0f, 1f);       // 0.0 ~ 1.0,可以取到 1.0

很多资料把浮点版也写成"不包含最大值",这是错的,会导致边界概率计算出现偏差。

14.2 常用随机值

csharp 复制代码
Random.value;                 // 0.0~1.0 的随机浮点
Random.rotation;              // 随机四元数旋转
Random.rotationUniform;       // 均匀分布的随机旋转(更好的各向同性)
Random.insideUnitCircle;      // 单位圆内随机 Vector2
Random.onUnitSphere;          // 单位球面上随机 Vector3(均匀)
Random.insideUnitSphere;      // 单位球内随机 Vector3

// 按权重抽取(掉落系统常用)
float r = Random.value;
string item = r < 0.7f ? "普通" : r < 0.95f ? "稀有" : "传说";

14.3 随机种子与可复现

csharp 复制代码
Random.InitState(12345);       // 全局种子,相同种子产生相同序列

用途 :关卡生成、回放系统、自动化测试。注意Random 是全局状态,任何模块调用都会影响序列;联机同步场景应使用独立的 System.Random 实例或种子化的确定性随机。

14.4 坑

  • 大量调用 Random.Range 会累积开销,能预先算好就不要放进 Update
  • 需要用 System.Random 做与 Unity 无关的确定性随机时,不要与 UnityEngine.Random 混用。

15. OnMouseXXX:鼠标回调事件

回调 触发时机
OnMouseEnter() 鼠标进入碰撞体范围
OnMouseExit() 鼠标离开碰撞体范围
OnMouseOver() 鼠标停留在碰撞体上,每帧调用
OnMouseDown() 在碰撞体上按下鼠标左键
OnMouseUp() 按下的左键在碰撞体上释放
OnMouseDrag() 按住左键在碰撞体上拖动,每帧调用

15.1 悬停高亮示例(含材质泄漏修复)

csharp 复制代码
[RequireComponent(typeof(Collider))]
public class HoverHighlight : MonoBehaviour
{
    [SerializeField] private Color hoverColor = Color.grey;
    private Renderer rend;
    private Material mat;      // 实例化后的材质副本
    private Color originalColor;

    private void Awake()
    {
        rend = GetComponent<Renderer>();
        mat = rend.material;               // 访问 .material 会自动实例化,避免污染共享材质
        originalColor = mat.color;
    }

    private void OnMouseEnter() => mat.color = hoverColor;
    private void OnMouseExit()  => mat.color = originalColor;
}

常见坑 :直接改 rend.sharedMaterial.color 会修改所有使用该材质的物体;而反复访问 rend.material 会不断产生材质实例,造成泄漏。正确做法是 Awake 里取一次并缓存。

15.2 必须记住的四个前提

  1. 对象必须有 Collider,否则回调完全不触发。
  2. 对象不能在 Ignore Raycast(Layer 2),该层会被鼠标事件的射线忽略。
  3. 前方不能有 Collider 遮挡,被挡住的对象收不到事件。
  4. 项目必须启用旧版 Input ManagerActive Input HandlingInput Manager (Old)Both),否则回调失效------这是新输入系统项目里"鼠标悬停没反应"的头号原因。

15.3 性能说明

OnMouseOver / OnMouseDrag每帧回调 ,不要在里面做 FindGetComponent 等重操作。另外 Unity 会为鼠标事件做射线检测,场景中 Collider 数量极多时也会带来开销。


16. 协程 Coroutine:分帧与延时执行

协程不是线程:它仍然在主线程执行,只是能被"切成分帧的片段"。因此协程内做耗时同步计算,照样卡帧。

16.1 基本结构

csharp 复制代码
private IEnumerator Countdown(int from)
{
    for (int i = from; i > 0; i--)
    {
        Debug.Log(i);
        yield return new WaitForSeconds(1f); // 每秒一次,切回主线程
    }
    Debug.Log("开始!");
}

private void Start()
{
    StartCoroutine(Countdown(3));
}

16.2 各种 yield 指令的语义

yield 指令 恢复时机 timeScale 影响
yield return null 下一帧 Update 之后
yield return new WaitForSeconds(t) t 秒后
yield return new WaitForSecondsRealtime(t) 真实 t 秒后
yield return new WaitForFixedUpdate() 下一个物理步之后
yield return new WaitForEndOfFrame() 渲染完成后 否(不能用于打包后截图之外的帧内循环)
yield return StartCoroutine(other) 等待子协程结束 ---
yield break 直接结束协程 ---
yield return new WaitUntil(() => flag) 条件为真时
yield return new WaitWhile(() => flag) 条件为假时

16.3 启动与停止

csharp 复制代码
// 保存引用,才能精确停止
private Coroutine routine;

private void Start() => routine = StartCoroutine(Loop());

private void Stop() 
{
    if (routine != null) { StopCoroutine(routine); routine = null; }
}

停止协程的三种写法必须参数类型一致,否则停不掉:

csharp 复制代码
StartCoroutine("Loop");     // 字符串启动 → StopCoroutine("Loop")
StartCoroutine(Loop());     // IEnumerator → StopCoroutine(routine)

16.4 协程的生命周期(重点,容易答错)

操作 协程是否停止
gameObject.SetActive(false) 停止,且重新激活后不会恢复
monoBehaviour.enabled = false 不停止,继续运行
Destroy(gameObject) 停止
Time.timeScale = 0 不停止,但 WaitForSeconds 不再推进

还有个细节:SetActive(false) 之后,协程会先执行到下一个 yield 才真正停下 ,所以 yield 前的代码还是会跑完。

另外,对未激活对象调用 StartCoroutine 会直接报错

Coroutine couldn't be started because the game object 'X' is inactive!

16.5 实战用法

csharp 复制代码
// 1) 带取消的倒计时
private IEnumerator Cooldown(float cd, System.Action onReady)
{
    float t = cd;
    while (t > 0f)
    {
        t -= Time.deltaTime;
        cooldownUI.fillAmount = t / cd;
        yield return null;      // 逐帧更新 UI
    }
    onReady?.Invoke();
}

// 2) 分帧处理大量数据,避免一次性卡顿
private IEnumerator SpawnWave(int count)
{
    for (int i = 0; i < count; i++)
    {
        Instantiate(enemyPrefab, RandomPos(), Quaternion.identity);
        if (i % 10 == 0) yield return null;   // 每 10 个让出一帧
    }
}

16.6 Unity 6 的替代方案:Awaitable / async-await

Unity 6 引入了 Awaitable,可以在 MonoBehaviour 中直接用 async/await 而不必依赖 UniTask 等第三方库:

csharp 复制代码
private async Awaitable LoadAsync()
{
    await Awaitable.WaitForSecondsAsync(1f);      // 受 timeScale 影响
    await Awaitable.NextFrameAsync();             // 等一帧
    // 也可以用 Awaitable.EndOfFrameAsync() / Awaitable.FixedUpdateAsync()
    Debug.Log("加载完成");
}

协程 vs async/await 怎么选?

维度 Coroutine async/await + Awaitable
返回值 不支持 支持 await 结果
异常处理 无法 try/catch 跨 yield 可用 try/catch
耦合 必须挂在 MonoBehaviour 上 可写在纯 C# 类中
取消 手动 StopCoroutine CancellationToken
生态 资料最多,最稳 Unity 6+,逐步成为主流

新项目建议:简单定时逻辑用协程,涉及网络 / 资源 / 需要返回值和异常处理的,用 async/await。

16.7 性能提示

  • 每启动一个协程都会产生一次 GC 分配(编译器生成的迭代器对象),不要在高频循环里疯狂 StartCoroutine
  • 几乎每帧运行的协程,直接用 Update 更划算。
  • 嵌套协程会叠加内存开销,能合并就合并。
  • 复用等待对象:new WaitForSeconds(1f) 缓存成静态字段,可显著减少 GC。
csharp 复制代码
private static readonly WaitForSeconds OneSec = new WaitForSeconds(1f);
// ...
yield return OneSec;

17. Invoke:不写协程的延时调用

csharp 复制代码
Invoke(nameof(Explode), 2f);                    // 2 秒后调用一次
InvokeRepeating(nameof(Tick), 1f, 0.5f);        // 1 秒后开始,每 0.5 秒调用一次
CancelInvoke();                                  // 取消本脚本上全部待调用
CancelInvoke(nameof(Tick));                      // 取消指定方法

17.1 特性与坑

  • 基于字符串 + 反射查找方法 ,写错方法名不会报错,只是静默不执行。务必用 nameof() 而非硬编码字符串。
  • 不支持参数,需要传参请改用协程或 lambda 包装的状态变量。
  • Time.timeScale 影响:暂停时 Invoke 计时停止,恢复后继续。
  • 对象被 Destroy 时自动取消;但对象仅被 SetActive(false) 时不会取消,重新激活后会继续计时。
  • 建议在 OnDisable / OnDestroy 里主动 CancelInvoke(),避免复活后莫名其妙的回调。

17.2 与协程的取舍

需求 推荐
单次简单延时、无需参数 Invoke
重复定时、逻辑简单 InvokeRepeating
需要参数、需要中途等待/条件 协程
需要返回值、异常处理 async/await

注意:Invoke(nameof(F), 0f) 这类零延时 调用几乎会立即执行,暂停状态下也会触发------想彻底暂停请避免使用 Invoke


18. Rigidbody:物理驱动

18.1 Unity 6 的 API 变更(复制旧教程必踩)

Unity 5 / 2022 及以前 Unity 6(6000.x)
rb.velocity rb.linearVelocity
rb.drag rb.linearDamping
rb.angularDrag rb.angularDamping
rb.angularVelocity 不变

Rigidbody.velocity 在 Unity 6 中已被重命名,目的是与 angularVelocity 保持命名对称。旧写法会产生编译警告(部分版本直接报错),网络上大量 2023 年以前的教程代码在这里都会失效

18.2 两种移动方式,按需求选

csharp 复制代码
[RequireComponent(typeof(Rigidbody))]
public class PlayerMover : MonoBehaviour
{
    [SerializeField] private float moveSpeed = 6f;
    [SerializeField] private float jumpForce = 7f;
    [SerializeField] private float maxSpeed = 8f;

    private Rigidbody rb;
    private Vector3 input;
    private bool grounded;

    private void Awake() => rb = GetComponent<Rigidbody>();

    private void Update()
    {
        float h = Input.GetAxisRaw("Horizontal");
        float v = Input.GetAxisRaw("Vertical");
        input = Vector3.ClampMagnitude(new Vector3(h, 0f, v), 1f);

        if (Input.GetKeyDown(KeyCode.Space) && grounded)
            rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse);
    }

    private void FixedUpdate()
    {
        // 方式一:直接设速度 ------ 手感立刻响应,适合动作游戏
        Vector3 flat = new Vector3(input.x * moveSpeed, rb.linearVelocity.y, input.z * moveSpeed);
        if (flat.sqrMagnitude > maxSpeed * maxSpeed)
            flat = flat.normalized * maxSpeed;
        rb.linearVelocity = flat;

        // 方式二:施加力 ------ 更有惯性和"重量感",适合拟真类(二选一)
        // rb.AddForce(input * accel, ForceMode.Acceleration);
    }

    private void OnCollisionStay(Collision c) => grounded = true;
    private void OnCollisionExit(Collision c)  => grounded = false;
}

18.3 ForceMode 四种模式

模式 连续性 是否考虑质量 典型场景
Force 持续 引擎推力、持续加速
Acceleration 持续 与质量无关的恒定加速度
Impulse 瞬时 跳跃、被击退、爆炸
VelocityChange 瞬时 击退距离与质量无关时

记忆口诀 :持续推动用 Force,一次性冲击用 Impulse

18.4 碰撞与触发器

对比项 碰撞 OnCollisionXXX 触发器 OnTriggerXXX
Collider 的 Is Trigger 不勾选 勾选
是否产生物理阻挡 否,直接穿过
参数类型 Collision Collider
回调 Enter / Stay / Exit Enter / Stay / Exit
触发条件 双方都有 Collider,至少一方有非 Kinematic Rigidbody 同上
csharp 复制代码
private void OnCollisionEnter(Collision collision)
{
    // 用相对速度判断撞击力度,做伤害/音效
    float impact = collision.relativeVelocity.magnitude;
    if (impact > 5f) AudioManager.PlayHit(impact);
    Debug.Log($"撞到 {collision.gameObject.name},接触点 {collision.contactCount} 个");
}

private void OnTriggerEnter(Collider other)
{
    if (other.CompareTag("Pickup")) Destroy(other.gameObject);
}

18.5 物理调试的三个高频问题

现象 原因 解决
高速子弹穿墙 物理按离散步进计算,两帧之间越过了墙体 把 Rigidbody 的 Collision Detection 改为 Continuous / ContinuousDynamic
移动物体带相机拍摄时抖动 物理步进频率低于渲染帧率 Rigidbody 的 Interpolate 设为 Interpolate,或相机跟随放 LateUpdate
角色被碰撞撞倒 胶囊体容易被撞翻 Constraints → Freeze Rotation 勾选 X、Z

18.6 铁律

  • 物理相关一律放 FixedUpdate
  • 不要直接改带 Rigidbody 物体的 transform.position ,会瞬移并破坏碰撞求解;需要位移用 MovePosition(Kinematic 体质)或改 linearVelocity
  • 静态场景物体不要挂 Rigidbody,用 Collider 即可。
  • 数量多的小物体考虑 ECS/Jobs 或关闭不必要的插值。

19. 对象的创建与销毁

csharp 复制代码
// 创建
var go = Instantiate(prefab, position, rotation);
var go2 = Instantiate(prefab, parent);              // 指定父物体
var comp = Instantiate(componentRef, parent);       // 也可以直接实例化组件

// 销毁
Destroy(go);                    // 帧末销毁
Destroy(go, 3f);               // 3 秒后销毁(常用于特效、音效)
DestroyImmediate(go);          // 立即销毁,仅限编辑器代码
DontDestroyOnLoad(go);         // 跨场景保留

19.1 坑

  • Destroy延迟到帧末 执行的,调用后当帧内对象依然存在,== null 判断要小心:
csharp 复制代码
Destroy(go);
if (go == null) { }   // Unity 重载了 == ,这里为 true;但 go.transform 仍可访问
  • 频繁 Instantiate / Destroy(子弹、粒子)会产生大量 GC,必须用对象池
  • 跨场景对象的重复创建是最常见的"游戏越玩越卡"原因。

19.2 极简对象池

csharp 复制代码
public class SimplePool
{
    private readonly Queue<GameObject> pool = new();
    private readonly GameObject prefab;

    public SimplePool(GameObject prefab, int prewarm = 10)
    {
        this.prefab = prefab;
        for (int i = 0; i < prewarm; i++)
        {
            var go = Object.Instantiate(prefab);
            go.SetActive(false);
            pool.Enqueue(go);
        }
    }

    public GameObject Get(Vector3 pos, Quaternion rot)
    {
        var go = pool.Count > 0 ? pool.Dequeue() : Object.Instantiate(prefab);
        go.transform.SetPositionAndRotation(pos, rot);
        go.SetActive(true);
        return go;
    }

    public void Release(GameObject go)
    {
        go.SetActive(false);
        pool.Enqueue(go);
    }
}

Unity 2021+ 内置了 UnityEngine.Pool.ObjectPool<T>,也可以用官方版本替代手写。


20. 性能与 GC 避坑清单(可直接用于 Code Review)

# 反模式 正确做法
1 UpdateGetComponent / Find / Camera.main Awake 缓存为字段
2 tag == "Player" CompareTag("Player")
3 Vector3.Distance 只做范围比较 sqrMagnitude 省去开方
4 每帧字符串拼接日志 $"" 插值 + [Conditional] 日志门面
5 频繁 Instantiate / Destroy 对象池
6 UI 显隐反复 SetActive CanvasGroup.alpha / interactable
7 GetComponents / FindObjectsByType 每帧调用 初始化时查一次并缓存
8 new WaitForSeconds(1f) 写在循环里 缓存为静态只读字段
9 Update() 遍布全场景脚本 移除空生命周期函数,用事件驱动
10 修改 sharedMaterial 或反复取 .material 缓存材质实例,或明确用 sharedMaterial
11 协程几乎每帧运行 直接改用 Update
12 正式包保留上百条 Debug.Log 编译期剥离

收益最高的三条 :缓存引用、用 sqrMagnitude、对象池。这三条能解决大部分中低端机的帧率问题。


21. 常见面试题速答

Q1:AwakeStart 有什么区别?

Awake 在对象实例化时调用,即使脚本 enabled = false 也会执行;Start 在首次 Update 前调用,且晚于场景中所有对象的 Awake。因此 Awake 做自身初始化,Start 做依赖外部对象的初始化。

Q2:UpdateFixedUpdate 怎么分工?

Update 频率随帧率变化,用于输入与非物理逻辑;FixedUpdate 以固定步长执行,用于刚体受力等物理运算。输入应在 Update 中读取,在 FixedUpdate 中消费。

Q3:为什么移动要乘 Time.deltaTime

让位移与时间成正比而非与帧数成正比,从而在不同帧率下速度一致。

Q4:协程和线程有什么区别?

协程运行在主线程,只是可被分帧暂停/恢复,不能利用多核,耗时同步计算照样卡帧;线程是真正并行,但不能直接调用 Unity API。

Q5:SetActive(false) 会停止协程吗?

会。而且重新激活后不会自动恢复。相反,仅设置 monoBehaviour.enabled = false 不会停止协程。

Q6:GetComponentFind 的性能差距?

Find 需要遍历整个场景,GetComponent 只需查当前对象(含类型索引),两者都应在初始化阶段调用一次并缓存。TryGetComponent 相比 GetComponent 在"找不到"时不会产生 GC。

Q7:Random.Range(0, 10) 能取到 10 吗?

不能,整数版是 [min, max)。而浮点版 Random.Range(0f, 1f) 可以取到 1.0。

Q8:Unity 6 里 rb.velocity 为什么编译不过?

已重命名为 linearVelocity,同理 drag → linearDampingangularDrag → angularDamping

Q9:为什么我的 OnMouseDown 没反应?

四个检查点:是否有 Collider、是否在 Ignore Raycast 层、是否被其他 Collider 遮挡、项目是否只启用了新输入系统。

Q10:暂停游戏后 UI 按钮还能点,为什么?

Time.timeScale = 0 不会停止 Update,而 UGUI 的 EventSystem 依赖帧更新,因此需要在暂停时禁用 EventSystem 或对应的 GraphicRaycaster


22. 结语

Unity 的 API 数量很多,但真正决定项目质量的不是"记得多少方法",而是"知道什么时候不该用"。回顾一下这篇文章里最重要的四个判断:

  1. 时间点决定一切Awake 拿引用、Start 连外部、Update 读输入、FixedUpdate 推物理、LateUpdate 跟相机。
  2. 引用一律缓存GetComponentFindCamera.main 只允许出现在初始化阶段。
  3. 帧率无关 :涉及时间的量一律乘 Time.deltaTime,暂停时牢记哪些会停、哪些不会停。
  4. 别和物理引擎抢方向盘 :带 Rigidbody 的对象用物理 API 移动,不要直接改 transform.position

后续可以继续深入的方向:对象池与性能分析(Profiler / Memory Profiler)、新 Input System、Addressables 资源管理、以及 Unity 6 的 Awaitable 异步体系。

如果这篇帮你少踩了一个坑,欢迎点赞 + 收藏,评论区补充你遇到过的"奇葩 Bug",我会持续更新这份清单。


更新日志

日期 内容
2026-09-18 首版:覆盖 22 个主题,新增 Unity 6 API 变更说明、性能避坑清单、面试速答;修正 Random.Range 边界语义、Mathf.Round 舍入规则、协程停止条件等常见错误表述

参考

  • Unity 官方文档:Script Execution Order、Coroutines、Rigidbody、Mathf.Round
  • Unity 6 Release Notes(Rigidbody.velocity 重命名说明)
  • Unity Discussions / Stack Overflow 相关议题
相关推荐
sensen_kiss3 小时前
CPT306 Coursework 1 个人游戏作业——坦克大战
unity·游戏程序·游戏策划
蒸鱼Yuzheng3 小时前
Unity 与 Unreal 测试工具怎么回答:Profiler、日志、自动化与证据链
测试工具·unity·性能分析·游戏测试·unrealengine
野区捕龙为宠4 小时前
Unity WebGL平台 通过 URL 加载图片(UGUI RawImage / 模型材质)
unity·团结引擎
阿松爱学习1 天前
【Unity开发】FileStream 详解及用法指南
unity·c#·unity开发
想做后端的前端1 天前
Unity-UIGI优化Rebatch与Rebuild
unity
玖玥拾2 天前
Unity Shader 基础与图形学(一)
unity·图形渲染·图形学·shader
WiChP2 天前
【V0.1B16】从零开始的2D游戏引擎开发之路
开发语言·算法·游戏引擎
百里香酚兰2 天前
【Unity学习笔记】如何把粉色Prefab还原
笔记·学习·unity
xcLeigh2 天前
Unity基础:使用Transform控制物体旋转——Rotate与LookAt详解
unity·教程·transform·rotate·lookat