5.YooAsset战机示例工程源码解析

  1. 5.YooAsset太空战机示例工程源码解析
    1. 5.1 知识点
      1. 流程概述
      2. Boot 入口引导脚本
        1. 资源系统运行模式
        2. Awake 方法中的初始化
        3. Start 协程中的初始化流程
          1. 初始化 GameManager 与事件系统
          2. 初始化 YooAsset 资源系统
          3. 加载补丁更新 UI
          4. 开始补丁更新流程
          5. 设置默认资源包
          6. 切换到主页面场景
      3. PatchOperation 补丁操作脚本
        1. 类定义及成员变量
        2. 构造函数初始化
        3. OnStart 方法
        4. OnUpdate 方法
        5. OnAbort 方法
        6. SetFinish 方法
        7. OnHandleEventMessage 方法
      4. FsmInitializePackage 初始化资源包状态节点
        1. 状态节点生命周期
        2. 获取服务器地址及远程服务实现
          1. GetHostServerURL 方法
          2. RemoteServices 内部类
        3. InitPackage 协程解析
      5. FsmRequestPackageVersion 请求资源包版本状态节点
        1. 状态节点生命周期
        2. UpdatePackageVersion 协程解析
      6. FsmUpdatePackageManifest 更新资源清单状态节点
        1. 状态节点生命周期
        2. UpdateManifest 协程解析
      7. FsmCreateDownloader 创建资源下载器状态节点
        1. 状态节点生命周期
        2. CreateDownloader 方法解析
      8. FsmDownloadPackageFiles 资源文件下载状态节点
        1. 状态节点生命周期
        2. BeginDownload 协程解析
      9. FsmDownloadPackageOver 资源下载完成状态节点
        1. 类定义及设计特性
        2. 状态节点生命周期解析
      10. FsmClearCacheBundle 清理缓存文件状态节点
        1. 状态节点生命周期
        2. Operation_Completed 回调解析
      11. FsmStartGame 游戏启动状态节点解析
        1. 状态节点生命周期
    2. 5.2 知识点代码
      1. Boot.cs
      2. GameManager.cs
      3. PatchOperation.cs
      4. FsmInitializePackage.cs
      5. FsmRequestPackageVersion.cs
      6. FsmUpdatePackageManifest.cs
      7. FsmCreateDownloader.cs
      8. FsmDownloadPackageFiles.cs
      9. FsmDownloadPackageOver.cs
      10. FsmClearCacheBundle.cs
      11. FsmStartGame.cs

5.YooAsset太空战机示例工程源码解析


5.1 知识点

流程概述

graph TD
    启动引导 --> 初始化YooAsset
    初始化YooAsset --> 加载补丁窗口UI
    加载补丁窗口UI --> 启动补丁操作

    启动补丁操作 --> 初始化资源包
    初始化资源包 -->|成功| 请求资源包版本
    初始化资源包 -->|失败| 初始化失败事件
    请求资源包版本 -->|成功| 更新资源清单
    请求资源包版本 -->|失败| 版本请求失败事件
    更新资源清单 -->|成功| 创建下载器
    更新资源清单 -->|失败| 清单更新失败事件
    创建下载器 -->|无更新| 开始游戏
    创建下载器 -->|发现更新| 下载资源文件
    下载资源文件 -->|成功| 资源下载完成
    下载资源文件 -->|失败| 下载失败事件
    资源下载完成 --> 清理缓存文件
    清理缓存文件 --> 开始游戏
    开始游戏 --> 设置默认资源包
    设置默认资源包 --> 加载主页面场景
    加载主页面场景 --> 结束

    classDef state fill:#f9f,stroke:#333,stroke-width:2px
    classDef event fill:#e0f0ff,stroke:#333,stroke-width:2px

    class 启动引导,初始化YooAsset,加载补丁窗口UI,启动补丁操作,初始化资源包,请求资源包版本,更新资源清单,创建下载器,下载资源文件,资源下载完成,清理缓存文件,开始游戏,设置默认资源包,加载主页面场景,结束 state
    class 初始化失败事件,版本请求失败事件,清单更新失败事件,下载失败事件 event
  1. 启动引导:游戏启动时的入口点,执行基础的初始化操作。
  2. 初始化 YooAsset:对 YooAsset 资源管理系统进行初始化。
  3. 加载补丁窗口 UI:加载用于显示补丁更新进度的用户界面。
  4. 启动补丁操作:开始整个补丁更新流程。
  5. 初始化资源包:根据不同的运行模式(如编辑器模拟、单机、联机等)初始化资源包。如果初始化成功,进入请求资源包版本阶段;如果失败,触发初始化失败事件。
  6. 请求资源包版本:向服务器请求当前资源包的版本信息。若请求成功,进入更新资源清单阶段;若失败,触发版本请求失败事件。
  7. 更新资源清单:根据获取到的版本信息,更新本地的资源清单。成功后进入创建下载器阶段;失败则触发清单更新失败事件。
  8. 创建下载器:检查是否有需要更新的资源文件。如果没有更新,直接进入开始游戏阶段;如果发现有更新,进入下载资源文件阶段。
  9. 下载资源文件:执行资源文件的下载操作。下载成功后进入资源下载完成阶段;下载失败则触发下载失败事件。
  10. 资源下载完成:资源文件下载完成后,进入清理缓存文件阶段。
  11. 清理缓存文件:清理不再使用的缓存文件,完成后进入开始游戏阶段。
  12. 开始游戏:完成所有更新操作后,设置默认资源包,加载主页面场景,最终游戏启动。

Boot 入口引导脚本

这是直接挂载到 Scene 场景的脚本,也是游戏的入口脚本。下面我们详细解析这个脚本的各个部分,了解它做了什么,以及哪些部分依赖了 YooAsset。

资源系统运行模式

首先,脚本中提供了一个字段,用于设置资源系统的运行模式,并将其暴露到外部,以便开发者在编辑器中直接进行选择。

/// <summary>
/// 资源系统运行模式
/// </summary>
public EPlayMode PlayMode = EPlayMode.EditorSimulateMode;

Awake 方法中的初始化

Awake() 方法中,脚本设置了应用的帧率和后台运行属性,并确保该对象不会在场景切换时销毁。这保证了入口脚本在整个游戏生命周期内都能起作用。

void Awake()
{
    Debug.Log($"资源系统运行模式:{PlayMode}");
    Application.targetFrameRate = 60;
    Application.runInBackground = true;
    DontDestroyOnLoad(this.gameObject);
}

Start 协程中的初始化流程

