想象你刚被任命为剧院的舞台导演。剧团给了你一座剧场,舞台、灯光、道具间一应俱全。你的任务,是排出一台好戏。Unity 游戏引擎就像这座剧场:每个游戏关卡是舞台布景;演员和道具是一个个“游戏物体”;贴在它们身上的“能力卡”决定它们会不会发光、会不会碰撞、会不会运动;而你写的“剧本”则告诉它们何时走、何时停、何时开枪。这篇文章要教你的,就是这台戏的完整排法:先熟悉剧场环境,再把演员请上台,给演员发能力卡,最后写好剧本和流程安排。读完,你就能亲手做出一个可操控的 3D 小世界,并真正理解游戏引擎搭积木式的设计逻辑,为后续 AI 仿真、智能体训练等热门方向打下基础。

岗位关联

掌握游戏物体的组织方式和脚本编写,等于拿到了进入游戏开发、AI 虚拟仿真、程序化内容生成等前沿领域的万能钥匙。

目录
  1. 可视化演示
  2. Unity 环境准备
  3. GameObject 基础
  4. 组件与依赖
  5. C# 脚本写法
  6. 生命周期函数
  7. 场景搭建实战
  8. 项目整合进阶
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

本节配有交互动效(进入视口后自动加载,约 3KB)。

Unity 环境准备

你刚被任命为一家剧院的舞台导演。拿到钥匙的第一天,你干的第一件事是什么?当然是把剧场逛一遍:舞台在哪、灯光控制台在哪、道具库怎么走、观众买票后从哪个门进场。Unity 编辑器就是你游戏项目的“剧场”,先熟悉它,后面的一切才不会手忙脚乱。

Unity 编辑器有五个最常用的窗口,它们像剧场的五个区域。Scene 视图是导演从高处俯瞰舞台的视角,你在里面摆放和调整一切;Game 视图是观众看到的最终画面;Hierarchy 面板是舞台上的全员名单,列出每一个 GameObject;Inspector 面板是你走近一名演员后看到的详细信息——他穿了什么、拿了什么道具;Project 面板是道具储藏室,存放模型、贴图、音频和脚本。

创建一个新项目时,Unity 会生成 Assets 和 ProjectSettings 等文件夹。Assets 相当于剧院的道具仓库:你导入的素材、写的脚本都放在这里;Scenes 文件夹则存放一幕幕具体的舞台布景(场景文件)。双击场景文件,就相当于拉开这一场的幕布。在你的第一个项目中,你应该养成习惯:所有素材先归类放入 Assets 下的子文件夹,场景文件也按幕次命名。

在场景这个三维空间里,每个物体都要有一个“舞台坐标”,方便导演精确指挥站位。这个坐标由一个三元组表示:

当你打开 Unity 的那一刻,就像拿到了剧场后台的全套钥匙。别急着写代码,先在四个窗口之间转一转,在 Project 里建好文件夹,在 Hierarchy 里新建一个空物体,观察 Inspector 的变化。等你对“剧场”了如指掌,再开始排戏。

p⃗=(x,y,z)\displaystyle \vec{p} = (x, y, z)
通俗地说,这就是舞台地板上的定位贴纸:横着、竖着、高矮各写一个数字,三点合一确定唯一位置。
  1. 位置向量场景内任一物体位置用坐标三元组表示,三个分量分别对应左右、上下、前后三条轴。
    p⃗=(x,y,z)\displaystyle \vec{p} = (x, y, z)
  2. X 轴分量控制物体左右方向,通常向右为正。
    x∈R\displaystyle x \in \mathbb{R}
  3. Y 轴分量控制物体上下方向,向上为正。
    y∈R\displaystyle y \in \mathbb{R}
  4. Z 轴分量控制物体前后方向(深度),向屏幕外为正。
    z∈R\displaystyle z \in \mathbb{R}
  5. 原点概念场景中心视为坐标原点,即三个分量都为 0 的位置。
    p⃗0=(0,0,0)\displaystyle \vec{p}_0 = (0, 0, 0)

GameObject 基础

