第27章:项目架构与设计模式
第27章:项目架构与设计模式
本章目标
通过本章学习,你将掌握:
- 项目文件夹结构最佳实践(基于功能 vs 基于类型)
- Assembly Definition 加速编译和模块化
- Unity 中常用的设计模式(单例、观察者、状态机、命令、对象池、服务定位器)
- ScriptableObject 架构(数据驱动设计、事件通道、运行时集合)
- 依赖注入基础(VContainer/Zenject 概览)
- 代码组织原则(Manager vs System vs Component)
- Unity 测试框架(Edit Mode 和 Play Mode 测试)
- 调试工具和技巧
- Git 版本控制最佳实践(Git LFS、.gitignore)
- CI/CD 与 GitHub Actions 集成
- 与前端架构的对比和思维迁移
预计学习时间
约 4-5 小时(含代码实践时间)
27.1 文件夹结构
27.1.1 两种主流组织方式
基于类型(Type-Based)
Assets/
├── Scripts/
│ ├── PlayerController.cs
│ ├── EnemyAI.cs
│ ├── InventoryManager.cs
│ └── QuestSystem.cs
├── Prefabs/
│ ├── Player.prefab
│ ├── Enemy.prefab
│ └── Item.prefab
├── Materials/
│ ├── PlayerMaterial.mat
│ └── EnemyMaterial.mat
└── Animations/
├── PlayerAnim.controller
└── EnemyAnim.controller
优点:简单直观、传统做法 缺点:功能相关的文件分散在不同目录,当项目变大时难以维护
前端类比:这类似于早期 React 项目把所有组件放在
components/,所有样式放在styles/,所有 hook 放在hooks/。
基于功能(Feature-Based)
Assets/
├── Features/
│ ├── Player/
│ │ ├── Scripts/
│ │ │ ├── PlayerController.cs
│ │ │ └── PlayerHealth.cs
│ │ ├── Prefabs/
│ │ │ └── Player.prefab
│ │ ├── Animations/
│ │ │ └── PlayerAnim.controller
│ │ └── Materials/
│ │ └── PlayerMaterial.mat
│ │
│ ├── Enemy/
│ │ ├── Scripts/
│ │ ├── Prefabs/
│ │ └── Animations/
│ │
│ ├── Inventory/
│ │ ├── Scripts/
│ │ ├── UI/
│ │ └── Data/
│ │
│ └── Quest/
│ ├── Scripts/
│ ├── Data/
│ └── UI/
│
├── Core/ // 共享的核心系统
│ ├── Scripts/
│ ├── Utilities/
│ └── ScriptableObjects/
│
└── Shared/ // 共享资源
├── Shaders/
├── Fonts/
└── CommonUI/
优点:高内聚低耦合,功能相关的文件在一起,易于维护和重构 缺点:初始设置稍复杂
前端类比:这类似于现代 React/Next.js 的 feature-based 结构——每个功能模块包含自己的组件、hooks、样式和测试。
27.1.2 推荐的混合方案
对于开放世界移动游戏项目,推荐使用功能为主、类型为辅的混合结构:
Assets/
├── _Game/ // 项目核心(下划线使其排在最前)
│ │
│ ├── Core/ // 核心框架(不依赖具体游戏逻辑)
│ │ ├── EventSystem/ // 事件系统
│ │ │ ├── EventChannel.cs
│ │ │ └── GameEvent.cs
│ │ ├── ServiceLocator/ // 服务定位器
│ │ │ └── ServiceLocator.cs
│ │ ├── StateMachine/ // 状态机
│ │ │ └── StateMachine.cs
│ │ ├── ObjectPool/ // 对象池
│ │ │ └── ObjectPool.cs
│ │ ├── Singleton/ // 单例基类
│ │ │ └── Singleton.cs
│ │ └── Utilities/ // 工具类
│ │ ├── Timer.cs
│ │ └── Extensions.cs
│ │
│ ├── Features/ // 游戏功能模块
│ │ ├── Player/
│ │ ├── Combat/
│ │ ├── Inventory/
│ │ ├── Quest/
│ │ ├── Dialogue/
│ │ ├── WorldMap/
│ │ └── SaveLoad/
│ │
│ ├── Managers/ // 全局管理器
│ │ ├── GameManager.cs
│ │ ├── AudioManager.cs
│ │ └── UIManager.cs
│ │
│ ├── UI/ // 用户界面
│ │ ├── Screens/ // 全屏界面
│ │ ├── Widgets/ // 可复用 UI 组件
│ │ └── HUD/ // 游戏内 HUD
│ │
│ ├── Data/ // ScriptableObject 数据
│ │ ├── Items/
│ │ ├── Enemies/
│ │ └── Config/
│ │
│ ├── Scenes/ // 场景文件
│ │ ├── MainMenu.unity
│ │ ├── Gameplay.unity
│ │ └── Loading.unity
│ │
│ └── Resources/ // Resources(尽量少用)
│
├── Art/ // 美术资源
│ ├── Models/
│ ├── Textures/
│ ├── Materials/
│ ├── Animations/
│ ├── Audio/
│ └── VFX/
│
├── ThirdParty/ // 第三方资源
│
├── Plugins/ // 原生插件
│
└── Editor/ // 编辑器扩展
├── BuildHelper.cs
├── AssetValidator.cs
└── TextureOptimizer.cs
27.2 Assembly Definition(程序集定义)
27.2.1 为什么需要 Assembly Definition
默认情况下,Unity 会把 Assets/ 下的所有 C# 脚本编译成一个程序集(Assembly-CSharp.dll)。这意味着:
- 改一个文件,重新编译所有脚本
- 项目越大,编译越慢
- 无法控制模块间的依赖关系
Assembly Definition(.asmdef)把代码拆分成多个程序集,实现:
- 增量编译:只重新编译修改了的程序集
- 模块化:明确声明模块间的依赖关系
- 编译隔离:减少编译时间
前端类比:Assembly Definition 类似于 monorepo 中的
package.json——每个包声明自己的依赖,独立编译。就像 npm workspace 或 Turborepo。
27.2.2 创建 Assembly Definition
在文件夹中右键 → Create → Assembly Definition:
[截图:创建 Assembly Definition 文件]
推荐的 Assembly Definition 结构:
Assembly 结构:
├── Game.Core.asmdef // 核心框架(不依赖任何游戏代码)
│ └── 引用: Unity 内置程序集
│
├── Game.Features.asmdef // 游戏功能(依赖 Core)
│ └── 引用: Game.Core
│
├── Game.UI.asmdef // UI 系统(依赖 Core 和 Features)
│ └── 引用: Game.Core, Game.Features
│
├── Game.Managers.asmdef // 全局管理器
│ └── 引用: Game.Core, Game.Features, Game.UI
│
├── Game.Editor.asmdef // 编辑器工具(仅编辑器)
│ └── 引用: Game.Core(平台: Editor)
│
└── Game.Tests.asmdef // 测试(仅编辑器)
└── 引用: Game.Core, Game.Features
27.2.3 Assembly Definition 配置
Assembly Definition 设置:
├── Name: Game.Core
├── Allow 'unsafe' Code: ✗ (除非需要使用指针)
├── Auto Referenced: ✓
├── Override References: ✗
├── No Engine References: ✗ (需要引用 UnityEngine)
├── Root Namespace: YourGame.Core (可选,设置默认命名空间)
│
├── Assembly Definition References:
│ └── (添加依赖的其他 asmdef)
│
├── Platforms:
│ ├── Any Platform: ✓ (通常选这个)
│ └── 或者手动选择目标平台
│
└── Version Defines:
└── (用于条件编译,如 #if UNITY_EDITOR)
[截图:Assembly Definition Inspector 配置面板]
27.3 设计模式
27.3.1 单例模式(Singleton)
用途:确保一个类只有一个实例,如 GameManager、AudioManager。
前端类比:类似于 React Context 中的全局 Provider,或者 Redux Store——全局唯一的状态容器。
// ============================================================
// Singleton.cs — 通用单例基类
// 放置路径:Assets/_Game/Core/Singleton/Singleton.cs
// ============================================================
using UnityEngine;
/// <summary>
/// MonoBehaviour 单例基类
/// 继承此类即可获得线程安全的单例行为
/// 子类只需要: public class GameManager : Singleton<GameManager> { }
/// </summary>
/// <typeparam name="T">子类类型</typeparam>
public class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
/// <summary>单例实例</summary>
private static T instance;
/// <summary>线程锁</summary>
private static readonly object lockObj = new object();
/// <summary>应用是否正在退出(防止退出时创建新实例)</summary>
private static bool applicationIsQuitting = false;
/// <summary>
/// 获取单例实例
/// 如果不存在会自动创建
/// </summary>
public static T Instance
{
get
{
// 应用退出时不要创建新实例
if (applicationIsQuitting)
{
Debug.LogWarning($"[Singleton] 应用退出中," +
$"不创建 {typeof(T).Name} 的新实例");
return null;
}
lock (lockObj)
{
if (instance == null)
{
// 尝试在场景中查找
instance = FindAnyObjectByType<T>();
if (instance == null)
{
// 不存在则创建
var singletonObj = new GameObject($"[{typeof(T).Name}]");
instance = singletonObj.AddComponent<T>();
DontDestroyOnLoad(singletonObj);
Debug.Log($"[Singleton] 创建了 {typeof(T).Name} 实例");
}
}
return instance;
}
}
}
/// <summary>检查实例是否存在(不会自动创建)</summary>
public static bool HasInstance => instance != null;
protected virtual void Awake()
{
if (instance != null && instance != this as T)
{
Debug.LogWarning($"[Singleton] {typeof(T).Name} 重复实例,销毁多余的");
Destroy(gameObject);
return;
}
instance = this as T;
DontDestroyOnLoad(gameObject);
}
protected virtual void OnDestroy()
{
if (instance == this as T)
{
instance = null;
}
}
protected virtual void OnApplicationQuit()
{
applicationIsQuitting = true;
}
}
27.3.2 观察者模式 / 事件总线(Observer / Event Bus)
用途:解耦系统间的通信。一个系统发出事件,其他系统监听并响应。
前端类比:
- React:
useContext+dispatch- Vue:
EventBus或mitt- Node.js:
EventEmitter- Redux:
store.subscribe()和dispatch(action)
使用 ScriptableObject 实现事件通道
这是 Unity 社区推荐的优雅方案——用 ScriptableObject 作为事件的”邮局”:
// ============================================================
// EventChannel.cs — 基于 ScriptableObject 的事件通道系统
// 放置路径:Assets/_Game/Core/EventSystem/EventChannel.cs
// ============================================================
using UnityEngine;
using UnityEngine.Events;
using System;
// ========================================
// 无参数事件通道
// ========================================
/// <summary>
/// 无参数事件通道
/// 用法:
/// 1. 在 Project 窗口右键 → Create → Events → Void Event Channel
/// 2. 创建一个事件资源(如 "OnPlayerDied")
/// 3. 发布者引用此资源并调用 Raise()
/// 4. 订阅者引用此资源并注册 OnEventRaised
///
/// 优势:
/// - 发布者和订阅者完全解耦
/// - 在 Inspector 中可以直观地看到事件的连接关系
/// - 不需要代码引用,通过 ScriptableObject 资源连接
/// </summary>
[CreateAssetMenu(menuName = "Events/Void Event Channel")]
public class VoidEventChannel : ScriptableObject
{
/// <summary>
/// 事件描述(在 Inspector 中显示,方便理解用途)
/// </summary>
[TextArea]
[SerializeField] private string description;
/// <summary>
/// C# 事件(代码订阅用)
/// 类似于 JavaScript 中的 addEventListener
/// </summary>
public event Action OnEventRaised;
/// <summary>
/// UnityEvent(Inspector 订阅用)
/// 可以在 Inspector 中直接拖拽绑定方法
/// </summary>
[SerializeField] private UnityEvent onEventRaisedInspector;
/// <summary>
/// 触发事件
/// 类似于 JavaScript 中的 dispatchEvent 或 emit
/// </summary>
public void Raise()
{
Debug.Log($"[EventChannel] 触发事件: {name}");
// 触发 C# 事件
OnEventRaised?.Invoke();
// 触发 Inspector 绑定的事件
onEventRaisedInspector?.Invoke();
}
}
// ========================================
// 带参数的事件通道(泛型)
// ========================================
/// <summary>
/// 泛型事件通道基类
/// </summary>
/// <typeparam name="T">事件参数类型</typeparam>
public abstract class EventChannel<T> : ScriptableObject
{
[TextArea]
[SerializeField] private string description;
public event Action<T> OnEventRaised;
public void Raise(T value)
{
Debug.Log($"[EventChannel] 触发事件: {name}, 值: {value}");
OnEventRaised?.Invoke(value);
}
}
// ========================================
// 常用类型的事件通道
// ========================================
/// <summary>整数事件通道(如:伤害值、分数变化)</summary>
[CreateAssetMenu(menuName = "Events/Int Event Channel")]
public class IntEventChannel : EventChannel<int> { }
/// <summary>浮点数事件通道(如:生命值百分比、时间)</summary>
[CreateAssetMenu(menuName = "Events/Float Event Channel")]
public class FloatEventChannel : EventChannel<float> { }
/// <summary>字符串事件通道(如:聊天消息、通知)</summary>
[CreateAssetMenu(menuName = "Events/String Event Channel")]
public class StringEventChannel : EventChannel<string> { }
/// <summary>布尔事件通道(如:暂停/恢复、显示/隐藏)</summary>
[CreateAssetMenu(menuName = "Events/Bool Event Channel")]
public class BoolEventChannel : EventChannel<bool> { }
/// <summary>Vector3 事件通道(如:位置、方向)</summary>
[CreateAssetMenu(menuName = "Events/Vector3 Event Channel")]
public class Vector3EventChannel : EventChannel<Vector3> { }
/// <summary>GameObject 事件通道(如:拾取物品、点击目标)</summary>
[CreateAssetMenu(menuName = "Events/GameObject Event Channel")]
public class GameObjectEventChannel : EventChannel<GameObject> { }
// ========================================
// 事件监听器组件(用于在 Inspector 中绑定响应)
// ========================================
/// <summary>
/// 无参数事件监听器
/// 添加到 GameObject 上,在 Inspector 中配置响应
/// </summary>
public class VoidEventListener : MonoBehaviour
{
[Tooltip("要监听的事件通道")]
[SerializeField] private VoidEventChannel eventChannel;
[Tooltip("事件触发时执行的方法")]
[SerializeField] private UnityEvent onEventRaised;
private void OnEnable()
{
if (eventChannel != null)
eventChannel.OnEventRaised += Respond;
}
private void OnDisable()
{
if (eventChannel != null)
eventChannel.OnEventRaised -= Respond;
}
private void Respond()
{
onEventRaised?.Invoke();
}
}
事件通道使用示例
使用流程:
1. 创建事件资源:
Project 窗口 → Create → Events → Void Event Channel
命名为 "OnPlayerDied"
2. 发布者(PlayerHealth.cs):
[SerializeField] private VoidEventChannel onPlayerDied;
void Die()
{
onPlayerDied.Raise(); // 触发事件
}
3. 订阅者A(GameOverUI.cs):
[SerializeField] private VoidEventChannel onPlayerDied;
void OnEnable() => onPlayerDied.OnEventRaised += ShowGameOver;
void OnDisable() => onPlayerDied.OnEventRaised -= ShowGameOver;
4. 订阅者B(EnemyAI.cs):
[SerializeField] private VoidEventChannel onPlayerDied;
void OnEnable() => onPlayerDied.OnEventRaised += Celebrate;
void OnDisable() => onPlayerDied.OnEventRaised -= Celebrate;
关键优势:PlayerHealth 不需要知道 GameOverUI 和 EnemyAI 的存在!
[截图:Inspector 中 VoidEventChannel 资源的配置,以及 EventListener 组件的绑定]
27.3.3 状态机模式(State Machine)
用途:管理对象的不同状态及状态间的转换。如:角色状态(Idle、Run、Jump、Attack)、游戏流程(Menu、Playing、Paused、GameOver)。
前端类比:
- XState(JavaScript 状态机库)
- Redux reducer 中的 action 处理
- React 组件的生命周期状态
// ============================================================
// StateMachine.cs — 通用有限状态机
// 放置路径:Assets/_Game/Core/StateMachine/StateMachine.cs
// ============================================================
using UnityEngine;
using System;
using System.Collections.Generic;
/// <summary>
/// 状态接口
/// 每个具体状态需要实现这个接口
/// 类似于 React 组件的生命周期方法
/// </summary>
public interface IState
{
/// <summary>进入状态时调用(类似于 componentDidMount / useEffect 初始化)</summary>
void Enter();
/// <summary>每帧更新(类似于 requestAnimationFrame 中的更新)</summary>
void Update();
/// <summary>物理更新(固定时间步长)</summary>
void FixedUpdate();
/// <summary>离开状态时调用(类似于 componentWillUnmount / useEffect cleanup)</summary>
void Exit();
}
/// <summary>
/// 状态基类(可选,提供默认空实现)
/// 类似于 React.Component 提供默认的生命周期方法
/// </summary>
public abstract class BaseState : IState
{
/// <summary>状态所属的状态机</summary>
protected StateMachine stateMachine;
public BaseState(StateMachine stateMachine)
{
this.stateMachine = stateMachine;
}
public virtual void Enter() { }
public virtual void Update() { }
public virtual void FixedUpdate() { }
public virtual void Exit() { }
}
/// <summary>
/// 通用有限状态机
/// 管理状态的注册、切换和更新
/// </summary>
public class StateMachine
{
/// <summary>当前活跃的状态</summary>
public IState CurrentState { get; private set; }
/// <summary>上一个状态</summary>
public IState PreviousState { get; private set; }
/// <summary>所有已注册的状态</summary>
private Dictionary<Type, IState> states = new Dictionary<Type, IState>();
/// <summary>状态变化事件</summary>
public event Action<IState, IState> OnStateChanged; // (旧状态, 新状态)
/// <summary>
/// 注册一个状态
/// </summary>
/// <typeparam name="T">状态类型</typeparam>
/// <param name="state">状态实例</param>
public void RegisterState<T>(T state) where T : IState
{
var type = typeof(T);
if (states.ContainsKey(type))
{
Debug.LogWarning($"[StateMachine] 状态 {type.Name} 已经注册过了");
return;
}
states[type] = state;
Debug.Log($"[StateMachine] 注册状态: {type.Name}");
}
/// <summary>
/// 切换到指定状态
/// </summary>
/// <typeparam name="T">目标状态类型</typeparam>
public void ChangeState<T>() where T : IState
{
var type = typeof(T);
if (!states.TryGetValue(type, out IState newState))
{
Debug.LogError($"[StateMachine] 未注册的状态: {type.Name}");
return;
}
// 退出当前状态
PreviousState = CurrentState;
CurrentState?.Exit();
// 进入新状态
CurrentState = newState;
CurrentState.Enter();
Debug.Log($"[StateMachine] 状态切换: " +
$"{PreviousState?.GetType().Name ?? "None"} → {type.Name}");
OnStateChanged?.Invoke(PreviousState, CurrentState);
}
/// <summary>
/// 更新当前状态(在 MonoBehaviour.Update 中调用)
/// </summary>
public void Update()
{
CurrentState?.Update();
}
/// <summary>
/// 物理更新当前状态(在 MonoBehaviour.FixedUpdate 中调用)
/// </summary>
public void FixedUpdate()
{
CurrentState?.FixedUpdate();
}
/// <summary>
/// 检查当前是否处于某个状态
/// </summary>
public bool IsInState<T>() where T : IState
{
return CurrentState != null && CurrentState.GetType() == typeof(T);
}
/// <summary>
/// 获取已注册的状态实例
/// </summary>
public T GetState<T>() where T : IState
{
if (states.TryGetValue(typeof(T), out IState state))
return (T)state;
return default;
}
/// <summary>
/// 回到上一个状态
/// </summary>
public void RevertToPreviousState()
{
if (PreviousState != null)
{
var type = PreviousState.GetType();
CurrentState?.Exit();
CurrentState = PreviousState;
CurrentState.Enter();
}
}
}
// ========================================
// 使用示例:角色状态
// ========================================
/// <summary>角色空闲状态</summary>
public class PlayerIdleState : BaseState
{
private readonly PlayerController player;
public PlayerIdleState(StateMachine sm, PlayerController player) : base(sm)
{
this.player = player;
}
public override void Enter()
{
Debug.Log("[PlayerState] 进入空闲状态");
player.PlayAnimation("Idle");
}
public override void Update()
{
// 检查是否开始移动
if (player.MoveInput.magnitude > 0.1f)
{
stateMachine.ChangeState<PlayerRunState>();
}
// 检查是否按下跳跃键
if (player.JumpPressed)
{
stateMachine.ChangeState<PlayerJumpState>();
}
// 检查是否按下攻击键
if (player.AttackPressed)
{
stateMachine.ChangeState<PlayerAttackState>();
}
}
public override void Exit()
{
Debug.Log("[PlayerState] 离开空闲状态");
}
}
/// <summary>角色奔跑状态</summary>
public class PlayerRunState : BaseState
{
private readonly PlayerController player;
public PlayerRunState(StateMachine sm, PlayerController player) : base(sm)
{
this.player = player;
}
public override void Enter()
{
Debug.Log("[PlayerState] 进入奔跑状态");
player.PlayAnimation("Run");
}
public override void Update()
{
// 执行移动逻辑
player.Move();
// 检查是否停止移动
if (player.MoveInput.magnitude < 0.1f)
{
stateMachine.ChangeState<PlayerIdleState>();
}
if (player.JumpPressed)
{
stateMachine.ChangeState<PlayerJumpState>();
}
}
}
/// <summary>角色跳跃状态</summary>
public class PlayerJumpState : BaseState
{
private readonly PlayerController player;
public PlayerJumpState(StateMachine sm, PlayerController player) : base(sm)
{
this.player = player;
}
public override void Enter()
{
Debug.Log("[PlayerState] 进入跳跃状态");
player.PlayAnimation("Jump");
player.ApplyJumpForce();
}
public override void Update()
{
// 检查是否落地
if (player.IsGrounded)
{
if (player.MoveInput.magnitude > 0.1f)
stateMachine.ChangeState<PlayerRunState>();
else
stateMachine.ChangeState<PlayerIdleState>();
}
}
}
/// <summary>角色攻击状态</summary>
public class PlayerAttackState : BaseState
{
private readonly PlayerController player;
private float attackTimer;
public PlayerAttackState(StateMachine sm, PlayerController player) : base(sm)
{
this.player = player;
}
public override void Enter()
{
Debug.Log("[PlayerState] 进入攻击状态");
player.PlayAnimation("Attack");
attackTimer = 0.5f; // 攻击持续时间
}
public override void Update()
{
attackTimer -= Time.deltaTime;
if (attackTimer <= 0f)
{
stateMachine.ChangeState<PlayerIdleState>();
}
}
}
// 注意:PlayerController 是一个假设的类,在这里作为示例引用
// 实际使用时需要根据你的项目来定义
27.3.4 命令模式(Command Pattern)
用途:将操作封装为对象,支持撤销/重做、操作队列、宏命令。
前端类比:Redux 的 Action 就是命令模式的应用——每个 action 是一个描述操作的对象。
/// <summary>命令接口</summary>
public interface ICommand
{
/// <summary>执行命令</summary>
void Execute();
/// <summary>撤销命令</summary>
void Undo();
}
/// <summary>命令管理器(支持撤销/重做)</summary>
public class CommandManager
{
private Stack<ICommand> undoStack = new Stack<ICommand>();
private Stack<ICommand> redoStack = new Stack<ICommand>();
/// <summary>执行命令并记录到历史</summary>
public void ExecuteCommand(ICommand command)
{
command.Execute();
undoStack.Push(command);
redoStack.Clear(); // 执行新命令后清空重做栈
}
/// <summary>撤销最后一个命令</summary>
public void Undo()
{
if (undoStack.Count > 0)
{
var command = undoStack.Pop();
command.Undo();
redoStack.Push(command);
}
}
/// <summary>重做最后撤销的命令</summary>
public void Redo()
{
if (redoStack.Count > 0)
{
var command = redoStack.Pop();
command.Execute();
undoStack.Push(command);
}
}
}
/// <summary>示例:移动物品命令</summary>
public class MoveItemCommand : ICommand
{
private readonly Transform target;
private readonly Vector3 newPosition;
private readonly Vector3 oldPosition;
public MoveItemCommand(Transform target, Vector3 newPosition)
{
this.target = target;
this.newPosition = newPosition;
this.oldPosition = target.position;
}
public void Execute() => target.position = newPosition;
public void Undo() => target.position = oldPosition;
}
27.3.5 对象池模式(Object Pool)
用途:重复使用对象而不是频繁创建和销毁,避免 GC(垃圾回收)导致的卡顿。
前端类比:类似于 React 中的虚拟列表(Virtual List)——只渲染可见的元素,离开视口的元素被回收复用。
using UnityEngine;
using UnityEngine.Pool;
using System.Collections.Generic;
/// <summary>
/// 通用对象池
/// Unity 2021+ 内置了 ObjectPool,但我们封装一层以添加预热和管理功能
/// </summary>
public class GameObjectPool : MonoBehaviour
{
[Tooltip("要池化的预制体")]
[SerializeField] private GameObject prefab;
[Tooltip("初始预热数量")]
[SerializeField] private int initialSize = 10;
[Tooltip("最大池大小")]
[SerializeField] private int maxSize = 100;
/// <summary>Unity 内置对象池</summary>
private ObjectPool<GameObject> pool;
/// <summary>所有活跃的对象</summary>
private HashSet<GameObject> activeObjects = new HashSet<GameObject>();
private void Awake()
{
// 创建对象池
pool = new ObjectPool<GameObject>(
createFunc: CreatePooledObject, // 如何创建新对象
actionOnGet: OnGetFromPool, // 从池中取出时
actionOnRelease: OnReturnToPool, // 归还到池中时
actionOnDestroy: OnDestroyPooled, // 池满时销毁多余对象
collectionCheck: true, // 防止重复归还
defaultCapacity: initialSize,
maxSize: maxSize
);
// 预热对象池(提前创建一批对象)
Prewarm();
}
/// <summary>预热:提前创建对象以避免运行时的创建开销</summary>
private void Prewarm()
{
var prewarmList = new List<GameObject>();
for (int i = 0; i < initialSize; i++)
{
var obj = pool.Get();
prewarmList.Add(obj);
}
// 全部归还到池中
foreach (var obj in prewarmList)
{
pool.Release(obj);
}
Debug.Log($"[ObjectPool] {prefab.name} 预热完成: {initialSize} 个对象");
}
/// <summary>从池中获取一个对象</summary>
public GameObject Get(Vector3 position, Quaternion rotation)
{
var obj = pool.Get();
obj.transform.position = position;
obj.transform.rotation = rotation;
activeObjects.Add(obj);
return obj;
}
/// <summary>将对象归还到池中</summary>
public void Release(GameObject obj)
{
if (activeObjects.Contains(obj))
{
activeObjects.Remove(obj);
pool.Release(obj);
}
}
/// <summary>归还所有活跃对象</summary>
public void ReleaseAll()
{
var toRelease = new List<GameObject>(activeObjects);
foreach (var obj in toRelease)
{
Release(obj);
}
}
// ---- 池回调方法 ----
private GameObject CreatePooledObject()
{
var obj = Instantiate(prefab, transform);
obj.name = $"{prefab.name}_Pooled";
return obj;
}
private void OnGetFromPool(GameObject obj)
{
obj.SetActive(true);
}
private void OnReturnToPool(GameObject obj)
{
obj.SetActive(false);
obj.transform.SetParent(transform);
}
private void OnDestroyPooled(GameObject obj)
{
Destroy(obj);
}
/// <summary>当前池中可用的对象数</summary>
public int CountInactive => pool.CountInactive;
/// <summary>当前活跃的对象数</summary>
public int CountActive => activeObjects.Count;
}
27.3.6 服务定位器模式(Service Locator)
用途:提供全局访问服务的方式,比单例更灵活——可以替换具体实现(用于测试)。
前端类比:类似于 Angular 的依赖注入容器,或 React Context 的 Provider。
// ============================================================
// ServiceLocator.cs — 服务定位器
// 放置路径:Assets/_Game/Core/ServiceLocator/ServiceLocator.cs
// ============================================================
using System;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 服务定位器
/// 提供全局的服务注册和获取功能
///
/// 优势相比单例:
/// 1. 面向接口编程——依赖抽象而非具体实现
/// 2. 可以替换实现(如测试时替换为 Mock)
/// 3. 更清晰的依赖关系
/// 4. 不强制继承特定基类
///
/// 前端类比:
/// - Angular: this.service = inject(MyService)
/// - React: const service = useContext(ServiceContext)
/// </summary>
public static class ServiceLocator
{
/// <summary>已注册的服务字典</summary>
private static Dictionary<Type, object> services = new Dictionary<Type, object>();
/// <summary>已注册的懒加载服务工厂</summary>
private static Dictionary<Type, Func<object>> factories = new Dictionary<Type, Func<object>>();
/// <summary>
/// 注册服务实例
/// 类似于 Angular 的 { provide: IService, useValue: serviceInstance }
/// </summary>
/// <typeparam name="T">服务接口类型</typeparam>
/// <param name="service">服务实例</param>
public static void Register<T>(T service) where T : class
{
var type = typeof(T);
if (services.ContainsKey(type))
{
Debug.LogWarning($"[ServiceLocator] 覆盖已存在的服务: {type.Name}");
}
services[type] = service;
Debug.Log($"[ServiceLocator] 注册服务: {type.Name} → {service.GetType().Name}");
}
/// <summary>
/// 注册服务工厂(懒加载)
/// 服务在首次获取时才创建
/// 类似于 Angular 的 { provide: IService, useFactory: () => new Service() }
/// </summary>
/// <typeparam name="T">服务接口类型</typeparam>
/// <param name="factory">创建服务实例的工厂方法</param>
public static void RegisterFactory<T>(Func<T> factory) where T : class
{
factories[typeof(T)] = () => factory();
Debug.Log($"[ServiceLocator] 注册服务工厂: {typeof(T).Name}");
}
/// <summary>
/// 获取服务实例
/// 类似于 Angular 的 inject(IService) 或 React 的 useContext()
/// </summary>
/// <typeparam name="T">服务接口类型</typeparam>
/// <returns>服务实例</returns>
/// <exception cref="InvalidOperationException">服务未注册时抛出</exception>
public static T Get<T>() where T : class
{
var type = typeof(T);
// 尝试直接获取
if (services.TryGetValue(type, out object service))
{
return (T)service;
}
// 尝试使用工厂创建
if (factories.TryGetValue(type, out Func<object> factory))
{
var instance = (T)factory();
services[type] = instance; // 缓存创建的实例
factories.Remove(type); // 移除工厂(只创建一次)
Debug.Log($"[ServiceLocator] 通过工厂创建服务: {type.Name}");
return instance;
}
throw new InvalidOperationException(
$"[ServiceLocator] 服务未注册: {type.Name}。" +
$"请在 Awake 或 初始化阶段调用 ServiceLocator.Register<{type.Name}>()");
}
/// <summary>
/// 尝试获取服务(不抛异常)
/// </summary>
/// <typeparam name="T">服务接口类型</typeparam>
/// <param name="service">获取到的服务实例</param>
/// <returns>是否成功获取</returns>
public static bool TryGet<T>(out T service) where T : class
{
try
{
service = Get<T>();
return true;
}
catch
{
service = null;
return false;
}
}
/// <summary>
/// 注销服务
/// </summary>
/// <typeparam name="T">服务接口类型</typeparam>
public static void Unregister<T>() where T : class
{
var type = typeof(T);
services.Remove(type);
factories.Remove(type);
Debug.Log($"[ServiceLocator] 注销服务: {type.Name}");
}
/// <summary>
/// 清除所有服务(场景切换时调用)
/// </summary>
public static void Clear()
{
services.Clear();
factories.Clear();
Debug.Log("[ServiceLocator] 清除所有服务");
}
/// <summary>
/// 检查服务是否已注册
/// </summary>
public static bool IsRegistered<T>() where T : class
{
var type = typeof(T);
return services.ContainsKey(type) || factories.ContainsKey(type);
}
}
// ========================================
// 使用示例
// ========================================
// 定义服务接口
public interface IAudioService
{
void PlaySFX(string clipName);
void PlayBGM(string clipName);
void StopAll();
}
public interface ISaveService
{
void Save(string key, object data);
T Load<T>(string key);
}
// 实现服务
public class UnityAudioService : MonoBehaviour, IAudioService
{
public void PlaySFX(string clipName)
{
Debug.Log($"播放音效: {clipName}");
}
public void PlayBGM(string clipName)
{
Debug.Log($"播放背景音乐: {clipName}");
}
public void StopAll()
{
Debug.Log("停止所有音频");
}
}
/*
// 在初始化阶段注册服务
void Awake()
{
var audioService = GetComponent<UnityAudioService>();
ServiceLocator.Register<IAudioService>(audioService);
ServiceLocator.Register<ISaveService>(new PlayerPrefsSaveService());
}
// 在其他地方使用服务
void PlayHitSound()
{
ServiceLocator.Get<IAudioService>().PlaySFX("hit_01");
}
*/
27.3.7 MVC/MVP 模式
前端类比:MVC 在 Unity 中的对应关系:
前端 MVC/MVVM Unity MVC ───────────── ───────── Model (数据/状态) → ScriptableObject / 数据类 View (UI 渲染) → MonoBehaviour + UI 组件 Controller (逻辑) → MonoBehaviour(游戏逻辑) React 对应: Model = useState / Redux Store View = JSX / 组件渲染 Controller = useEffect / event handlers
27.4 ScriptableObject 架构
27.4.1 什么是数据驱动设计
ScriptableObject 是 Unity 中非常强大但常被忽视的特性。它允许你将数据与逻辑分离,在编辑器中可视化地配置数据。
传统方式(数据硬编码在代码中):
class Sword : MonoBehaviour
{
int damage = 10; // 修改需要改代码,重新编译
float attackSpeed = 1.5f;
}
ScriptableObject 方式(数据存在资源文件中):
[CreateAssetMenu(menuName = "Items/Weapon")]
class WeaponData : ScriptableObject
{
public string weaponName;
public int damage;
public float attackSpeed;
public Sprite icon;
}
// 使用时引用资源
class Sword : MonoBehaviour
{
[SerializeField] WeaponData data; // 在 Inspector 中拖入
// 修改数据只需要改资源文件,不需要重新编译代码!
}
前端类比:ScriptableObject 类似于将配置外部化——如同将 UI 文案放在 JSON/i18n 文件中而不是硬编码在组件里。
27.4.2 运行时集合(Runtime Sets)
用 ScriptableObject 维护运行时的对象列表:
/// <summary>
/// 运行时 GameObject 集合
/// 场景中的对象在生成时自动注册,销毁时自动注销
/// 其他系统可以通过这个集合访问所有注册的对象
/// </summary>
[CreateAssetMenu(menuName = "Runtime/GameObject Set")]
public class RuntimeGameObjectSet : ScriptableObject
{
/// <summary>当前注册的对象列表</summary>
private List<GameObject> items = new List<GameObject>();
/// <summary>注册对象</summary>
public void Register(GameObject obj)
{
if (!items.Contains(obj))
{
items.Add(obj);
}
}
/// <summary>注销对象</summary>
public void Unregister(GameObject obj)
{
items.Remove(obj);
}
/// <summary>获取所有注册的对象</summary>
public IReadOnlyList<GameObject> GetAll() => items;
/// <summary>获取数量</summary>
public int Count => items.Count;
/// <summary>场景切换时清理</summary>
private void OnDisable()
{
items.Clear();
}
}
// 使用示例:自动注册的组件
public class RuntimeSetMember : MonoBehaviour
{
[SerializeField] private RuntimeGameObjectSet targetSet;
private void OnEnable() => targetSet.Register(gameObject);
private void OnDisable() => targetSet.Unregister(gameObject);
}
27.5 依赖注入简介
27.5.1 为什么需要依赖注入
当项目变大时,手动管理依赖变得困难:
// 没有 DI:硬编码依赖(紧耦合)
class PlayerController : MonoBehaviour
{
void Attack()
{
// 直接引用具体类——如果要改成网络版怎么办?
AudioManager.Instance.PlaySFX("attack");
UIManager.Instance.ShowDamage(10);
AnalyticsService.Instance.LogEvent("attack");
}
}
// 有 DI:注入抽象依赖(松耦合)
class PlayerController : MonoBehaviour
{
[Inject] private IAudioService audioService;
[Inject] private IUIService uiService;
[Inject] private IAnalyticsService analyticsService;
void Attack()
{
audioService.PlaySFX("attack");
uiService.ShowDamage(10);
analyticsService.LogEvent("attack");
}
}
27.5.2 VContainer(推荐)
VContainer 是一个轻量级的 Unity 依赖注入框架:
// 安装:Package Manager → Add package from Git URL
// https://github.com/hadashiA/VContainer.git?path=VContainer/Assets/VContainer
// 定义注入范围
public class GameLifetimeScope : LifetimeScope
{
protected override void Configure(IContainerBuilder builder)
{
// 注册服务
builder.Register<IAudioService, UnityAudioService>(Lifetime.Singleton);
builder.Register<ISaveService, PlayerPrefsSaveService>(Lifetime.Singleton);
// 注册 MonoBehaviour
builder.RegisterComponentInHierarchy<PlayerController>();
}
}
前端类比:VContainer 类似于 Angular 的 Module + Provider 系统,或者 InversifyJS。
27.5.3 选择建议
项目规模 推荐方案
────── ────────
小型/原型 ServiceLocator 或 Singleton
中型 ServiceLocator + ScriptableObject Events
大型 VContainer 或 Zenject
27.6 GameManager.cs — 游戏管理器
// ============================================================
// GameManager.cs — 游戏核心管理器
// 放置路径:Assets/_Game/Managers/GameManager.cs
// 功能:管理游戏生命周期、状态切换、全局配置
// ============================================================
using UnityEngine;
using UnityEngine.SceneManagement;
using System;
/// <summary>
/// 游戏全局状态
/// </summary>
public enum GameState
{
Initializing, // 初始化中
MainMenu, // 主菜单
Loading, // 加载中
Playing, // 游戏中
Paused, // 暂停
GameOver, // 游戏结束
Victory // 胜利
}
/// <summary>
/// 游戏核心管理器
/// 负责管理游戏的全局状态和生命周期
/// 使用状态机模式管理游戏流程
/// </summary>
public class GameManager : Singleton<GameManager>
{
// ========================================
// 配置
// ========================================
[Header("场景配置")]
[SerializeField] private string mainMenuScene = "MainMenu";
[SerializeField] private string gameplayScene = "Gameplay";
[SerializeField] private string loadingScene = "Loading";
[Header("游戏配置")]
[SerializeField] private int targetFrameRate = 60;
[SerializeField] private bool showDebugInfo = false;
[Header("事件通道")]
[SerializeField] private VoidEventChannel onGameStarted;
[SerializeField] private VoidEventChannel onGamePaused;
[SerializeField] private VoidEventChannel onGameResumed;
[SerializeField] private VoidEventChannel onGameOver;
// ========================================
// 状态
// ========================================
/// <summary>当前游戏状态</summary>
public GameState CurrentState { get; private set; } = GameState.Initializing;
/// <summary>游戏状态变化事件</summary>
public event Action<GameState, GameState> OnGameStateChanged;
/// <summary>游戏是否暂停</summary>
public bool IsPaused => CurrentState == GameState.Paused;
/// <summary>游戏是否正在进行</summary>
public bool IsPlaying => CurrentState == GameState.Playing;
/// <summary>游戏已运行时间(不含暂停)</summary>
public float PlayTime { get; private set; }
// ========================================
// 初始化
// ========================================
protected override void Awake()
{
base.Awake();
// 设置目标帧率
Application.targetFrameRate = targetFrameRate;
// 防止设备休眠(移动端游戏需要)
Screen.sleepTimeout = SleepTimeout.NeverSleep;
// 注册到服务定位器
ServiceLocator.Register<GameManager>(this);
Debug.Log("[GameManager] 初始化完成");
}
private void Start()
{
// 初始化完成,进入主菜单状态
ChangeState(GameState.MainMenu);
}
private void Update()
{
// 更新游戏时间
if (CurrentState == GameState.Playing)
{
PlayTime += Time.deltaTime;
}
// 处理暂停输入(Escape 键或返回键)
if (Input.GetKeyDown(KeyCode.Escape))
{
if (CurrentState == GameState.Playing)
{
PauseGame();
}
else if (CurrentState == GameState.Paused)
{
ResumeGame();
}
}
}
protected override void OnDestroy()
{
ServiceLocator.Unregister<GameManager>();
base.OnDestroy();
}
// ========================================
// 状态管理
// ========================================
/// <summary>
/// 切换游戏状态
/// </summary>
/// <param name="newState">目标状态</param>
public void ChangeState(GameState newState)
{
if (CurrentState == newState) return;
GameState oldState = CurrentState;
CurrentState = newState;
// 处理状态退出
OnExitState(oldState);
// 处理状态进入
OnEnterState(newState);
// 触发事件
OnGameStateChanged?.Invoke(oldState, newState);
Debug.Log($"[GameManager] 状态切换: {oldState} → {newState}");
}
/// <summary>
/// 进入状态时的处理
/// </summary>
private void OnEnterState(GameState state)
{
switch (state)
{
case GameState.MainMenu:
Time.timeScale = 1f;
// Cursor.visible = true;
// Cursor.lockState = CursorLockMode.None;
break;
case GameState.Playing:
Time.timeScale = 1f;
onGameStarted?.Raise();
// Cursor.visible = false;
// Cursor.lockState = CursorLockMode.Locked;
break;
case GameState.Paused:
Time.timeScale = 0f; // 暂停游戏(所有基于 Time.deltaTime 的逻辑会停止)
onGamePaused?.Raise();
// Cursor.visible = true;
// Cursor.lockState = CursorLockMode.None;
break;
case GameState.GameOver:
Time.timeScale = 0f;
onGameOver?.Raise();
break;
case GameState.Victory:
// 可以放慢时间做特效
Time.timeScale = 0.3f;
break;
}
}
/// <summary>
/// 离开状态时的处理
/// </summary>
private void OnExitState(GameState state)
{
switch (state)
{
case GameState.Paused:
Time.timeScale = 1f; // 恢复时间流速
onGameResumed?.Raise();
break;
}
}
// ========================================
// 公共方法
// ========================================
/// <summary>
/// 开始新游戏
/// </summary>
public void StartNewGame()
{
Debug.Log("[GameManager] 开始新游戏");
PlayTime = 0f;
LoadSceneAsync(gameplayScene, () =>
{
ChangeState(GameState.Playing);
});
}
/// <summary>
/// 暂停游戏
/// </summary>
public void PauseGame()
{
if (CurrentState == GameState.Playing)
{
ChangeState(GameState.Paused);
}
}
/// <summary>
/// 恢复游戏
/// </summary>
public void ResumeGame()
{
if (CurrentState == GameState.Paused)
{
ChangeState(GameState.Playing);
}
}
/// <summary>
/// 游戏结束
/// </summary>
public void TriggerGameOver()
{
ChangeState(GameState.GameOver);
}
/// <summary>
/// 回到主菜单
/// </summary>
public void ReturnToMainMenu()
{
Time.timeScale = 1f;
LoadSceneAsync(mainMenuScene, () =>
{
ChangeState(GameState.MainMenu);
});
}
/// <summary>
/// 重新开始当前关卡
/// </summary>
public void RestartLevel()
{
PlayTime = 0f;
Time.timeScale = 1f;
string currentScene = SceneManager.GetActiveScene().name;
LoadSceneAsync(currentScene, () =>
{
ChangeState(GameState.Playing);
});
}
/// <summary>
/// 退出游戏
/// </summary>
public void QuitGame()
{
Debug.Log("[GameManager] 退出游戏");
#if UNITY_EDITOR
UnityEditor.EditorApplication.isPlaying = false;
#else
Application.Quit();
#endif
}
// ========================================
// 场景加载
// ========================================
/// <summary>
/// 异步加载场景
/// </summary>
/// <param name="sceneName">场景名称</param>
/// <param name="onComplete">加载完成回调</param>
private async void LoadSceneAsync(string sceneName, Action onComplete = null)
{
ChangeState(GameState.Loading);
AsyncOperation operation = SceneManager.LoadSceneAsync(sceneName);
operation.allowSceneActivation = false;
// 等待加载到 90%(Unity 在 90% 时暂停,等待 allowSceneActivation)
while (operation.progress < 0.9f)
{
float progress = Mathf.Clamp01(operation.progress / 0.9f);
Debug.Log($"[GameManager] 加载进度: {progress * 100:F0}%");
await System.Threading.Tasks.Task.Yield();
}
// 激活场景
operation.allowSceneActivation = true;
// 等待场景完全加载
while (!operation.isDone)
{
await System.Threading.Tasks.Task.Yield();
}
Debug.Log($"[GameManager] 场景加载完成: {sceneName}");
onComplete?.Invoke();
}
// ========================================
// 调试信息
// ========================================
private void OnGUI()
{
if (!showDebugInfo) return;
GUILayout.BeginArea(new Rect(10, 10, 300, 200));
GUILayout.Label($"游戏状态: {CurrentState}");
GUILayout.Label($"帧率: {1f / Time.unscaledDeltaTime:F0} FPS");
GUILayout.Label($"游戏时间: {PlayTime:F1}s");
GUILayout.Label($"Time.timeScale: {Time.timeScale}");
GUILayout.EndArea();
}
}
27.7 Unity 测试框架
27.7.1 测试类型
Unity 支持两种测试模式:
Edit Mode 测试(快速,不需要运行游戏):
├── 测试纯 C# 逻辑
├── 不依赖 MonoBehaviour 生命周期
├── 运行速度快
└── 适合:数据处理、数学计算、状态机逻辑
Play Mode 测试(需要运行游戏场景):
├── 测试运行时行为
├── 可以测试 MonoBehaviour
├── 可以等待帧、等待时间
├── 运行速度慢
└── 适合:游戏逻辑、UI 交互、碰撞检测
前端类比:
- Edit Mode 测试 = Jest 单元测试
- Play Mode 测试 = Cypress / Playwright E2E 测试
27.7.2 设置测试
- 打开 Window → General → Test Runner
- 创建测试程序集:
- 在要放测试的文件夹中创建 Assembly Definition
- 勾选 Test Assemblies
- 添加引用:
UnityEngine.TestRunner和UnityEditor.TestRunner
27.7.3 Edit Mode 测试示例
// ============================================================
// StateMachineTests.cs — 状态机单元测试
// 放置路径:Assets/Tests/EditMode/StateMachineTests.cs
// ============================================================
using NUnit.Framework;
[TestFixture]
public class StateMachineTests
{
private StateMachine stateMachine;
/// <summary>每个测试前执行(类似于 Jest 的 beforeEach)</summary>
[SetUp]
public void SetUp()
{
stateMachine = new StateMachine();
}
[Test]
public void RegisterState_ShouldStoreState()
{
// Arrange(准备)
var mockState = new MockState(stateMachine);
// Act(执行)
stateMachine.RegisterState<MockState>(mockState);
// Assert(断言)
Assert.IsNotNull(stateMachine.GetState<MockState>());
}
[Test]
public void ChangeState_ShouldCallEnterAndExit()
{
// Arrange
var stateA = new MockState(stateMachine);
var stateB = new MockState(stateMachine);
stateMachine.RegisterState<MockState>(stateA);
stateMachine.RegisterState<MockStateB>(stateB);
// Act
stateMachine.ChangeState<MockState>();
stateMachine.ChangeState<MockStateB>();
// Assert
Assert.IsTrue(stateA.WasEntered, "StateA 应该被进入过");
Assert.IsTrue(stateA.WasExited, "StateA 应该被退出过");
Assert.IsTrue(stateB.WasEntered, "StateB 应该被进入过");
}
[Test]
public void IsInState_ShouldReturnCorrectState()
{
var state = new MockState(stateMachine);
stateMachine.RegisterState<MockState>(state);
stateMachine.ChangeState<MockState>();
Assert.IsTrue(stateMachine.IsInState<MockState>());
}
[Test]
public void ChangeState_ShouldFireEvent()
{
var state = new MockState(stateMachine);
stateMachine.RegisterState<MockState>(state);
bool eventFired = false;
stateMachine.OnStateChanged += (oldState, newState) => eventFired = true;
stateMachine.ChangeState<MockState>();
Assert.IsTrue(eventFired, "状态变化事件应该被触发");
}
// ---- Mock 状态类 ----
private class MockState : BaseState
{
public bool WasEntered { get; private set; }
public bool WasExited { get; private set; }
public MockState(StateMachine sm) : base(sm) { }
public override void Enter()
{
WasEntered = true;
}
public override void Exit()
{
WasExited = true;
}
}
private class MockStateB : BaseState
{
public bool WasEntered { get; private set; }
public MockStateB(StateMachine sm) : base(sm) { }
public override void Enter()
{
WasEntered = true;
}
}
}
// ========================================
// ServiceLocator 测试
// ========================================
[TestFixture]
public class ServiceLocatorTests
{
[SetUp]
public void SetUp()
{
ServiceLocator.Clear();
}
[TearDown]
public void TearDown()
{
ServiceLocator.Clear();
}
[Test]
public void Register_And_Get_ShouldReturnSameInstance()
{
// Arrange
var service = new MockAudioService();
// Act
ServiceLocator.Register<IMockService>(service);
var retrieved = ServiceLocator.Get<IMockService>();
// Assert
Assert.AreSame(service, retrieved);
}
[Test]
public void Get_UnregisteredService_ShouldThrow()
{
Assert.Throws<System.InvalidOperationException>(() =>
{
ServiceLocator.Get<IMockService>();
});
}
[Test]
public void RegisterFactory_ShouldCreateOnFirstGet()
{
int createCount = 0;
ServiceLocator.RegisterFactory<IMockService>(() =>
{
createCount++;
return new MockAudioService();
});
// 获取两次,应该只创建一次(缓存)
ServiceLocator.Get<IMockService>();
ServiceLocator.Get<IMockService>();
Assert.AreEqual(1, createCount);
}
[Test]
public void IsRegistered_ShouldReturnCorrectly()
{
Assert.IsFalse(ServiceLocator.IsRegistered<IMockService>());
ServiceLocator.Register<IMockService>(new MockAudioService());
Assert.IsTrue(ServiceLocator.IsRegistered<IMockService>());
}
// ---- Mock ----
private interface IMockService { }
private class MockAudioService : IMockService { }
}
[截图:Test Runner 窗口,显示测试结果(绿色通过标记)]
27.7.4 Play Mode 测试示例
using System.Collections;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
public class GameManagerPlayTests
{
[UnityTest]
public IEnumerator GameManager_PauseAndResume_ShouldChangeTimeScale()
{
// Arrange: 创建 GameManager
var go = new GameObject("GameManager");
var gm = go.AddComponent<GameManager>();
yield return null; // 等待一帧让 Start 执行
// 模拟进入 Playing 状态
gm.ChangeState(GameState.Playing);
yield return null;
// Act: 暂停
gm.PauseGame();
yield return null;
// Assert: 时间应该停止
Assert.AreEqual(0f, Time.timeScale, "暂停时 timeScale 应为 0");
Assert.AreEqual(GameState.Paused, gm.CurrentState);
// Act: 恢复
gm.ResumeGame();
yield return null;
// Assert: 时间应该恢复
Assert.AreEqual(1f, Time.timeScale, "恢复时 timeScale 应为 1");
Assert.AreEqual(GameState.Playing, gm.CurrentState);
// Cleanup
Object.Destroy(go);
}
}
27.8 调试工具和技巧
27.8.1 常用调试方法
// 基础日志
Debug.Log("普通信息");
Debug.LogWarning("警告信息");
Debug.LogError("错误信息");
// 带颜色的日志(在 Console 中更容易识别)
Debug.Log("<color=green>[成功]</color> 玩家已生成");
Debug.Log("<color=yellow>[警告]</color> 内存使用超过 80%");
Debug.Log("<color=red>[错误]</color> 网络连接失败");
// 带上下文对象的日志(点击日志会选中对应 GameObject)
Debug.Log("玩家受伤", this.gameObject);
// 在 Scene 视图中绘制调试图形
Debug.DrawRay(transform.position, transform.forward * 10, Color.red, 1f);
Debug.DrawLine(pointA, pointB, Color.green);
// 在 Game 视图中绘制(需要在 OnDrawGizmos 中)
void OnDrawGizmosSelected()
{
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(transform.position, attackRange);
}
27.8.2 性能分析工具
Unity 内置工具:
├── Profiler (Window → Analysis → Profiler)
│ ├── CPU Usage: 查看每帧的 CPU 时间分布
│ ├── GPU Usage: 查看渲染性能
│ ├── Memory: 查看内存分配和 GC
│ └── 可以连接到真机进行分析
│
├── Frame Debugger (Window → Analysis → Frame Debugger)
│ └── 逐步查看每个 Draw Call
│
└── Memory Profiler(Package Manager 安装)
└── 查看详细的内存快照
27.9 版本控制最佳实践
27.9.1 .gitignore
# Unity 项目 .gitignore 核心内容
# Unity 生成的文件夹
/[Ll]ibrary/
/[Tt]emp/
/[Oo]bj/
/[Bb]uild/
/[Bb]uilds/
/[Ll]ogs/
/[Uu]ser[Ss]ettings/
# IDE
.vs/
.vscode/
*.csproj
*.sln
*.suo
*.user
*.pidb
*.booproj
# OS
.DS_Store
Thumbs.db
# 构建输出
*.apk
*.aab
*.ipa
*.unitypackage
# Crashlytics
crashlytics-build.properties
# Addressables
/[Aa]ssets/[Aa]ddressable[Aa]ssets[Dd]ata/*/*.bin*
/[Aa]ssets/[Ss]treamingAssets/aa.meta
/[Aa]ssets/[Ss]treamingAssets/aa/*
27.9.2 Git LFS
大文件不适合直接存在 Git 中。使用 Git LFS(Large File Storage):
# 安装 Git LFS(Mac)
brew install git-lfs
git lfs install
# 跟踪大文件类型
git lfs track "*.png"
git lfs track "*.jpg"
git lfs track "*.wav"
git lfs track "*.mp3"
git lfs track "*.fbx"
git lfs track "*.psd"
git lfs track "*.tga"
git lfs track "*.tif"
git lfs track "*.exr"
git lfs track "*.unitypackage"
# 确保 .gitattributes 被提交
git add .gitattributes
git commit -m "Configure Git LFS tracking"
27.9.3 Unity 项目版本控制设置
Edit → Project Settings → Editor
├── Version Control
│ └── Mode: Visible Meta Files
│ (确保 .meta 文件可见并提交到 Git)
│
└── Asset Serialization
└── Mode: Force Text
(使用文本格式序列化,便于 Git diff 和合并)
27.10 CI/CD 与 GitHub Actions
27.10.1 使用 GameCI 的 GitHub Actions
GameCI 提供了专门用于 Unity 的 GitHub Actions:
# .github/workflows/unity-build.yml
# Unity CI/CD 构建管线
name: Unity Build
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
jobs:
# ============================================
# 测试任务
# ============================================
test:
name: Run Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
lfs: true
- uses: actions/cache@v3
with:
path: Library
key: Library-test-${{ hashFiles('Assets/**', 'Packages/**', 'ProjectSettings/**') }}
restore-keys: Library-test-
# 运行 Edit Mode 和 Play Mode 测试
- uses: game-ci/unity-test-runner@v4
env:
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
with:
projectPath: .
testMode: all
artifactsPath: test-results
githubToken: ${{ secrets.GITHUB_TOKEN }}
# 上传测试结果
- uses: actions/upload-artifact@v3
if: always()
with:
name: Test Results
path: test-results
# ============================================
# Android 构建任务
# ============================================
build-android:
name: Build Android
runs-on: ubuntu-latest
needs: test # 测试通过后才构建
steps:
- uses: actions/checkout@v4
with:
lfs: true
- uses: actions/cache@v3
with:
path: Library
key: Library-android-${{ hashFiles('Assets/**', 'Packages/**', 'ProjectSettings/**') }}
restore-keys: Library-android-
- uses: game-ci/unity-builder@v4
env:
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
with:
targetPlatform: Android
androidAppBundle: true
androidKeystoreName: user.keystore
androidKeystoreBase64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
androidKeystorePass: ${{ secrets.ANDROID_KEYSTORE_PASS }}
androidKeyaliasName: ${{ secrets.ANDROID_KEY_ALIAS }}
androidKeyaliasPass: ${{ secrets.ANDROID_KEY_ALIAS_PASS }}
- uses: actions/upload-artifact@v3
with:
name: Android Build
path: build/Android
# ============================================
# iOS 构建任务(需要 macOS runner)
# ============================================
build-ios:
name: Build iOS
runs-on: macos-latest
needs: test
steps:
- uses: actions/checkout@v4
with:
lfs: true
- uses: actions/cache@v3
with:
path: Library
key: Library-ios-${{ hashFiles('Assets/**', 'Packages/**', 'ProjectSettings/**') }}
restore-keys: Library-ios-
- uses: game-ci/unity-builder@v4
env:
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
with:
targetPlatform: iOS
- uses: actions/upload-artifact@v3
with:
name: iOS Build
path: build/iOS
27.10.2 设置 GitHub Secrets
在 GitHub 仓库的 Settings → Secrets → Actions 中添加:
必须的 Secrets:
├── UNITY_LICENSE // Unity 许可证文件内容
├── ANDROID_KEYSTORE_BASE64 // Keystore 文件的 Base64 编码
├── ANDROID_KEYSTORE_PASS // Keystore 密码
├── ANDROID_KEY_ALIAS // Key 别名
└── ANDROID_KEY_ALIAS_PASS // Key 密码
获取 Unity License:
# 使用 GameCI 的 activation 工作流获取许可证
# 详见: https://game.ci/docs/github/activation
27.11 与前端架构的对比
27.11.1 架构模式映射
前端概念 Unity 对应
────── ──────────
组件 (React Component) → MonoBehaviour (GameObject 上的组件)
Props → [SerializeField] 字段
State (useState) → private 字段 + 属性
Context / Provider → ServiceLocator / ScriptableObject
Redux Store → ScriptableObject 数据 + 事件
Redux Action / Dispatch → EventChannel.Raise() / ServerRpc
useEffect / Lifecycle → Awake/Start/OnEnable/OnDestroy
React.lazy / Suspense → Addressables 异步加载
CSS Modules → Material / Shader
React Router → SceneManager
npm packages → Unity Package Manager
Webpack/Vite → Asset Import Pipeline
ESLint → Roslyn Analyzers
Jest → Unity Test Framework (NUnit)
Storybook → Prefab Preview / Custom Editor
Monorepo → Assembly Definitions
GitHub Actions → GameCI + GitHub Actions
27.11.2 思维转换要点
1. 面向对象 vs 组合
前端: 函数式组合 (hooks + HOC)
Unity: 组件组合 (多个 MonoBehaviour 附加到 GameObject)
2. 渲染循环
前端: 虚拟 DOM diff + 按需渲染
Unity: 每帧全部重绘 (Update 每帧调用)
3. 状态管理
前端: 不可变状态 + 单向数据流
Unity: 可变状态 + 事件通知 (或不可变 ScriptableObject 数据)
4. 异步处理
前端: Promise / async-await
Unity: Coroutine / async-await / UniTask
5. 性能优化关注点
前端: 减少渲染、减少包体、代码分割
Unity: 减少 Draw Call、GC 优化、对象池、LOD
练习题
练习1:搭建项目框架(难度:简单)
按照 27.1 节推荐的文件夹结构,为你的开放世界游戏项目创建完整的文件夹结构。创建必要的 Assembly Definition 文件并验证编译正确。
练习2:实现事件系统(难度:中等)
使用 EventChannel 实现以下功能:
- 玩家拾取金币 → 更新 UI 金币数量 + 播放音效 + 记录统计
- 玩家死亡 → 显示 Game Over UI + 停止背景音乐 + 保存数据
- 确保所有系统之间完全解耦(不直接引用彼此)
练习3:状态机实践(难度:中等)
为你的游戏实现两个状态机:
- 角色状态机(Idle、Run、Jump、Attack、Die)
- 游戏流程状态机(MainMenu、Loading、Playing、Paused、GameOver) 编写 Edit Mode 单元测试验证状态切换逻辑。
练习4:完整架构实践(难度:高级)
综合运用本章所学,搭建一个包含以下内容的完整项目框架:
- ServiceLocator + ScriptableObject Events
- GameManager 管理游戏流程
- 对象池管理子弹/特效
- Addressables 管理场景和预制体
- 完整的 Edit Mode 测试覆盖核心逻辑
- GitHub Actions CI/CD 配置
练习5:代码审查(难度:中等)
审查你项目中已有的代码,识别可以改进的地方:
- 是否有硬编码的数据可以改用 ScriptableObject?
- 是否有直接引用可以改用事件通道解耦?
- 是否有频繁 Instantiate/Destroy 可以改用对象池?
- 代码是否遵循命名规范?
下一章预告
恭喜你完成了项目架构与设计模式的学习!到这里,你已经掌握了从零开始构建一个专业级 Unity 项目所需的所有核心知识。
在后续的章节中,我们将把所有学到的知识综合运用,开始实际构建我们的 3D 开放世界移动游戏:
- 设计游戏的核心玩法循环
- 构建开放世界地图
- 实现任务系统
- 添加 NPC 和对话系统
- 集成所有系统形成完整游戏
作为一个有前端/全栈经验的开发者,你现在拥有了将 Web 开发的工程化思维与 Unity 游戏开发结合的能力。这是一个独特的优势——很多传统游戏开发者缺乏现代软件工程的经验,而你已经具备了这些。让我们继续前进,将愿景变为现实!