林川~01头像
关注

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 已更名为 linearVelocity,FindObjectOfType 已弃用。
  4. 增加横向对比表和一张性能避坑清单,让这篇文章可以当中速手册用,也可以当代码 Review 的检查表用。

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


1. 全文知识地图

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

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


2. 调试输出:Debug

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

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

2.1 最小示例

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,减少临时字符串)。

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

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 完整执行顺序

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

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

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

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

3.2 Awake vs Start

对比项Awake()Start()
调用时机对象实例化后立即首次 Update 前,且晚于所有 Awake
对象被禁用时仍然调用(只要对象被创建)不调用,直到对象被激活
依赖其他对象不安全相对安全
典型用途缓存自身组件引用、初始化字段注册事件、开启协程、访问外部对象
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.deltaTimeTime.fixedDeltaTime(此处读 deltaTime 返回的也是它)Time.deltaTime
Time.timeScale = 0 时仍然执行停止执行仍然执行
主要用途输入检测、UI、非物理逻辑刚体受力、物理相关运动相机跟随、IK、依赖他人最终位置的逻辑
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 的经典用法是相机跟随,保证相机在角色移动之后再计算位置,否则画面会抖动:

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 与资源释放

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

坑:OnDestroy 在 OnDisable 之后调用;如果对象在销毁前先被 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
// 已弃用(Unity 2023.1 起)
// var gm = FindObjectOfType<GameManager>();
// 推荐写法
var gm = FindFirstObjectByType<GameManager>();

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

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

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 激活状态

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

注意点

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

4.3 动态增删组件

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

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

[RequireComponent(typeof(Rigidbody))]
[DisallowMultipleComponent]
public class PhysicsObject : MonoBehaviour { }

4.4 标签比较

// 差:== 会分配字符串并做完整比较
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 暴露字段的正确姿势

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 示例

// 差:空引用风险,且每帧都在查找
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.Find、GetComponentInChildren —— 层级内可控
  4. GetComponentsInChildren —— 递归 + 数组分配
  5. GameObject.Find / FindObjectsByType —— 全场景遍历,永远不要放在 Update 里

经验法则:任何 GetComponent、Find、Camera.main、GetComponents 出现的位置,只允许在 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——层级信息
// 世界坐标与局部坐标的相互转换,做 UI/挂点计算时非常有用
Vector3 worldPos = transform.TransformPoint(localPoint);
Vector3 localPos = transform.InverseTransformPoint(worldPos);

7.2 移动:Translate

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

// 沿自身前方移动(默认 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

// 绕自身 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 缩放

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

// 有开方,开销大
if (Vector3.Distance(a, b) < detectRange) { }

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

8.3 点积的实战意义

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) 的异常:
if (dir.sqrMagnitude > 0.0001f) { dir = dir.normalized; }
  • Vector2 与 Vector3 之间存在隐式转换,但 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)),Input 与 OnMouseXXX 系列全部失效。需要兼容时请改为 Both,或统一迁移到新输入系统。

9.1 键盘

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

9.2 鼠标

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 轴输入(推荐用于移动)

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

9.4 触摸

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

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 系列低无仅用于极简原型
// 更现代的事件写法
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 参数驱动状态切换

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 直接播放与过渡

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

11.3 动画事件

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

// 挂在带 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

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

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

12.2 暂停的正确姿势

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 的影响范围,必须清楚:

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

由此推出两个高频 Bug:

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

13. Mathf:常用数学函数

13.1 取整三兄弟

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 时返回偶数:

Debug.Log(Mathf.Round(10.5f)); // 10
Debug.Log(Mathf.Round(11.5f)); // 12
Debug.Log(Mathf.Round(-10.5f)); // -10

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

int RoundHalfUp(float v) => Mathf.FloorToInt(v + 0.5f);

13.2 限制与插值

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 三角函数与角度

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 更稳

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

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

13.4 其他

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
int level = Random.Range(5, 11);      // 5~10,想取到 10 必须写 11
float f = Random.Range(0f, 1f);       // 0.0 ~ 1.0,可以取到 1.0

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

14.2 常用随机值

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 随机种子与可复现

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 悬停高亮示例(含材质泄漏修复)

[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 Manager(Active Input Handling 为 Input Manager (Old) 或 Both),否则回调失效——这是新输入系统项目里"鼠标悬停没反应"的头号原因。

15.3 性能说明

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


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

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

16.1 基本结构

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 启动与停止

// 保存引用,才能精确停止
private Coroutine routine;

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

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

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

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 实战用法

// 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 等第三方库:

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

协程 vs async/await 怎么选?

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

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

16.7 性能提示

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

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

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.velocityrb.linearVelocity
rb.dragrb.linearDamping
rb.angularDragrb.angularDamping
rb.angularVelocity不变

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

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

[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不勾选勾选
是否产生物理阻挡是否,直接穿过
参数类型CollisionCollider
回调Enter / Stay / ExitEnter / Stay / Exit
触发条件双方都有 Collider,至少一方有非 Kinematic Rigidbody同上
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. 对象的创建与销毁

// 创建
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 判断要小心:
Destroy(go);
if (go == null) { }   // Unity 重载了 == ,这里为 true;但 go.transform 仍可访问
  • 频繁 Instantiate / Destroy(子弹、粒子)会产生大量 GC,必须用对象池。
  • 跨场景对象的重复创建是最常见的"游戏越玩越卡"原因。

19.2 极简对象池

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)

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

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


21. 常见面试题速答

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

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

Q3:为什么移动要乘 Time.deltaTime?
让位移与时间成正比而非与帧数成正比,从而在不同帧率下速度一致。

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

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

Q6:GetComponent 和 Find 的性能差距?
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 → linearDamping、angularDrag → 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. 引用一律缓存:GetComponent、Find、Camera.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 相关议题

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/weixin_46292030/article/details/165886877

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--