好,现在正式把演员请上台。在 Unity 里,演员不叫你认识的“张三”或“李四”,它叫 GameObject(游戏物体)。不过请记住:GameObject 既不是演员本人,也不是演员体型,它只是一张“演员登记表”——上面有名字、有一个站位信息,以及一叠可以夹更多资料的空夹子。

在 Hierarchy 面板右键选择 “Create Empty“,一个空 GameObject 诞生了。你可以给它改名叫 ”Player“ 或 ”Enemy“。选中它,Inspector 里会出现一个组件:Transform。Transform 相当于站位卡,记录了这个物体在场景中的位置(Position)、旋转(Rotation)和缩放(Scale)。没有 Transform 的物体是无法存在于场景中的——它是所有 GameObject 的”出生自带组件”。

GameObject 还可以形成“父子关系”。把两个物体拖成父子,面板上就形成一个层级树。移动父物体,所有子物体跟着移动,但反过来移动子物体,父物体纹丝不动。这就像舞台上的移动餐桌:桌布、杯子和花瓶是子物体,只要餐桌一动,整桌跟着走;如果单独挪动花瓶,桌子不会动。利用父子层级,你可以轻松搭建人体骨骼、车辆部件、队伍阵型等复杂结构。

Transform 中的旋转用三个角度描述物体朝向,就像一张“转台说明卡”,告诉演员该面朝哪个方向:

现在你的演员有三样基本装备:名字(Name)、站位卡(Transform)、以及“可以挂更多卡”的组件区。GameObject 本身不渲染、不发声、不运动,它的宿命就是“承载组件”。这种设计很反直觉,但正是理解 Unity 的钥匙。

θ=(α,β,γ)\displaystyle \theta = (\alpha, \beta, \gamma)
通俗地说,就是分别绕三个轴转多少度:绕左右轴抬头低头、绕上下轴左右看、绕前后轴歪头。
  1. 欧拉角表示Transform 的旋转由三个角度组成,表示物体依次绕三条轴旋转。
    θ=(α,β,γ)\displaystyle \theta = (\alpha, \beta, \gamma)
  2. α 对应 X 轴控制俯仰角,像演员点头抬头。
    α:绕 X 轴\displaystyle \alpha : \text{绕 } X \text{ 轴}
  3. β 对应 Y 轴控制偏航角,像演员左右转头。
    β:绕 Y 轴\displaystyle \beta : \text{绕 } Y \text{ 轴}
  4. γ 对应 Z 轴控制翻滚角,像演员歪头看人。
    γ:绕 Z 轴\displaystyle \gamma : \text{绕 } Z \text{ 轴}

组件与依赖

钢铁侠托尼·斯塔克本人只是一副肉体凡胎,他能飞、能抗打、能射激光,是因为穿上了战甲——每种能力都来自战甲上的一个模块。Unity 中 GameObject 就是托尼,组件(Component)就是战甲模块:想飞就挂飞行模块,想发射导弹就挂武器模块。

Unity 内置了丰富的常用组件:MeshRenderer 给物体“穿衣服”,让摄像机看得见它;Collider 给物体一个“隐形的身体”,让碰撞检测有依据;Rigidbody 给物体“物理属性”,让它受重力、碰撞后反弹;AudioSource 让物体能发出声音;Light 则是舞台上的灯,照亮整个场景。

给物体添加组件,只需选中物体后,在 Inspector 点击 “Add Component“ 搜索并选择。脚本也是一种组件!你写好一个继承 MonoBehaviour 的 C# 类,拖到物体上,它就变成该物体的一块”行为模块“。运行时要拿到别人身上的模块,用 GetComponent<T>()——它会从组件列表里找出你要的类型,就像从腰带口袋中抽出一张你想要的能力卡。物理组件 Rigidbody 之所以能让物体”活起来”,是因为它内部不断结算速度、加速度和位置的关系:

为什么 Unity 要这么设计?因为“组合优于继承”。你不需要一个“会飞的敌人”类、一个“会飞的 Boss”类、一个“会飞的小怪”类;你只需要一个“飞行组件”,谁想飞就挂给谁。这种模块化思想贯穿整个引擎,也让 AI 自动化搭建场景成为可能。

