一、Unity 编辑器版本选择

1. 为什么面试会问 Unity 使用哪个版本?

面试官询问 Unity 版本,主要是想考察以下方面:
  • 是否真实使用过 Unity 开发项目
  • 是否了解商业项目中的版本管理意识
  • 是否知道版本升级的风险
实际项目不会随意升级 Unity,因为可能影响:
  • AssetBundle 资源
  • Shader
  • Package Manager
  • 第三方插件
  • Android/iOS 打包环境
  • IL2CPP 配置
所以项目通常优先选择稳定版本,而不是最新版本。

2. Unity 版本类型

Tech Stream
特点:
  • 新功能更新快
  • 可以体验最新特性
  • 适合实验、研究
缺点:
  • 稳定性相对不足
  • 插件兼容风险较高

LTS(Long Term Support)
长期支持版本。
例如:
  • Unity 2021.3 LTS
  • Unity 2022.3 LTS
特点:
  • 稳定
  • Bug 修复周期长
  • 商业项目常用
  • 插件和平台支持成熟
 

二、Unity 项目模板区别

Unity Hub 创建项目时常见模板分为核心模板、示例模板、学习模板。

1. 核心模板(Core Template)

用途:正式开发项目。
特点:
  • 基础、干净
  • 不包含大量示例资源
  • 方便自己配置项目环境
例如:
  • 2D Core
  • 3D Core
商业项目通常从 Core 模板开始,然后根据需求添加:
  • UGUI
  • Cinemachine
  • Addressables
  • 第三方插件

2. 示例模板(Sample Template)

用途:学习和展示 Unity 技术效果。
包含:
  • 示例场景
  • 模型资源
  • Shader
  • 特效
  • 高质量材质
适合:
  • 查看 Unity 能实现的效果
  • 学习渲染技术
不适合作为正式项目起点,因为内容复杂,资源较多。

3. 学习模板(Learning Template)

用途:学习 Unity 工作流。
特点:
  • 包含教程内容
  • 有示例代码
  • 适合新手学习系统
例如:
  • FPS Microgame
  • 2D Game Kit

三、Unity 渲染管线

1. 什么是渲染管线?

渲染管线(Rendering Pipeline)是 Unity 将场景中的:
  • 模型
  • 材质
  • 灯光
  • Shader
转换成屏幕像素的过程。
流程:
场景物体 → 材质 → 光照计算 → Shader → GPU绘制 → 屏幕显示

2. Built-in Render Pipeline、URP、HDRP区别

1. Built-in Render Pipeline(内置渲染管线)
Unity 最传统的渲染管线。
优点:
  • 成熟稳定
  • 兼容性最好
  • 学习资料最多
缺点:
  • 对现代渲染优化能力有限
  • 自定义渲染流程能力较弱
适合:
  • 老项目
  • 小型项目
  • 教学项目

2. URP(Universal Render Pipeline)
通用渲染管线。
目标:
在性能和画质之间取得平衡。
适合:
  • 手机游戏
  • PC游戏
  • VR
  • 跨平台项目
优点:
  • 性能优化更好
  • 支持 Shader Graph
  • 支持 SRP Batcher
  • 支持现代后处理效果
缺点:
  • 部分 Built-in Shader 不兼容
  • 老插件可能需要适配
手游项目目前大量使用 URP。

3. HDRP(High Definition Render Pipeline)
高清渲染管线。
目标:最高画质。
适合:
  • 高性能 PC
  • 主机
  • 3A 游戏
特点:
  • 高质量光照
  • 高级材质
  • 实时光追
  • 高级后处理
缺点:
  • 性能消耗高
  • 不适合移动平台

三种管线总结

管线
特点
常见场景
Built-in
成熟稳定、兼容性最好
老项目、小项目
URP
性能和画质平衡
手游、跨平台
HDRP
最高画质
PC、主机、3A

四、游戏循环、帧率与 Unity Update

1. 游戏循环(Game Loop)

游戏运行时,本质上是在不断重复执行一个循环:
这个循环每执行一次,就产生一帧画面。
一帧包含了:
  • 游戏状态更新
  • 逻辑计算
  • 画面绘制
因此:
FPS表示一秒内游戏循环执行多少次。

2. 什么是帧率(FPS)?

FPS(Frames Per Second)表示:
每秒生成和显示多少帧画面。
例如:
  • 30 FPS:1秒生成30帧
  • 60 FPS:1秒生成60帧
  • 120 FPS:1秒生成120帧
帧率越高,单位时间内画面变化越细,运动表现越流畅。

3. 什么是帧时间(Frame Time)?

