Unity 游戏启动流程(Boot脚本)

功能概述:

  • 初始化游戏运行环境(帧率、后台运行等)
  • 初始化核心系统(事件系统、资源系统)
  • 加载并显示补丁更新界面
  • 启动补丁更新流程(异步)
  • 更新完成后切换到主场景
public class Boot : MonoBehaviour
{
    /// <summary>
    /// 资源系统运行模式
    /// </summary>
    public EPlayMode PlayMode = EPlayMode.EditorSimulateMode;

    void Awake()
    {
        Debug.Log($"资源系统运行模式:{PlayMode}");
        Application.targetFrameRate = 60;
        Application.runInBackground = true;
        DontDestroyOnLoad(this.gameObject);
    }
    IEnumerator Start()
    {
        // 游戏管理器
        GameManager.Instance.Behaviour = this;

        // 初始化事件系统
        UniEvent.Initalize();

        // 初始化资源系统
        YooAssets.Initialize();

        // 加载补丁更新页面
        var go = Resources.Load<GameObject>("PatchWindow");
        GameObject.Instantiate(go);

        // 开始补丁更新流程
        var operation = new PatchOperation("DefaultPackage", PlayMode);
        YooAssets.StartOperation(operation);
        yield return operation;

        // 设置默认的资源包
        var gamePackage = YooAssets.GetPackage("DefaultPackage");
        YooAssets.SetDefaultPackage(gamePackage);

        // 切换到主页面场景
        SceneEventDefine.ChangeToHomeScene.SendEventMessage();
    }
}

        如果你希望支持纯离线模式(比如单机游戏不更新),通常会在 Boot.Start() 中判断 PlayMode跳过 PatchOperation 直接进入游戏。

补丁更新操作(异步状态机)PatchOperation

使用 UniFramework.Machine.StateMachine 构建一个状态机,包含多个状态节点(FsmXXX):

状态节点 功能
FsmInitializePackage 初始化资源包
FsmRequestPackageVersion 请求远程版本信息
FsmUpdatePackageManifest 更新清单文件
FsmCreateDownloader 创建下载器
FsmDownloadPackageFiles 下载资源文件
FsmDownloadPackageOver 下载完成处理
FsmClearCacheBundle 清理缓存
FsmStartGame 准备进入游戏
using UnityEngine;
using UniFramework.Machine;
using UniFramework.Event;
using YooAsset;

public class PatchOperation : GameAsyncOperation
{
    private enum ESteps
    {
        None,
        Update,
        Done,
    }

    private readonly EventGroup _eventGroup = new EventGroup();
    private readonly StateMachine _machine;
    private readonly string _packageName;
    private ESteps _steps = ESteps.None;

    public PatchOperation(string packageName, EPlayMode playMode)
    {
        _packageName = packageName;

        // 注册监听事件
        _eventGroup.AddListener<UserEventDefine.UserTryInitialize>(OnHandleEventMessage);
        _eventGroup.AddListener<UserEventDefine.UserBeginDownloadWebFiles>(OnHandleEventMessage);
        _eventGroup.AddListener<UserEventDefine.UserTryRequestPackageVersion>(OnHandleEventMessage);
        _eventGroup.AddListener<UserEventDefine.UserTryUpdatePackageManifest>(OnHandleEventMessage);
        _eventGroup.AddListener<UserEventDefine.UserTryDownloadWebFiles>(OnHandleEventMessage);

        // 创建状态机
        _machine = new StateMachine(this);
        _machine.AddNode<FsmInitializePackage>();
        _machine.AddNode<FsmRequestPackageVersion>();
        _machine.AddNode<FsmUpdatePackageManifest>();
        _machine.AddNode<FsmCreateDownloader>();
        _machine.AddNode<FsmDownloadPackageFiles>();
        _machine.AddNode<FsmDownloadPackageOver>();
        _machine.AddNode<FsmClearCacheBundle>();
        _machine.AddNode<FsmStartGame>();

        _machine.SetBlackboardValue("PackageName", packageName);
        _machine.SetBlackboardValue("PlayMode", playMode);
    }
    protected override void OnStart()
    {
        _steps = ESteps.Update;
        _machine.Run<FsmInitializePackage>();
    }
    protected override void OnUpdate()
    {
        if (_steps == ESteps.None || _steps == ESteps.Done)
            return;

        if (_steps == ESteps.Update)
        {
            _machine.Update();
        }
    }
    protected override void OnAbort()
    {
    }