v⃗=Δs⃗Δt\displaystyle \vec{v} = \frac{\Delta \vec{s}}{\Delta t}
通俗地说,速度就是“位置变化量除以时间变化量”:物理引擎每隔一小段固定时间算一次这个比值,物体就动起来了。
  1. 变化量符号Δ 表示“末值减去初值”,即一段变化量。
    Δs⃗=s⃗末−s⃗初\displaystyle \Delta \vec{s} = \vec{s}_{\text{末}} - \vec{s}_{\text{初}}
  2. 时间间隔从初状态到末状态经过的时间。
    Δt=t末−t初\displaystyle \Delta t = t_{\text{末}} - t_{\text{初}}
  3. 速度定义位移变化量除以时间变化量,就是速度。
    v⃗=Δs⃗Δt\displaystyle \vec{v} = \frac{\Delta \vec{s}}{\Delta t}
  4. 物理组件应用Rigidbody 基于力、质量和加速度关系结算运动,本质是速度的再变化。
    F⃗=ma⃗=mΔv⃗Δt\displaystyle \vec{F} = m \vec{a} = m \frac{\Delta \vec{v}}{\Delta t}

C# 脚本写法

场景和演员都准备好了,现在该给演员写“剧本”了。这个剧本就是 C# 脚本。剧本不直接讲故事,它只规定“在哪个时间点做什么动作”:开场第 5 秒走到沙发前,遇到障碍停下来,被碰到时播放受伤动画。

C# 脚本是一个 .cs 文本文件。它的骨架非常统一:引入命名空间、声明类、类名与文件名保持一致、类继承 MonoBehaviour。继承 MonoBehaviour 是“演员报名表”的认证章——有了它,Unity 才知道这是一个可以挂到 GameObject 上的行为模块,才会在合适的时间调用你写的生命周期函数。

写脚本时要注意三个最常见的元素:变量、方法、注释。变量是给演员“记住状态”用的,比如 speed=5 表示移动速度;方法封装一系列动作,比如 MoveForward();public 修饰的变量会直接暴露在 Inspector 面板上,你可以在不打开代码的情况下调整参数——就像导演不找演员谈话,直接在剧本上比划“这里走快一点”。脚本中最常出现的运动计算就是“路程等于速度乘时间”:

别小看 public 变量的威力:把速度字段设为 public,你就能在 Inspector 里调出“5、8、20“三种速度,而完全不动代码。这就是引擎与纯编程的区别——Unity 想让非程序员也能参与”调参”。你的第一个脚本可以是 public class Move : MonoBehaviour { public float speed = 5f; },挂到立方体上,立竿见影。

s=v×t\displaystyle s = v \times t
通俗地说,只要知道速度和时间,就能算出物体走了多远;在游戏里把 t 换成 deltaTime,就是这一帧该走的路。
  1. 路程定义匀速运动下,路程等于速度乘以时间。
    s=v×t\displaystyle s = v \times t
  2. 速度单位速度 v 表示每秒移动几米,单位是 m/s。
    v:m/s\displaystyle v : \text{m/s}
  3. 时间单位时间 t 的单位是秒。
    t:s\displaystyle t : \text{s}
  4. 脚本中的应用把 t 替换为每一帧的耗时 deltaTime,就得到这一帧的位移量。
    s一帧=v×Δt\displaystyle s_{\text{一帧}} = v \times \Delta t

生命周期函数

一场戏有固定的流程:演员提前一小时到后台化妆(Awake),开幕那一刻站到台上做自我介绍(Start),之后每一秒都在表演(Update)。Unity 给所有脚本规定了一套“演出流程模板”,称为生命周期函数。你写的脚本只要重写这些函数,Unity 就会自动在对应时刻调用。

三个最核心的函数:Awake() 在脚本被加载时立刻调用一次,适合获取组件引用、初始化私有变量;Start() 在第一次 Update 之前调用一次,适合做所有物体都准备好后的开局逻辑;Update() 每一帧调用一次,适合处理持续的移动、输入和动画。帧率不固定时,Update 每秒可能调用 30 次、60 次或更多。