帧时间表示:
完成一帧所需要的时间。
帧时间也叫Time.deltaTime, 表示上一帧到当前帧之间经过的时间,单位是秒。
例如:
60 FPS:
所以:
换成 Unity:
注意:
Unity 使用的是秒,不是毫秒。
所以:
同理:
FPS
每帧时间
30 FPS
33.3ms
60 FPS
16.7ms
120 FPS
8.3ms
FPS越高,帧间隔越短,画面更新越频繁。
示例:打印出游戏帧率

4. 30 FPS、60 FPS、120 FPS区别

30 FPS
每帧约33.3ms。
特点:
  • GPU压力较低
  • 功耗较低
  • 动作变化不够细腻
常见于:
  • 剧情游戏
  • 对画质要求较高但性能有限的场景

60 FPS
每帧约16.7ms。
特点:
  • 流畅度和性能平衡较好
  • 大多数游戏常用目标

120 FPS
每帧约8.3ms。
特点:
  • 动作更加平滑
  • 输入延迟更低
  • 快速移动更清晰
缺点:
  • CPU/GPU压力增加
  • 功耗提高
 
演示动画
 

5. Unity Update 与帧率的关系

Unity中的 Update:默认每帧执行一次。
注意:Update没有固定时间间隔,执行频率取决于当前游戏帧率。
例如:
60 FPS:
30 FPS:
120 FPS:
因此更准确的说法是:
Update每帧调用一次,但每帧持续时间不固定。

6. Time.deltaTime和 speed 的关系

错误写法:
表示:每帧移动speed。
问题:
60 FPS设备:
120 FPS设备:
同样代码,不同设备速度不同。
正确写法:
表示:根据这一帧实际经过的时间 计算移动距离。
这样可以保证不同帧率下运动速度一致。
 

7. Time.deltaTime 的影响因素

Time.deltaTime 是由什么决定的?
Time.deltaTime 不是由某个固定设备参数决定,也不是只由 GPU 决定。
它取决于:
一帧执行过程包括:
因此:
  • CPU计算过多,会导致帧时间增加
  • GPU渲染压力过大,会导致帧时间增加
  • 场景复杂度提高,也会导致帧时间增加

Time.deltaTime 与设备性能的关系
同一个 Unity 项目:
高性能设备:
低性能设备:
例如:

 

8. 1 / Time.deltaTime 的含义

公式:
表示:
根据当前这一帧耗时,估算如果保持该耗时,1秒可以运行多少帧。
例如:
得到:
它是一个瞬时 FPS 估计值,不是统计出来的平均 FPS。
区别:
平均 FPS:
表示一段时间内平均运行多少帧。
瞬时 FPS:
表示当前帧时间对应的 FPS 估计。

 

五、Unity Legacy Input System(旧输入系统)

Unity 旧输入系统主要通过三个 API 获取玩家输入:
  • Input.GetKey():检测具体按键状态
  • Input.GetButton():检测游戏功能按钮
  • Input.GetAxis():获取连续输入值
三者区别:

1. Input.GetKey:检测具体按键

表示:
检测某个具体按键是否处于按下状态。
GetKey
按住期间,每一帧返回 true。
例如:
适合:
  • 持续移动
  • 持续攻击
  • 蓄力
例如:

GetKeyDown
表示:
只在按下的第一帧返回 true。
例如:
适合一次性事件:
  • 跳跃
  • 开枪
  • 打开背包

GetKeyUp
表示:
只在按键从按下变为释放的第一帧返回 true。
例如:
适合:
  • 释放技能
  • 蓄力攻击结束

2. Input.GetButton:输入功能映射

与 GetKey 最大区别:
GetKey:
我关心哪个键。
例如:
GetButton:
我关心哪个游戏功能。
例如:

Input Manager 中可以配置:
代码:
无论玩家使用:
  • 键盘 Space
  • 手柄按钮
都会触发跳跃。

3. Input.GetAxis:获取连续输入

与前两个不同,它返回的是 float:
例如:
常用于:
  • 角色移动
  • 摄像机旋转
  • 摇杆控制
例如:

 

4. Joystick Button 是什么?

Joystick Button 指游戏手柄按钮。
例如:
在 Unity 旧输入系统中通常表示为:
它可以绑定到游戏功能,例如:
实现键盘和手柄共同控制。

5. GetAxis 和 GetAxisRaw 区别

GetAxis
带平滑处理。
输入变化:
适合:
  • 角色移动
  • 摄像机移动
  • 赛车方向控制

GetAxisRaw
无平滑,直接返回输入值。
例如:
适合:
  • UI控制
  • 需要立即响应的操作
  • 格斗游戏输入
 
示例:

三个 API 总结

API
返回值
关注内容
使用场景
GetKey
bool
具体按键
WASD、Space
GetButton
bool
游戏功能
Jump、Fire
GetAxis
float
输入方向和强度
移动、摇杆
参考 Unity keycode:
 