Start() 方法采用协程方式来执行初始化操作,这样可以异步执行多个初始化任务,避免阻塞主线程。下面逐步解析其中的各个步骤:

初始化 GameManager 与事件系统

这里将当前脚本实例赋值给全局的 GameManager,便于后续管理协程等操作。同时,初始化了事件系统,准备处理后续场景切换等业务逻辑事件。

// 游戏管理器
GameManager.Instance.Behaviour = this;

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

下面是 GameManager 代码,用于注册监听场景切换事件,并通过事件响应调用 YooAsset 加载场景:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UniFramework.Event;
using YooAsset;

public class GameManager
{
    private static GameManager _instance;
    public static GameManager Instance
    {
        get
        {
            if (_instance == null)
                _instance = new GameManager();
            return _instance;
        }
    }

    private readonly EventGroup _eventGroup = new EventGroup();

    /// <summary>
    /// 协程启动器
    /// </summary>
    public MonoBehaviour Behaviour;

    private GameManager()
    {
        // 注册监听事件
        _eventGroup.AddListener<SceneEventDefine.ChangeToHomeScene>(OnHandleEventMessage);
        _eventGroup.AddListener<SceneEventDefine.ChangeToBattleScene>(OnHandleEventMessage);
    }

    /// <summary>
    /// 开启一个协程
    /// </summary>
    public void StartCoroutine(IEnumerator enumerator)
    {
        Behaviour.StartCoroutine(enumerator);
    }

    /// <summary>
    /// 接收事件
    /// </summary>
    private void OnHandleEventMessage(IEventMessage message)
    {
        if (message is SceneEventDefine.ChangeToHomeScene)
        {
            YooAssets.LoadSceneAsync("scene_home");
        }
        else if (message is SceneEventDefine.ChangeToBattleScene)
        {
            YooAssets.LoadSceneAsync("scene_battle");
        }
    }
}
初始化 YooAsset 资源系统

调用 YooAssets.Initialize() 初始化资源系统,这是使用 YooAsset 进行资源管理和热更新的基础步骤。

// 初始化资源系统
YooAssets.Initialize();
加载补丁更新 UI

加载更新页面的预制件并实例化,这步用于展示资源更新进度的 UI,不必过于关注内部逻辑。

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

开始补丁更新流程

使用 YooAsset 提供的 PatchOperation 类启动补丁更新流程。通过 yield return operation 等待更新流程完成。内部其实开启了一个状态机来管理各个更新步骤,是热更新机制的关键所在。

// 开始补丁更新流程
var operation = new PatchOperation("DefaultPackage", PlayMode);
YooAssets.StartOperation(operation);
yield return operation;
设置默认资源包

在补丁更新完成后,通过 YooAssets.GetPackage("DefaultPackage") 获取更新后的资源包,并设置为默认包。这样,后续调用的加载接口会从该资源包中获取资源。

// 设置默认的资源包
var gamePackage = YooAssets.GetPackage("DefaultPackage");
YooAssets.SetDefaultPackage(gamePackage);
切换到主页面场景

最后,通过事件发送机制触发切换到主页面场景。GameManager 中已经注册了对该事件的监听,收到事件后会调用 YooAsset 的异步加载场景接口来加载主页面场景。

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

相关的事件定义和监听代码如下:

public class ChangeToHomeScene : IEventMessage
{
    public static void SendEventMessage()
    {
        var msg = new ChangeToHomeScene();
        UniEvent.SendMessage(msg);
    }
}
private GameManager()
{
    // 注册监听事件
    _eventGroup.AddListener<SceneEventDefine.ChangeToHomeScene>(OnHandleEventMessage);
    _eventGroup.AddListener<SceneEventDefine.ChangeToBattleScene>(OnHandleEventMessage);
}
/// <summary>
/// 接收事件
/// </summary>
private void OnHandleEventMessage(IEventMessage message)
{
    if (message is SceneEventDefine.ChangeToHomeScene)
    {
        YooAssets.LoadSceneAsync("scene_home");
    }
    else if (message is SceneEventDefine.ChangeToBattleScene)
    {
        YooAssets.LoadSceneAsync("scene_battle");
    }
}

PatchOperation 补丁操作脚本

这是一个补丁操作类,继承自 GameAsyncOperation(YooAsset提供的抽象补丁操作类),用于实现异步补丁处理流程。该脚本主要通过内部状态机来管理补丁更新的各个步骤,并监听用户事件以控制状态切换,从而完成补丁的整个流程。

  • 主要职责
    PatchOperation 类负责管理整个补丁更新流程,通过内部状态机逐步执行从补丁包初始化到下载、更新以及最终开始游戏的各个阶段。

  • 状态机的作用
    利用状态机节点(如 FsmInitializePackageFsmRequestPackageVersion 等)来控制补丁更新的流程,并在运行过程中根据用户事件动态切换状态。

  • 事件监听与响应
    通过注册多个用户事件,PatchOperation 能够实时响应用户操作,并切换到对应的状态节点,确保补丁更新流程灵活可控。

  • 异步操作与流程控制
    继承自 GameAsyncOperation 使得该类能够在异步环境下平稳运行,并通过 OnUpdate() 持续推进内部状态机的执行。

这种设计使得补丁更新流程模块化、易于管理,同时借助状态机机制实现了灵活的流程控制,为游戏热更新提供了强有力的支持。

类定义及成员变量

脚本首先定义了操作步骤枚举 ESteps,用于表示补丁操作的不同状态,如未开始、更新中和完成。接着声明了以下成员变量:

  • _eventGroup:用于注册和移除事件监听。
  • _machine:内部状态机,用于管理补丁操作的各个状态节点。
  • _packageName:当前补丁包的名称。
  • _steps:当前操作的步骤状态。
// 定义操作步骤枚举
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; // 当前操作步骤

构造函数初始化

在构造函数中,脚本完成了以下工作:

  1. 保存传入的补丁包名称。
  2. 注册多个用户事件的监听器,通过 OnHandleEventMessage 方法处理对应的状态切换。
  3. 创建并初始化状态机,添加了各个状态节点(如初始化补丁包、请求版本信息、更新清单、创建下载器、下载补丁包文件、下载结束处理、清理缓存、以及开始游戏)。
  4. 在状态机黑板中设置初始参数,如 PackageNamePlayMode
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);
}

OnStart 方法

当操作开始时,OnStart() 方法会被调用,主要完成以下操作:

  • 将当前操作步骤设置为“更新中”状态。
  • 通过状态机启动初始化补丁包的状态节点。