所以千万不要用 Update 做精确计时,比如累加秒数。帧率波动会让你的计时忽快忽慢。正确做法是使用 Time.deltaTime——它代表“从上一帧到当前帧的真实耗时”,单位是秒。用它乘上速度,就得到这一帧应移动的距离。帧率与帧时间的关系很简单:

把 Update 想象成演员的心跳:每次心跳都告诉你“我又跳了一下”,但只有 deltaTime 才告诉你“这一跳持续了多久”。想模拟真实物理,Unity 还提供 FixedUpdate(固定时间步长),它像节拍器,稳定地每秒调用约 50 次,适合在 Rigidbody 上加力、检测碰撞。记住一句话:表现用 Update,物理用 FixedUpdate。

Δt=1FPS\displaystyle \Delta t = \frac{1}{\text{FPS}}
通俗地说,如果游戏每秒渲染 60 帧,那么平均每帧之间大约隔着 0.0167 秒;帧率越高,这个时间缺口越短。
  1. FPS 含义帧率表示每秒渲染的画面数量,单位是“帧/秒”。
    FPS=渲染帧数秒\displaystyle \text{FPS} = \frac{\text{渲染帧数}}{\text{秒}}
  2. 一帧耗时把 1 秒平均分成 FPS 份,每一份就是一帧的时间。
    Δt=1FPS\displaystyle \Delta t = \frac{1}{\text{FPS}}
  3. 60 帧代入FPS = 60 时,每帧约 0.0167 秒。
    160 秒≈0.0167 秒\displaystyle \frac{1}{60}\ \text{秒} \approx 0.0167\ \text{秒}
  4. 反比关系帧率越高,单帧时间越短。
    FPS↑⇒Δt↓\displaystyle \text{FPS} \uparrow \Rightarrow \Delta t \downarrow

场景搭建实战

幕布拉开,该你来指挥现场了——搭建场景。场景搭建就像是摆舞台:先铺地板,再放背景墙,然后把灯光、障碍物、可收集的金币逐步摆到位。

在 Hierarchy 右键菜单中,你可以创建 Cube(立方体)、Sphere(球体)、Capsule(胶囊体)、Plane(平面)等基础物体。选中物体后,Scene 视图上方的移动(W)、旋转(E)、缩放(R)工具可以快速操作它,也可以在 Inspector 的 Transform 中精确输入坐标 (x, y, z),完成毫米级摆放。

搭完一阵后,按 Ctrl+S 保存场景,场景文件 .unity 就记录了当前所有物体的摆放和组件配置。点击编辑器顶部的播放按钮可进入游戏模式,实时游玩。但要当心:游戏模式下的所有修改都不会保存——这就像彩排,无论你在舞台上怎么临场发挥,正式剧本都不会更改。想保存修改,必须先退出播放模式再操作。场景一旦开始运行,脚本里最常做的事就是让物体动起来,而移动的标准算式如下:

在 Update 中让物体移动,最标准的写法就是“用旧位置加一段位移得出新位置”。注意速度必须乘 deltaTime,否则帧率不同会导致移动速度不一致。这一行简单的公式,是无数游戏角色的移动根基,也是从“摆静态场景”走向“动态交互”的第一步。

p⃗new=p⃗old+v⃗×Δt\displaystyle \vec{p}_{\text{new}} = \vec{p}_{\text{old}} + \vec{v} \times \Delta t
通俗地说,新位置 = 旧位置 + 速度乘以帧时间;每一帧都按这个式子算一次,物体就连贯地走起来了。
  1. 旧位置当前帧开始时,物体所在的位置。
    p⃗old=(xold,yold,zold)\displaystyle \vec{p}_{\text{old}} = (x_{\text{old}}, y_{\text{old}}, z_{\text{old}})
  2. 单帧位移速度乘以帧时间,得到这一帧移动的距离。
    Δp⃗=v⃗×Δt\displaystyle \Delta \vec{p} = \vec{v} \times \Delta t
  3. 新位置旧位置加上单帧位移,得到新位置。
    p⃗new=p⃗old+Δp⃗\displaystyle \vec{p}_{\text{new}} = \vec{p}_{\text{old}} + \Delta \vec{p}
  4. 每帧循环Update 每帧重复这个计算,物体就持续运动。
    Update⇒p⃗old+v⃗×Δt⇒p⃗new\displaystyle \text{Update} \Rightarrow \vec{p}_{\text{old}} + \vec{v} \times \Delta t \Rightarrow \vec{p}_{\text{new}}