六、Unity 角色移动、速度与 Rigidbody2D 物理运动

玩家按下按键后,角色为什么会移动、速度为什么会受重力和碰撞影响,以及什么情况下该用 Rigidbody2D。
本节沿用第五点的 Legacy Input System 示例。新版 Input System 的输入 API 不同,但“读取输入 → 形成运动意图 → 在物理步中驱动刚体”的思路相同。
版本提示:本笔记前面的代码以 Unity 2022 LTS 常见的 Rigidbody2D.velocity 为例。Unity 6 的二维刚体线速度成员名为 linearVelocity。它们表达的是同一个“二维线速度”概念;写代码时请以项目实际 Unity 版本的 API 为准,不要混用成员名。

1. 从输入到角色移动的完整流程

角色移动不是“按键直接改位置”,而是一个连续链路:
其中:
  • Input.GetAxisRaw("Horizontal"):读取输入方向。键盘通常得到 -1 / 0 / 1;手柄轴可能得到中间值。
  • xInput:玩家这一帧想向左、静止还是向右的“意图”,不是位置。
  • moveSpeed:设计上的移动速度参数。
  • Rigidbody2D:让物体进入 Unity 2D 物理模拟。
  • velocity:刚体当前的速度,单位是世界单位/秒,不是“每一帧移动距离”。

2. 推荐的基础写法:Update 采样,FixedUpdate 应用物理

这里的职责划分很重要:
FixedUpdate 的次数与 Update 不一一对应:低帧率时一帧渲染之间可能执行多次物理步,高帧率时也可能隔若干渲染帧才执行一次物理步。因此不能简单把 GetButtonDown 放到 FixedUpdate;否则短暂按键可能恰好发生在两次物理步之间而被漏掉。

3. moveSpeed 到底表示什么

若键盘输入为:
xInput
moveSpeed
目标 X 速度
含义
-1
5
-5
向左,每秒 5 个世界单位
0
5
0
停止施加水平移动意图
1
5
5
向右,每秒 5 个世界单位
所以 moveSpeed 的正负会影响方向,绝对值决定速度大小。通常把它设为正数,方向交给 xInput 控制。
常见错误:
Time.deltaTime 用于“直接计算位移”,例如 transform.position += direction * speed * Time.deltaTime。而 velocity 表示的是单位时间内的速度,直接赋 5 就已经表示“每秒 5 个世界单位”。

4. Rigidbody2D.velocity 与 Vector2:为什么要保留另一个分量

Rigidbody2D.velocity 的类型是 Vector2:
例如:
速度
运动含义
(5, 0)
向右移动
(-5, 0)
向左移动
(5, 10)
向右上方运动
(5, -10)
向右下方运动
水平移动只应改 X:
跳跃只应改 Y:
每次赋值其实都在写入一个完整的 Vector2,并不是两个赋值会自动合并。之所以不会互相“搞没”,是因为我们主动读取并带回不想改的那一项:
反例:
也不要写 rb.velocity.x = ...。velocity 返回的是一个 Vector2 值;正确做法是“读出 → 修改分量 → 整体写回”,正如上面的 Vector2 velocity = rb.velocity 示例。
Vector2 是 struct 值类型。这里的 new Vector2(x, y) 表示构造一个新的二维数值,不应把它简单等同于 new GameObject()、new List<T>() 等引用类型对象分配。把速度设置封装进 SetVelocity(x, y) 后,方法内部仍然需要构造并写回一个完整的 Vector2;因此选择方法封装还是直接赋值,主要依据职责和可维护性,不是为了少写一次 new。

5. 速度还会受到哪些物理因素影响

角色最终运动不是只由输入决定:
因素
作用
要点
Gravity Scale
持续改变 Y 方向速度
跳起后 Y 速度会逐步减小、到 0、再为负值,形成上升和下落。
Linear Damping / Drag
让速度逐步衰减
Unity 6 称为 Linear Damping;旧版本 Inspector 常称 Drag。
碰撞
接触墙、地面后产生碰撞响应
Collider 只定义形状;实际反应还取决于 Rigidbody2D、材质和碰撞配置。
摩擦与弹性
影响沿表面的滑动和反弹
通常由 Physics Material 2D 配合 Collider 设置。
AddForce
施加持续力或瞬时冲量
更符合“推、撞、爆炸、后坐力”等物理感需求。
Mass
影响受力后的加速度与碰撞表现
不会改变手动直接赋值的 velocity,但会影响 AddForce 的效果。
一个很实用的判断是:
  • 每个 FixedUpdate 都直接写入 velocity.x:角色横向控制会很“跟手”,但摩擦或外力试图改变 X 速度时,下一物理步可能又被你的代码覆盖。
  • 用 AddForce:更遵从质量、阻力和外力,手感更有惯性,但不如直接设置速度稳定。
  • 不要让多个脚本在同一个物理步同时设置同一刚体的 velocity;最后写入的一方会覆盖前者的意图。