protected override void OnStart()
{
    _steps = ESteps.Update; // 设置当前步骤为更新中
    _machine.Run<FsmInitializePackage>(); // 运行初始化补丁包的状态节点
}

OnUpdate 方法

在每帧更新过程中,OnUpdate() 方法会检查当前操作的状态:

  • 如果操作处于未开始或已完成状态,则不进行更新。
  • 当处于更新状态时,调用状态机的 Update() 方法,推进内部状态的处理。
protected override void OnUpdate()
{
    // 如果状态为未开始或已完成,则不进行更新
    if (_steps == ESteps.None || _steps == ESteps.Done)
        return;

    // 在更新状态下,更新状态机
    if (_steps == ESteps.Update)
    {
        _machine.Update();
    }
}

OnAbort 方法

在操作中断时会调用 OnAbort() 方法。

protected override void OnAbort()
{
}

SetFinish 方法

当补丁操作完成时,SetFinish() 方法会被调用,主要执行以下操作:

  • 将操作状态标记为完成。
  • 移除所有注册的事件监听器。
  • 将操作状态更新为成功,并输出调试日志。
public void SetFinish()
{
    _steps = ESteps.Done; // 标记为完成状态
    _eventGroup.RemoveAllListener(); // 移除所有事件监听
    Status = EOperationStatus.Succeed; // 更新操作状态为成功
    Debug.Log($"Package {_packageName} patch done !");
}

OnHandleEventMessage 方法

该方法用于处理接收到的用户事件,根据事件类型控制状态机的状态切换。不同事件会触发状态机切换到相应的状态节点,例如:

  • 用户尝试初始化补丁包时,切换到 FsmInitializePackage 状态;
  • 用户开始下载补丁包文件时,切换到 FsmDownloadPackageFiles 状态;
  • 其他事件对应切换到请求版本、更新清单、或创建下载器等状态。
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 初始化资源包状态节点

这是一个用于初始化资源包的状态节点,实现了 IStateNode 接口。该节点负责根据不同的运行模式(编辑器模拟、单机、联机、WebGL 等)来初始化资源包,并在初始化完成后切换到请求资源包版本状态。下面我们详细解析各个部分的实现与逻辑。

  • 主要职责
    FsmInitializePackage 作为资源包初始化的状态节点,根据不同的运行模式,选择对应的初始化参数并调用 package.InitializeAsync() 进行异步初始化。

  • 流程控制
    通过状态机黑板获取必要参数,并在初始化完成后根据结果:

    • 初始化失败:发送初始化失败事件;
    • 初始化成功:切换到 FsmRequestPackageVersion 状态,继续后续流程。
  • 平台适配
    通过 GetHostServerURL 方法和 RemoteServices 类,实现了针对不同平台(编辑器、移动设备、WebGL)的资源服务器地址适配,确保初始化流程的灵活性与扩展性。

这种设计使得资源包的初始化逻辑模块化,并能灵活应对不同平台与运行模式下的资源管理需求,为后续的热更新流程奠定了坚实基础。

状态节点生命周期

  • OnEnter()
    进入该状态时,发送提示消息“初始化资源包!”,并启动初始化资源包的协程 InitPackage()

    void IStateNode.OnEnter()
    {
        PatchEventDefine.PatchStepsChange.SendEventMessage("初始化资源包!");
        GameManager.Instance.StartCoroutine(InitPackage());
    }
    

获取服务器地址及远程服务实现

这是为联机模式以及WebGL模式提供的。

GetHostServerURL 方法

该方法根据平台返回对应的资源服务器地址,支持编辑器模式和运行时模式。示例中使用了本地测试地址 http://127.0.0.1 并根据不同平台构造 URL 路径。

    /// <summary>
    /// 获取资源服务器地址,根据平台选择不同的URL路径
    /// </summary>
    private string GetHostServerURL()
    {
        // 本地测试地址(可根据需要调整)
        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
    }
RemoteServices 内部类

该内部类实现了 IRemoteServices 接口,用于提供默认与备用服务器的资源文件下载 URL。主要方法包括:

  • GetRemoteMainURL:返回默认服务器上的文件 URL。
  • GetRemoteFallbackURL:返回备用服务器上的文件 URL。
/// <summary>
/// 远程资源地址服务实现类,实现 IRemoteServices 接口
/// 用于提供默认和备用资源文件下载地址
/// </summary>
private class RemoteServices : IRemoteServices
{
    private readonly string _defaultHostServer; // 默认服务器地址
    private readonly string _fallbackHostServer; // 备用服务器地址

    public RemoteServices(string defaultHostServer, string fallbackHostServer)
    {
        _defaultHostServer = defaultHostServer;
        _fallbackHostServer = fallbackHostServer;
    }

    /// <summary>
    /// 获取远程主服务器文件 URL
    /// </summary>
    string IRemoteServices.GetRemoteMainURL(string fileName)
    {
        return $"{_defaultHostServer}/{fileName}";
    }

    /// <summary>
    /// 获取远程备用服务器文件 URL
    /// </summary>
    string IRemoteServices.GetRemoteFallbackURL(string fileName)
    {
        return $"{_fallbackHostServer}/{fileName}";
    }
}

InitPackage 协程解析

InitPackage() 协程负责根据黑板中存储的运行模式和包名称,进行资源包的初始化。主要逻辑如下:

  1. 获取参数与资源包对象
    从状态机黑板中获取 PlayModePackageName,然后尝试获取或创建对应的资源包。

     // 从黑板中获取运行模式和包名称
     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;
    
  2. 根据运行模式选择不同的初始化参数
    针对不同的运行模式,分别采用不同的参数进行异步初始化:

    • 编辑器模拟模式
      利用 EditorSimulateModeHelper.SimulateBuild 获取包根目录,并创建模拟模式参数。

         // 编辑器下的模拟模式初始化
         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 运行模式
      针对 WebGL 平台(包含微信小游戏情况)做了特殊处理,分别配置不同的参数。

                 // 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
                     // 非微信小游戏下的默认WebGL模式初始化
                     var createParameters = new WebPlayModeParameters();
                     createParameters.WebServerFileSystemParameters =
                         FileSystemParameters.CreateDefaultWebServerFileSystemParameters();
                     initializationOperation = package.InitializeAsync(createParameters);
                 #endif
                 }
      
  3. 等待初始化操作完成与状态切换
    协程等待 initializationOperation 完成。如果初始化失败,则发送初始化失败事件;若成功,则切换到请求资源包版本的状态节点。

    yield return initializationOperation;
    
    if (initializationOperation.Status != EOperationStatus.Succeed)
    {
        Debug.LogWarning($"{initializationOperation.Error}");
        PatchEventDefine.InitializeFailed.SendEventMessage();
    }
    else
    {
        _machine.ChangeState<FsmRequestPackageVersion>();
    }
    