项目整合进阶

首演之夜终于到了。我们把前面所有零件组装成一个小项目:一个玩家控制的立方体在场景中移动,绕过障碍物,走近金币并“吃掉”它。这个案例综合了场景搭建、组件挂载、脚本控制、生命周期、碰撞检测的全部知识点。

操作流程:先创建地面 Plane、玩家 Cube、若干障碍 Cube、一个金币 Sphere;给玩家添加 Rigidbody(受重力)和 Collider(碰撞体);写一个 PlayerMove 脚本控制方向移动;写一个 RotateCoin 脚本让金币旋转;再用 OnCollisionEnter 或 OnTriggerEnter 检测玩家是否碰到了金币,碰到了就销毁金币。

当项目运行出错时,记住三个排障工具:Console 控制台会打印日志和报错(Debug.Log() 是主力输出);Inspector 实时显示变量,帮你确认数值;Scene/Game 视图可以观察物体的实际运动。排障原则是“先看 Console,再查 Hierarchy”,并沿着报错信息定位到具体脚本,不要盲目改代码。脚本中判断“是否碰到”最常用的数学依据,是计算两个物体之间的直线距离:

首演成功的关键不在于某一个物体的表演有多惊艳,而在于整个系统——场景、物体、组件、脚本、生命周期——协同工作。Unity 的世界里没有“超级怪物”,只有“聪明的组合”。学会把大问题拆成小组件,再用脚本把它们串联起来,你就掌握了游戏开发的核心心法。下一站,可以继续学习物理引擎、动画系统、UI 界面,甚至接入 ML-Agents 让 AI 智能体走进你的场景。

d=(x2−x1)2+(y2−y1)2\displaystyle d = \sqrt{(x_2 - x_1)^2 + (y_2 - y_1)^2}
通俗地说,就是利用两点的横纵坐标差算出直线距离;当玩家与金币的距离小于某个小数值时,就判定“吃到了”。
  1. 坐标差两个物体在横纵方向上的距离差。
    Δx=x2−x1,Δy=y2−y1\displaystyle \Delta x = x_2 - x_1, \quad \Delta y = y_2 - y_1
  2. 平方求和差值平方后相加,消除方向正负的影响。
    d2=Δx2+Δy2\displaystyle d^2 = \Delta x^2 + \Delta y^2
  3. 开根号对平方和取平方根,得到直线距离。
    d=Δx2+Δy2\displaystyle d = \sqrt{\Delta x^2 + \Delta y^2}
  4. 经典示例横纵差分别为 3 和 4 时,距离为 5。
    d=32+42=5\displaystyle d = \sqrt{3^2 + 4^2} = 5

AI 应用场景

场景1:AI 强化学习训练场搭建

阶段:数据生成 / 环境搭建

解决的问题:强化学习(RL)需要大量试错,若在现实世界训练机器人,耗时慢、成本高、有安全风险。传统网格仿真器又过于抽象,难以反映真实物理规律。Unity 提供物理引擎和渲染能力,可用 GameObject、组件和脚本快速搭建逼真的 3D 训练场,让 AI 在虚拟世界里获得可迁移的决策经验。

具体案例:一只机械臂要学习抓取积木。工程师在 Unity 中创建机械臂和积木模型 GameObject,给每个积木挂 Rigidbody 和 Collider;编写 C# 脚本接收 AI 网络输出的力矩数据,通过 AddForce 驱动关节;每帧返回关节角度、末端位置等状态。AI 在 10 万次虚拟回合后学会了抓取,策略直接迁移到真实机械臂上微调即用。

