路线切换:C# 基础路线完成后,进入 Unity 工程实践。
本章目标:建立一个长期可维护的 Unity 工程骨架,而不是一次性 Demo 脚本堆叠。
学习目标
完成本章后,你应该能做到:
- 按职责拆分 Unity 项目目录,避免后期“脚本垃圾场”。
- 建立统一启动入口(Bootstrap)和运行时服务容器。
- 设计可测试、可替换、可迁移到 WebGL 的更新循环。
- 为后续战斗、UI、资源、输入系统留好扩展点。
为什么第一章不先做玩法
新手常见路径是先把角色动起来,再逐步堆功能。短期很快,但两个月后常见问题:
- 场景脚本互相引用,改一个地方会崩一片。
- 管理器过多且初始化顺序混乱。
- PC 跑得动,WebGL 构建后卡顿和加载问题暴露。
- 任何模块都无法独立测试。
这章先把“盘子”搭稳,后续章节迭代成本会明显下降。
目录规范(首版)
建议从一开始就统一:
Assets/
Game/
Runtime/
Bootstrap/
Core/
Gameplay/
UI/
Services/
Editor/
Scenes/
Boot.unity
Main.unity
Prefabs/
Art/
Audio/
Plugins/
StreamingAssets/
规则:
Runtime只放运行时代码,禁止编辑器代码混入。Boot.unity只做启动和装配,不放玩法对象。Main.unity才承载可玩内容。
启动流程设计
统一启动链路:
- 启动进入
Boot.unity GameBootstrap初始化核心服务- 加载
Main.unity - 进入主循环
GameLoop
GameBootstrap
public sealed class GameBootstrap : MonoBehaviour
{
private ServiceRegistry _services;
private GameLoop _loop;
private void Awake()
{
DontDestroyOnLoad(gameObject);
_services = new ServiceRegistry();
RegisterCoreServices(_services);
_loop = new GameLoop(_services);
_loop.Initialize();
FoundationLog.Info("Bootstrap", "Core services initialized");
StartCoroutine(LoadMainScene());
}
private IEnumerator LoadMainScene()
{
var op = SceneManager.LoadSceneAsync("Main");
while (!op.isDone)
{
yield return null;
}
_loop.Start();
FoundationLog.Info("Bootstrap", "Main scene loaded and loop started");
}
private void Update()
{
if (_loop != null)
{
_loop.Tick(Time.deltaTime, Time.unscaledDeltaTime);
}
}
private void OnDestroy()
{
if (_loop != null)
{
_loop.Dispose();
_loop = null;
}
if (_services != null)
{
_services.Dispose();
_services = null;
}
}
private static void RegisterCoreServices(ServiceRegistry services)
{
services.Register<IGameTime>(new UnityGameTime());
services.Register<IEventBus>(new UnityEventBusAdapter());
services.Register<IInputService>(new UnityInputService());
services.Register<IResourceService>(new UnityResourceService());
}
}
服务注册与解耦
ServiceRegistry
public sealed class ServiceRegistry : IDisposable
{
private readonly Dictionary<Type, object> _map = new Dictionary<Type, object>(32);
public void Register<T>(T instance) where T : class
{
var key = typeof(T);
if (_map.ContainsKey(key))
{
throw new InvalidOperationException("Service already registered: " + key.Name);
}
_map.Add(key, instance);
}
public T Resolve<T>() where T : class
{
object value;
if (!_map.TryGetValue(typeof(T), out value))
{
throw new InvalidOperationException("Service not found: " + typeof(T).Name);
}
return (T)value;
}
public void Dispose()
{
foreach (var kv in _map)
{
var disposable = kv.Value as IDisposable;
if (disposable != null)
{
disposable.Dispose();
}
}
_map.Clear();
}
}
关键点:
- 明确注册时机:只在 Bootstrap 注册。
- 明确生命周期:退出时统一
Dispose。 - 明确依赖方向:Gameplay 只依赖接口,不依赖具体实现。
运行循环(替代到处写 Update)
public interface IUpdatable
{
int Order { get; }
void Tick(float dt, float unscaledDt);
}
public sealed class GameLoop : IDisposable
{
private readonly ServiceRegistry _services;
private readonly List<IUpdatable> _updates = new List<IUpdatable>(32);
private bool _started;
public GameLoop(ServiceRegistry services)
{
_services = services;
}
public void Initialize()
{
_updates.Clear();
_updates.Add(new InputSystem(_services));
_updates.Add(new GameplaySystem(_services));
_updates.Add(new UISystem(_services));
_updates.Sort(delegate(IUpdatable a, IUpdatable b)
{
return a.Order.CompareTo(b.Order);
});
}
public void Start()
{
_started = true;
}
public void Tick(float dt, float unscaledDt)
{
if (!_started)
{
return;
}
for (var i = 0; i < _updates.Count; i++)
{
_updates[i].Tick(dt, unscaledDt);
}
}
public void Dispose()
{
_updates.Clear();
}
}
好处:
- 更新顺序可控。
- 单模块可替换可下线。
- 后续加“暂停系统/回放系统”只需插入一层。
WebGL 迁移前置约束
从第一章就按 WebGL 要求编码:
- 避免线程依赖(WebGL 主线程限制)。
- 避免大量反射和动态加载。
- 资源加载统一走
IResourceService,为后续 Addressables/WebGL 加载策略留口。 - 输入层隔离,未来可接移动端触控而不改 Gameplay。
场景装配实践
Boot.unity 最小对象:
BootstrapRoot(挂GameBootstrap)UICamera(可选)
Main.unity 最小对象:
WorldRootGameplayRootUIRoot
原则:场景只放数据与引用关系,业务代码不绑定具体场景层级字符串。
与 C# Foundation 的衔接
前面 C# 路线沉淀的 foundation/Runtime 可直接接入:
- 日志:
FoundationLog - 断言:
FoundationAssert - 事件:
EventBus - 调度:
Scheduler - 对象池:
ObjectPool - 命令与回放:
Commands
Unity 层只做适配器,不重写核心逻辑。
验收清单
- 项目启动后稳定进入
Main.unity,无初始化顺序异常。 - 所有核心系统通过
ServiceRegistry注入,不出现FindObjectOfType依赖。 - 主循环内模块执行顺序可配置、可追踪。
- 切场景与退出流程中资源可正常释放,无幽灵回调。
常见坑
坑 1:Bootstrap 做成“万能 God Object”
它只负责装配,不应该包含具体玩法逻辑。
坑 2:服务到处注册
注册入口分散会导致生命周期不可控。必须集中在 Bootstrap。
坑 3:Gameplay 直接依赖 Unity 静态 API
会让逻辑难测难迁移。应通过接口适配访问引擎能力。
本月作业
完成一版“可运行空壳工程”:
- 有 Boot/Main 双场景与统一启动流程。
- 有 3 个可插拔系统(输入、玩法、UI)并按顺序更新。
- 接入
FoundationLog,启动到进入主场景全链路有日志。
下一章开始做 Unity 玩法第一步:角色移动与摄像机跟随,并保持可迁移到 WebGL 的输入架构。