FsmRequestPackageVersion 请求资源包版本状态节点

该状态节点继承自 IStateNode 接口,负责执行资源包版本检查流程。通过异步请求获取当前资源包版本号,并根据结果触发状态切换或错误处理。以下是其核心实现解析。

状态节点生命周期

OnEnter()
进入状态时发送提示消息,并启动版本检查协程。

void IStateNode.OnEnter()
{
    PatchEventDefine.PatchStepsChange.SendEventMessage("请求资源版本 !");
    GameManager.Instance.StartCoroutine(UpdatePackageVersion());
}

UpdatePackageVersion 协程解析

该协程是版本请求的核心逻辑,流程如下:

  1. 获取包名称与资源包实例
    从状态机黑板中读取预先配置的包名称,并通过 YooAssets 获取对应的资源包实例。

    private IEnumerator UpdatePackageVersion()
    {
         // 从共享数据区获取预先配置的包名称
         var packageName = (string)_machine.GetBlackboardValue("PackageName");
         
         // 获取YooAssets资源包实例
         var package = YooAssets.GetPackage(packageName);
    
        //...
    }
    
  2. 发起异步版本请求
    调用 RequestPackageVersionAsync 方法异步请求版本信息,并等待操作完成。

        var operation = package.RequestPackageVersionAsync();
        yield return operation;
    
  3. 处理请求结果
    失败处理:记录错误日志并发送全局失败事件。
    成功处理:缓存版本号至黑板,切换至清单更新状态。

     // 错误处理分支
     if (operation.Status != EOperationStatus.Succeed)
     {
         Debug.LogWarning($"版本请求失败: {operation.Error}");
         // 触发全局失败事件通知
         PatchEventDefine.PackageVersionRequestFailed.SendEventMessage();
     }
     else
     {
         // 记录并存储有效版本号
         Debug.Log($"获取到资源包版本: {operation.PackageVersion}");
         _machine.SetBlackboardValue("PackageVersion", operation.PackageVersion);
         
         // 切换至清单更新状态
         _machine.ChangeState<FsmUpdatePackageManifest>();
     }
    

FsmUpdatePackageManifest 更新资源清单状态节点

该状态节点继承自 IStateNode 接口,负责执行资源清单(Manifest)的更新流程。通过异步请求最新资源清单文件,确保后续资源下载流程的准确性。以下是其实现逻辑的深度解析。

状态节点生命周期

OnEnter()
进入状态时发送进度提示,并启动清单更新协程。通过事件系统通知 UI 更新进度信息。

void IStateNode.OnEnter()
{
    PatchEventDefine.PatchStepsChange.SendEventMessage("更新资源清单!");
    GameManager.Instance.StartCoroutine(UpdateManifest());
}

UpdateManifest 协程解析

该协程是清单更新的核心逻辑,包含以下关键步骤:

  1. 获取配置信息
    从状态机黑板中读取包名称和版本号,这两个参数由前置状态 FsmRequestPackageVersion 提供。

    private IEnumerator UpdateManifest()
    {
         // 从共享数据区获取包配置信息
         var packageName = (string)_machine.GetBlackboardValue("PackageName");
         var packageVersion = (string)_machine.GetBlackboardValue("PackageVersion");
    
         // 获取目标资源包实例
         var package = YooAssets.GetPackage(packageName);
    
        //...
     }
    
  2. 发起异步清单请求
    调用 UpdatePackageManifestAsync 方法,传入版本号进行清单更新。此处建议通过 YooAsset 配置超时机制增强健壮性。

     // 发起清单更新请求(建议超时机制通过YooAsset配置实现)
     var operation = package.UpdatePackageManifestAsync(packageVersion);
     yield return operation;
    
  3. 结果处理与状态流转
    失败处理:记录错误日志并触发全局事件,终止当前流程。
    成功处理:自动切换至下载器创建状态,进入资源下载阶段。

     // 错误处理分支
     if (operation.Status != EOperationStatus.Succeed)
     {
         // 记录错误日志并发送全局事件[1](@ref)
         Debug.LogWarning($"清单更新失败: {operation.Error}");
         PatchEventDefine.PackageManifestUpdateFailed.SendEventMessage();
         yield break; // 提前终止流程
     }
    
     // 成功时自动切换至下载器创建状态
     _machine.ChangeState<FsmCreateDownloader>();
    

FsmCreateDownloader 创建资源下载器状态节点

该状态节点继承自 IStateNode 接口,负责创建资源下载器并检测热更新需求。作为资源更新流程的决策中枢,根据检测结果决定直接进入游戏或发起下载流程。以下是其核心逻辑解析。

状态节点生命周期

OnEnter()
进入状态时发送操作提示,并立即执行下载器创建逻辑。

void IStateNode.OnEnter()
{
    PatchEventDefine.PatchStepsChange.SendEventMessage("创建资源下载器!");
    CreateDownloader(); // 同步执行无需协程
}

CreateDownloader 方法解析

该方法通过三步决策机制控制流程走向,是状态节点的核心逻辑载体。

private void CreateDownloader()
{
    // 从共享数据区获取包名称
    var packageName = (string)_machine.GetBlackboardValue("PackageName");
    var package = YooAssets.GetPackage(packageName);

    // 初始化下载配置(建议参数配置化)
    int downloadingMaxNum = 10;    // 最大并发下载数
    int failedTryAgain = 3;        // 下载失败重试次数
    var downloader = package.CreateResourceDownloader(downloadingMaxNum, failedTryAgain);
    
    // 存储下载器实例供后续状态使用
    _machine.SetBlackboardValue("Downloader", downloader);

    // 检测到无更新文件的快速通道
    if (downloader.TotalDownloadCount == 0)
    {
        Debug.Log("未检测到需要下载的资源!");
        _machine.ChangeState<FsmStartGame>();
    }
    else
    {
        // 计算更新文件详情
        int totalDownloadCount = downloader.TotalDownloadCount;
        long totalDownloadBytes = downloader.TotalDownloadBytes;
        
        // 发送发现更新文件事件
        PatchEventDefine.FoundUpdateFiles.SendEventMessage(totalDownloadCount, totalDownloadBytes);
    }
}

FsmDownloadPackageFiles 资源文件下载状态节点

该状态节点继承自 IStateNode 接口,负责执行资源文件的实际下载流程。通过事件回调机制实现下载进度监控与错误处理,是热更新流程的核心执行阶段。以下是其实现逻辑的详细解析。