6. 角色移动的四种常见方式

方式
本质
适合场景
主要注意点
修改 Transform
直接改位置
不参与物理的展示物体、简单非物理移动
动态 Rigidbody2D 不应靠它移动,否则会绕开物理模拟并可能造成穿透或抖动。
设置 Rigidbody2D.velocity
直接指定目标速度
平台跳跃角色、需要即时停走的控制
速度已有“每秒”含义,不再乘 deltaTime;要保留不想修改的分量。
Rigidbody2D.AddForce
施加力或冲量
推力、爆炸、击退、真实惯性
质量、阻尼和 ForceMode2D 会影响结果。
Rigidbody2D.MovePosition
在下一物理步请求刚体移动到目标位置
需要走 Rigidbody2D 流程的受控位移
在 FixedUpdate 调用,并结合刚体 Body Type 与碰撞方案设计。
选择原则不是“哪个 API 更高级”,而是这个对象是否需要由物理系统负责。同一对象的同一段运动不要同时让 Transform、velocity 和 AddForce 三套逻辑各自决定。

7. 本节常见错误清单

  • 把 Rigidbody2D.velocity 乘 Time.deltaTime,把“速度”误当成“位移”。
  • 用 new Vector2(x, 0) 控制水平移动,意外清空跳跃和下落的 Y 速度。
  • 动态刚体用 transform.position 或 Translate 直接移动,导致物理响应异常。
  • 把 GetButtonDown 放到 FixedUpdate,偶发漏跳。
  • 忘记做落地检测,让角色可以在空中无限跳。
  • 同一个刚体被多个脚本反复写速度,排查时看起来像“物理失控”。

8. velocity 是持续状态,不是单帧移动命令

这不是“让刚体只在当前帧移动一次”,而是把刚体当前线速度设为 (5, 12)。之后 Y 会受重力影响;如果没有脚本、阻尼、摩擦、碰撞冲量或其他外力修改 X,velocity.x 可以继续保持为 5。
因此停止输入不等于刚体自动停止。只有后续代码把目标水平速度写成 0,或者物理系统通过阻尼、摩擦等机制改变速度,刚体才会减速或停止。

9. 输入意图、逻辑状态与物理速度

数据
表示什么
示例
xInput
玩家当前想向左、静止还是向右
-1、0、1
currentState
当前允许执行哪组逻辑
Idle、Walk、Jump、Air
rb.velocity
刚体当前真实运动状态
(5, -3)
三者不会自动同步。例如 xInput = 0 只表示玩家已经松开方向键,刚体仍可能保留 velocity.x = 5。输入是意图,状态是规则,Rigidbody2D 是物理结果;它们之间必须由代码显式转换。

10. Linear Damping、摩擦力与显式速度控制

方式
本质
适合解决的问题
Linear Damping / 旧版 Drag
让整个线速度逐渐衰减
整体阻尼和惯性手感
Physics Material 2D Friction
通过接触面的切向摩擦减速
不同表面和物理对象的接触表现
显式设置 velocity
控制代码直接指定当前目标速度
需要即时停走、结果可预测的角色控制
如果角色落地后因旧的 velocity.x 继续滑动,首先要检查哪个状态负责更新或清除水平速度。用 Drag 让问题看起来减轻,并没有补上缺失的状态规则,而且会同时改变跳跃、下落、冲刺和击退的速度衰减。

11. 用方法封装 Rigidbody2D 速度写入

统一方法让调用方只表达目标速度,拥有 Rigidbody2D 的对象负责真正写入。以后增加限速、状态保护或调试记录时,可以集中修改一个入口。
但封装方法可能带有额外行为。例如某些项目的 SetVelocity 还会根据 X 速度调用翻转逻辑。调用者必须知道这个副作用;当击退方向、移动方向和视觉朝向不再完全一致时,应考虑把“设置速度”和“更新朝向”拆开。

七、C# 字段声明与访问权限

第七点只讨论 C# 中“数据怎么定义、谁能访问它”。\[SerializeField\]、Inspector 和 Unity 保存数据的机制放到第八点单独解释。

1. 什么是字段(Field)

字段是定义在类内部、方法外部的数据。它属于对象,用来保存该对象持续存在的状态或配置。
这里:
部分
含义
private
访问权限
float / int
数据类型
moveSpeed / currentHp
字段名
= 5f / = 100
创建组件实例时使用的默认初始值
每个挂有 PlayerController 的 GameObject 都有自己的实例字段。例如两个敌人可以分别拥有不同的 currentHp,互不共享。

2. 字段、局部变量与属性的区别