场景2:AIGC 程序化关卡生成

阶段:内容生成

解决的问题:开放世界游戏的关卡靠美术人工搭建,效率低、成本高。AI 模型(如扩散模型、GAN)能批量生成关卡布局,但输出往往只是二维数组或图像,无法直接变成可以游玩的三维场景。

具体案例:Python 端训练了一个“迷宫生成器”模型,输出 0/1/2 组成的网格地图(0=空地,1=墙,2=金币)。Unity 端编写一个 C# 脚本读取该数组,遇到 1 时在对应位置创建 Cube 并挂 Collider,遇到 2 时创建旋转的 Sphere,最后把玩家放到入口。整个关卡从数据到场景,约 2 秒完成,比人工搭建快百倍。

Python 演示

python95 行
# -*- coding: utf-8 -*-"""用Python模拟Unity的GameObject与生命周期机制"""  class Vector3:    """三维向量, 模拟Unity的Vector3"""    def __init__(self, x=0.0, y=0.0, z=0.0):        self.x = x        self.y = y        self.z = z     def __add__(self, other):        return Vector3(self.x + other.x, self.y + other.y, self.z + other.z)     def __mul__(self, scalar):        return Vector3(self.x * scalar, self.y * scalar, self.z * scalar)     def __str__(self):        return "({:.2f}, {:.2f}, {:.2f})".format(self.x, self.y, self.z)  class Transform:    """模拟Unity的Transform组件"""    def __init__(self):        self.position = Vector3()        self.rotation = Vector3()        self.scale = Vector3(1, 1, 1)  class Component:    """组件基类"""    def __init__(self):        self.game_object = None     def awake(self):        pass     def start(self):        pass     def update(self, delta_time):        pass  class MoveComponent(Component):    """让物体沿X轴匀速移动"""    def __init__(self, speed=3.0):        super().__init__()        self.speed = speed     def start(self):        print("Start: 移动组件初始化完成, 速度为 {} 米/秒".format(self.speed))     def update(self, delta_time):        go = self.game_object        go.transform.position = go.transform.position + Vector3(self.speed, 0, 0) * delta_time  class GameObject:    """模拟Unity的GameObject"""    def __init__(self, name):        self.name = name        self.transform = Transform()        self.components = []     def add_component(self, component):        component.game_object = self        self.components.append(component)        return component     def get_component(self, component_type):        for comp in self.components:            if isinstance(comp, component_type):                return comp        return None     def simulate(self, frames=60, fps=60):        delta_time = 1.0 / fps        for comp in self.components:            comp.awake()        for comp in self.components:            comp.start()        for i in range(frames):            for comp in self.components:                comp.update(delta_time)            if i % 10 == 0 or i == frames - 1:                print("帧{:>3} | {}位置: {}".format(i + 1, self.name, self.transform.position))  if __name__ == "__main__":    player = GameObject("Player")    player.add_component(MoveComponent(speed=2.0))    player.simulate(frames=30, fps=60)    move = player.get_component(MoveComponent)    print("找到组件:", type(move).__name__)
目的:用纯 Python 复刻 Unity 的 GameObject、组件与生命周期模型,演示一个对象在模拟的 Update 循环中按“速度 × 时间”公式连续移动。
  1. class MoveComponent(Component):继承组件基类,就像给演员写了一张“移动能力卡”。
  2. go.transform.position = go.transform.position + Vector3(self.speed, 0, 0) * delta_time用“旧位置 + 速度 × 帧时间 = 新位置”更新坐标,对应正文第 6 节公式。
  3. player.simulate(frames=30, fps=60)模拟 Unity 生命周期:awake → start → update 循环 30 帧。
  4. def get_component(self, component_type):等价于 Unity 的 GetComponent<T>(),从组件列表中取出指定类型。
  5. delta_time = 1.0 / fps这就是正文第 5 节的帧时间公式:1 秒除以帧率。

常见误区

误区我以为 GameObject 就是屏幕上能看到的一个立方体或球体模型,删除它模型就没了、画面就空了。