状态节点生命周期

OnEnter()
进入状态时发送下载开始通知,并启动下载协程。

void IStateNode.OnEnter()
{
    PatchEventDefine.PatchStepsChange.SendEventMessage("开始下载资源文件!");
    GameManager.Instance.StartCoroutine(BeginDownload());
}

BeginDownload 协程解析

该协程是下载流程的核心控制器,包含完整的下载生命周期管理。

private IEnumerator BeginDownload()
{
    // 从共享数据区获取已初始化的下载器
    var downloader = (ResourceDownloaderOperation)_machine.GetBlackboardValue("Downloader");

    // 配置回调监听
    downloader.DownloadErrorCallback = PatchEventDefine.WebFileDownloadFailed.SendEventMessage; // 单个文件下载失败
    downloader.DownloadUpdateCallback = PatchEventDefine.DownloadUpdate.SendEventMessage; // 下载进度更新

    // 启动下载器(建议在此处添加网络状态检测)
    downloader.BeginDownload();
    yield return downloader; // 等待下载器完成(自动包含超时机制)

    /* 下载结果处理 */
    // 失败时仅终止流程(错误已通过回调通知)
    if (downloader.Status != EOperationStatus.Succeed)
        yield break;

    // 下载成功切换至完成状态
    _machine.ChangeState<FsmDownloadPackageOver>();
}

FsmDownloadPackageOver 资源下载完成状态节点

该状态节点继承自 IStateNode 接口,是资源更新流程中的瞬时过渡状态,负责在资源文件下载完成后触发后续操作。作为状态机流转的枢纽节点,其核心价值在于实现流程的无缝衔接与模块解耦。以下是其实现逻辑的深度解析。

类定义及设计特性

瞬时状态设计:不持有任何资源或计时器,生命周期仅持续1帧
事件驱动中转:通过发送完成通知触发UI响应,同时启动清理流程
模块化扩展性:后续可插入版本校验、解压操作等扩展逻辑

状态节点生命周期解析

OnEnter()
实现状态核心逻辑的双通道触发:

void IStateNode.OnEnter()
{
    // 通道1:UI层进度反馈
    PatchEventDefine.PatchStepsChange.SendEventMessage("资源文件下载完毕!");
    
    // 通道2:立即切换至缓存清理状态
    _machine.ChangeState<FsmClearCacheBundle>();
}

FsmClearCacheBundle 清理缓存文件状态节点

该状态节点继承自 IStateNode 接口,负责在资源更新完成后清理冗余缓存文件,优化存储空间并完成热更新流程的最后准备。以下是其核心实现逻辑的解析。

状态节点生命周期

OnEnter()
进入状态时发送清理提示,并启动异步缓存清理流程:

void IStateNode.OnEnter()
{
    // 通知UI更新进度提示
    PatchEventDefine.PatchStepsChange.SendEventMessage("清理未使用的缓存文件!");
    
    // 获取目标资源包实例
    var packageName = (string)_machine.GetBlackboardValue("PackageName");
    var package = YooAssets.GetPackage(packageName);

    // 启动安全模式清理
    var operation = package.ClearCacheFilesAsync(EFileClearMode.ClearUnusedBundleFiles);
    operation.Completed += Operation_Completed;
}

Operation_Completed 回调解析

该回调处理清理操作完成后的状态流转:

private void Operation_Completed(AsyncOperationBase obj)
{
    // 无论清理结果如何都进入游戏
    _machine.ChangeState<FsmStartGame>();
}

FsmStartGame 游戏启动状态节点解析

该状态节点是资源更新流程的最终状态,承担着系统资源回收与游戏主逻辑启动的双重职责。

状态节点生命周期

OnCreate()
通过依赖注入获取补丁系统控制权:

void IStateNode.OnCreate(StateMachine machine)
{
    // 安全获取补丁操作上下文
    _owner = machine.Owner as PatchOperation;
}

OnEnter()
执行双通道终结操作:

void IStateNode.OnEnter()
{
    // 通道1:全局事件通知
    PatchEventDefine.PatchStepsChange.SendEventMessage("开始游戏!");
    
    // 通道2:系统级资源回收
    _owner.SetFinish(); // 示例代码建议增加try-catch块
}

5.2 知识点代码

Boot.cs

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UniFramework.Event;
using YooAsset;

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();
    }
}

GameManager.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UniFramework.Event;
using YooAsset;

public class GameManager
{
    private static GameManager _instance;
    public static GameManager Instance
    {
        get
        {
            if (_instance == null)
                _instance = new GameManager();
            return _instance;
        }
    }

    private readonly EventGroup _eventGroup = new EventGroup();

    /// <summary>
    /// 协程启动器
    /// </summary>
    public MonoBehaviour Behaviour;


    private GameManager()
    {
        // 注册监听事件
        _eventGroup.AddListener<SceneEventDefine.ChangeToHomeScene>(OnHandleEventMessage);
        _eventGroup.AddListener<SceneEventDefine.ChangeToBattleScene>(OnHandleEventMessage);
    }

    /// <summary>
    /// 开启一个协程
    /// </summary>
    public void StartCoroutine(IEnumerator enumerator)
    {
        Behaviour.StartCoroutine(enumerator);
    }

    /// <summary>
    /// 接收事件
    /// </summary>
    private void OnHandleEventMessage(IEventMessage message)
    {
        if (message is SceneEventDefine.ChangeToHomeScene)
        {
            YooAssets.LoadSceneAsync("scene_home");
        }
        else if (message is SceneEventDefine.ChangeToBattleScene)
        {
            YooAssets.LoadSceneAsync("scene_battle");
        }
    }
}

PatchOperation.cs

using UnityEngine;
using UniFramework.Machine;
using UniFramework.Event;
using YooAsset;