概念
声明位置
生命周期/用途
字段 Field
类中、方法外
保存对象状态或配置;多次方法调用之间仍可使用。
局部变量 Local Variable
方法或代码块内
只服务于当前一次执行,适合临时计算。
属性 Property
类中、带 get/set
为外部读取或修改字段建立受控入口。
自动属性可以只公开读取权限:
它大致可以理解为由编译器生成一个隐藏的后备字段,再通过属性控制访问:
public get 允许外部读取,private set 表示只有声明该属性的类能修改。自动属性并不是“不保存数据”,只是后备字段由编译器生成。
例如 xInput 只在 Update 中用一次,可以是局部变量;而“当前生命值”“移动速度”“当前目标”等需要跨方法保存的数据,通常是字段。

3. public、private、protected

修饰符
谁可以直接访问
适用场景
public
任何能拿到该对象引用的代码
真正需要对外公开的接口或数据。
private
当前类内部
默认选择:内部状态、内部实现细节。
protected
当前类与继承它的子类
基类故意留给子类扩展的状态或行为。
public 和 private 解决的是C# 访问权限,不是“要不要显示在 Inspector”的唯一开关;Unity 的序列化和 Inspector 规则在第八点处理。

4. 为什么字段通常优先 private

对象自己的内部状态应该由对象自己维护。若任何脚本都能随意写:
那么排查“谁把数据改坏了”会非常困难。
常见做法是字段保持 private,再按需要提供受控方法或只读属性:
这样外部不能跳过规则直接篡改 currentHp,而是必须走 TakeDamage 的校验逻辑。这就是封装:数据与维护数据的规则放在同一个对象中。
当外部需要读取状态,但修改必须由对象自己完成时,可以使用:
它比 public float facingDir 更能限制错误写入。外部状态可以用 FacingDir 计算冲刺方向,却不能绕过角色自己的 Flip 规则直接改变朝向。

5. 实例字段与 static 字段

static 字段属于“类”,不是某一个 GameObject。适合全局计数、静态配置等;不适合保存每个角色各自的血量、速度或 Inspector 配置。不要因为想“全局都能访问”就滥用 static。

6. 字段声明的常见错误

  • 把需要持续保存的数据写成局部变量,导致每次进入方法都重新初始化。
  • 仅为了让 Inspector 可见就把所有字段写成 public,破坏封装。
  • 以为字段默认值一定会覆盖场景中已有对象的值;带序列化时,Inspector 保存值可能优先,见第八点。
  • 用一个 static 字段存所有角色的独立状态,造成多个对象互相影响。
  • 名称不表达含义。普通私有字段通常使用 camelCase,例如 moveSpeed、currentHp。

八、Unity 字段序列化与 Inspector 特性

这一点解释三个必须分开的层级:

1. 什么是 Unity 序列化

序列化可以理解为:Unity 将对象字段的状态转换为可保存的数据,并在之后重新恢复。
这就是 Inspector 能显示和编辑脚本字段的基础。Inspector 改动的是 Unity 保存的字段数据,不是“只改代码里的文本默认值”。
例如:
第一次给对象添加组件时,Inspector 通常显示 5。后来若在 Inspector 改为 8 并保存,场景或 Prefab 保存的是 8。此后即使把代码改成 = 10f,已经存在的对象仍可能继续使用保存的 8。

2. [SerializeField] 的原理与作用

这句的真实含义是:
  • private 仍然是 private:其他脚本不能写 player.moveSpeed。
  • \[SerializeField\] 告诉 Unity:即使该字段不是 public,也要按 Unity 的字段序列化规则保存并显示它。
  • 因此它适合“需要在 Inspector 配置,但不想成为对外公开接口”的数据。
它不会:
  • 把 private 变成 public。
  • 自动做范围校验。
  • 把字段变成只读或线程安全。
  • 让任何 C# 属性自动被 Inspector 序列化。
Unity 的常规字段序列化要求至少满足:
  • 字段是 public,或标记了 \[SerializeField\]。
  • 不是 static、const、readonly。
  • 字段类型可由 Unity 序列化,例如基础类型、enum、Vector2/Vector3、UnityEngine.Object 引用、可序列化数组/List,以及标记 \[System.Serializable\] 的自定义类型。
Unity 通常针对字段工作,不是普通 C# 属性。不要以为写了 public float Speed \{ get; set; \} 就会和字段一样出现在 Inspector。
例如下面的自动属性默认不会因为 public 就像 public 字段一样显示:
如果确实需要让 Unity 序列化编译器生成的后备字段,可以把特性明确应用到 field:
但朝向、当前状态、计时器等运行时数据通常由代码维护,不一定需要暴露给 Inspector。public 解决外部代码能否访问,[SerializeField] 解决 Unity 是否保存字段并在 Inspector 中显示,两者不能混为一谈。