正解GameObject 只是一个空容器。你在场景中看到的立方体,实际上是“GameObject + MeshFilter(存网格数据)+ MeshRenderer(负责显示)”三个部分协作的结果。删除 GameObject 会同时删掉它身上所有组件,但这不代表 GameObject 本身是模型——它只是容器被拆了而已。

为何常见:初学者打开 Unity 就拖了个 Cube,看到 Hierarchy 里叫 Cube,便误以为 GameObject 就是这个立方体的代名词,忽略了“容器 + 组件”的真相。

误区我以为 Update 固定每秒执行 60 次,就像秒表一样准确,可以直接在里面累加计时。

正解Update 的调用频率取决于设备帧率。显示器可能是 30、60、90 甚至 144Hz,所以 Update 每秒调用次数并不固定。若在 Update 里累加时间,不同设备会得出不同结果。Unity 为此提供了 Time.deltaTime 作为每帧真实耗时;需要物理精确计算时应使用 FixedUpdate。

为何常见:游戏行业常提“60 帧”,配上直觉“一秒 60 次”,很容易让人误以为帧率恒定不变。

误区反正 Awake 和 Start 都在一开始调用,哪个顺手用哪个,效果差不多。

正解虽然两者都只调用一次,但顺序是 Awake 先、Start 后。Awake 适合获取自身组件引用、初始化私有变量,因为此时物体已加载完毕;Start 适合依赖其他物体状态的逻辑,因为此时所有物体都已完成 Awake。如果一个变量依赖其他物体,在 Awake 里可能取不到。

为何常见:二者差异微妙,初学者脚本简单,感受不到顺序问题,于是视为等价。

误区我感觉一个物体挂上一个 C# 脚本后,就不能再挂别的脚本了,否则会冲突。

正解GameObject 可以同时挂无数个组件和脚本。每个脚本之间通过 GetComponent 相互通信。这恰恰是组件化设计的优势:把移动、跳跃、播动画拆成多个小脚本,按需组合,而不是写一个巨大的“上帝脚本”。

为何常见:传统编程习惯是“一个类负责一件事”,初学者把脚本当成“每物体只能一个”的附属品,实际 Unity 鼓励组合而非堆叠。

误区我需要哪个组件就 `GetComponent` 一次,代码里到处写 `GetComponent` 挺省事的。

正解GetComponent 在编辑器模式下很方便,但在 Update 等高频调用中频繁查找会带来性能浪费。推荐在 Awake 或 Start 中获取一次,缓存在私有字段里,之后直接复用。

为何常见:初学阶段物体少、场景小,感受不到性能瓶颈,习惯一旦养成,在大型项目里就很容易卡顿。

面试题

Q1. Unity 中每个 GameObject 自带且不可被删除的组件是?

单选题 · easy

  • A.Collider 组件负责物理碰撞
  • B.MeshRenderer 负责网格渲染
  • C.Transform 组件负责位置变换← 正确答案
  • D.Camera 摄像机负责画面输出

答案:C。Transform 是每个 GameObject 自带的唯一“站位卡”,负责记录位置、旋转和缩放,不能被删除。其他组件(渲染、碰撞、摄像机)都需要显式添加,因此只有 Transform 是必选项。

Q2. Awake、Start、Update 三者的执行顺序是?

单选题 · mid

  • A.Start、Awake、Update 依次
  • B.Awake、Start、Update 依次← 正确答案
  • C.Start、Update、Awake 依次
  • D.Awake、Update、Start 依次

答案:B。Awake 在物体加载时立即执行;Start 在所有物体的 Awake 完成后、第一帧 Update 前执行;Update 在每一帧执行。调用顺序固定为 Awake → Start → Update,这与舞台演出“化妆→上场→表演”完全一致。

Q3. 要让物体受重力影响下落,应添加的组件组合是?

单选题 · easy

  • A.Collider 与 Rigidbody 组合← 正确答案
  • B.MeshRenderer 与 Camera 组合
  • C.Transform 与 Light 组合
  • D.AudioSource 与 Collider 组合

