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
- 启动引导:游戏启动时的入口点,执行基础的初始化操作。
- 初始化 YooAsset:对 YooAsset 资源管理系统进行初始化。
- 加载补丁窗口 UI:加载用于显示补丁更新进度的用户界面。
- 启动补丁操作:开始整个补丁更新流程。
- 初始化资源包:根据不同的运行模式(如编辑器模拟、单机、联机等)初始化资源包。如果初始化成功,进入请求资源包版本阶段;如果失败,触发初始化失败事件。
- 请求资源包版本:向服务器请求当前资源包的版本信息。若请求成功,进入更新资源清单阶段;若失败,触发版本请求失败事件。
- 更新资源清单:根据获取到的版本信息,更新本地的资源清单。成功后进入创建下载器阶段;失败则触发清单更新失败事件。
- 创建下载器:检查是否有需要更新的资源文件。如果没有更新,直接进入开始游戏阶段;如果发现有更新,进入下载资源文件阶段。
- 下载资源文件:执行资源文件的下载操作。下载成功后进入资源下载完成阶段;下载失败则触发下载失败事件。
- 资源下载完成:资源文件下载完成后,进入清理缓存文件阶段。
- 清理缓存文件:清理不再使用的缓存文件,完成后进入开始游戏阶段。
- 开始游戏:完成所有更新操作后,设置默认资源包,加载主页面场景,最终游戏启动。
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
类负责管理整个补丁更新流程,通过内部状态机逐步执行从补丁包初始化到下载、更新以及最终开始游戏的各个阶段。状态机的作用:
利用状态机节点(如FsmInitializePackage
、FsmRequestPackageVersion
等)来控制补丁更新的流程,并在运行过程中根据用户事件动态切换状态。事件监听与响应:
通过注册多个用户事件,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; // 当前操作步骤
构造函数初始化
在构造函数中,脚本完成了以下工作:
- 保存传入的补丁包名称。
- 注册多个用户事件的监听器,通过
OnHandleEventMessage
方法处理对应的状态切换。 - 创建并初始化状态机,添加了各个状态节点(如初始化补丁包、请求版本信息、更新清单、创建下载器、下载补丁包文件、下载结束处理、清理缓存、以及开始游戏)。
- 在状态机黑板中设置初始参数,如
PackageName
和PlayMode
。
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()
协程负责根据黑板中存储的运行模式和包名称,进行资源包的初始化。主要逻辑如下:
获取参数与资源包对象
从状态机黑板中获取PlayMode
和PackageName
,然后尝试获取或创建对应的资源包。// 从黑板中获取运行模式和包名称 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;
根据运行模式选择不同的初始化参数
针对不同的运行模式,分别采用不同的参数进行异步初始化:编辑器模拟模式
利用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 }
等待初始化操作完成与状态切换
协程等待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 协程解析
该协程是版本请求的核心逻辑,流程如下:
获取包名称与资源包实例
从状态机黑板中读取预先配置的包名称,并通过YooAssets
获取对应的资源包实例。private IEnumerator UpdatePackageVersion() { // 从共享数据区获取预先配置的包名称 var packageName = (string)_machine.GetBlackboardValue("PackageName"); // 获取YooAssets资源包实例 var package = YooAssets.GetPackage(packageName); //... }
发起异步版本请求
调用RequestPackageVersionAsync
方法异步请求版本信息,并等待操作完成。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 更新资源清单状态节点
该状态节点继承自 IStateNode
接口,负责执行资源清单(Manifest)的更新流程。通过异步请求最新资源清单文件,确保后续资源下载流程的准确性。以下是其实现逻辑的深度解析。
状态节点生命周期
• OnEnter()
进入状态时发送进度提示,并启动清单更新协程。通过事件系统通知 UI 更新进度信息。
void IStateNode.OnEnter()
{
PatchEventDefine.PatchStepsChange.SendEventMessage("更新资源清单!");
GameManager.Instance.StartCoroutine(UpdateManifest());
}
UpdateManifest 协程解析
该协程是清单更新的核心逻辑,包含以下关键步骤:
获取配置信息
从状态机黑板中读取包名称和版本号,这两个参数由前置状态FsmRequestPackageVersion
提供。private IEnumerator UpdateManifest() { // 从共享数据区获取包配置信息 var packageName = (string)_machine.GetBlackboardValue("PackageName"); var packageVersion = (string)_machine.GetBlackboardValue("PackageVersion"); // 获取目标资源包实例 var package = YooAssets.GetPackage(packageName); //... }
发起异步清单请求
调用UpdatePackageManifestAsync
方法,传入版本号进行清单更新。此处建议通过 YooAsset 配置超时机制增强健壮性。// 发起清单更新请求(建议超时机制通过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 创建资源下载器状态节点
该状态节点继承自 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