3. 为什么项目里常用 [SerializeField] private

这种写法同时满足:
典型适用对象:
  • 数值配置:移动速度、伤害、冷却、检测距离。
  • 外部引用:目标 Transform、UI、音效、Prefab。
  • 资源引用:AudioClip、Animator、ScriptableObject。
若字段只是运行时缓存,例如“通过 GetComponent 找到的自身 Rigidbody2D”,通常不必拖到 Inspector,也不必序列化:

4. 常用 Inspector 特性

特性
作用
示例
常见误解
[Header]
给后续字段添加标题分组
\[Header("Movement")\]
只影响 Inspector 排版,不影响访问权限和逻辑。
[Tooltip]
鼠标悬停时显示说明
\[Tooltip("角色水平移动速度")\]
不会验证数值正确性。
[Range]
在 Inspector 显示滑杆
\[Range(0f, 10f)\]
主要是编辑器 UI;运行时代码仍可写出范围外数值。
[Space]
在字段之间留出视觉间距
\[Space(12)\]
只改变排版。
[HideInInspector]
隐藏字段在 Inspector 中的显示
\[HideInInspector\] public int debugId;
不会把 public 变 private;序列化规则也不会因此自动改变。
组合示例:
如果数值必须始终合法,不能只依赖 \[Range\];还应在运行逻辑中校验或夹紧,例如 Mathf.Clamp,必要时在 OnValidate 中做编辑器期检查。

5. Inspector 值、代码默认值与常见问题

现象
原因
处理思路
改了 = 5f,场景中的对象还是旧值
该实例已有 Inspector 序列化值
检查场景对象和 Prefab Override,按需求重新设置或重置组件。
字段没有出现在 Inspector
不满足序列化条件,或被隐藏
检查访问修饰符、\[SerializeField\]、类型、static/readonly 等限制。
拖入引用后运行时报空
引用没有在当前对象/Prefab 实例上正确保存,或目标运行时被销毁
在 Awake/OnValidate 做必要检查,并确认 Prefab Override。
重命名字段后旧配置丢失
序列化数据按字段名关联
重构已发布数据时,学习并使用 \[FormerlySerializedAs\] 做迁移。
本节结论: private 管“谁能从代码访问”;\[SerializeField\] 管“Unity 要不要保存和在 Inspector 显示”;Inspector 特性管“编辑器如何展示”。三者职责不同,不能混为一谈。

九、Unity 常用组件

Unity 采用组件化(Component-based)结构:GameObject 是对象容器,真正的能力由挂在它身上的组件提供。
自己的 MonoBehaviour 脚本在挂到 GameObject 后,也是一种组件。一个 GameObject 可以同时挂多个组件;组件协作才形成一个角色、摄像机或 UI 对象。

1. Transform:每个 GameObject 都有的基础组件

Transform 是每个 GameObject 必有的组件,负责:
  • Position:位置。
  • Rotation:旋转。
  • Scale:缩放。
  • Parent / Child:层级关系。
对不参与物理的对象,直接修改 Transform 很自然。对动态 Rigidbody2D 或 Rigidbody,应通过刚体 API 移动,而不是一边由物理系统计算、一边手改 Transform。

2. Rigidbody 与 Rigidbody2D:让对象参与物理

组件
负责内容
常见用途
Rigidbody
3D 物理:速度、重力、力、碰撞响应
3D 角色、箱子、车辆、抛射物。
Rigidbody2D
2D 物理:二维速度、重力、力、碰撞响应
横版角色、2D 子弹、2D 推箱子。
关键点:
  • Rigidbody/Rigidbody2D 不是“碰撞形状”,而是“会被物理系统模拟的刚体”。
  • 使用 Rigidbody2D 就配 Collider2D;使用 Rigidbody 就配 3D Collider。
  • 2D 与 3D 物理系统是分开的,OnCollisionEnter2D 不会因为 3D Collider 而触发,反过来也一样。

3. Collider 与 Collider2D:定义碰撞范围

Collider 定义物体的几何边界,本身不负责渲染。
常见 2D Collider:
  • BoxCollider2D:矩形,如箱子、平台。
  • CircleCollider2D:圆形,如球、圆形范围。
  • CapsuleCollider2D:胶囊形,常用于人物。
  • PolygonCollider2D:不规则 2D 轮廓。
常见 3D Collider:
  • BoxCollider、SphereCollider、CapsuleCollider、MeshCollider。
若勾选 Is Trigger,它通常用于“进入范围”“拾取区域”等触发事件,而不是物理阻挡。要有实际碰撞响应,则要正确组合 Collider、Rigidbody(或 Rigidbody2D)和物理层设置。

4. SpriteRenderer、MeshRenderer 与 SkinnedMeshRenderer:把对象显示出来

