AsyncMixin · 回调顺序与独立作用域

理解并行加载与有序回调的区别,使用 FAsyncScope 分开取消任务,了解 AsyncEvent、开始完成通知和等待条件的当前边界。

本页内容6 节

加载可以同时进行,回调按序推进

连续排入 A、B 两个 AsyncLoad 时,两个加载请求都已交给 StreamableManager。Mixin 从队首推进步骤,遇到尚未完成的步骤就停下并等待;已完成的前序步骤立即执行回调,然后继续检查下一步。

因此,A 完成而 B 仍在加载时,A 的回调可以先执行,不必等整批资源全部完成。若 B 先完成,它的回调则要等 A 处理完。将 A、B 放在同一个路径数组中,才表示“一组路径完成后执行同一个回调”,见软引用加载。

B 先加载完成,业务回调仍然先 A 后 B时序
正在整理图表…
B 先加载完成,业务回调仍然先 A 后 B
查看图表源码
sequenceDiagram
  participant Host as 业务宿主
  participant Mixin as AsyncMixin
  participant Stream as StreamableManager
  Host->>Mixin: 排入 A
  Mixin->>Stream: 请求 A
  Host->>Mixin: 排入 B
  Mixin->>Stream: 请求 B
  Host->>Mixin: StartAsyncLoading
  Stream-->>Stream: B 已完成,A 尚未完成
  Note over Mixin,Stream: 队首 A 尚未完成,暂不执行 B 回调
  Stream-->>Mixin: A 完成
  Mixin-->>Host: A 的业务回调
  Mixin-->>Host: B 的业务回调
  Mixin-->>Host: OnFinishedLoading

这个顺序约束作用于回调。若 B 的请求必须等 A 回调拿到结果后才能确定,应在 A 回调中决定并排入 B;提前排入 B 不会把底层加载变为串行。回调可以追加新步骤,但要考虑已驻留资源导致的同步执行与重入,不在回调中直接销毁当前正在执行的宿主。

用 AsyncEvent 插入普通步骤

AsyncEvent 只添加一个有序回调,不加载资源,也不创建后台任务。它适合在前面若干加载步骤之后刷新列表、组合已取得的资源或发送本地完成通知。

下面代码放在已有 FAsyncMixin 派生宿主的成员函数中,FirstAsset / SecondAsset 是它的软资源配置,ApplyFirst / ApplySecond / RefreshPreview 是自己实现的业务方法:

AsyncLoad(FirstAsset, [this]()
{
	ApplyFirst();
});

AsyncLoad(SecondAsset, [this]()
{
	ApplySecond();
});

AsyncEvent([this]()
{
	RefreshPreview();
});

StartAsyncLoading();

执行顺序是 ApplyFirst → ApplySecond → RefreshPreview。事件只等待排在它前面的步骤;后面再追加的加载不属于它的前置条件。若前面的步骤已完成,RefreshPreview 可以在本次 StartAsyncLoading() 调用栈内执行,不能把 AsyncEvent 当作“延迟一帧”。

业务回调应保持短小。需要 CPU 计算、后台线程或网络工作时,使用对应任务能力,完成后按其线程约定回到游戏线程;AsyncMixin 本身不把 lambda 转移到工作线程。

按取消范围建立 FAsyncScope

同一个 Mixin 中,慢步骤会挡住它后面的回调,CancelAsyncLoading 也会取消这一整组。如果图标切换和面板加载应该分别替换,就给宿主保存两个 FAsyncScope 成员。

下面的 FManualPreviewLoads 是需要自己创建的原生 C++ 示例。它将两组请求分开:LoadIcon 只替换图标组,LoadPanel 只替换面板组,业务整体退出时调用 CancelAll。

ManualPreviewLoads.h:

#pragma once

#include "CoreMinimal.h"
#include "AsyncMixin.h"

/** 为两个独立取消范围保存加载状态,操作和销毁都在游戏线程。 */
class FManualPreviewLoads
{
public:
	/** 替换图标请求,OnLoaded 读取资源后需交给实际使用者保活。 */
	void LoadIcon(FSoftObjectPath Path, const FSimpleDelegate& OnLoaded);

	/** 面板资源完成后执行 OnLoaded,再推进不绑定资源的 OnReady。 */
	void LoadPanel(
		FSoftObjectPath Path,
		const FSimpleDelegate& OnLoaded,
		const FSimpleDelegate& OnReady);

	/** 业务退出时撤销两组尚未执行的步骤。 */
	void CancelAll();

private:
	FAsyncScope IconScope;
	FAsyncScope PanelScope;
};

ManualPreviewLoads.cpp:

#include "ManualPreviewLoads.h"

void FManualPreviewLoads::LoadIcon(
	FSoftObjectPath Path, const FSimpleDelegate& OnLoaded)
{
	IconScope.CancelAsyncLoading();
	IconScope.AsyncLoad(Path, OnLoaded);
	IconScope.StartAsyncLoading();
}

void FManualPreviewLoads::LoadPanel(
	FSoftObjectPath Path,
	const FSimpleDelegate& OnLoaded,
	const FSimpleDelegate& OnReady)
{
	PanelScope.CancelAsyncLoading();
	PanelScope.AsyncLoad(Path, OnLoaded);
	PanelScope.AsyncEvent(OnReady);
	PanelScope.StartAsyncLoading();
}

void FManualPreviewLoads::CancelAll()
{
	IconScope.CancelAsyncLoading();
	PanelScope.CancelAsyncLoading();
}

把此对象作为宿主成员保存,在业务退出时先 CancelAll,再拆除回调依赖的界面与数据。两个 Scope 之间不保证完成顺序,也不会因取消其中一个而撤销另一个;若两者本来有依赖,就应使用同一序列或显式建立依赖。

这里传入的委托并不会自动保护它捕获的其他对象。UObject 使用方可选择 FSimpleDelegate::CreateWeakLambda 或 CreateUObject,其他对象按自己的所有权约定处理。结果保活和实际控件销毁仍由使用方负责,完整资源示例见加载与清理。

开始、完成与状态查询

接口当前行为使用约束
StartAsyncLoading()取消自动启动计时并推进已排入的步骤已完成资源与空队列可同步通知;不等待未完成加载
OnStartedLoading()本批次开始推进时通知派生宿主适合打开加载状态;在同一调用栈内可能紧接着完成
OnFinishedLoading()全部当前步骤推进结束时通知派生宿主不代表每个资源成功;取消不会调用该钩子
IsAsyncLoadingInProgress()查询当前步骤是否仍在加载或尚有后续步骤不包含所有“等待回调”的情况,也不表示预加载句柄已经释放
CancelAsyncLoading()撤销本作用域未执行步骤并安排清理不调用完成钩子,不回滚已执行的业务副作用

IsAsyncLoadingInProgress() 可能在最后一个资源已经加载、但其业务回调尚未执行时返回 false;单个纯事件步骤也可能如此。不要用它代替“这批业务结果已经应用”或“所有资源已释放”的标记;需要界面状态时,以自己的开始/完成/主动取消路径维护。

取消后可以排入新批次,示例都先取消再排请求。没有显式启动时,CoreTicker 会自动推进,因此应在入队前完成依赖状态的准备,不把一个尚未调用 Start 的队列当作长期暂停的任务。

AsyncCondition 的机制与当前限制

插件还定义了 FAsyncCondition 和 AsyncCondition:条件返回 EAsyncConditionResult::TryAgain 时继续等待,返回 Complete 时推进。它检查当前条件,并在需要等待时通过 CoreTicker 约每 0.16 秒重试;不是逐帧检查,实际间隔也取决于游戏线程的推进。

条件可能在检查步骤和绑定完成委托时被多次求值,函数应只读取状态,避免在其中消耗资源、递增业务计数或执行不可重复的动作。它不自带超时、失败结果或网络等待协议;条件永不满足时,后续回调就会持续被挡住。

当前跨模块构造受限:本机头文件的 FAsyncCondition 构造/析构没有 ASYNCMIXIN_API 导出标记;现有 UnrealEditor-AsyncMixin.dll 也未导出它们,仅导出了 Mixin 接收条件对象的入口。不能把在 StarryGame 中直接 MakeShared<FAsyncCondition> 当作当前已经可用并验证过的用法。若后续确需接入,应先处理模块导出并验证链接;本轮保持插件源码原样。

当前项目源码没有查到该条件接口的实际使用,本页记录已有机制及模块边界,没有新增条件运行示例。条件对象可能被外部 TSharedPtr 保留;取消作用域只是释放自己的引用,外部保留者仍需管理它的寿命和所捕获的对象。

排查回调停滞或过期结果

  • 后面的资源已加载却没回调:检查同一作用域更早的资源或条件;按需拆分真正独立的任务链。
  • 切换目标后仍应用旧结果:替换整个请求时先取消对应 Scope;共享队列不能整体取消时,像指示器画布一样检查业务对象和当前归属。
  • 关闭界面后还有请求:确认主动退出路径取消了持久成员中的作用域,没有只等 UObject 最终析构,也没有把其他加载系统的请求误认为此 Scope 管理。
  • 定位推进顺序:启动参数可用 -LogCmds="LogAsyncMixin Verbose",结合请求路径、状态地址和步骤编号查看日志。日志记录请求与推进,不会替你判断业务对象仍否有效。