/// <summary>
/// 补丁操作类,继承自 GameAsyncOperation 实现异步补丁处理流程
/// </summary>
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; // 当前操作步骤

    /// <summary>
    /// 构造函数,初始化补丁操作
    /// </summary>
    /// <param name="packageName">补丁包名称</param>
    /// <param name="playMode">运行模式</param>
    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);
    }

    /// <summary>
    /// 开始操作时调用
    /// </summary>
    protected override void OnStart()
    {
        _steps = ESteps.Update; // 设置当前步骤为更新中
        _machine.Run<FsmInitializePackage>(); // 运行初始化补丁包的状态节点
    }

    /// <summary>
    /// 每帧更新操作
    /// </summary>
    protected override void OnUpdate()
    {
        // 如果状态为未开始或已完成,则不进行更新
        if (_steps == ESteps.None || _steps == ESteps.Done)
            return;

        // 在更新状态下,更新状态机
        if (_steps == ESteps.Update)
        {
            _machine.Update();
        }
    }

    /// <summary>
    /// 中断操作时调用(此处未做处理)
    /// </summary>
    protected override void OnAbort()
    {
    }

    /// <summary>
    /// 标记操作完成,并清理事件监听及状态机
    /// </summary>
    public void SetFinish()
    {
        _steps = ESteps.Done; // 标记为完成状态
        _eventGroup.RemoveAllListener(); // 移除所有事件监听
        Status = EOperationStatus.Succeed; // 更新操作状态为成功
        Debug.Log($"Package {_packageName} patch done !");
    }

    /// <summary>
    /// 处理接收到的事件,根据事件类型切换状态机状态
    /// </summary>
    /// <param name="message">接收到的事件消息</param>
    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.cs

using System;
using System.IO;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UniFramework.Machine;
using YooAsset;

/// <summary>
/// 初始化资源包状态节点,实现 IStateNode 接口
/// 负责根据不同的运行模式初始化资源包
/// </summary>
internal class FsmInitializePackage : IStateNode
{
    private StateMachine _machine; // 状态机引用,用于状态切换和获取黑板数据

    /// <summary>
    /// 状态节点创建时调用,保存状态机引用
    /// </summary>
    void IStateNode.OnCreate(StateMachine machine)
    {
        _machine = machine;
    }

    /// <summary>
    /// 进入状态时调用,发送提示消息并启动初始化协程
    /// </summary>
    void IStateNode.OnEnter()
    {
        PatchEventDefine.PatchStepsChange.SendEventMessage("初始化资源包!");
        GameManager.Instance.StartCoroutine(InitPackage());
    }

    /// <summary>
    /// 每帧更新(本状态节点无需持续更新)
    /// </summary>
    void IStateNode.OnUpdate()
    {
    }

    /// <summary>
    /// 退出状态时调用(本状态节点无需处理退出逻辑)
    /// </summary>
    void IStateNode.OnExit()
    {
    }

    /// <summary>
    /// 初始化资源包的协程,根据不同的运行模式选择不同的初始化参数
    /// </summary>
    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
            // 非微信小游戏下的默认WebGL模式初始化
            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>
    /// 获取资源服务器地址,根据平台选择不同的URL路径
    /// </summary>
    private string GetHostServerURL()
    {
        // 本地测试地址(可根据需要调整)
        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>
    /// 远程资源地址服务实现类,实现 IRemoteServices 接口
    /// 用于提供默认和备用资源文件下载地址
    /// </summary>
    private class RemoteServices : IRemoteServices
    {
        private readonly string _defaultHostServer; // 默认服务器地址
        private readonly string _fallbackHostServer; // 备用服务器地址

        public RemoteServices(string defaultHostServer, string fallbackHostServer)
        {
            _defaultHostServer = defaultHostServer;
            _fallbackHostServer = fallbackHostServer;
        }

        /// <summary>
        /// 获取远程主服务器文件 URL
        /// </summary>
        string IRemoteServices.GetRemoteMainURL(string fileName)
        {
            return $"{_defaultHostServer}/{fileName}";
        }

        /// <summary>
        /// 获取远程备用服务器文件 URL
        /// </summary>
        string IRemoteServices.GetRemoteFallbackURL(string fileName)
        {
            return $"{_fallbackHostServer}/{fileName}";
        }
    }
}

FsmRequestPackageVersion.cs

using System.Collections;
using UnityEngine;
using UniFramework.Machine;
using YooAsset;

/// <summary>
/// 资源更新流程 - 请求资源包版本状态
/// 继承状态机接口实现资源版本检查逻辑
/// </summary>
internal class FsmRequestPackageVersion : IStateNode
{
    private StateMachine _machine;

    /// <summary>
    /// 状态节点创建时初始化状态机引用
    /// </summary>
    void IStateNode.OnCreate(StateMachine machine)
    {
        _machine = machine; // 保持状态机实例引用
    }

    /// <summary>
    /// 进入状态时触发版本请求流程
    /// 1.发送步骤更新事件
    /// 2.启动版本检查协程
    /// </summary>
    void IStateNode.OnEnter()
    {
        // 通知UI更新进度提示
        PatchEventDefine.PatchStepsChange.SendEventMessage("请求资源版本 !");
        // 启动版本检查协程
        GameManager.Instance.StartCoroutine(UpdatePackageVersion());
    }

    // 空置状态更新方法(当前状态无需每帧更新)
    void IStateNode.OnUpdate() { }

    // 状态退出时无特殊操作
    void IStateNode.OnExit() { }

    /// <summary>
    /// 执行包版本检查的核心协程
    /// 流程:
    /// 1.从状态机黑板获取包名称
    /// 2.异步请求包版本信息
    /// 3.处理请求结果(成功缓存版本号/失败发送事件)
    /// </summary>
    private IEnumerator UpdatePackageVersion()
    {
        // 从共享数据区获取预先配置的包名称
        var packageName = (string)_machine.GetBlackboardValue("PackageName");
        
        // 获取YooAssets资源包实例
        var package = YooAssets.GetPackage(packageName);
        
        // 发起异步版本请求(超时时间建议通过配置管理)
        var operation = package.RequestPackageVersionAsync();
        yield return operation;

        // 错误处理分支
        if (operation.Status != EOperationStatus.Succeed)
        {
            Debug.LogWarning($"版本请求失败: {operation.Error}");
            // 触发全局失败事件通知
            PatchEventDefine.PackageVersionRequestFailed.SendEventMessage();
        }
        else
        {
            // 记录并存储有效版本号
            Debug.Log($"获取到资源包版本: {operation.PackageVersion}");
            _machine.SetBlackboardValue("PackageVersion", operation.PackageVersion);
            
            // 切换至清单更新状态
            _machine.ChangeState<FsmUpdatePackageManifest>();
        }
    }
}

FsmUpdatePackageManifest.cs

using System.Collections;
using UnityEngine;
using UniFramework.Machine;
using YooAsset;

/// <summary>
/// 资源更新流程 - 更新资源清单状态
/// 负责请求并更新资源包的清单文件(Manifest)
/// 前置依赖:已获取有效的资源包版本号
/// 后置操作:进入下载器创建阶段
/// </summary>
public class FsmUpdatePackageManifest : IStateNode
{
    private StateMachine _machine;

