Home 场景的启动引导脚本,负责设置相机背景色、确保 UI 事件系统存在、加载并实例化主菜单预制体。 从零开始逐行拆解,类比如前端 Vue / React 组件生命周期与 DOM 操作。
先整体看一眼,建立全局印象,然后逐行拆开讲。
using UnityEngine;
using UnityEngine.EventSystems;
// 主页场景引导(放在 Home 场景里)。对应 Main 场景的 GameRoot:
// 负责设好相机、EventSystem,并实例化主页界面预制体 HomeScreen.prefab。
public sealed class HomeRoot : MonoBehaviour
{
void Awake()
{
var cam = Camera.main;
if (cam != null)
{
cam.clearFlags = CameraClearFlags.SolidColor;
cam.backgroundColor = Theme.MainBackgroundColor;
}
EnsureEventSystem();
var prefab = Resources.Load<GameObject>("Prefabs/HomeScreen");
if (prefab != null)
Instantiate(prefab);
else
Debug.LogError("主页预制体缺失...");
}
static void EnsureEventSystem()
{
if (Object.FindObjectOfType<EventSystem>() == null)
{
var es = new GameObject("EventSystem");
es.AddComponent<EventSystem>();
es.AddComponent<StandaloneInputModule>();
}
}
}
每一行的 API 来源、C# 语法、前端等价写法。不留坑。
第 1~2 行,告诉编译器我要用哪些命名空间里的东西。
using UnityEngine;
C# 的 命名空间导入语句。using = 前端的 import。
写了这一行后,下面的 MonoBehaviour、Camera、GameObject、Debug 才能直接用短名字。
不写的话得写全限定名 UnityEngine.MonoBehaviour。
using UnityEngine.EventSystems;
导入 EventSystems 命名空间,这样下面才能用 EventSystem 和 StandaloneInputModule 这两个类(UGUI 点击事件所需)。
第 6 行,定义这个脚本的类型。
public sealed class HomeRoot : MonoBehaviour
四个关键字,逐个解释:
| 关键字 | 含义 | JS 类比 |
|---|---|---|
public | 任何地方都能访问 | export class |
sealed | 禁止被继承 | Object.freeze() 对类的效果 |
class HomeRoot | 定义一个类 | class HomeRoot |
: MonoBehaviour | 继承 Unity 内置基类 | extends MonoBehaviour |
为什么所有 Unity 脚本都要继承它?
MonoBehaviour 是 Unity 引擎内置的基类(UnityEngine.MonoBehaviour),所有挂到 GameObject 上的脚本
必须继承它。继承之后,你的类才能拥有 Awake()、Start()、Update() 等生命周期方法。
Mono = 来自 Mono 框架(Unity 早期的 C# 运行时,跨平台 .NET 实现)。
Behaviour = 行为/组件,表示可以附加到 GameObject 上的"行为脚本"。
Unity 每帧自动遍历场景里所有 MonoBehaviour 子类,按顺序调用:
Awake() → Start() → Update() → Update() → ... → OnDestroy()
第 8 行,Unity 自动调用的入口方法。
void Awake()
void 表示不返回任何值(= TypeScript 的 void)。
Awake 是 Unity 规定的钩子名(类似 Vue 的 created() / mounted())。
没有写访问修饰符,C# 类成员默认是 private,等价于 private void Awake()。
第 10 行。
var cam = Camera.main;
var = C# 的编译时类型推断。编译器看到右边返回 Camera 类型,自动推断 cam 是 Camera。
注意:它不是 JS 的 var(JS 是运行时),C# 的 var 在编译后类型就固定了,是静态类型。
Camera = Unity 内置类,代表场景里的摄像机组件(玩家看到的画面由它渲染)。
.main = Camera 的静态属性,返回场景里 tag 为 "MainCamera" 的第一个摄像机。
如果场景里没有 MainCamera,返回 null。
第 11~15 行。
if (cam != null)
null = C# 的空引用,等同于 JS 的 null / undefined。
判空防御:如果场景里没有主摄像机,后面代码不执行,防止 NullReferenceException(= JS 的 Cannot read property of undefined)。
cam.clearFlags = CameraClearFlags.SolidColor;
clearFlags = Camera 的实例属性,控制每帧渲染前怎么清屏。
CameraClearFlags = Unity 内置枚举,选项集合:
• Skybox — 显示天空盒(默认)
• SolidColor — 纯色背景(本项目选这个)
• Depth — 只清深度缓冲
• Nothing — 不清屏
cam.backgroundColor = Theme.MainBackgroundColor;
backgroundColor = Camera 的实例属性,纯色清屏时用什么颜色。
Theme = 本项目手写的静态类(Assets/_Game/Themes/Theme.cs),集中管理所有颜色常量。
MainBackgroundColor = new Color(1f, 1f, 1f, 1f),即白色。
第 17 行,调用自定义静态工具方法。
EnsureEventSystem();
调用下面定义的静态方法。方法名带 Ensure 是编码习惯:确保某东西存在,没有就创建。
第 19~23 行。
var prefab = Resources.Load<GameObject>("Prefabs/HomeScreen");
Resources = Unity 内置静态类,从 Resources/ 文件夹动态加载资源。
.Load<T>() = 泛型方法。 <GameObject> 告诉它返回类型是 GameObject。
"Prefabs/HomeScreen" = 相对路径。实际对应的文件:
| 文件实际位置 | Load 的路径 |
|---|---|
| Assets/_Game/Home/Resources/Prefabs/HomeScreen.prefab | Prefabs/HomeScreen |
规则:去掉 Resources/ 前缀,去掉文件扩展名。
if (prefab != null)
Instantiate(prefab);
else
Debug.LogError("主页预制体缺失...");
Instantiate(prefab) = Unity 最重要的方法之一,克隆一个 GameObject 放到场景里。
Debug.LogError("...") = 红色错误日志,输出到 Console 窗口(= JS 的 console.error())。
第 26~34 行。
static void EnsureEventSystem()
static = 静态方法,属于类本身,不需要 new HomeRoot() 就可以调。
同类的静态方法可以省略类名直接调 EnsureEventSystem(),不同类要用 HomeRoot.EnsureEventSystem()。
if (Object.FindObjectOfType<EventSystem>() == null)
Object = UnityEngine.Object,所有 Unity 对象的基类(⚠️ 不是 C# 的 object)。
FindObjectOfType<T>() = 全局搜索,在场景中遍历所有 GameObject,找第一个挂有 EventSystem 组件的。
性能警告:这个方法很慢,只能在初始化时偶尔用,绝不能在 Update 里每帧调用。
var es = new GameObject("EventSystem");
new = C# 的 new,创建一个空的 GameObject,控制台里显示名为 "EventSystem"。
GameObject = Unity 中一切场景对象的基类。空 GameObject = 一个只有 Transform 的空容器。
| 写法 | 性质 | 作用 |
|---|---|---|
"EventSystem"(引号字符串) |
随便取的 显示名 | Hierarchy 窗口里看到的 GameObject 名字,写 "foo" 也行 |
EventSystem(无引号,第 31 行) |
Unity 内置类 | 真正的 UGUI 事件调度引擎,来自 UnityEngine.EventSystems |
第 30 行只是创建了一个空容器并取名叫 "EventSystem"。真正让 UI 能点击的,是第 31~32 行通过 AddComponent 挂上去的两个内置组件。
es.AddComponent<EventSystem>();
es.AddComponent<StandaloneInputModule>();
AddComponent<T>() = 给 GameObject挂一个组件。
这里挂了两个组件:
从场景加载到主菜单显示,HomeRoot 做了三件事。
HomeRoot 是一个极简的场景启动器,只做三件事:设相机背景、确保 UGUI 能响应点击、加载主菜单 UI。 对比 Main 场景的 GameRoot,它非常轻量 —— 因为主菜单是一个静态 UI,不需要 ECS 世界、关卡系统、作弊面板等复杂逻辑。
本文件中出现的所有 Unity/C# 概念及其前端等价物。
| Unity / C# | 含义 | 前端类比 |
|---|---|---|
using | 导入命名空间 | import |
class A : B | A 继承 B | class A extends B |
MonoBehaviour | Unity 脚本基类 | React.Component / Vue 组件 |
Awake() | 生命周期钩子,最早调用 | created() / mounted() |
var | 编译时类型推断(静态) | const(但概念不完全相同,C# var 是静态) |
null | 空引用 | null / undefined |
static | 属于类,不属于实例 | static |
enum | 枚举类型 | enum |
GameObject | 场景中的一切对象 | DOM 元素 <div>, <button> |
Component | 挂到 GameObject 上的功能模块 | HTML 属性 / 指令 |
| Prefab | 可复用的 GameObject 模板 | React 组件模板 |
Resources.Load() | 动态加载资源 | import() 动态导入 |
Instantiate() | 克隆对象到场景 | cloneNode(true) + appendChild |
Debug.Log() | 普通日志 | console.log() |
Debug.LogError() | 错误日志 | console.error() |
<T> 泛型 | 类型参数 | <T> in TypeScript |
Color(r,g,b,a) | RGBA 颜色(值范围 0~1) | rgba(r,g,b,a)(值范围 0~255) |
CH 01~04 已逐行拆解了所有代码逻辑。这里补充几个对前端转型者来说「一眼看不出来」的关键差异。
第 6 行 public sealed class HomeRoot 中的 sealed。
public sealed class HomeRoot : MonoBehaviour
sealed 阻止继承:别的类不能写 class Xxx : HomeRoot。Unity 里大多数场景脚本都标记 sealed,原因有三:
代码中每一对大括号的含义和 C# 特有的风格。
{ ... } // 类体
第 7 行 { 和第 35 行 } 包裹整个类体。C# 的花括号和 JS 一样表示「作用域块」,但有几个差异:
| 差异点 | C# | JS |
|---|---|---|
| 换行风格 | 花括号另起一行(Allman 风格) | 花括号放在同一行末尾(K&R 风格) |
| 作用域 | { } 内声明的变量离开 {} 就销毁 |
var 会提升,let/const 块级作用域 |
| 单行 if | 可以省略花括号但本项目不允许 | 可以省略但 ESLint 通常禁止 |
| 表达式 vs 语句 | { } 永远是语句块,不返回值 |
对象字面量 {} 是表达式,会混淆 |
Color(1f, 1f, 1f, 1f) 中的 f 是什么意思?
new Color(1f, 1f, 1f, 1f)
C# 的Color 构造函数接受 float 类型参数(范围 0.0 ~ 1.0)。
在 C# 中,带小数点的数字字面量默认是 double 类型(双精度浮点),不是 float。写 1.0 = double,传给 float 参数需要显式转换。
f 后缀告诉编译器:「这是 float 类型,不是 double」。
| 写法 | C# 类型 | 说明 |
|---|---|---|
1 | int | 整数 |
1.0 | double | 双精度(64位) |
1f 或 1.0f | float | 单精度(32位),Unity 常用 |
1d 或 1.0d | double | 显式双精度(不常用) |
为什么 Awake() 前面没有 public/private?
void Awake() // 没有 public
static void EnsureEventSystem() // 也没有 public
C# 中,类成员(方法、字段、属性)不写访问修饰符时默认是 private。
| 场景 | C# 默认 | JS 默认 |
|---|---|---|
| 类成员(方法/字段) | private | public(无原生 private,用 # 前缀) |
| 类本身 | internal | export 才公开,否则模块私有 |
| 接口/枚举成员 | public | public |
⚠️ 重要:即使不写 public,Unity 引擎依然能找到并调用 Awake()、Start()、Update() 等生命周期方法。这是 Unity 使用反射(Reflection)做到的 —— 不管方法是 public 还是 private,引擎都能调用。
一些看起来不一样但其实只是习惯差异的地方。
C#:PascalCase(类/方法/属性)、camelCase(局部变量)
JS:camelCase(全部)
本例:HomeRoot(类)、Awake(方法)、cam(局部变量)
C# 方法直接写:void MethodName() { }
不需要 function 关键字。
返回类型写在前面(void / int / string 等)。
new GameObject() — 实例化对象(同 JS)
sealed class 配合 new 可以隐藏父类成员(但本项目未使用此用法)
C# 每条语句必须以 ; 结尾。
JS 有 ASI(自动分号插入),C# 没有。
| 行号 | 符号 | 作用 |
|---|---|---|
| 7 | { | 类 HomeRoot 体开始 |
| 9 | { | Awake() 方法体开始 |
| 12 | { | if (cam != null) 块开始 |
| 15 | } | if (cam != null) 块结束 |
| 24 | } | Awake() 方法体结束 |
| 27 | { | EnsureEventSystem() 方法体开始 |
| 29 | { | if (FindObjectOfType == null) 块开始 |
| 33 | } | if 块结束 |
| 34 | } | EnsureEventSystem() 方法体结束 |
| 35 | } | 类 HomeRoot 体结束 |
C# 中每对 { } 都定义一个作用域。花括号内声明的变量离开作用域就失效。这和 JS 的 let/const 块级作用域是一样的。
左栏:高亮源码 | 右栏:解释、来源、JS 类比。标签:C# 语言 / JS 类比 / src 来源 / ! 注意
using ≈ JS 的 import,但它是命名空间级别的,不导入具体文件而是导入整个命名空间。Unity 把核心 API 全放在 UnityEngine 命名空间里,一行 using 就能用 MonoBehaviour、GameObject、Debug 等几百个类。sealed = 不可被继承,标记后别人不能 extend 这个类。MonoBehaviour = Unity 脚本基类,挂 GameObject 上才有生命周期。C#js: class HomeRoot extends Component
Camera.main = 返回场景中 tag="MainCamera" 的第一个摄像机。C#
Skybox(默认)= 渲染天空盒,3D 游戏用。SolidColor = 纯色填充,2D 游戏标配,最省性能。DepthOnly = 只清深度,多相机叠加时用。DontClear = 不清,会拖影。Theme.MainBackgroundColor 是项目自定义常量,定义在 Theme.cs。src: Theme.csjs: cam.style.backgroundColor = Theme.mainBg
Color.white。这和前端用 CSS 变量(var(--bg))不用硬编码颜色值的道理一样。"Prefabs/HomeScreen" = 对应 Resources/Prefabs/HomeScreen.prefab。src: Resources/Prefabs/HomeScreen.prefabjs: const prefab = await import('Prefabs/HomeScreen')
import() 心智模型一致。.Bind(_contexts) 注入 ECS 上下文。