组件
用途
典型对象
SpriteRenderer
绘制 2D Sprite
2D 角色、图标、地面图片。
MeshRenderer
绘制普通 3D Mesh
墙、道具、静态模型。
SkinnedMeshRenderer
绘制绑定骨骼、可变形的 3D Mesh
3D 人物和带骨骼动画的模型。
Renderer 决定“看得到什么”,Collider 决定“碰撞到什么”,两者是不同职责。看起来有图像的物体不一定能碰撞,有 Collider 的透明物体也可能阻挡角色。

5. Animator:管理动画状态与过渡

Animator 通过 Animator Controller 管理动画状态机,例如:
脚本通常只负责设置参数:
Animator Controller 决定何时从 Idle 过渡到 Run、从 Jump 过渡到 Fall。常见错误是参数名拼错,或只给对象加了 Animator 却没有分配正确的 Controller。

6. AudioSource:播放音效与音乐

AudioSource 负责从对象位置或全局播放音频;AudioClip 是具体的声音资源。
常见用途:
  • PlayOneShot:跳跃、攻击、受击等短音效。
  • Play:播放 AudioSource 当前设置的 Clip。
  • 3D 音效:位置、距离衰减、空间混音由 AudioSource 配置。

7. Camera:决定最终看到什么

Camera 将场景从指定视角渲染到屏幕或 Render Texture。它决定:
  • 玩家看到的区域。
  • 透视或正交投影方式。
  • 画面深度、背景、裁剪范围。
  • 多个 Camera 的渲染顺序与目标。
2D 项目常使用 Orthographic Camera;3D 项目通常使用 Perspective Camera。摄像机跟随角色是“脚本修改 Camera 的 Transform”,而 Camera 本身负责将画面输出。

8. 其他高频组件

  • Light:3D 场景光照。
  • Canvas:UGUI 的根容器。
  • Button、Image、TextMeshProUGUI:常见 UI 组件。
  • ParticleSystem:粒子特效。
  • Tilemap / TilemapCollider2D:2D 地图和对应碰撞。
先记住组件职责,再记 API。 当你要“显示、碰撞、播放声音、播放动画、跟随镜头”时,先判断需要哪个组件,再决定如何从脚本引用它。

十、Unity 组件获取方式

第九点解决“有哪些能力”,第十点解决“脚本怎样拿到某个具体能力”。
例如:
这行没有创建新的 Rigidbody2D,只是声明了一个变量,准备保存对某个已存在组件的引用。只有把引用赋给它之后,才能操作真实组件。

1. Inspector 拖拽引用:显式配置外部依赖

在 Inspector 中把对象或组件拖进对应槽位。Unity 会将这个引用随场景/Prefab 序列化保存。
适合:
  • 目标是其他 GameObject,例如敌人追踪的玩家。
  • 目标由不同 Prefab 或场景配置决定。
  • 希望引用关系在 Inspector 可见、可由策划/关卡配置。
优点:没有运行时查找,关系直观。
风险:忘拖、拖错或运行时销毁目标时,字段可能为 null。对必需引用,可在 Awake 或 OnValidate 中尽早报错。

2. GetComponent<T>():获取当前 GameObject 上的组件

GetComponent\<T\>() 的含义是:
常见例子:
它不会自动到父物体、子物体或全场景搜索。组件不在同一个 GameObject 时,要使用后面的层级查找,或通过 Inspector 传入明确引用。
对“脚本运行必须存在”的自身组件,可配合:
RequireComponent 会在添加脚本时要求/补充对应组件,减少遗漏配置;它不是替代空引用排查的万能手段。

3. TryGetComponent<T>():获取可选组件并立即判断

它返回 bool,把“是否找到”和“拿到的引用”放在一处表达,适合组件可有可无的情况。Unity 文档还指出:在 Editor 中请求的组件不存在时,它避免了 GetComponent 的相关分配。
若组件是必需依赖,使用 GetComponent + RequireComponent 往往更能表达“缺失就是配置错误”;若组件是可选能力,TryGetComponent 的语义更清晰。

4. GetComponentInChildren<T>():查当前对象和子层级

若 PlayerController 在 Player 上,而 Animator 在 Model 子节点:
它会先检查当前 GameObject,再向下递归查找子对象。默认不查找未激活的子对象;需要时可写:
若层级中可能有多个同类型组件,单个 GetComponentInChildren 只会返回第一个匹配项,且不要把“第一个”当成稳定业务规则;需要全部结果时使用 GetComponentsInChildren\<T\>() 后自行筛选。

5. GetComponentInParent<T>():查当前对象和父层级

Hitbox 想通知拥有它的角色:
它同样会先检查当前 GameObject,然后向父级逐层查找。默认只包含激活层级;必要时可传入 true 以包含未激活对象。
常见用途:子碰撞器、武器节点、特效节点需要找到父角色的控制器。不要把它误认为“只查父对象”——当前对象也在搜索范围内。