    /// <summary>
    /// 状态节点初始化,绑定状态机实例
    /// </summary>
    void IStateNode.OnCreate(StateMachine machine)
    {
        _machine = machine; // 保持状态机引用
    }

    /// <summary>
    /// 进入状态时执行:
    /// 1. 发送步骤更新事件
    /// 2. 启动清单更新协程
    /// </summary>
    void IStateNode.OnEnter()
    {
        // 通知UI更新进度提示[4](@ref)
        PatchEventDefine.PatchStepsChange.SendEventMessage("更新资源清单!");
        // 启动清单更新流程
        GameManager.Instance.StartCoroutine(UpdateManifest());
    }

    // 空置状态更新方法(当前状态无需每帧逻辑)
    void IStateNode.OnUpdate()
    {
    }

    // 状态退出时无特殊资源释放需求
    void IStateNode.OnExit()
    {
    }

    /// <summary>
    /// 执行清单更新的核心协程
    /// 流程:
    /// 1. 从状态机黑板获取包信息
    /// 2. 异步请求清单更新
    /// 3. 处理结果(失败发送事件/成功切换状态)
    /// </summary>
    private IEnumerator UpdateManifest()
    {
        // 从共享数据区获取包配置信息
        var packageName = (string)_machine.GetBlackboardValue("PackageName");
        var packageVersion = (string)_machine.GetBlackboardValue("PackageVersion");

        // 获取目标资源包实例
        var package = YooAssets.GetPackage(packageName);

        // 发起清单更新请求(建议超时机制通过YooAsset配置实现)
        var operation = package.UpdatePackageManifestAsync(packageVersion);
        yield return operation;

        // 错误处理分支
        if (operation.Status != EOperationStatus.Succeed)
        {
            // 记录错误日志并发送全局事件[1](@ref)
            Debug.LogWarning($"清单更新失败: {operation.Error}");
            PatchEventDefine.PackageManifestUpdateFailed.SendEventMessage();
            yield break; // 提前终止流程
        }

        // 成功时自动切换至下载器创建状态
        _machine.ChangeState<FsmCreateDownloader>();
    }
}

FsmCreateDownloader.cs

using UnityEngine;
using UniFramework.Machine;
using YooAsset;

/// <summary>
/// 资源更新流程 - 创建资源下载器状态
/// 核心职责:
/// 1. 创建带参数配置的下载器实例
/// 2. 检测热更文件情况(无更新直接进游戏/有更新触发通知)
/// 前置依赖:已获取有效的资源清单
/// 后置操作:根据检测结果跳转下载或直接启动游戏
/// </summary>
public class FsmCreateDownloader : IStateNode
{
    private StateMachine _machine;

    /// <summary>
    /// 状态机初始化,绑定父级状态机实例
    /// </summary>
    void IStateNode.OnCreate(StateMachine machine)
    {
        _machine = machine;
    }

    /// <summary>
    /// 进入状态时执行:
    /// 1. 发送进度更新事件
    /// 2. 执行下载器创建流程
    /// </summary>
    void IStateNode.OnEnter()
    {
        PatchEventDefine.PatchStepsChange.SendEventMessage("创建资源下载器!");
        CreateDownloader();
    }

    // 无需每帧更新逻辑
    void IStateNode.OnUpdate() { }

    // 无状态退出时的资源释放需求
    void IStateNode.OnExit() { }

    /// <summary>
    /// 下载器创建核心逻辑
    /// 流程:
    /// 1. 从状态机获取包配置
    /// 2. 初始化下载器参数
    /// 3. 检测更新文件并处理结果
    /// </summary>
    /// <remarks>
    /// 注意事项:
    /// - 下载参数建议通过配置文件管理
    /// - 正式项目需添加磁盘空间检查逻辑
    /// - 多线程下载需考虑设备性能差异
    /// </remarks>
    private void CreateDownloader()
    {
        // 从共享数据区获取包名称
        var packageName = (string)_machine.GetBlackboardValue("PackageName");
        var package = YooAssets.GetPackage(packageName);

        // 初始化下载配置(建议参数配置化)
        int downloadingMaxNum = 10;    // 最大并发下载数
        int failedTryAgain = 3;        // 下载失败重试次数
        var downloader = package.CreateResourceDownloader(downloadingMaxNum, failedTryAgain);
        
        // 存储下载器实例供后续状态使用
        _machine.SetBlackboardValue("Downloader", downloader);

        // 检测到无更新文件的快速通道
        if (downloader.TotalDownloadCount == 0)
        {
            Debug.Log("未检测到需要下载的资源!");
            _machine.ChangeState<FsmStartGame>();
        }
        else
        {
            // 计算更新文件详情
            int totalDownloadCount = downloader.TotalDownloadCount;
            long totalDownloadBytes = downloader.TotalDownloadBytes;
            
            // 发送发现更新文件事件
            PatchEventDefine.FoundUpdateFiles.SendEventMessage(totalDownloadCount, totalDownloadBytes);
        }
    }
}

FsmDownloadPackageFiles.cs

using System.Collections;
using UniFramework.Machine;
using YooAsset;


/// <summary>
/// 资源更新流程 - 资源文件下载状态
/// 核心职责:
/// 1. 配置下载器回调监听
/// 2. 执行实际资源下载流程
/// 3. 处理下载结果并流转状态
/// 前置依赖:已成功创建资源下载器
/// </summary>
public class FsmDownloadPackageFiles : IStateNode
{
    private StateMachine _machine;

    /// <summary>
    /// 状态机初始化,绑定父级状态机
    /// </summary>
    void IStateNode.OnCreate(StateMachine machine)
    {
        _machine = machine;
    }

    /// <summary>
    /// 进入下载状态时:
    /// 1. 发送进度更新通知
    /// 2. 启动下载协程
    /// </summary>
    void IStateNode.OnEnter()
    {
        PatchEventDefine.PatchStepsChange.SendEventMessage("开始下载资源文件!");
        GameManager.Instance.StartCoroutine(BeginDownload());
    }

    // 空置更新方法
    void IStateNode.OnUpdate()
    {
    }

    // 无退出时的特殊操作
    void IStateNode.OnExit()
    {
    }