    public void SetFinish()
    {
        _steps = ESteps.Done;
        _eventGroup.RemoveAllListener();
        Status = EOperationStatus.Succeed;
        Debug.Log($"Package {_packageName} patch done !");
    }

    /// <summary>
    /// 接收事件
    /// </summary>
    private void OnHandleEventMessage(IEventMessage message)
    {
        if (message is UserEventDefine.UserTryInitialize)
        {
            _machine.ChangeState<FsmInitializePackage>();
        }
        else if (message is UserEventDefine.UserBeginDownloadWebFiles)
        {
            _machine.ChangeState<FsmDownloadPackageFiles>();
        }
        else if (message is UserEventDefine.UserTryRequestPackageVersion)
        {
            _machine.ChangeState<FsmRequestPackageVersion>();
        }
        else if (message is UserEventDefine.UserTryUpdatePackageManifest)
        {
            _machine.ChangeState<FsmUpdatePackageManifest>();
        }
        else if (message is UserEventDefine.UserTryDownloadWebFiles)
        {
            _machine.ChangeState<FsmCreateDownloader>();
        }
        else
        {
            throw new System.NotImplementedException($"{message.GetType()}");
        }
    }
}

FsmInitializePackage(有限状态机(FSM)的流程控制架构

        这段代码是 热更新/资源管理系统中的一个状态节点(State Node),它实现了 IStateNode 接口,属于一个 基于有限状态机(FSM)的流程控制架构。其核心作用是:

该状态节点负责:

  1. 从状态机的“黑板”(Blackboard)中读取:
    • 资源包名称(PackageName
    • 当前运行模式(PlayMode:编辑器模拟、离线、联机、WebGL 等)
  2. 使用 YooAsset(一个 Unity 资源管理框架)创建并初始化对应的资源包(Package)。
  3. 根据不同平台和运行模式,配置不同的文件系统参数(如本地内置资源、缓存、远程服务器等)。
  4. 异步执行初始化操作(InitializeAsync)。
  5. 初始化成功后,切换状态机到下一个状态FsmRequestPackageVersion(请求资源包版本)。
  6. 若失败,则发送失败事件(InitializeFailed),通常用于弹出错误提示。
using System;
using System.IO;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UniFramework.Machine;
using YooAsset;

internal class FsmInitializePackage : IStateNode
{
    private StateMachine _machine;

    void IStateNode.OnCreate(StateMachine machine)
    {
        _machine = machine;
    }
    void IStateNode.OnEnter()
    {
        PatchEventDefine.PatchStepsChange.SendEventMessage("初始化资源包!");
        GameManager.Instance.StartCoroutine(InitPackage());
    }
    void IStateNode.OnUpdate()
    {
    }
    void IStateNode.OnExit()
    {
    }

    private IEnumerator InitPackage()
    {
        var playMode = (EPlayMode)_machine.GetBlackboardValue("PlayMode");
        var packageName = (string)_machine.GetBlackboardValue("PackageName");

        // 创建资源包裹类
        var package = YooAssets.TryGetPackage(packageName);
        if (package == null)
            package = YooAssets.CreatePackage(packageName);

        // 编辑器下的模拟模式
        InitializationOperation initializationOperation = null;
        if (playMode == EPlayMode.EditorSimulateMode)
        {
            var buildResult = EditorSimulateModeHelper.SimulateBuild(packageName);
            var packageRoot = buildResult.PackageRootDirectory;
            var createParameters = new EditorSimulateModeParameters();
            createParameters.EditorFileSystemParameters = FileSystemParameters.CreateDefaultEditorFileSystemParameters(packageRoot);
            initializationOperation = package.InitializeAsync(createParameters);
        }

        // 单机运行模式
        if (playMode == EPlayMode.OfflinePlayMode)
        {
            var createParameters = new OfflinePlayModeParameters();
            createParameters.BuildinFileSystemParameters = FileSystemParameters.CreateDefaultBuildinFileSystemParameters();
            initializationOperation = package.InitializeAsync(createParameters);
        }

        // 联机运行模式
        if (playMode == EPlayMode.HostPlayMode)
        {
            string defaultHostServer = GetHostServerURL();
            string fallbackHostServer = GetHostServerURL();
            IRemoteServices remoteServices = new RemoteServices(defaultHostServer, fallbackHostServer);
            var createParameters = new HostPlayModeParameters();
            createParameters.BuildinFileSystemParameters = FileSystemParameters.CreateDefaultBuildinFileSystemParameters();
            createParameters.CacheFileSystemParameters = FileSystemParameters.CreateDefaultCacheFileSystemParameters(remoteServices);
            initializationOperation = package.InitializeAsync(createParameters);
        }

        // WebGL运行模式
        if (playMode == EPlayMode.WebPlayMode)
        {
#if UNITY_WEBGL && WEIXINMINIGAME && !UNITY_EDITOR
            var createParameters = new WebPlayModeParameters();
			string defaultHostServer = GetHostServerURL();
            string fallbackHostServer = GetHostServerURL();
            string packageRoot = $"{WeChatWASM.WX.env.USER_DATA_PATH}/__GAME_FILE_CACHE"; //注意:如果有子目录,请修改此处!
            IRemoteServices remoteServices = new RemoteServices(defaultHostServer, fallbackHostServer);
            createParameters.WebServerFileSystemParameters = WechatFileSystemCreater.CreateFileSystemParameters(packageRoot, remoteServices);
            initializationOperation = package.InitializeAsync(createParameters);
#else
            var createParameters = new WebPlayModeParameters();
            createParameters.WebServerFileSystemParameters = FileSystemParameters.CreateDefaultWebServerFileSystemParameters();
            initializationOperation = package.InitializeAsync(createParameters);
#endif
        }

        yield return initializationOperation;

        // 如果初始化失败弹出提示界面
        if (initializationOperation.Status != EOperationStatus.Succeed)
        {
            Debug.LogWarning($"{initializationOperation.Error}");
            PatchEventDefine.InitializeFailed.SendEventMessage();
        }
        else
        {
            _machine.ChangeState<FsmRequestPackageVersion>();
        }
    }

    /// <summary>
    /// 获取资源服务器地址
    /// </summary>
    private string GetHostServerURL()
    {
        //string hostServerIP = "http://10.0.2.2"; //安卓模拟器地址
        string hostServerIP = "http://127.0.0.1";
        string appVersion = "v1.0";

#if UNITY_EDITOR
        if (UnityEditor.EditorUserBuildSettings.activeBuildTarget == UnityEditor.BuildTarget.Android)
            return $"{hostServerIP}/CDN/Android/{appVersion}";
        else if (UnityEditor.EditorUserBuildSettings.activeBuildTarget == UnityEditor.BuildTarget.iOS)
            return $"{hostServerIP}/CDN/IPhone/{appVersion}";
        else if (UnityEditor.EditorUserBuildSettings.activeBuildTarget == UnityEditor.BuildTarget.WebGL)
            return $"{hostServerIP}/CDN/WebGL/{appVersion}";
        else
            return $"{hostServerIP}/CDN/PC/{appVersion}";
#else
        if (Application.platform == RuntimePlatform.Android)
            return $"{hostServerIP}/CDN/Android/{appVersion}";
        else if (Application.platform == RuntimePlatform.IPhonePlayer)
            return $"{hostServerIP}/CDN/IPhone/{appVersion}";
        else if (Application.platform == RuntimePlatform.WebGLPlayer)
            return $"{hostServerIP}/CDN/WebGL/{appVersion}";
        else
            return $"{hostServerIP}/CDN/PC/{appVersion}";
#endif
    }

    /// <summary>
    /// 远端资源地址查询服务类
    /// </summary>
    private class RemoteServices : IRemoteServices
    {
        private readonly string _defaultHostServer;
        private readonly string _fallbackHostServer;

        public RemoteServices(string defaultHostServer, string fallbackHostServer)
        {
            _defaultHostServer = defaultHostServer;
            _fallbackHostServer = fallbackHostServer;
        }
        string IRemoteServices.GetRemoteMainURL(string fileName)
        {
            return $"{_defaultHostServer}/{fileName}";
        }
        string IRemoteServices.GetRemoteFallbackURL(string fileName)
        {
            return $"{_fallbackHostServer}/{fileName}";
        }
    }
}

流程机制详解

1. 状态机集成(FSM Integration)

  • 实现了 IStateNode 接口(来自 UniFramework.Machine),这是 UniFramework 提供的状态机节点标准。
  • 在 OnEnter() 中启动协程 InitPackage(),这是状态进入时的主逻辑。
  • 成功后调用 _machine.ChangeState<FsmRequestPackageVersion>(),实现状态转移,这是 FSM 的典型行为。

这里详细说明下IStateNode:

public interface IStateNode
{
    void OnCreate(StateMachine machine);  // 状态被创建时调用(绑定状态机)
    void OnEnter();                       // 进入该状态时调用(一次)
    void OnUpdate();                      // 每帧调用(可选,用于持续逻辑)
    void OnExit();                        // 离开该状态时调用(一次)
}
方法 调用时机 典型用途 特性
OnCreate 状态首次被 StateMachine.AddNode<T>() 注册后创建实例时 缓存 StateMachine 引用、初始化内部变量 只调用一次,用于“注入上下文”
OnEnter 状态机切换到该状态时 启动逻辑(如协程、加载、发送事件) 每次进入都调用,是状态的“入口”
OnUpdate 每帧(或每 tick)状态机更新时 持续检测条件、处理输入、计时等 可选实现,若无持续逻辑可留空
OnExit 状态机即将切换到其他状态前 清理资源、取消协程、保存状态 每次离开都调用,是状态的“出口”

在你的 FsmInitializePackage 中:

  • OnCreate:保存 _machine 引用
  • OnEnter:启动 InitPackage() 协程(核心逻辑)
  • OnUpdate:留空(因为初始化是异步一次性操作)
  • OnExit:留空(无需要清理的内容)

2. 协程 InitPackage():按模式初始化

1:获取上下文参数
var playMode = (EPlayMode)_machine.GetBlackboardValue("PlayMode");
var packageName = (string)_machine.GetBlackboardValue("PackageName");
  • 从状态机的“黑板(Blackboard)”中读取启动时传入的配置

步骤 2:获取或创建 Package
var package = YooAssets.TryGetPackage(packageName);
if (package == null)
    package = YooAssets.CreatePackage(packageName);
  • 确保目标资源包存在

步骤 3:根据 playMode 分支处理
模式 行为 是否联网 用途
EditorSimulateMode 调用 EditorSimulateModeHelper.SimulateBuild(),从本地模拟构建目录加载 ❌ 否 编辑器调试,模拟热更新流程
OfflinePlayMode 使用内置资源(StreamingAssets ❌ 否 完全离线单机游戏
HostPlayMode 从远程服务器(CDN)下载资源,使用缓存系统 ✅ 是 标准在线热更新
WebPlayMode 针对 WebGL(或微信小游戏)的特殊文件系统 ✅ 是(WebGL) / 特殊存储(小游戏) Web 或小程序平台

补充:联机模式(HostPlayMode)详解
string defaultHostServer = GetHostServerURL();
IRemoteServices remoteServices = new RemoteServices(defaultHostServer, fallbackHostServer);
var createParameters = new HostPlayModeParameters();
createParameters.BuildinFileSystemParameters = ...; // 内置资源(如初始资源)
createParameters.CacheFileSystemParameters = ...;  // 远程资源缓存到本地
initializationOperation = package.InitializeAsync(createParameters);
  • BuildinFileSystem:读取 StreamingAssets 中的初始资源(随包发布)
  • CacheFileSystem:将从服务器下载的更新资源缓存到 Application.persistentDataPath
  • IRemoteServices:提供资源文件的 URL 拼接逻辑(主地址 + 备用地址)

🔥 这是 远程热更新的核心:初始化阶段就建立了“本地缓存 + 远程下载”的混合文件系统。


3. GetHostServerURL():动态构建 CDN 地址

string hostServerIP = "http://127.0.0.1"; // ← 本地测试服务器
string appVersion = "v1.0";
return $"{hostServerIP}/CDN/Android/{appVersion}";
  • 根据 构建平台(Android/iOS/WebGL/PC) 返回对应 CDN 路径
  • 当前配置为 127.0.0.1,说明是 本地测试(需运行本地 HTTP 服务器)
  • 正式上线时应改为真实 CDN 地址,如 https://cdn.yourgame.com

⚠️ 注意:这段代码硬编码了 IP 和版本号,实际项目中通常从配置文件或远程配置接口获取。


4. 初始化结果处理

yield return initializationOperation;

if (initializationOperation.Status != EOperationStatus.Succeed)
{
    PatchEventDefine.InitializeFailed.SendEventMessage();
}
else
{
    _machine.ChangeState<FsmRequestPackageVersion>(); // 进入下一步
}
  • 等待异步初始化完成
  • 成功则进入 请求远程版本号 阶段(热更新流程继续)
  • 失败则通知 UI(用户可能看到“初始化失败,请检查网络”)

RemoteServices 

RemoteServices 类的具体作用:

1. 存储两个服务器地址

private readonly string _defaultHostServer;   // 主 CDN 地址
private readonly string _fallbackHostServer;  // 备用 CDN 地址

2. 实现两个关键方法

方法 作用
GetRemoteMainURL(fileName) 返回主服务器上的资源完整 URL
GetRemoteFallbackURL(fileName) 返回备用服务器上的资源完整 URL

// 内部实现:真正负责拼接远程地址的服务类
private class RemoteServices : IRemoteServices
{
    // 默认主站地址(CDN 主节点)
    private readonly string _defaultHostServer;
    // 备用站地址(CDN 备节点或灾备域名)
    private readonly string _fallbackHostServer;

    /// <summary>
    /// 构造远程服务实例
    /// </summary>
    /// <param name="defaultHostServer">主站基准地址,例如 "https://cdn.main.example.com"</param>
    /// <param name="fallbackHostServer">备用站基准地址,例如 "https://cdn.backup.example.com"</param>
    public RemoteServices(string defaultHostServer, string fallbackHostServer)
    {
        _defaultHostServer = defaultHostServer;
        _fallbackHostServer = fallbackHostServer;
    }

    // 显式接口实现:返回主站完整文件链接
    string IRemoteServices.GetRemoteMainURL(string fileName)
    {
        return $"{_defaultHostServer}/{fileName}";
    }

    // 显式接口实现:返回备用站完整文件链接
    string IRemoteServices.GetRemoteFallbackURL(string fileName)
    {
        return $"{_fallbackHostServer}/{fileName}";
    }
}

IRemoteServices接口:

namespace YooAsset
{
    public interface IRemoteServices
    {
        /// <summary>
        /// 获取主资源站的资源地址
        /// </summary>
        /// <param name="fileName">请求的文件名称</param>
        string GetRemoteMainURL(string fileName);

        /// <summary>
        /// 获取备用资源站的资源地址
        /// </summary>
        /// <param name="fileName">请求的文件名称</param>
        string GetRemoteFallbackURL(string fileName);
    }
}

补充说明:补丁更新操作中   

状态机和有限状态机

  • StateMachine 是一个自定义类,用于管理一组状态(节点)。
  • 每个状态(如 FsmInitializePackage)是一个具体的类,代表 FSM 中的一个状态。

2. 你代码中的 _machine 是什么

_machine = new StateMachine(this);
_machine.AddNode<FsmInitializePackage>();
// ... 添加多个状态节点

从命名和用法来看:

  • StateMachine 是一个自定义类,用于管理一组状态(节点)。
  • 每个状态(如 FsmInitializePackage)是一个具体的类,代表 FSM 中的一个状态。
  • 状态之间通过某种机制(可能在每个状态内部调用 _machine.TransitionTo<NextState>())进行转移。

并且从代码结构上看:

判断依据:

特征 是否满足 说明
有限个状态 只添加了 8 个明确的状态类
明确的状态转移 ✅(隐含) 虽然没看到转移代码,但每个状态类内部通常会决定下一步跳转
同一时间只处在一个状态 ✅(通常如此) 标准 FSM 行为
状态行为封装 每个 FsmXXX 类封装了该状态的逻辑

因此,它的结构就是 FSM,只是用类和泛型做了封装,使其更模块化、可维护。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