答案:A。物理下落需要两个组件配合:Collider 提供碰撞体,Rigidbody 提供物理属性并响应重力。渲染、摄像机、音频和光照都与重力没有直接关系,因此选 A。

Q4. C# 脚本要挂到 GameObject 上,类必须继承?

单选题 · easy

  • A.UnityEngine.Object 基类
  • B.Transform 组件基类
  • C.Physics Component 类
  • D.MonoBehaviour 组件基类← 正确答案

答案:D。C# 脚本要成为 GameObject 的组件,类必须继承 MonoBehaviour,这是 Unity 脚本系统识别“行为模块”的关键。继承后脚本才能被拖到物体上,并接收到生命周期函数的回调。

Q5. 以下关于场景 Scene 的说法,正确的是?

多选题 · mid

  • A.场景用于搭建游戏关卡← 正确答案
  • B.场景文件保存物体配置← 正确答案
  • C.一个项目只能有一个场景
  • D.场景之间可以相互切换加载← 正确答案

答案:ABD。场景是 Unity 搭建关卡的基本单位,可以保存物体的摆放和组件配置,也是脚本切换关卡的目标。一个项目可以有多个场景,并非只能有一个,因此 C 错误。

Q6. Update 中移动物体,为什么要乘以 deltaTime?

单选题 · hard

  • A.让物体移动速度大幅加快
  • B.将移动速度与帧率解耦← 正确答案
  • C.强制游戏运行在 60fps
  • D.减少每帧渲染的开销

答案:B。deltaTime 把“位置增量 = 速度 × 时间”中的时间补上,使得 30fps 与 60fps 设备下物体的移动总距离一致。它不是加速工具,也不保证帧率,更不会减少渲染开销。

Q7. 下列属于 Unity 生命周期函数的有?

多选题 · mid

  • A.Awake() 唤醒与初始化← 正确答案
  • B.Start() 首次启动逻辑← 正确答案
  • C.Update() 每帧更新逻辑← 正确答案
  • D.Main() 主程序函数入口

答案:ABC。Unity 生命周期函数的核心包括 Awake、Start、Update、FixedUpdate、OnDestroy 等。Main() 不属于 Unity 生命周期函数,而是传统 C# 程序的可执行入口,因此 D 错误。

Q8. 脚本中获取同物体上的 Rigidbody 组件,正确写法是?

单选题 · mid

  • A.GameObject.Find 查找指定物体
  • B.GetComponent() 泛型方法获取← 正确答案
  • C.FindObjectOfType() 全场景搜索
  • D.new 直接创建新的组件

答案:B。GetComponent() 是从物体组件列表中获取指定类型组件的标准方法;GameObject.Find 和 FindObjectOfType 会搜索整个场景,代价更高;直接 new 一个组件会被 Unity 禁止。

Q9. 关于 FixedUpdate 与 Update 的区别,正确的是?

多选题 · hard

  • A.以固定时间步长调用← 正确答案
  • B.适合物理模拟与碰撞← 正确答案
  • C.每次调用间隔一致← 正确答案
  • D.一定比 Update 调用更频繁

答案:ABC。FixedUpdate 以固定时间步长调用,适合物理模拟(刚体加力、碰撞检测)。Update 每帧调用且帧间隔不等;当帧率高于 FixedUpdate 频率时,Update 调用次数反而更多,因此 D 错误。

Q10. 某个游戏在 30fps 和 60fps 下运行,物体每帧按 speed × deltaTime 更新位置,1 秒后的总位移相比?

单选题 · extreme

  • A.30fps 下移动约为 60fps 的一半
  • B.60fps 下移动约为 30fps 的两倍
  • C.两种帧率下总位移基本一致← 正确答案
  • D.30fps 下单帧移动距离更小

答案:C。每帧位移 = speed × deltaTime,同速度下 1 秒累计位移 = speed × ΣdeltaTime ≈ speed × 1 秒,与帧率无关。30fps 下每帧位移更大、60fps 下每帧位移更小,但总位移基本一致。这正是 deltaTime 存在的意义。