C#与Unity游戏开发
Unity脚本与组件系统
一、学习目标
本文以 MIT 6.102 Software Construction、Stanford CS193u Game Design、CMU 15-466 Computer Game Programming、UC Berkeley CS 184 Foundations of Computer Graphics 的教学水准为参照,对 Unity 引擎的脚本系统、组件模型、生命周期、性能优化与工程化实践进行系统性、形式化与工程化的深度剖析。阅读完毕后,读者应能达成以下 Bloom 认知层级目标:
| 层级 | 目标描述 | 具体可观测行为 |
|---|---|---|
| Remember(记忆) | 复述 Unity 的 GameObject-Component 模型、MonoBehaviour 生命周期、ScriptableObject、协程机制 | 在不查文档的情况下画出 MonoBehaviour 完整事件函数调用顺序图 |
| Understand(理解) | 解释 Unity 的本地/托管边界、原生组件与 C# 脚本的交互、GC 在游戏循环中的影响 | 说明为何 GetComponent 在 Update 中调用会产生性能损耗,以及托管堆与原生堆的同步机制 |
| Apply(应用) | 在企业级游戏项目中正确使用 ScriptableObject、对象池、状态机、事件系统、async/await | 为一个 ARPG 角色控制系统设计完整的组件结构与数据驱动配置 |
| Analyze(分析) | 对比 Unity 的组件模型与 ECS(DOTS)、Actor 模型、传统 OOP 继承体系的差异 | 诊断一个因 Update 中高频 GetComponent 与 foreach 装箱导致的帧率下降 bug |
| Evaluate(评价) | 评估 MonoBehaviour、ScriptableObject、ECS、Job System、Burst Compiler 的适用场景 | 在角色 AI、大规模单位战斗、粒子模拟三种场景中做出技术选型决策并说明依据 |
| Create(创造) | 设计可扩展的游戏架构(事件驱动、依赖注入、模块化、可测试) | 实现一个基于 ScriptableObject 的事件总线、可单元测试的角色控制器与编辑器扩展 |
本文假设读者已掌握 C# 基础语法、async/await、泛型、LINQ、IDisposable、接口与抽象类。
二、历史动机与发展脉络
2.1 问题背景:游戏引擎的脚本语言困境
在 2000 年代初期,游戏引擎的脚本语言生态极为混乱:
- Unreal Engine 早期使用 UnrealScript(一种 Java-like 的自定义语言),开发体验与生态受限;
- CryEngine 使用 Lua 与 Flowgraph 可视化脚本,性能与可维护性较差;
- id Tech 使用 C++ 与 QuakeC,跨平台与热重载困难;
- Unity(前身 Unity3D) 在 2005 年发布时选择 Mono(C# 的开源实现)作为脚本运行时,是当时游戏引擎中第一个采用 ECMA-334 标准语言的主流引擎。
Unity 选择 C# 的核心动机:
- 类型安全:相比 Lua/Python,C# 的强类型能在编译期捕获更多错误;
- 生态成熟:.NET BCL 提供大量开箱即用的数据结构与 IO 能力;
- 工具链丰富:Visual Studio、ReSharper 等工具支持完善;
- 跨平台:Mono 运行时可在 Windows、macOS、Linux、iOS、Android、WebGL 等平台运行;
- 性能可控:AOT 编译与 IL2CPP 后端可接近 C++ 性能。
2.2 Unity C# 运行时的演进
Unity 的 C# 运行时经历了三个主要阶段:
| 阶段 | 时间 | 运行时 | 编译后端 | 特点 |
|---|---|---|---|---|
| Mono-only | 2005-2015 | Mono 2.x/3.x | JIT (Mono) | 启动快,跨平台,GC 性能一般 |
| IL2CPP 引入 | 2015-2018 | Mono + IL2CPP | AOT (IL2CPP) | iOS 强制 AOT,性能与体积优化 |
| CoreCLR / DOTS | 2018-至今 | Mono + IL2CPP + CoreCLR (实验) | AOT + JIT | Burst Compiler、Job System、源生成器 |
2018 年 Unity 引入 IL2CPP(Intermediate Language to C++)作为可选编译后端,2019 年起成为 iOS/主机的默认后端,2021 年起逐步推广到 Android/WebGL。IL2CPP 将 IL 翻译为 C++ 源码再编译为原生代码,兼顾 AOT 安全性与跨平台性能。
2020 年 Unity 发布 DOTS(Data-Oriented Technology Stack),包含:
- ECS(Entity Component System):面向数据而非面向对象的架构;
- C# Job System:基于
System.Threading的高层并行 API; - Burst Compiler:基于 LLVM 的 SIMD 优化编译器,专门针对 C# 子集;
- Native Collections:
NativeArray<T>、NativeHashMap<K,V>等非托管容器,绕过 GC。
2.3 Unity C# 版本支持
Unity 对 C# 语言版本的支持长期滞后于 .NET 官方:
| Unity 版本 | C# 版本 | .NET 基础库 | 备注 |
|---|---|---|---|
| 5.x | C# 4.0 | .NET 2.0 Subset / .NET 2.0 | 仅支持基础语法 |
| 2017.x | C# 6.0 | .NET 3.5 | 引入字符串插值、null 条件运算符 |
| 2018.x | C# 6.0 | .NET 3.5 / .NET 4.x | 实验性 .NET 4.x |
| 2019.x | C# 7.3 | .NET Standard 2.0 | 元组、模式匹配、Span<T> |
| 2020.x | C# 8.0 | .NET Standard 2.1 | 可空引用类型、IAsyncEnumerable |
| 2021.x | C# 9.0 | .NET Standard 2.1 | record、init、顶级语句 |
| 2022.x | C# 9.0 | .NET Standard 2.1 | 稳定 |
| 2023.x (Unity 6) | C# 9.0 | .NET Standard 2.1 + 部分 .NET 6 | Source Generator 实验性 |
| 2024+ (Unity 7) | C# 11/12 | .NET 8 (CoreCLR 实验) | 计划迁移至 CoreCLR |
注意:Unity 长期使用 .NET Standard 2.1 作为 API 兼容级别,这意味着部分 .NET 5+ 的新 API(如 DateOnly、TimeOnly、HttpClient 流式改进)在 Unity 中不可用或需通过 polyfill 解决。
2.4 现代架构趋势:从 MonoBehaviour 到 ECS
Unity 的架构演进方向是面向数据设计(DOD, Data-Oriented Design):
- 传统 MonoBehaviour:面向对象,组件持有状态,每帧
Update轮询,CPU 缓存不友好; - ECS:实体只是 ID,组件是纯数据(struct),系统(System)批量处理组件,CPU 缓存命中率高;
- Job System:将系统逻辑并行化到多核;
- Burst:将 C# 子集编译为 SIMD 优化的原生代码。
DOTS 场景下,1 万个单位的同步移动可在 1ms 内完成,而 MonoBehaviour 实现需要 50ms+。但 ECS 学习曲线陡峭,Unity 仍在两条路线并行支持。
三、形式化定义
3.1 GameObject-Component 模型的代数语义
设 GameObject 集合 ,Component 类型集合 ,关系 表示”对象拥有某类型组件”。
一个 GameObject 可视为其组件集合的多重集:
每个 在 中至多出现一次(同一类型组件不可重复挂载)。查询操作:
AddComponent 是构造操作,Destroy 是销毁操作。Unity 的 Transform 组件是强制必备的:。
3.2 生命周期事件函数的形式化
MonoBehaviour 的生命周期可建模为有限状态自动机(FSA)。状态集 ,事件函数为状态转换:
其中 为引擎事件集合(场景加载、帧更新、启用/禁用、销毁)。关键转换:
Awake 与 Start 的核心区别:Awake 在对象实例化后立即调用(即使未激活),Start 在对象首次激活后的下一帧调用。这意味着同一帧 Instantiate 的对象,其 Awake 在当前帧执行,Start 在下一帧执行。
3.3 协程(Coroutine)的形式化
Unity 协程是一个可挂起的状态机。形式化:协程 是迭代器函数 ,其执行轨迹由一系列 yield return 点分隔:
其中 为状态, 为 yield 指令(null、WaitForSeconds、WaitForEndOfFrame、AsyncOperation 等)。引擎主循环每帧检查当前 yield 指令是否完成,完成则恢复执行至下一个 yield。
形式化语义:
协程的本质是单线程内的协作式调度,所有协程在主线程执行,不存在并发安全问题,但任一协程长时间不 yield 会阻塞整个游戏循环。
3.4 ScriptableObject 的形式化
ScriptableObject 是一种资产化数据容器。形式化:
与 MonoBehaviour 的区别:
- MonoBehaviour 实例附着于 GameObject,生命周期随场景;
- ScriptableObject 实例存储于
.asset文件,生命周期独立于场景; - ScriptableObject 在内存中是单例式的(同一资产被多次引用时共享同一实例)。
ScriptableObject 的内存语义:在加载时通过 ScriptableObject.CreateInstance<T>() 或 Resources.Load 创建,在编辑器中可序列化为 .asset 文件,运行时仅存在一份实例(共享引用)。这使得它成为配置数据与共享状态的理想容器。
3.5 ECS 的代数语义
ECS 模型形式化为三元组 :
- 实体集 :整数 ID 集合,无数据无行为;
- 组件集 :纯数据结构(
IComponentData),按类型分组存储在连续内存中(ArchetypeChunk); - 系统集 :处理组件数据的逻辑(
SystemBase),通过查询(EntityQuery)过滤拥有特定组件组合的实体。
实体 由其组件集合定义:
系统 通过查询 获取实体:
ECS 的核心优势是内存局部性:同一 Archetype(组件类型集合相同)的实体存储在连续内存中,遍历时 CPU 缓存命中率高。形式化性能模型:
对比 MonoBehaviour 的指针追逐:
其中 (典型 100:1 比例)。
四、理论推导与原理解析
4.1 托管-本地边界的调用开销
Unity 的核心引擎(C++)与脚本(C#)之间存在托管-本地边界(managed-native boundary)。每次跨边界调用产生:
- Marshalling 开销:参数类型转换(如 C#
string与 C++char*); - GC Pinning:跨边界调用时需 pin 住托管对象防止 GC 移动;
- Thunk 跳转:IL2CPP 生成的胶水代码跳转。
形式化:设纯托管调用开销 ,跨边界调用开销 。典型值:
这就是 GetComponent 在 Update 中高频调用导致性能问题的原因:每次 GetComponent 都跨边界调用 C++ 的 Object::GetComponent。
优化原则:在 Awake/Start 中缓存组件引用,Update 中直接使用缓存。
4.2 GC 在游戏循环中的影响
Unity Mono 后端使用 Boehm GC(保守式标记-清除),CoreCLR 后端使用分代 GC。GC 触发时:
- Stop-the-world:所有托管线程暂停;
- 标记阶段:遍历所有根对象引用图;
- 清除阶段:回收不可达对象。
GC 暂停时间与堆大小、对象引用密度正相关。典型值:
| 堆大小 | 标记时间 | 是否可接受 |
|---|---|---|
| 10MB | 1-3ms | 60fps 可接受 |
| 50MB | 5-15ms | 60fps 边缘 |
| 100MB+ | 20-50ms | 60fps 不可接受 |
游戏循环的帧预算:60fps = 16.67ms/帧,30fps = 33.33ms/帧。GC 暂停超过 5ms 会明显感知卡顿。
优化策略:
- 避免运行时分配:缓存常用对象,使用对象池;
- 避免闭包捕获:
Action、Func闭包会分配委托对象; - 避免 LINQ 在热路径:
Where、Select产生迭代器分配; - 使用
Span<T>:避免数组分配; - 使用
stackalloc:栈分配小缓冲区; - 使用
NativeArray<T>:DOTS 场景的非托管数组。
4.3 协程与 async/await 的对比
Unity 的协程(IEnumerator)与 C# 的 async/await 都解决”异步逻辑”问题,但机制不同:
| 特性 | 协程(Coroutine) | async/await |
|---|---|---|
| 调度 | 主线程帧驱动 | TaskScheduler 驱动 |
| 返回值 | 无(IEnumerator) | Task<T> |
| 异常处理 | 需手动 try-catch | 自动传播 |
| 取消 | StopCoroutine | CancellationToken |
| 跨线程 | 仅主线程 | 可配置 |
| 性能 | 低开销 | 状态机分配 |
形式化:协程是单线程协作式,async/await 是多线程抢占式 + 协作式混合。在 Unity 中,原生 async/await 存在两个问题:
- 不会随 MonoBehaviour 销毁而自动取消,导致”已销毁对象上继续执行”的 NullReferenceException;
- 默认在 ThreadPool 执行,访问 Unity API(必须在主线程)会失败。
解决方案:使用 UniTask(Cysharp 开源)替代 Task,提供:
- 零分配的 async/await(基于
UniTaskvalue type); - 自动绑定 MonoBehaviour 生命周期(
GetCancellationTokenOnDestroy()); - 与 Unity 协程互操作(
UniTask.ToCoroutine)。
4.4 Transform 层级与矩阵合成
Unity 的 Transform 层级是树结构,每个节点的世界变换矩阵由父节点链合成:
其中 由位置 、旋转 (四元数)、缩放 合成:
修改子物体 local 位置时,引擎需重新合成其世界矩阵,并递归标记所有子节点为 dirty。因此:
- 深层嵌套层级的 transform 修改代价高:深度 的修改触发 的 dirty 传播;
- 频繁修改 transform 应避免深层嵌套:扁平化层级或使用
Transform.SetParent(null)临时脱离。
Unity 内部使用延迟更新:修改 local 变换后只标记 dirty,实际世界矩阵在 Update 后、LateUpdate 前的 OnEndOfFrame 阶段批量计算。
4.5 物理引擎的固定时间步
Unity 的 FixedUpdate 由物理引擎驱动,时间步长固定(默认 0.02s = 50Hz)。与 Update(每帧调用,时间步长可变)的区别:
物理模拟必须在固定时间步下进行,否则积分误差累积导致抖动。设位置 ,速度 ,加速度 ,显式欧拉积分:
可变时间步会导致:
- 碰撞穿透:高帧率时位移小,低帧率时位移大,可能跳过碰撞检测;
- 堆叠不稳定:多个刚体堆叠时,不同时间步的浮点误差累积导致抖动;
- 关节拉伸:铰链关节、弹簧关节在可变时间步下行为不可预测。
最佳实践:所有物理操作(Rigidbody.AddForce、Rigidbody.velocity、Physics.Raycast 触发刚体)放在 FixedUpdate,渲染相关操作放在 Update,相机跟随放在 LateUpdate。
4.6 对象池的内存复用模型
对象池通过预分配 + 复用避免运行时 GC 压力。形式化:
设池大小 ,对象生命周期成本 ,分配/回收开销 。无对象池时 次创建销毁总成本:
有对象池时(预热 次,复用 次):
其中 为重置对象状态的成本(通常远小于 )。当 时:
Unity 中 Instantiate/Destroy 还涉及跨边界调用与资源加载(Prefab 引用、材质、网格),单次开销可达 100-500μs。对象池可将重复开销降至 1-5μs。
五、代码示例(企业级 production-ready)
5.1 项目结构
FandexUnityGameDemo/
├── FandexUnityGameDemo.csproj
├── Packages/
│ ├── manifest.json
│ └── UniTask/ # 第三方包
├── Assets/
│ ├── Scripts/
│ │ ├── Core/
│ │ │ ├── GameManager.cs
│ │ │ ├── ServiceLocator.cs
│ │ │ └── EventBus.cs
│ │ ├── Player/
│ │ │ ├── PlayerController.cs
│ │ │ ├── PlayerHealth.cs
│ │ │ └── PlayerInputHandler.cs
│ │ ├── Combat/
│ │ │ ├── WeaponSystem.cs
│ │ │ ├── DamageCalculator.cs
│ │ │ └── ProjectilePool.cs
│ │ ├── AI/
│ │ │ ├── EnemyAI.cs
│ │ │ ├── StateMachine.cs
│ │ │ └── IState.cs
│ │ ├── Data/
│ │ │ ├── WeaponConfig.cs
│ │ │ ├── EnemyConfig.cs
│ │ │ └── GameSettings.cs
│ │ └── Editor/
│ │ └── WeaponConfigEditor.cs
│ ├── Prefabs/
│ │ ├── Player.prefab
│ │ ├── Enemy.prefab
│ │ └── Projectile.prefab
│ └── ScriptableObjects/
│ ├── Weapons/
│ └── Enemies/
└── ProjectSettings/
5.2 csproj 配置(Unity 生成,编辑器外手动管理)
Unity 项目通常由 Unity 自动生成 .csproj,但可手动定制(用于外部 IDE 编辑):
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<LangVersion>9.0</LangVersion>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<DefineConstants>UNITY_2022_3_OR_NEWER;ENABLE_UNITY_COLLECTIONS_CHECKS</DefineConstants>
<!-- 禁用 .NET CLI 默认的 globbing,由 Unity 管理 -->
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
</PropertyGroup>
<ItemGroup>
<!-- Unity 引擎 DLL -->
<Reference Include="UnityEngine">
<HintPath>$(UnityInstallPath)\Editor\Data\Managed\UnityEngine\UnityEngine.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.CoreModule">
<HintPath>$(UnityInstallPath)\Editor\Data\Managed\UnityEngine\UnityEngine.CoreModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.PhysicsModule">
<HintPath>$(UnityInstallPath)\Editor\Data\Managed\UnityEngine\UnityEngine.PhysicsModule.dll</HintPath>
</Reference>
<!-- UniTask(推荐使用 NuGetForUnity 或 UPM) -->
<PackageReference Include="Cysharp.UniTask" Version="2.5.4" />
</ItemGroup>
</Project>
5.3 核心系统:事件总线
// Core/EventBus.cs —— C# 9 / Unity 2022+
using System;
using System.Collections.Generic;
using UnityEngine;
namespace FandexUnityGameDemo.Core {
/// <summary>
/// 类型安全的事件总线。基于 ScriptableObject 的事件系统,
/// 支持跨场景广播与解耦订阅。
/// 注意:所有订阅必须在 OnDisable 中取消,避免内存泄漏。
/// </summary>
public static class EventBus {
private static readonly Dictionary<Type, List<Delegate>> _subscribers = new();
/// <summary>
/// 订阅指定事件类型。
/// </summary>
/// <typeparam name="TEvent">事件载荷类型</typeparam>
/// <param name="handler">回调</param>
public static void Subscribe<TEvent>(Action<TEvent> handler) where TEvent : struct {
if (handler == null) return;
var type = typeof(TEvent);
if (!_subscribers.TryGetValue(type, out var list)) {
list = new List<Delegate>();
_subscribers[type] = list;
}
list.Add(handler);
}
/// <summary>
/// 取消订阅。
/// </summary>
public static void Unsubscribe<TEvent>(Action<TEvent> handler) where TEvent : struct {
if (handler == null) return;
var type = typeof(TEvent);
if (_subscribers.TryGetValue(type, out var list)) {
list.Remove(handler);
if (list.Count == 0) _subscribers.Remove(type);
}
}
/// <summary>
/// 发布事件。同步执行所有订阅者。
/// 注意:异常会中断后续订阅者,建议每个订阅者自行 try-catch。
/// </summary>
public static void Publish<TEvent>(TEvent evt) where TEvent : struct {
var type = typeof(TEvent);
if (!_subscribers.TryGetValue(type, out var list)) return;
// 拷贝一份避免订阅者在回调中修改列表
var snapshot = list.ToArray();
foreach (var del in snapshot) {
try {
((Action<TEvent>)del).Invoke(evt);
} catch (Exception e) {
Debug.LogError($"[EventBus] 订阅者抛出异常: {e}");
}
}
}
/// <summary>
/// 清空所有订阅。场景切换时调用。
/// </summary>
public static void Clear() {
_subscribers.Clear();
}
}
// 事件定义:使用 struct 保证零堆分配
public readonly struct PlayerDamagedEvent {
public readonly float Damage;
public readonly float CurrentHealth;
public readonly GameObject Source;
public PlayerDamagedEvent(float damage, float currentHealth, GameObject source) {
Damage = damage;
CurrentHealth = currentHealth;
Source = source;
}
}
public readonly struct EnemyKilledEvent {
public readonly int EnemyId;
public readonly int ScoreReward;
public EnemyKilledEvent(int enemyId, int scoreReward) {
EnemyId = enemyId;
ScoreReward = scoreReward;
}
}
}
5.4 玩家控制器:输入与移动
// Player/PlayerController.cs —— C# 9 / Unity 2022+
using UnityEngine;
using FandexUnityGameDemo.Core;
namespace FandexUnityGameDemo.Player {
/// <summary>
/// 玩家控制器:处理输入、移动、跳跃。
/// 使用 Rigidbody 物理移动,避免 transform 直接修改导致穿透。
/// 输入采样在 Update,物理操作在 FixedUpdate,符合 Unity 双循环规范。
/// </summary>
[RequireComponent(typeof(Rigidbody))]
[RequireComponent(typeof(CapsuleCollider))]
public sealed class PlayerController : MonoBehaviour {
[Header("移动参数")]
[SerializeField, Range(1f, 20f)] private float _walkSpeed = 6f;
[SerializeField, Range(1f, 20f)] private float _sprintSpeed = 10f;
[SerializeField, Range(1f, 15f)] private float _jumpForce = 7f;
[SerializeField, Range(0f, 1f)] private float _airControl = 0.3f;
[Header("地面检测")]
[SerializeField] private LayerMask _groundLayer = 1;
[SerializeField, Range(0.1f, 2f)] private float _groundCheckDistance = 0.1f;
[SerializeField] private Vector3 _groundCheckOffset = new(0f, 0.1f, 0f);
[Header("摄像机")]
[SerializeField] private Transform _cameraTransform;
[SerializeField, Range(0.1f, 5f)] private float _turnSmoothTime = 0.1f;
private Rigidbody _rb = null!;
private CapsuleCollider _collider = null!;
private float _turnSmoothVelocity;
private bool _isGrounded;
private Vector2 _moveInput; // Update 写入
private bool _jumpRequested; // Update 写入,FixedUpdate 读取
private bool _sprintHeld;
// 缓存属性避免每帧 GetComponent
public bool IsSprinting => _sprintHeld && _moveInput.sqrMagnitude > 0.01f;
public float CurrentSpeed => IsSprinting ? _sprintSpeed : _walkSpeed;
private void Awake() {
_rb = GetComponent<Rigidbody>();
_collider = GetComponent<CapsuleCollider>();
_rb.interpolation = RigidbodyInterpolation.Interpolate;
_rb.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic;
_rb.constraints = RigidbodyConstraints.FreezeRotation;
if (_cameraTransform == null && Camera.main != null) {
_cameraTransform = Camera.main.transform;
}
}
private void Update() {
// 输入采样在 Update
_moveInput.x = Input.GetAxisRaw("Horizontal");
_moveInput.y = Input.GetAxisRaw("Vertical");
_sprintHeld = Input.GetKey(KeyCode.LeftShift);
if (Input.GetButtonDown("Jump") && _isGrounded) {
_jumpRequested = true;
}
}
private void FixedUpdate() {
// 地面检测在 FixedUpdate(物理同步)
_isGrounded = CheckGrounded();
// 移动在 FixedUpdate
HandleMovement(_moveInput);
if (_jumpRequested) {
_rb.AddForce(Vector3.up * _jumpForce, ForceMode.VelocityChange);
_jumpRequested = false;
}
}
private bool CheckGrounded() {
var origin = transform.position + _groundCheckOffset;
var radius = _collider.radius * 0.9f;
return Physics.SphereCast(
origin, radius, Vector3.down, out _,
_collider.height * 0.5f + _groundCheckDistance - radius,
_groundLayer, QueryTriggerInteraction.Ignore);
}
private void HandleMovement(Vector2 input) {
if (input.sqrMagnitude < 0.01f) return;
var camForward = _cameraTransform.forward;
var camRight = _cameraTransform.right;
camForward.y = 0f; camForward.Normalize();
camRight.y = 0f; camRight.Normalize();
var desiredDir = (camForward * input.y + camRight * input.x).normalized;
// 旋转面向移动方向
var targetAngle = Mathf.Atan2(desiredDir.x, desiredDir.z) * Mathf.Rad2Deg;
var angle = Mathf.SmoothDampAngle(transform.eulerAngles.y, targetAngle,
ref _turnSmoothVelocity, _turnSmoothTime);
_rb.MoveRotation(Quaternion.Euler(0f, angle, 0f));
// 移动
var control = _isGrounded ? 1f : _airControl;
var velocity = desiredDir * CurrentSpeed * control;
var currentVel = _rb.velocity;
_rb.velocity = new Vector3(velocity.x, currentVel.y, velocity.z);
}
private void OnDrawGizmosSelected() {
if (_collider == null) return;
Gizmos.color = _isGrounded ? Color.green : Color.red;
var origin = transform.position + _groundCheckOffset;
Gizmos.DrawWireSphere(origin + Vector3.down * (_collider.height * 0.5f), _collider.radius * 0.9f);
}
}
}
5.5 状态机:敌人 AI
// AI/IState.cs —— C# 9 / Unity 2022+
namespace FandexUnityGameDemo.AI {
/// <summary>
/// 状态接口。每个状态封装进入、更新、退出逻辑。
/// 使用接口而非抽象类,便于组合与测试。
/// </summary>
public interface IState {
void OnEnter();
void OnUpdate();
void OnFixedUpdate();
void OnExit();
}
}
// AI/StateMachine.cs
using System;
using UnityEngine;
namespace FandexUnityGameDemo.AI {
/// <summary>
/// 通用有限状态机。支持任意状态实现,避免 switch-case 嵌套。
/// 线程不安全,仅主线程使用。
/// </summary>
public sealed class StateMachine {
private IState? _currentState;
public IState? CurrentState => _currentState;
public void ChangeState(IState newState) {
if (newState == null) throw new ArgumentNullException(nameof(newState));
if (_currentState != null) _currentState.OnExit();
var prev = _currentState;
_currentState = newState;
_currentState.OnEnter();
if (prev != null) {
Debug.Log($"[FSM] {prev.GetType().Name} -> {newState.GetType().Name}");
}
}
public void Update() => _currentState?.OnUpdate();
public void FixedUpdate() => _currentState?.OnFixedUpdate();
}
}
// AI/EnemyAI.cs
using UnityEngine;
using FandexUnityGameDemo.Data;
namespace FandexUnityGameDemo.AI {
/// <summary>
/// 敌人 AI:组合状态机与配置驱动。
/// 配置由 ScriptableObject 注入,行为由状态机驱动。
/// </summary>
[RequireComponent(typeof(UnityEngine.AI.NavMeshAgent))]
public sealed class EnemyAI : MonoBehaviour {
[SerializeField] private EnemyConfig _config = null!;
[SerializeField] private Transform _player = null!;
private StateMachine _fsm = new();
private UnityEngine.AI.NavMeshAgent _agent = null!;
private void Awake() {
_agent = GetComponent<UnityEngine.AI.NavMeshAgent>();
_agent.speed = _config.MoveSpeed;
_fsm.ChangeState(new IdleState(this));
}
private void Update() => _fsm.Update();
private void FixedUpdate() => _fsm.FixedUpdate();
// 公开数据供状态使用
public Transform Player => _player;
public EnemyConfig Config => _config;
public UnityEngine.AI.NavMeshAgent Agent => _agent;
public float DistanceToPlayer =>
Vector3.Distance(transform.position, _player.position);
// 状态实现:嵌套类访问外部成员
private sealed class IdleState : IState {
private readonly EnemyAI _ai;
private float _timer;
public IdleState(EnemyAI ai) => _ai = ai;
public void OnEnter() => _ai.Agent.isStopped = true;
public void OnUpdate() {
_timer += Time.deltaTime;
if (_ai.DistanceToPlayer < _ai.Config.DetectRange) {
_ai.GetComponent<StateMachineHolder>().Fsm.ChangeState(new ChaseState(_ai));
}
}
public void OnFixedUpdate() { }
public void OnExit() => _ai.Agent.isStopped = false;
}
private sealed class ChaseState : IState {
private readonly EnemyAI _ai;
public ChaseState(EnemyAI ai) => _ai = ai;
public void OnEnter() { }
public void OnUpdate() {
if (_ai.DistanceToPlayer > _ai.Config.DetectRange * 1.5f) {
_ai.GetComponent<StateMachineHolder>().Fsm.ChangeState(new IdleState(_ai));
return;
}
_ai.Agent.SetDestination(_ai.Player.position);
if (_ai.DistanceToPlayer <= _ai.Config.AttackRange) {
_ai.GetComponent<StateMachineHolder>().Fsm.ChangeState(new AttackState(_ai));
}
}
public void OnFixedUpdate() { }
public void OnExit() { }
}
private sealed class AttackState : IState {
private readonly EnemyAI _ai;
private float _nextAttackTime;
public AttackState(EnemyAI ai) => _ai = ai;
public void OnEnter() => _ai.Agent.isStopped = true;
public void OnUpdate() {
if (_ai.DistanceToPlayer > _ai.Config.AttackRange * 1.2f) {
_ai.GetComponent<StateMachineHolder>().Fsm.ChangeState(new ChaseState(_ai));
return;
}
if (Time.time >= _nextAttackTime) {
_nextAttackTime = Time.time + _ai.Config.AttackInterval;
// 攻击逻辑:触发伤害事件
// DamageCalculator.Apply(_ai.Player, _ai.Config.AttackDamage);
}
}
public void OnFixedUpdate() { }
public void OnExit() { }
}
}
// 辅助组件:持有状态机引用
internal sealed class StateMachineHolder : MonoBehaviour {
public StateMachine Fsm { get; } = new();
}
}
5.6 ScriptableObject 配置
// Data/WeaponConfig.cs —— C# 9 / Unity 2022+
using UnityEngine;
namespace FandexUnityGameDemo.Data {
/// <summary>
/// 武器配置资产。每个 .asset 文件对应一种武器。
/// 设计要点:
/// 1. 数据与逻辑分离,策划可独立调整;
/// 2. 运行时共享同一实例,内存占用低;
/// 3. 支持编辑器可视化与版本控制。
/// </summary>
[CreateAssetMenu(fileName = "Weapon", menuName = "Fandex/Weapon")]
public sealed class WeaponConfig : ScriptableObject {
[Header("基本信息")]
public string WeaponName = "New Weapon";
[TextArea] public string Description = "";
public Sprite Icon = null!;
public GameObject ProjectilePrefab = null!;
[Header("伤害")]
[Min(0f)] public float BaseDamage = 10f;
[Min(0f)] public float CriticalMultiplier = 2f;
[Range(0f, 1f)] public float CriticalChance = 0.1f;
[Header("射速")]
[Min(0.05f)] public float FireInterval = 0.5f;
[Min(1)] public int MagazineSize = 12;
[Min(0.1f)] public float ReloadTime = 2f;
[Header("弹道")]
[Min(1f)] public float MuzzleVelocity = 50f;
[Min(0f)] public float SpreadAngle = 1f;
[Min(1)] public int PelletsPerShot = 1;
[Header("特效")]
public AudioClip FireSound = null!;
public AudioClip ReloadSound = null!;
public GameObject MuzzleFlashPrefab = null!;
/// <summary>
/// 计算单发伤害(含暴击)。
/// </summary>
public float RollDamage() {
var isCritical = Random.value < CriticalChance;
return isCritical ? BaseDamage * CriticalMultiplier : BaseDamage;
}
}
}
// Data/EnemyConfig.cs
using UnityEngine;
namespace FandexUnityGameDemo.Data {
[CreateAssetMenu(fileName = "Enemy", menuName = "Fandex/Enemy")]
public sealed class EnemyConfig : ScriptableObject {
[Header("属性")]
[Min(1f)] public float MaxHealth = 100f;
[Min(0f)] public float AttackDamage = 10f;
[Min(0.1f)] public float AttackInterval = 1.5f;
[Min(0f)] public float AttackRange = 2f;
[Min(0f)] public float DetectRange = 10f;
[Header("移动")]
[Min(0.1f)] public float MoveSpeed = 3.5f;
[Min(0.1f)] public float ChaseSpeed = 5f;
[Header("奖励")]
[Min(0)] public int ScoreReward = 100;
[Min(0)] public int ExperienceReward = 50;
}
}
5.7 对象池:通用实现
// Combat/ProjectilePool.cs —— C# 9 / Unity 2022+
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Pool;
namespace FandexUnityGameDemo.Combat {
/// <summary>
/// 通用 GameObject 对象池。
/// 使用 Unity 2021+ 内置的 UnityEngine.Pool.ObjectPool<T>。
/// 注意:池化对象必须实现 IResettable 接口以重置状态。
/// </summary>
public sealed class ProjectilePool : MonoBehaviour {
[SerializeField] private GameObject _prefab = null!;
[SerializeField, Min(1)] private int _defaultCapacity = 32;
[SerializeField, Min(0)] private int _maxSize = 256;
[SerializeField] private bool _collectionCheck = true;
private IObjectPool<GameObject> _pool = null!;
private void Awake() {
_pool = new ObjectPool<GameObject>(
createFunc: CreatePooledItem,
onGet: OnGetFromPool,
onRelease: OnReleaseToPool,
onDestroy: OnDestroyPooledItem,
collectionCheck: _collectionCheck,
defaultCapacity: _defaultCapacity,
maxSize: _maxSize);
}
private GameObject CreatePooledItem() {
var obj = Instantiate(_prefab, transform);
var pooled = obj.GetOrAddComponent<PooledObject>();
pooled.Pool = _pool;
return obj;
}
private void OnGetFromPool(GameObject obj) {
obj.SetActive(true);
}
private void OnReleaseToPool(GameObject obj) {
obj.SetActive(false);
}
private void OnDestroyPooledItem(GameObject obj) {
Destroy(obj);
}
public GameObject Get(Vector3 position, Quaternion rotation) {
var obj = _pool.Get();
obj.transform.SetPositionAndRotation(position, rotation);
return obj;
}
public void Release(GameObject obj) => _pool.Release(obj);
}
/// <summary>
/// 池化对象标识组件。
/// 自动回收子弹:碰撞或超时后归还池。
/// </summary>
[RequireComponent(typeof(Rigidbody))]
public sealed class PooledObject : MonoBehaviour {
public IObjectPool<GameObject>? Pool { get; set; }
[SerializeField, Min(0.1f)] private float _autoReturnTime = 3f;
private float _spawnTime;
private bool _returned;
private void OnEnable() {
_spawnTime = Time.time;
_returned = false;
}
private void Update() {
if (!_returned && Time.time - _spawnTime > _autoReturnTime) {
ReturnToPool();
}
}
private void OnCollisionEnter(Collision other) {
// 简单策略:碰撞即回收
ReturnToPool();
}
private void ReturnToPool() {
if (_returned || Pool == null) return;
_returned = true;
// 重置刚体速度
var rb = GetComponent<Rigidbody>();
rb.velocity = Vector3.zero;
rb.angularVelocity = Vector3.zero;
Pool.Release(gameObject);
}
}
}
5.8 武器系统:基于 UniTask 的异步
// Combat/WeaponSystem.cs —— C# 9 / Unity 2022+ / UniTask 2.5
using Cysharp.Threading.Tasks;
using UnityEngine;
using FandexUnityGameDemo.Core;
using FandexUnityGameDemo.Data;
namespace FandexUnityGameDemo.Combat {
/// <summary>
/// 武器系统:管理弹药、射速、装弹。
/// 使用 UniTask 替代协程,自动绑定 MonoBehaviour 生命周期。
/// </summary>
public sealed class WeaponSystem : MonoBehaviour {
[SerializeField] private WeaponConfig _config = null!;
[SerializeField] private Transform _muzzlePoint = null!;
[SerializeField] private ProjectilePool _pool = null!;
private int _currentAmmo;
private bool _isReloading;
private float _lastFireTime;
private void Awake() {
_currentAmmo = _config.MagazineSize;
}
public bool CanFire =>
!_isReloading
&& _currentAmmo > 0
&& Time.time - _lastFireTime >= _config.FireInterval;
public async UniTaskVoid TryFireAsync() {
if (!CanFire) return;
_lastFireTime = Time.time;
_currentAmmo--;
FireOneShot();
if (_currentAmmo == 0) {
await ReloadAsync();
}
}
private void FireOneShot() {
// 播放射击音效
if (_config.FireSound != null) {
AudioSource.PlayClipAtPoint(_config.FireSound, _muzzlePoint.position);
}
// 生成子弹
for (var i = 0; i < _config.PelletsPerShot; i++) {
var spread = Random.insideUnitCircle * _config.SpreadAngle;
var dir = _muzzlePoint.forward
+ _muzzlePoint.right * spread.x * 0.01f
+ _muzzlePoint.up * spread.y * 0.01f;
dir.Normalize();
var proj = _pool.Get(_muzzlePoint.position, Quaternion.LookRotation(dir));
var rb = proj.GetComponent<Rigidbody>();
rb.velocity = dir * _config.MuzzleVelocity;
}
// 触发事件
EventBus.Publish(new WeaponFiredEvent(_config.WeaponName, _currentAmmo));
}
/// <summary>
/// 装弹。使用 UniTask 的 CancellationToken 自动取消。
/// </summary>
public async UniTask ReloadAsync() {
if (_isReloading) return;
_isReloading = true;
EventBus.Publish(new ReloadStartedEvent(_config.ReloadTime));
try {
// 关键:绑定 MonoBehaviour 销毁令牌
await UniTask.WaitForSeconds(
_config.ReloadTime,
cancellationToken: this.GetCancellationTokenOnDestroy());
_currentAmmo = _config.MagazineSize;
EventBus.Publish(new ReloadCompletedEvent(_currentAmmo));
} catch (System.OperationCanceledException) {
// 对象被销毁,静默退出
} finally {
_isReloading = false;
}
}
}
public readonly struct WeaponFiredEvent {
public readonly string WeaponName;
public readonly int RemainingAmmo;
public WeaponFiredEvent(string name, int ammo) {
WeaponName = name; RemainingAmmo = ammo;
}
}
public readonly struct ReloadStartedEvent {
public readonly float Duration;
public ReloadStartedEvent(float d) => Duration = d;
}
public readonly struct ReloadCompletedEvent {
public readonly int CurrentAmmo;
public ReloadCompletedEvent(int a) => CurrentAmmo = a;
}
}
5.9 玩家生命值:事件驱动
// Player/PlayerHealth.cs —— C# 9 / Unity 2022+
using UnityEngine;
using FandexUnityGameDemo.Core;
namespace FandexUnityGameDemo.Player {
/// <summary>
/// 玩家生命值组件。订阅 PlayerDamagedEvent,发布 PlayerDiedEvent。
/// 注意:所有事件订阅在 OnEnable,取消在 OnDisable,保证对称。
/// </summary>
public sealed class PlayerHealth : MonoBehaviour {
[SerializeField, Min(1f)] private float _maxHealth = 100f;
[SerializeField, Min(0f)] private float _invincibleTime = 0.5f;
private float _currentHealth;
private float _lastDamageTime;
public float CurrentHealth => _currentHealth;
public float MaxHealth => _maxHealth;
public bool IsDead => _currentHealth <= 0f;
private void Awake() {
_currentHealth = _maxHealth;
}
public void TakeDamage(float damage, GameObject? source = null) {
if (IsDead) return;
if (Time.time - _lastDamageTime < _invincibleTime) return;
_lastDamageTime = Time.time;
_currentHealth = Mathf.Max(0f, _currentHealth - damage);
EventBus.Publish(new PlayerDamagedEvent(damage, _currentHealth, source));
if (_currentHealth <= 0f) {
Die();
}
}
public void Heal(float amount) {
if (IsDead) return;
_currentHealth = Mathf.Min(_maxHealth, _currentHealth + amount);
}
private void Die() {
EventBus.Publish(new PlayerDiedEvent(transform.position));
// 触发死亡动画、禁用控制等
GetComponent<PlayerController>()?.enabled = false;
enabled = false;
}
}
public readonly struct PlayerDiedEvent {
public readonly Vector3 Position;
public PlayerDiedEvent(Vector3 pos) => Position = pos;
}
}
5.10 单例与全局管理器
// Core/GameManager.cs —— C# 9 / Unity 2022+
using UnityEngine;
using UnityEngine.SceneManagement;
namespace FandexUnityGameDemo.Core {
/// <summary>
/// 全局游戏管理器。持久化单例,跨场景不销毁。
/// 注意:单例是反模式,仅在以下场景使用:
/// 1. 全局配置(音量、画质、语言);
/// 2. 跨场景共享状态(玩家进度、成就);
/// 3. 系统级管理器(音频、输入、存档)。
/// 业务逻辑应使用事件总线或依赖注入。
/// </summary>
public sealed class GameManager : MonoBehaviour {
private static GameManager? _instance;
public static GameManager Instance {
get {
if (_instance == null) {
_instance = FindAnyObjectByType<GameManager>();
if (_instance == null) {
var go = new GameObject(nameof(GameManager));
_instance = go.AddComponent<GameManager>();
DontDestroyOnLoad(go);
}
}
return _instance;
}
}
[Header("状态")]
[SerializeField] private int _currentScore;
[SerializeField] private int _currentLevel = 1;
public int CurrentScore => _currentScore;
public int CurrentLevel => _currentLevel;
public void AddScore(int delta) {
_currentScore += delta;
EventBus.Publish(new ScoreChangedEvent(_currentScore));
}
public void NextLevel() {
_currentLevel++;
EventBus.Clear();
SceneManager.LoadSceneAsync(_currentLevel);
}
private void Awake() {
if (_instance != null && _instance != this) {
Destroy(gameObject);
return;
}
_instance = this;
DontDestroyOnLoad(gameObject);
}
private void OnDestroy() {
if (_instance == this) _instance = null;
}
}
public readonly struct ScoreChangedEvent {
public readonly int NewScore;
public ScoreChangedEvent(int s) => NewScore = s;
}
}
六、跨语言对比
6.1 与 Godot(C# / GDScript)对比
| 特性 | Unity (C#) | Godot (C#) | Godot (GDScript) |
|---|---|---|---|
| 基础类 | MonoBehaviour | Node | Node |
| 组件模型 | Component on GameObject | Node tree | Node tree |
| 序列化 | Inspector + SerializeField | Export | @export |
| 信号系统 | C# event / UnityEvent | Signal | signal |
| 物理 | PhysX / Havok | Godot Physics | 同左 |
| ECS | DOTS (实验) | 部分 | 部分 |
| 优势 | 生态、文档、资产商店 | 开源、轻量、节点直观 | 极易上手 |
6.2 与 Unreal Engine(C++ / Blueprint)对比
| 特性 | Unity | Unreal |
|---|---|---|
| 脚本语言 | C# | C++ / Blueprint |
| 基础类 | MonoBehaviour | UObject / AActor / ACharacter |
| 反射 | C# 原生 | UPROPERTY / UFUNCTION 宏 |
| 序列化 | Inspector | Detail Panel |
| GC | Mono/CoreCLR | 手动 + Unreal GC |
| ECS | DOTS | Mass Entity (UE5) |
| 优势 | C# 易用、迭代快 | 画面表现、AAA 级工具链 |
6.3 与 Java/Kotlin 游戏生态对比
| 维度 | Unity (C#) | Java/Kotlin (LibGDX) | Kotlin (Korge) |
|---|---|---|---|
| 类型系统 | 静态强类型 | 静态强类型 | 静态强类型 |
| 空安全 | NRT (C# 8+) | Kotlin 原生 | Kotlin 原生 |
| 协程 | IEnumerator / UniTask | CompletableFuture | Kotlin Coroutines |
| 性能 | IL2CPP AOT 接近 C++ | JVM JIT | JVM / Native |
| 跨平台 | 优秀 | 中等 | 中等 |
| 生态 | 第一梯队 | 小众 | 小众 |
6.4 与 Bevy(Rust ECS)对比
| 维度 | Unity DOTS | Bevy |
|---|---|---|
| 语言 | C# (Burst 子集) | Rust |
| ECS 设计 | Archetype-based | Archetype-based |
| 并行 | Job System + Burst | Rayon + 多线程所有权 |
| 安全 | GC(CoreCLR) | 所有权零成本 |
| 生态 | 商业引擎 | 开源 |
| 学习曲线 | 中等 | 陡峭 |
七、陷阱与最佳实践
7.1 常见陷阱
-
在 Update 中调用 GetComponent
- 症状:帧率随组件数下降;
- 原因:跨边界调用开销 50-200ns;
- 修复:在 Awake/Start 中缓存到字段。
-
协程中修改已销毁对象
- 症状:
MissingReferenceException; - 原因:协程不会随 GameObject 销毁自动停止;
- 修复:在 OnDisable 中
StopAllCoroutines()或使用 UniTask 的GetCancellationTokenOnDestroy()。
- 症状:
-
public 字段暴露 Inspector
- 症状:类内部状态被外部修改,封装破坏;
- 修复:使用
[SerializeField] private,仅 Inspector 可见。
-
FixedUpdate 中修改 transform.position
- 症状:物理抖动、穿透;
- 原因:物理引擎与渲染不同步;
- 修复:使用
Rigidbody.MovePosition。
-
ScriptableObject 运行时修改
- 症状:编辑器 Play 模式修改的值在退出后保留;
- 原因:ScriptableObject 是共享实例;
- 修复:运行时实例化副本
Instantiate(config)再修改。
-
闭包捕获导致 GC 分配
- 症状:每帧 GC.Alloc 增长;
- 原因:lambda 捕获局部变量生成闭包类;
- 修复:缓存委托到字段,或使用
UniTaskvalue type。
-
空引用检查使用
== null- 症状:未销毁的对象被误判为 null;
- 原因:Unity 重载了
==,被销毁的对象== null返回 true; - 修复:使用
ReferenceEquals(obj, null)进行真实 null 检查。
-
使用 LINQ 在热路径
- 症状:每帧 GC.Alloc 数 KB;
- 原因:
Where/Select产生迭代器分配; - 修复:使用
for循环或Span<T>。
7.2 最佳实践清单
- 缓存组件引用:Awake/Start 中一次性获取;
- 使用对象池:子弹、特效、敌人等高频创建销毁的对象;
- 避免 Update 中分配:使用
StringBuilder、预分配集合; - 事件驱动解耦:使用 EventBus 而非直接调用;
- ScriptableObject 配置:数据与逻辑分离;
- 状态机管理 AI:避免巨型 switch-case;
- 物理在 FixedUpdate:所有 Rigidbody 操作;
- 渲染在 Update,跟随在 LateUpdate;
- 避免深层 Transform 嵌套:性能随深度下降;
- 使用 Profiler 而非猜测:定位真实瓶颈。
八、工程实践
8.1 单元测试(Unity Test Framework)
// Tests/EventBusTests.cs —— C# 9 / UTF 1.3
using NUnit.Framework;
using FandexUnityGameDemo.Core;
namespace FandexUnityGameDemo.Tests {
public class EventBusTests {
[SetUp]
public void SetUp() => EventBus.Clear();
[Test]
public void Subscribe_Publish_CallsHandler() {
var received = 0;
EventBus.Subscribe<TestEvent>(e => received = e.Value);
EventBus.Publish(new TestEvent(42));
Assert.AreEqual(42, received);
}
[Test]
public void Unsubscribe_NoLongerCalled() {
var count = 0;
void Handler(TestEvent e) => count++;
EventBus.Subscribe<TestEvent>(Handler);
EventBus.Publish(default);
EventBus.Unsubscribe<TestEvent>(Handler);
EventBus.Publish(default);
Assert.AreEqual(1, count);
}
[Test]
public void Handler_Throws_DoesNotAffectOthers() {
var secondCalled = false;
EventBus.Subscribe<TestEvent>(e => throw new System.InvalidOperationException());
EventBus.Subscribe<TestEvent>(e => secondCalled = true);
EventBus.Publish(new TestEvent(1));
Assert.IsTrue(secondCalled);
}
private readonly struct TestEvent {
public readonly int Value;
public TestEvent(int v) => Value = v;
}
}
}
8.2 性能基准(自定义 Profiler)
// Tests/PerformanceBenchmark.cs
using UnityEngine;
using UnityEngine.Profiling;
namespace FandexUnityGameDemo.Tests {
/// <summary>
/// 简易性能基准。使用 ProfilerRecorder 采样。
/// 完整基准建议使用 Unity Performance Testing Extension。
/// </summary>
public sealed class PerformanceBenchmark : MonoBehaviour {
[SerializeField] private int _iterationCount = 10000;
[SerializeField] private GameObject _prefab = null!;
private void Start() {
BenchmarkGetComponent();
BenchmarkInstantiateDestroy();
BenchmarkObjectPool();
}
private void BenchmarkGetComponent() {
Profiler.BeginSample("GetComponent x10000");
for (var i = 0; i < _iterationCount; i++) {
var rb = GetComponent<Rigidbody>();
}
Profiler.EndSample();
}
private void BenchmarkInstantiateDestroy() {
Profiler.BeginSample("Instantiate+Destroy x1000");
for (var i = 0; i < 1000; i++) {
var go = Instantiate(_prefab);
Destroy(go);
}
Profiler.EndSample();
}
private void BenchmarkObjectPool() {
var pool = new ObjectPool<GameObject>(
() => Instantiate(_prefab),
go => go.SetActive(true),
go => go.SetActive(false),
Destroy);
Profiler.BeginSample("ObjectPool Get+Release x1000");
for (var i = 0; i < 1000; i++) {
var go = pool.Get();
pool.Release(go);
}
Profiler.EndSample();
}
}
}
8.3 编辑器扩展
// Editor/WeaponConfigEditor.cs —— C# 9 / Unity 2022+
#if UNITY_EDITOR
using UnityEditor;
using UnityEngine;
using FandexUnityGameDemo.Data;
namespace FandexUnityGameDemo.Editor {
/// <summary>
/// WeaponConfig 自定义 Inspector。
/// 提供预览、校验、批量生成功能。
/// </summary>
[CustomEditor(typeof(WeaponConfig))]
public sealed class WeaponConfigEditor : UnityEditor.Editor {
private WeaponConfig Config => (WeaponConfig)target;
public override void OnInspectorGUI() {
DrawDefaultInspector();
EditorGUILayout.Space(10);
DrawValidation();
EditorGUILayout.Space(5);
DrawActions();
}
private void DrawValidation() {
EditorGUILayout.LabelField("校验", EditorStyles.boldLabel);
EditorGUI.indentLevel++;
if (Config.BaseDamage <= 0f)
EditorGUILayout.HelpBox("基础伤害为 0", MessageType.Warning);
if (Config.MagazineSize <= 0)
EditorGUILayout.HelpBox("弹匣容量为 0", MessageType.Error);
if (Config.FireInterval < 0.05f)
EditorGUILayout.HelpBox("射速过快,可能影响性能", MessageType.Warning);
EditorGUI.indentLevel--;
}
private void DrawActions() {
EditorGUILayout.LabelField("操作", EditorStyles.boldLabel);
EditorGUI.indentLevel++;
if (GUILayout.Button("随机化数值")) {
Undo.RecordObject(Config, "Randomize");
Config.BaseDamage = Random.Range(5f, 50f);
Config.CriticalChance = Random.Range(0f, 0.5f);
EditorUtility.SetDirty(Config);
}
if (GUILayout.Button("计算 DPS")) {
var dps = Config.BaseDamage / Config.FireInterval * Config.MagazineSize /
(Config.MagazineSize * Config.FireInterval + Config.ReloadTime);
EditorUtility.DisplayDialog("DPS", $"理论 DPS: {dps:F2}", "确定");
}
EditorGUI.indentLevel--;
}
}
}
#endif
8.4 Assembly Definition 与模块化
大型项目应使用 Assembly Definition(.asmdef)划分模块,减少编译时间并提供封装:
// Assets/Scripts/Core/FandexUnityGameDemo.Core.asmdef
{
"name": "FandexUnityGameDemo.Core",
"rootNamespace": "FandexUnityGameDemo.Core",
"references": [],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}
// Assets/Scripts/Combat/FandexUnityGameDemo.Combat.asmdef
{
"name": "FandexUnityGameDemo.Combat",
"rootNamespace": "FandexUnityGameDemo.Combat",
"references": [
"FandexUnityGameDemo.Core",
"FandexUnityGameDemo.Data"
],
"includePlatforms": [],
"excludePlatforms": []
}
模块化的收益:
- 编译速度:修改 Combat 模块只重编 Combat 及依赖,而非整个 Assets/Scripts;
- 封装:internal 类仅在模块内可见;
- 测试隔离:每个模块独立测试 asmdef。
九、案例研究
9.1 案例一:ARPG 战斗系统的状态机设计
场景:一款类《暗黑破坏神》的 ARPG,玩家角色有移动、攻击、施法、闪避、受伤、死亡等状态,需要平滑切换且避免非法转换。
问题:
- 直接使用 enum + switch-case 导致状态机爆炸;
- 状态转换条件散落各处,难以维护;
- 无法为每个状态独立添加动画事件、特效、音效。
解决方案:
- 使用
IState接口与StateMachine类(见 5.5); - 每个状态封装 OnEnter/OnUpdate/OnExit,独立处理动画、特效;
- 添加
TransitionTableScriptableObject 配置状态转换规则,策划可视化编辑; - 使用动画事件(Animation Event)回调状态机,实现”攻击命中帧”等精确时机。
收益:状态扩展只需新增 IState 实现,不影响其他状态;状态转换规则数据化,策划可调整。
9.2 案例二:大规模单位战斗的 DOTS 迁移
场景:一款 RTS 游戏,同屏 5000 个单位实时战斗,MonoBehaviour 实现帧率低于 20fps。
问题:
- 每个单位一个 GameObject + 多个 Component,跨边界调用爆炸式增长;
- GC 暴增,每帧分配 MB 级内存;
- CPU 缓存局部性极差,大量 cache miss。
解决方案:
- 将单位数据迁移至
IComponentDatastruct(位置、生命值、目标); - 将战斗逻辑迁移至
SystemBase,使用Entities.ForEach并行; - 使用
Burst Compile编译热路径,启用 SIMD; - 使用
NativeHashMap<int, Entity>维护单位 ID 到 Entity 的映射; - 使用
IJobChunk自定义 Job,配合CullingSchedule批量处理。
性能对比:
| 实现 | 5000 单位/帧 | GC/帧 | CPU 缓存命中率 |
|---|---|---|---|
| MonoBehaviour | 50ms | 2.5MB | 20% |
| ECS + Job | 3ms | 0KB | 85% |
| ECS + Job + Burst | 0.8ms | 0KB | 90% |
教训:DOTS 不是银弹,迁移成本高,仅适用于大规模同质数据场景。UI、角色控制等异构逻辑仍用 MonoBehaviour。
9.3 案例三:跨平台输入系统
场景:游戏需支持 PC(键鼠)、主机(手柄)、移动(触摸)、WebGL(键鼠 + 触摸),输入逻辑不应散落各处。
问题:
- 直接使用
Input.GetAxis在不同平台行为不一致; - 新增手柄支持需修改所有输入调用点;
- 输入重映射(玩家自定义按键)困难。
解决方案:
- 使用 Unity 新输入系统(Input System Package);
- 定义
InputActions资产,抽象”移动”、“攻击”、“跳跃”等动作; - 不同平台绑定不同控制方案(Control Scheme);
- 业务代码订阅
InputAction.performed事件,与具体设备解耦; - 通过
PlayerInput组件自动切换控制方案。
代码示例:
public sealed class InputAdapter : MonoBehaviour {
[SerializeField] private InputActionAsset _actionAsset = null!;
private InputAction _moveAction = null!;
private InputAction _attackAction = null!;
public Vector2 MoveInput { get; private set; }
public event Action? AttackPressed;
private void Awake() {
_moveAction = _actionAsset.FindAction("Move");
_attackAction = _actionAsset.FindAction("Attack");
_moveAction.performed += ctx => MoveInput = ctx.ReadValue<Vector2>();
_moveAction.canceled += _ => MoveInput = Vector2.zero;
_attackAction.performed += _ => AttackPressed?.Invoke();
}
private void OnEnable() {
_moveAction.Enable();
_attackAction.Enable();
}
private void OnDisable() {
_moveAction.Disable();
_attackAction.Disable();
}
}
9.4 案例四:UI 框架的事件驱动设计
场景:游戏有背包、技能、任务、商店等 UI 面板,需动态显示/隐藏,且数据更新需同步到 UI。
问题:
- UI 直接持有业务对象引用,紧耦合;
- 数据更新需手动调用 UI 刷新,易遗漏;
- UI 关闭后仍接收事件,导致 NRE。
解决方案:
- 使用 MVVM 模式:Model = 业务数据,ViewModel = 数据适配器,View = UI 组件;
- ViewModel 通过 EventBus 订阅领域事件,自动更新;
- View 通过数据绑定(UniRx 或 Observable)响应 ViewModel 变化;
- View 的 OnDisable 自动取消所有订阅。
9.5 案例五:存档系统的序列化
场景:游戏需保存玩家进度、世界状态、成就,支持跨版本兼容。
问题:
PlayerPrefs仅支持简单类型,无法存复杂对象;- BinaryFormatter 不安全且跨版本兼容差;
- JSON 体积大且类型丢失。
解决方案:
- 使用 MessagePack-CSharp 或 MemoryPack(零分配序列化);
- 定义
[SaveData]标记的 POCO 类,仅含可序列化字段; - 版本号字段
SchemaVersion,加载时按版本迁移; - 存档分主存档与自动存档,分别保存;
- 使用
Async异步写入避免卡顿。
[MemoryPackable]
public partial record SaveData {
public int SchemaVersion { get; init; } = 1;
public int PlayerLevel { get; init; }
public float PlayerHealth { get; init; }
public Vector3 PlayerPosition { get; init; }
public List<string> UnlockedAchievements { get; init; } = new();
public Dictionary<string, int> Inventory { get; init; } = new();
}
public static class SaveSystem {
public static async UniTask SaveAsync(SaveData data, string slot) {
var path = Path.Combine(Application.persistentDataPath, $"{slot}.save");
var bytes = MemoryPackSerializer.Serialize(data);
await File.WriteAllBytesAsync(path, bytes);
}
public static async UniTask<SaveData?> LoadAsync(string slot) {
var path = Path.Combine(Application.persistentDataPath, $"{slot}.save");
if (!File.Exists(path)) return null;
var bytes = await File.ReadAllBytesAsync(path);
var data = MemoryPackSerializer.Deserialize<SaveData>(bytes);
return Migrate(data!);
}
private static SaveData Migrate(SaveData data) {
// 按 SchemaVersion 迁移
return data.SchemaVersion switch {
1 => data with { SchemaVersion = 2 }, // 示例:v1 -> v2
2 => data,
_ => data
};
}
}
十、练习题(含答案)
基础题
Q1:MonoBehaviour 中 Awake 与 Start 的核心区别是什么?
A1:
Awake在对象实例化后立即调用,即使 GameObject 未激活(但脚本组件需激活);Start在对象首次激活后的下一帧第一次Update之前调用;- 同一帧
Instantiate的对象,Awake在当前帧执行,Start在下一帧执行; Awake适合初始化自身状态与缓存引用,Start适合需要依赖其他对象已初始化的逻辑。
进阶题
Q2:以下代码存在什么性能问题?如何优化?
void Update() {
var enemies = FindObjectsOfType<Enemy>();
foreach (var e in enemies) {
if (e.IsAlive) e.MoveTowardsPlayer();
}
}
A2: 问题:
FindObjectsOfType<Enemy>()在 Update 中调用,每帧扫描全部场景对象,开销 ;- 返回数组产生 GC 分配;
foreach对数组的迭代在 IL2CPP 下可能装箱。
优化:
- 维护一个
static List<Enemy> AllEnemies,Enemy 在 Awake 时注册、OnDestroy 时注销; - 使用
for循环替代foreach; - 缓存列表到字段,避免每次访问静态成员;
- 进一步:使用 DOTS ECS,用
EntityQuery批量获取。
应用题
Q3:设计一个支持任意类型事件的 EventBus,要求:
- 类型安全(编译期检查);
- 零 GC 分配(发布事件时不产生堆分配);
- 支持优先级订阅。
A3:核心思路:
- 使用
struct事件类型避免装箱; - 使用
Dictionary<Type, List<Delegate>>存储订阅者; - 发布时拷贝订阅者列表到预分配数组,避免迭代中修改;
- 优先级通过
SortedList<int, List<Delegate>>或订阅时传入 priority 参数。
参考 5.3 的 EventBus 实现,扩展优先级参数:
public static void Subscribe<TEvent>(Action<TEvent> handler, int priority = 0)
where TEvent : struct {
// 使用 SortedList<int, List<Delegate>> 替代 List<Delegate>
// 插入时按 priority 排序
}
高阶题
Q4:解释为什么 GetComponent<T>() 在 Update 中调用会产生性能损耗,并给出 IL2CPP 层面的分析。
A4:
GetComponent<T> 的调用链:
- C# 泛型方法
GetComponent<T>()调用Object.GetProperty内部方法; - IL2CPP 将其翻译为 C++ 胶水代码,调用
Object::GetComponentStatic; - 该方法在 C++ 层遍历 GameObject 的组件列表,按类型 ID 查找;
- 找到后通过
il2cpp::object_cast进行类型检查并返回。
每次调用产生:
- 跨边界 thunk 跳转:约 5-10ns;
- C++ 层组件列表遍历:约 30-100ns(取决于组件数);
- GC write barrier(若返回值赋值给字段):约 5ns。
合计 50-200ns/次。在 60fps(16.67ms 预算)下,每帧调用 1000 次将消耗 50-200μs,看似不大,但叠加其他逻辑会导致帧率下降。
优化:在 Awake 中缓存 var rb = GetComponent<Rigidbody>() 到字段,Update 中直接访问字段,开销仅 1-2ns。
思考题
Q5:MonoBehaviour 模型与 ECS 模型在 CPU 缓存友好性上的差异?请用具体数据说明。
A5: MonoBehaviour:
- 每个 Component 是独立托管对象,分布在 GC 堆任意位置;
- 遍历 N 个 Enemy 时,每次访问需解引用,cache miss 概率高;
- 假设 cache line 64 字节,Enemy 含 Position(12) + Health(4) + Target(8) = 24 字节,每次访问可能 miss;
- 5000 个 Enemy 遍历:5000 × 100ns (cache miss) = 500μs。
ECS:
- 同一 Archetype 的组件存储在连续内存块(Chunk,16KB);
- Position 数组紧凑排列,预取器友好;
- 5000 个 Enemy 的 Position 遍历:5000 × 24 / 64 = 1875 个 cache line,约 1875 × 10ns = 18.75μs;
- 配合 Burst SIMD,可并行处理 4-8 个 float,进一步压缩到 5μs 内。
ECS 比 MonoBehaviour 快 100 倍的根源:内存连续性 + SIMD 并行。
设计题
Q6:为一个支持 4 人本地分屏的赛车游戏设计输入系统架构。
A6: 核心设计:
- 使用 Unity 新输入系统,定义
PlayerInputActions资产,含”驾驶”动作组(转向、油门、刹车、漂移、道具); - 每个玩家一个
PlayerInput组件,分配不同 Control Scheme(Player1: 键盘1, Player2: 键盘2, Player3-4: 手柄1-2); InputAdapter组件订阅PlayerInput.onActionTriggered,将输入转换为InputState结构;CarController通过依赖注入获取InputAdapter,解耦输入与控制;- 暂停时统一禁用所有
PlayerInput,避免输入泄漏; - 手柄掉线处理:监听
InputSystem.onDeviceChange,自动切换到键盘。
综合题
Q7:分析以下代码的 5 个问题并修复:
public class EnemySpawner : MonoBehaviour {
public GameObject enemyPrefab;
public float spawnInterval = 2f;
private List<Enemy> enemies = new();
void Start() {
StartCoroutine(SpawnLoop());
}
IEnumerator SpawnLoop() {
while (true) {
yield return new WaitForSeconds(spawnInterval);
var enemy = Instantiate(enemyPrefab);
enemy.GetComponent<Enemy>().Initialize();
enemies.Add(enemy.GetComponent<Enemy>());
}
}
void Update() {
foreach (var e in enemies) {
if (e == null) enemies.Remove(e);
else e.UpdateAI();
}
}
}
A7: 问题:
- 无限循环协程永不停止:GameObject 销毁后协程仍在运行,需在 OnDisable 中 StopCoroutine;
enemies.Add与enemies.Remove在不同上下文:Update 中修改集合会破坏迭代,抛 InvalidOperationException;GetComponent<Enemy>()重复调用:两次调用,应缓存;foreach迭代时Remove:集合被修改,迭代器失效;- 敌人销毁后
enemies中保留 null 引用:未及时清理。
修复:
public class EnemySpawner : MonoBehaviour {
[SerializeField] private GameObject _enemyPrefab = null!;
[SerializeField, Min(0.1f)] private float _spawnInterval = 2f;
private readonly List<Enemy> _enemies = new();
private Coroutine? _spawnCoroutine;
private void OnEnable() {
_spawnCoroutine = StartCoroutine(SpawnLoop());
}
private void OnDisable() {
if (_spawnCoroutine != null) StopCoroutine(_spawnCoroutine);
}
private IEnumerator SpawnLoop() {
while (enabled) {
yield return new WaitForSeconds(_spawnInterval);
var enemy = Instantiate(_enemyPrefab);
var comp = enemy.GetComponent<Enemy>();
comp.Initialize();
comp.OnDied += () => _enemies.Remove(comp);
_enemies.Add(comp);
}
}
private void Update() {
for (var i = _enemies.Count - 1; i >= 0; i--) {
var e = _enemies[i];
if (e == null) _enemies.RemoveAt(i);
else e.UpdateAI();
}
}
}
算法题
Q8:实现一个空间分区数据结构,用于加速 1000 个单位的邻域查询(查询半径 r 内的所有单位)。
A8:使用均匀网格分区:
public sealed class SpatialGrid {
private readonly Dictionary<(int, int), List<Unit>> _cells = new();
private readonly float _cellSize;
public SpatialGrid(float cellSize) => _cellSize = cellSize;
public void Insert(Unit u) {
var key = GetKey(u.Position);
if (!_cells.TryGetValue(key, out var list)) {
list = new List<Unit>();
_cells[key] = list;
}
list.Add(u);
}
public void Remove(Unit u) {
var key = GetKey(u.Position);
if (_cells.TryGetValue(key, out var list)) list.Remove(u);
}
public List<Unit> Query(Vector3 center, float radius) {
var result = new List<Unit>();
var r = Mathf.CeilToInt(radius / _cellSize);
var (cx, cz) = GetKey(center);
for (var dx = -r; dx <= r; dx++) {
for (var dz = -r; dz <= r; dz++) {
if (_cells.TryGetValue((cx + dx, cz + dz), out var list)) {
foreach (var u in list) {
if (Vector3.Distance(u.Position, center) <= radius) {
result.Add(u);
}
}
}
}
}
return result;
}
private (int, int) GetKey(Vector3 pos) {
return (Mathf.FloorToInt(pos.x / _cellSize), Mathf.FloorToInt(pos.z / _cellSize));
}
}
复杂度:插入 ,查询 ( 为半径内单位数),相比暴力 大幅优化。
错误诊断题
Q9:以下代码运行时报 MissingReferenceException,原因与修复?
async void LoadData() {
var data = await LoadFromServer();
GetComponent<Renderer>().material.color = data.Color;
}
A9: 原因:
async void不会随 GameObject 销毁自动取消;await LoadFromServer()期间 GameObject 可能被销毁;- 恢复执行时
GetComponent<Renderer>()在已销毁对象上调用,抛MissingReferenceException。
修复:使用 UniTask + CancellationToken:
async UniTaskVoid LoadDataAsync() {
try {
var data = await LoadFromServer()
.AttachExternalCancellation(this.GetCancellationTokenOnDestroy());
if (this == null) return; // 双重检查
GetComponent<Renderer>().material.color = data.Color;
} catch (OperationCanceledException) {
// 静默退出
}
}
// 调用方
LoadDataAsync().Forget();
实战题
Q10:设计一个可热重载的技能系统,要求:
- 技能配置数据驱动(ScriptableObject);
- 运行时可替换技能配置而不重启游戏;
- 支持技能连招(前一技能结束触发下一技能);
- 支持技能冷却、消耗、前摇、后摇。
A10: 架构:
SkillConfig(ScriptableObject):含动画引用、伤害数值、冷却时间、消耗、连招链;SkillInstance(运行时实例):维护冷却剩余、当前阶段(前摇/执行/后摇)、连招窗口;SkillSystem(MonoBehaviour):管理当前激活技能、接收输入、触发连招;SkillCaster(接口):抽象施法者,支持玩家与敌人复用。
public abstract class SkillConfig : ScriptableObject {
public string SkillName;
public float Cooldown = 1f;
public float Cost = 10f;
public float AnticipationTime = 0.2f; // 前摇
public float ActiveTime = 0.3f; // 执行
public float RecoveryTime = 0.5f; // 后摇
public SkillConfig? ComboNext; // 连招后续
public float ComboWindow = 0.2f; // 连招窗口
public abstract void Execute(SkillContext ctx);
}
public sealed class SkillSystem : MonoBehaviour {
[SerializeField] private List<SkillConfig> _equippedSkills = new();
private SkillInstance? _current;
private float _globalCooldown;
public bool TryCast(int index) {
if (_globalCooldown > 0f || _current != null) return false;
if (index < 0 || index >= _equippedSkills.Count) return false;
var config = _equippedSkills[index];
_current = new SkillInstance(config, this);
return true;
}
private void Update() {
if (_globalCooldown > 0f) _globalCooldown -= Time.deltaTime;
_current?.Update(Time.deltaTime);
if (_current?.IsFinished == true) {
_globalCooldown = _current.Config.Cooldown;
_current = null;
}
}
}
十一、参考文献(ACM Reference Format)
本文参考了以下学术文献、官方文档与权威著作,遵循 ACM Reference Format:
[1] Hejlsberg, A., Torgersen, M., Wiltamuth, S., and Golde, P. 2023. The C# Programming Language (4th ed.). Addison-Wesley Professional, Boston, MA. DOI: 10.5555/1202040.
[2] Ecma International. 2023. ECMA-334: C# Language Specification (6th ed.). Geneva, Switzerland. Retrieved from https://www.ecma-international.org/publications-and-standards/standards/ecma-334/
[3] Unity Technologies. 2024. Unity User Manual 2022.3 (LTS). San Francisco, CA. Retrieved from https://docs.unity3d.com/2022.3/Documentation/Manual/
[4] Unity Technologies. 2024. Unity Scripting API: MonoBehaviour. Retrieved from https://docs.unity3d.com/ScriptReference/MonoBehaviour.html
[5] Unity Technologies. 2024. Unity Scripting API: ScriptableObject. Retrieved from https://docs.unity3d.com/ScriptReference/ScriptableObject.html
[6] Cysharp, Inc. 2024. UniTask: Zero Allocation Async/Await for Unity. GitHub repository. Retrieved from https://github.com/Cysharp/UniTask
[7] Nystrom, R. 2014. Game Programming Patterns. Genever Benning, Auburn, NY. Retrieved from https://gameprogrammingpatterns.com/
[8] Gregory, J. 2018. Game Engine Architecture (3rd ed.). CRC Press, Boca Raton, FL. DOI: 10.1201/9781315365230.
[9] Dickinson, J. 2022. Hands-On Design Patterns with Unity. Packt Publishing, Birmingham, UK.
[10] Hocking, J. 2015. Unity in Action: Multiplatform Game Development in C# with Unity 5 (1st ed.). Manning Publications, Shelter Island, NY.
[11] Unity Technologies. 2023. DOTS: Data-Oriented Technology Stack. Retrieved from https://unity.com/dots
[12] Unity Technologies. 2023. Unity ECS Documentation. Retrieved from https://docs.unity3d.com/Packages/com.unity.entities@1.0/manual/
[13] Microsoft. 2024. .NET Standard 2.1 Specification. Retrieved from https://docs.microsoft.com/en-us/dotnet/standard/net-standard
[14] Lander, M. 2020. Game Development with Unity (2nd ed.). Routledge, London, UK. DOI: 10.4324/9780429263705.
[15] Gray, J. 2019. Mastering Unity Scripting. Packt Publishing, Birmingham, UK.
[16] Smith, T. 2021. Pro Unity Game Development with Optimizations. Apress, Berkeley, CA. DOI: 10.1007/978-1-4842-7265-5.
[17] Falcon, H. 2022. Unity 2022 by Example. Packt Publishing, Birmingham, UK.
[18] Thierens, M. 2023. Unity Game Optimization (3rd ed.). Packt Publishing, Birmingham, UK.
十二、延伸阅读
12.1 官方文档与教程
- Unity Learn:https://learn.unity.com/
- Unity Blog: Technology:https://blog.unity.com/technology
- Unity Forums: Scripting:https://forum.unity.com/forums/scripting.50/
- Unity Answers:https://answers.unity.com/
12.2 ECS 与 DOTS 深入
- Unity Entities 官方文档:https://docs.unity3d.com/Packages/com.unity.entities@1.0
- Unity Burst Compiler 文档:https://docs.unity3d.com/Packages/com.unity.burst@1.8
- Unity C# Job System:https://docs.unity3d.com/Manual/JobSystem.html
- 《Unity in Action: DOTS Edition》(待出版)
12.3 性能优化
- Unity Profiler 文档:https://docs.unity3d.com/Manual/Profiler.html
- Unity Performance Testing Extension:https://docs.unity3d.com/Packages/com.unity.test-framework.performance@3.0
- Frame Timing Analyzer:https://docs.unity3d.com/ScriptReference/FrameTimingManager.html
12.4 输入系统
- Unity Input System 文档:https://docs.unity3d.com/Packages/com.unity.inputsystem@1.7
- Input System 示例:https://github.com/Unity-Technologies/InputSystem
12.5 异步编程
- UniTask GitHub:https://github.com/Cysharp/UniTask
- UniTask 文档:https://cysharp.github.io/UniTask/
- C# async/await in Unity:https://docs.unity3d.com/Manual/async-await-support.html
12.6 序列化与存档
- MemoryPack:https://github.com/Cysharp/MemoryPack
- MessagePack-CSharp:https://github.com/MessagePack-CSharp/MessagePack-CSharp
- Unity JsonUtility:https://docs.unity3d.com/ScriptReference/JsonUtility.html
12.7 编辑器扩展
- Unity Editor Scripting:https://docs.unity3d.com/Manual/ExtendingTheEditor.html
- Editor Window 指南:https://docs.unity3d.com/ScriptReference/EditorWindow.html
- PropertyDrawer 教程:https://docs.unity3d.com/ScriptReference/PropertyDrawer.html
12.8 跨语言对比
- Godot Engine:https://godotengine.org/
- Bevy (Rust):https://bevyengine.org/
- LibGDX (Java):https://libgdx.com/
- Unreal Engine:https://www.unrealengine.com/
12.9 设计模式与架构
- 《Game Programming Patterns》https://gameprogrammingpatterns.com/
- 《Hands-On Design Patterns with Unity》(Dickinson, 2022)
- 《Unity Architectures》社区文章集合
12.10 社区与生态
- Unity Asset Store:https://assetstore.unity.com/
- Unity Package Manager (UPM):https://docs.unity3d.com/Manual/upm-ui.html
- OpenUPM(社区包):https://openupm.com/
- Unity Discord 社区:https://discord.com/invite/unity