    /// <summary>
    /// 执行下载流程的核心协程
    /// 流程:
    /// 1. 获取下载器实例
    /// 2. 配置回调事件绑定
    /// 3. 启动异步下载
    /// 4. 等待并处理下载结果
    /// </summary>
    private IEnumerator BeginDownload()
    {
        // 从共享数据区获取已初始化的下载器
        var downloader = (ResourceDownloaderOperation)_machine.GetBlackboardValue("Downloader");

        // 配置回调监听
        downloader.DownloadErrorCallback = PatchEventDefine.WebFileDownloadFailed.SendEventMessage; // 单个文件下载失败
        downloader.DownloadUpdateCallback = PatchEventDefine.DownloadUpdate.SendEventMessage; // 下载进度更新

        // 启动下载器(建议在此处添加网络状态检测)
        downloader.BeginDownload();
        yield return downloader; // 等待下载器完成(自动包含超时机制)

        /* 下载结果处理 */
        // 失败时仅终止流程(错误已通过回调通知)
        if (downloader.Status != EOperationStatus.Succeed)
            yield break;

        // 下载成功切换至完成状态
        _machine.ChangeState<FsmDownloadPackageOver>();
    }
}

FsmDownloadPackageOver.cs

using UnityEngine;
using UniFramework.Machine;

/// <summary>
/// 资源更新流程 - 资源下载完成状态
/// 核心职责:
/// 1. 发送下载完成通知
/// 2. 触发后续缓存清理流程
/// 状态特性:
/// - 瞬时过渡状态(无具体业务逻辑)
/// - 作为下载完成到缓存清理的中转节点
/// </summary>
internal class FsmDownloadPackageOver : IStateNode
{
    private StateMachine _machine;

    /// <summary>
    /// 状态机初始化,绑定父级状态机
    /// </summary>
    void IStateNode.OnCreate(StateMachine machine)
    {
        _machine = machine;
    }

    /// <summary>
    /// 进入状态时执行:
    /// 1. 发送下载完成通知
    /// 2. 立即跳转至缓存清理状态
    /// </summary>
    void IStateNode.OnEnter()
    {
        // 通知界面更新进度提示
        PatchEventDefine.PatchStepsChange.SendEventMessage("资源文件下载完毕!");

        // 直接流转到缓存清理阶段(无延迟切换)
        _machine.ChangeState<FsmClearCacheBundle>();
    }

    // 空置状态更新方法(瞬时状态无需更新)
    void IStateNode.OnUpdate()
    {
    }

    // 空置状态退出方法(无资源需要释放)
    void IStateNode.OnExit()
    {
    }
}

FsmClearCacheBundle.cs

using UnityEngine;
using UniFramework.Machine;
using YooAsset;

/// <summary>
/// 资源更新流程 - 清理缓存文件状态
/// 核心职责:
/// 1. 清理未被引用的资源包文件
/// 2. 释放磁盘空间优化存储效率
/// 3. 完成资源更新最后准备步骤
/// 前置依赖:资源包已成功下载并验证
/// 设计特性:
/// - 异步清理避免主线程阻塞
/// - 采用安全清理模式防止误删关键文件
/// </summary>
internal class FsmClearCacheBundle : IStateNode
{
    private StateMachine _machine;

    /// <summary>
    /// 状态机初始化,绑定父级状态机实例
    /// </summary>
    void IStateNode.OnCreate(StateMachine machine)
    {
        _machine = machine;
    }

    /// <summary>
    /// 进入清理状态时执行:
    /// 1. 发送进度更新事件
    /// 2. 启动异步缓存清理流程
    /// </summary>
    void IStateNode.OnEnter()
    {
        // 通知UI更新清理进度提示
        PatchEventDefine.PatchStepsChange.SendEventMessage("清理未使用的缓存文件!");

        // 获取目标资源包配置
        var packageName = (string)_machine.GetBlackboardValue("PackageName");
        var package = YooAssets.GetPackage(packageName);

        /* 安全清理模式说明:
           EFileClearMode.ClearUnusedBundleFiles 模式特性:
           - 仅清除未被当前版本清单引用的历史文件
           - 保留正在使用的资源包文件
           - 避免影响正在运行的资源加载逻辑 */
        var operation = package.ClearCacheFilesAsync(EFileClearMode.ClearUnusedBundleFiles);
        operation.Completed += Operation_Completed;
    }

    // 空置状态更新方法(清理过程通过异步回调处理)
    void IStateNode.OnUpdate()
    {
    }

    // 无退出时的特殊资源释放需求
    void IStateNode.OnExit()
    {
    }

    /// <summary>
    /// 清理操作完成回调处理
    /// 设计规范:
    /// - 不进行显式错误处理(底层已日志记录)
    /// - 确保最终状态流转可靠性
    /// </summary>
    private void Operation_Completed(YooAsset.AsyncOperationBase obj)
    {
        // 无论清理成功与否都进入游戏启动流程
        // 注:实际项目可在此处添加清理结果埋点
        _machine.ChangeState<FsmStartGame>();
    }
}

FsmStartGame.cs

using UnityEngine;
using UniFramework.Machine;

/// <summary>
/// 资源更新流程 - 游戏启动状态
/// 核心职责:
/// 1. 完成资源更新流程最终收尾
/// 2. 触发游戏主逻辑启动
/// 3. 释放补丁系统相关资源
/// 前置依赖:所有资源更新及清理操作已完成
/// 状态特性:
/// - 流程终点状态(无后续状态切换)
/// - 承担资源更新系统与游戏主系统的衔接桥梁
/// </summary>
internal class FsmStartGame : IStateNode
{
    private PatchOperation _owner;

    /// <summary>
    /// 状态机初始化时获取所属补丁系统实例
    /// </summary>
    /// <remarks>
    /// 通过状态机Owner获取上下文,符合依赖注入设计原则
    /// </remarks>
    void IStateNode.OnCreate(StateMachine machine)
    {
        _owner = machine.Owner as PatchOperation; // 安全类型转换获取补丁操作实例
    }

    /// <summary>
    /// 进入游戏启动状态时:
    /// 1. 发送最终流程步骤通知
    /// 2. 执行补丁系统收尾工作
    /// 3. 触发游戏主入口逻辑
    /// </summary>
    void IStateNode.OnEnter()
    {
        // 发送全局流程完成通知(建议添加版本校验成功事件)
        PatchEventDefine.PatchStepsChange.SendEventMessage("开始游戏!");

        /* 补丁系统收尾流程:
           - 释放下载器占用的内存资源
           - 关闭补丁系统后台服务
           - 回收临时文件存储空间 */
        _owner.SetFinish(); // 调用补丁系统完成方法(建议在此处添加资源校验埋点)
    }

    // 空置状态更新方法(瞬时终点状态无需更新)
    void IStateNode.OnUpdate()
    {
    }

    // 空置状态退出方法(资源释放由SetFinish()处理)
    void IStateNode.OnExit()
    {
    }
}


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com

×

喜欢就点赞,疼爱就打赏