实现入口:插件 AsyncMixin.cpp 的 TryCompleteAsyncLoading、CompleteAsyncLoading、FAsyncStep 和 FAsyncCondition。本页按当前源码与现有 DLL 导出核对,没有进行 UE 编译、PIE 或多线程运行验收。

Starry手册导览从模块进入子模块与具体功能,查阅功能用途、配置步骤、技术架构和扩展方法。工程分层与系统协作了解 Framework、Gameplay 与通用插件的依赖方向,以及配置装配和运行状态的边界。模块总览与接入从行为系统、执行系统与机会调度了解 AstraAI,再为角色配置第一套组合决策。组合与覆盖正确区分优先级、行为分数与 Stack 顺序,按完整路径组织可撤销的配置。评分与抢占沿着条件、优先级、分数、持续有效性与冷却,解释一个行为为何开始或退出。Logic 与生命周期把动作接入 Start、Cancel、Cleanup,正确处理共享模板、蓝图实例与过期异步回调。BT、StateTree 与 HTN复用控制器上的树组件,正确传入数据、报告整树结果并等待安全退出。Objective 与机会生成把领域事实整理成当前机会,让评分只读数据,让生成器独立管理订阅。命令发布与领取发布一条命令后,经机会评估、Take、执行与 EndUse 完成一次受控使用。行为区域用可挂载的 Zone 组件分配行为层,并把回区机会与实际导航分开。调试与排查从初始化到候选、机会、应用层与后端,沿真实运行路径定位 AI 没有行动的原因。模块总览与使用了解通用调试工具的操作、远程连接与窗口扩展,再由各模块的排查说明定位具体问题。netImgui 远程面板在独立桌面程序查看调试窗口,区分远程 UI、游戏视口绘制与玩法网络权限。窗口扩展与属性绘制复用窗口生命周期、反射 Inspector 和场景绘制,避免为每个调试页重做基础设施。模块总览与接入从配置绑定、动作处理、通道缓存与玩家改键了解 Input,再接入角色的输入配置。绑定与配置生命周期理解跨 IMC 的共享绑定、配置所有权、延迟初始化,以及切换角色时的清理。Handler 与能力分派选择正确的输入阶段,把输入交给移动、GAS、GameplayEvent 或 UI,保持共享 Handler 无玩家状态。修饰器与双击选择输入值变换所在层,理解玩家设置修饰器与 Double Tap 的时序规则。通道输入缓存按 GameplayTag 通道开窗,保留首批或末批输入,正常关闭执行,中断取消丢弃。改键与玩家设置通过 GameSettings 统一编辑与应用,区分 SharedSettings 偏好和 Enhanced Input 键映射数据。调试与排查使用 Gamepad 与 Actions 观察和模拟输入,沿设备、映射、绑定、缓存与能力定位问题。模块总览与接入使用 UE 5.8 原生 Async Message System,把事件通知与接收方解耦,查阅消息收发、路由和 Starry 的实际接入。蓝图收发与解绑通过消息 Id、监听、载荷封装与解包的核心连线,掌握蓝图收发和任务解绑。C++ 收发与解绑按取得系统、登记监听、读取载荷、发送与解绑的顺序,查阅原生 C++ 接口用法。标签、时机与生命周期理解消息的标签层级、World 和 Endpoint 路由、游戏线程派发、载荷快照与 C++ 监听清理。项目消息与排查查阅 Starry 已接入的消息标签、载荷和触发点,区分本地通知、状态复制与 RPC 转发,并沿真实链路排查。模块总览与项目接入管理软引用加载、有序回调与任务作用域,了解指示器画布的实际接入,以及加载完成后的资源和宿主生命周期。软引用加载与 C++ 用法用组件加载软对象和控件类,保存加载结果,理解路径数组、同步回调、失败结果,以及重载与退出时的取消清理。主资产与资源驻留按主资产 ID 和 Bundle 预加载资源,理解句柄保留、取消与 GC 引用的关系,以及 AssetManager 和打包配置的职责。回调顺序与独立作用域理解并行加载与有序回调的区别,使用 FAsyncScope 分开取消任务,了解 AsyncEvent、开始完成通知和等待条件的当前边界。在文档中说明蓝图用法将真实蓝图节点嵌入正文,结合步骤、引脚和连线说明功能,节点文本随文章保存。维护这本手册本地文件与网页编辑共用一份 MDX,日常只改内容与关系,沿用共享版式和图表。