6. 为什么通常要在 Awake 缓存组件

不推荐每帧重复搜索:
更清晰的写法:
缓存的价值:
  • 避免反复查找,减少不必要工作。
  • 让依赖集中在初始化阶段,代码更容易检查。
  • 后续逻辑只关心“怎么使用组件”,不反复混入“怎么找到组件”。
Awake 通常适合初始化自身固定组件;Start 常适合依赖其他对象已经完成 Awake 的初始化。无论哪种方式,都应让“引用何时准备好”在代码中清楚可见。
不要把“缓存”理解成死规则:如果组件会被运行时替换、销毁或对象池复用,就要在相应生命周期重新获取或验证引用。

7. 组件获取方式怎么选

方式
查找范围
适合场景
Inspector [SerializeField]
你显式拖入的任意对象/组件
外部目标、UI、Prefab 配置、场景关系。
GetComponent&lt;T&gt;()
当前 GameObject
当前对象必备的 Rigidbody、Collider、同物体 Animator 等。
TryGetComponent&lt;T&gt;()
当前 GameObject
组件可选,需要先判断是否存在。
GetComponentInChildren&lt;T&gt;()
当前对象 + 子层级
模型节点 Animator、武器子节点、子 UI。
GetComponentInParent&lt;T&gt;()
当前对象 + 父层级
子碰撞器、武器节点访问父角色控制器。
最终选择依据是:组件在哪里、它是不是必需、引用关系是否应该让 Inspector 显式配置,而不是死记某一个 API。

十一、代码状态机的生命周期与转换边界

这里讨论由项目代码实现的有限状态机,不是 Animator Controller。它把一个对象在同一时刻允许执行的行为拆成独立状态,例如 Idle、Walk、Jump、Air 和 Dash。

1. 代码状态机解决什么问题

不使用状态机时,移动、跳跃、冲刺和攻击条件可能集中在一个 Update 中并互相覆盖。状态机保存一个 currentState,每帧只更新当前状态,让“此刻允许执行什么”有明确入口。
状态机不会自动理解 Idle 应该停止、Jump 应该上升;这些仍然要由各状态的 Enter、Update 和 Exit 明确实现。

2. currentState、Initialize 与 ChangeState

Initialize 建立第一个有效状态;ChangeState 先退出旧状态,再替换引用,最后进入新状态。

3. Enter、Update 与 Exit

阶段
调用时机
适合行为
Enter
进入状态时一次
设置跳跃初速度、初始化计时器、开启动画参数
Update
状态持续期间每帧
读取输入、递减计时器、更新持续行为、检测转换条件
Exit
离开状态时一次
关闭动画参数、清理只属于旧状态的数据
一次性行为放进 Update 会被重复执行。例如每帧把 velocity.y 设为正的跳跃速度,会持续覆盖重力结果,使角色无法正常到达最高点并下降。

4. 状态计时器与 <= 0

计时器通常在 Enter 初始化,在公共 Update 中每帧递减:
结束条件通常写成:
离散更新不保证数值刚好经过 0,可能从一个很小的正数直接变成负数。<= 0 同时覆盖归零和越过零;不要依赖浮点数精确等于 0。
同一个计时器只应在一个位置递减。若基类 Update 已经处理,子类再次减一次会让状态持续时间缩短。

5. ChangeState 不会终止当前方法

ChangeState 会更换状态机保存的引用,但调用它的旧 Update 仍会继续执行后面的 C# 语句。若后面还有其他状态条件,同一帧可能连续切换多次。
return 用来结束旧状态当前这一次方法调用。多个转换条件还应明确优先级,不能只依赖代码偶然的排列顺序。

6. 状态对行为和数据的所有权

状态机拆分以后,每种持续数据都要有明确维护者。例如 Walk 写入水平速度、Jump 在 Enter 写入一次跳跃初速度、Air 检测落地、Idle 清除主动水平速度。
如果旧状态写入了速度,新状态只改变动画而没有接管速度,Rigidbody2D 会继续保留旧物理状态。状态切换成功不等于速度、动画和输入意图已经自动同步。
速度的持久性和分量控制参见第六点。本节只负责状态生命周期与转换规则,不重复 Rigidbody2D 的物理原理。

7. 本节常见错误

  • 把一次性的跳跃初速度放在 Update,导致每帧重复写入。
  • 调用 ChangeState 后继续执行旧状态逻辑,同一帧又切换到第二个状态。
  • 基类和子类同时递减同一个 stateTimer。
  • 用刚体残留速度代替输入意图判断 Idle 或 Walk。
  • 状态只切换动画参数,却没有明确谁负责接管旧速度。