架构解析 · 含动画演示 · 含参考实现真实场景 Demo

动作序列 · 架构解析

覆盖:Assets\Revolution\Runtime\RevActionSequence\(Facade 接入入口 3 个 / Core 数据与基类 3 个 / Implementation 引擎内部 7 个 / Implementation\Steps 步骤库 6 个 / Interfaces 契约 2 个 / Support 宿主适配 1 个),另有 README.md = 3 分钟上手

本篇只讲"为什么":为什么这么设计、为什么这么写代码、每个决策换来了什么、代价是什么、边界画在哪。
想学怎么用 → 看同目录的《使用说明》,那里是手把手,本篇不重复。
面向小白:用大白话讲"不这么做会怎样",不复述代码。

源设计起源 · 为什么要做这个模块

一句话 游戏里总有一段"有先后顺序、要等外部条件、还可能被中途打断"的表现或流程;写久了就变成一堆散落字段和互相嵌套的回调。动作序列就是给这类代码一个结构。

本篇只讲为什么;怎么用见同目录《使用说明》。

0三句话速览(完整用法见《使用说明》)

第一步:它适合你的业务吗?

你的需求结论
"先 A 再 B,B 要等条件成立" —— 一串有先后顺序的表现/流程✅ 用它
"演出播完再进入下一步""资源加载完再继续"✅ 用它
"一个流程能被取消,而且取消后必须把现场恢复回来"✅ 用它(比原体系强的地方)
"要同时做两件事(音乐 + 演出)"✅ 用它(Parallel)
"状态之间互相跳转、有条件分支网络"❌ 用状态机(RevStateMachine)
"需要每帧持续决策(AI / 战斗状态)"❌ 用状态机
一句话记忆:序列管"一条路走到底",状态机管"我当前是什么状态"。

第二步:五行跑起来

// ① 构建一次(加载期),缓存到 static readonly —— 运行期零构建成本
private static readonly RevSequenceDefinition MyFlow =
    RevSequence.Create("演示\\一段流程")
        .Do("打个招呼", ctx => Debug.Log("你好"))
        .Wait(0.6f)                                          // 等 0.6 秒(用 Tick 的 dt,不读 Time.time)
        .WaitUntil("等条件成立", ctx => Ready(), 5f)          // 等条件(带 5 秒超时保护)
        .Publish(new MyEvent(1))                             // 发强类型事件(串联别的序列 / 通知系统)
        .Build();                                            // 构建会做静态校验,错误在启动时炸出来

// ② 播放(任何地方;不往场景里放任何东西也行)
RevSequencePlayer.Default.Play(MyFlow, source: gameObject);   // Default 自动创建驱动者(零配置)

第三步:记住三条铁律

  1. 状态不存在步骤里,存在"本次运行"里 —— 自定义步骤用 GetOrCreateState<T>(run),不要在 lambda 里闭包捕获字段(并发会互踩,这正是原体系的老毛病);
  2. 时间只认 Tick(deltaTime) —— 不读 Time.time,所以能暂停、快进、脱离引擎单测;
  3. 取消必须走收尾 —— 被 Stop / StopAll / 宿主销毁时,OnCancel 里的步骤会执行;耗资源的序列都该在这里回收。

第四步:上手地图(学的时候只需要认识第一层)

层类型什么时候会用到
核心 8
必学
RevSequence入口门面:RevSequence.Create("名字")
RevSequenceBuilderDSL:把步骤串起来(.Do / .Wait / .Parallel …)
RevSequenceDefinitionBuild() 的产物 → 缓存到 static readonly 即可复用 / 共享
RevSequenceRunner引擎:Play / Tick / StopAll / Dispose(+ Services / Events)
RevSequenceHandle句柄:取消与查询的唯一入口(Stop / IsValid / GetRun)
RevSequenceContext上下文:步骤里拿 Source / 服务 / 事件 / 时间(ctx.Get<T>())
RevSequenceConcurrency并发策略:同源重复触发怎么办(写第二条序列就会遇到)
RevStepBase自定义步骤基类(需要"本次运行私有状态"时用)
扩展 8
按需学
RevSequenceRun · RevSequenceStatus · RevISequenceStep · RevSequenceServices · RevSequenceEventBus · RevSequenceSubscription · RevITriggerSource · RevSequenceTriggerExtensions 进度查询、注册业务服务、事件串联、区域 / 事件触发 —— 用到再看
工具 4
查手册
RevSequencePlayer · RevTriggerSignal Unity 驱动、触发信号 —— 不影响上手
这张"三层地图"同时写在代码的入口文件头(Facade\RevSequence.cs 顶部;代码目录里还放了一份 README.md = 3 分钟上手,24 个文件里只有前 3 个需要读)—— 打开框架第一眼就能看到,不用翻文档。

第五步:常见需求 → 一行写法(背下这张表就会写 90% 的序列)

需求写法
等 2 秒.Wait(2f)
等 N 帧.WaitFrames(6)
等条件成立(带超时保护).WaitUntil("等点击", ctx => clicked, timeoutSeconds: 15f)
等加载 / 网络回包.WaitTask(ctx => 那个异步任务, "等服务器回包")(任务运行时才创建,清单可缓存)
马上做一件事.Do("播个音效", ctx => sound.Play("X"))
执行一次,之后每帧问是否完成.DoBlocking("等移动完", ctx => StartMove(), ctx => MoveDone)
同时做两件事.Parallel("音乐与演出", p => p.Do("音乐", …).Do("演出", …))
重复 N 轮.Repeat("闪三次", 3, r => r.Do("亮", …).WaitFrames(6))
串起另一条序列.Sequence("先播这个", OtherDefinition)
通知别的序列 / 业务系统.Publish(new MyEvent(1)) 或 ctx.Events.Publish(...)
取消时清理(回收 / 反注册).OnCancel(f => f.Do("回收", ctx => ...))
需要跨帧私有状态.Step(new MyStep())(自定义步骤,见 5.3)
取消一条序列handle.Stop()
查"现在卡在哪一步"在回调里自己记(框架不提供查询)
忘了 Tick 怎么办Unity 里用 RevSequencePlayer.Default(自动驱动,零配置)

1它解决什么问题

1.1 "流程代码"为什么会烂成面条

// 一开始长这样(可以接受)
PlaySound("Box_Appear");
// 然后需求来了:等玩家点了再开箱
_clicked = false; _waiting = true;         // ← 需要跨帧状态 → 只能存字段
// 再然后:要加超时、要能取消、取消后要恢复镜头、要并行播两件事…
// 最后:状态散在 5 个字段里,onUpdate/onComplete 回调互相嵌套,
//       没人能一眼说清"这个流程现在走到哪了、谁负责在什么时机清理"

动作序列就是给这类代码一个结构:把流程写成一棵可读的步骤列表,把跨帧状态交给引擎托管,把取消与收尾变成契约。

1.2 为什么不用协程

维度Unity 协程动作序列
时间来源yield return new WaitForSeconds(...) → 绑死引擎时间Tick(dt) 注入 → 可暂停/快进/单测
取消StopCoroutine(外部要记住句柄),没有收尾契约handle.Stop() + OnCancel 收尾(必然执行)
进度查询没有Progress01 / CurrentStepName / Status
组合嵌套 StartCoroutine,无并行/重复语义Parallel / Repeat / Sequence
复用参数靠闭包捕获(多实例共享,正是并发坑)定义不可变可复用;状态按运行实例隔离
脱离引擎不能内核可以(手动 Tick)
协程不是错,它是"轻量脚本级"的工具;一旦流程要复用、要取消、要诊断、要被别的系统驱动,就需要像动作序列这样的结构。

1.3 它和状态机怎么分工

动作序列状态机
关注点一条流程的推进(步骤 → 步骤)一个对象的当前形态(状态 → 状态)
典型业务开箱演出、进入场景、新手引导步骤、加载流程怪物 AI、Boss 阶段、UI 页面栈、玩法模式
结束有明确终点(跑完 / 被取消)通常没有终点(持续运行)
分支弱(写在步骤里,或构建两条序列)强(枚举 + 条件跳转)

实战里最常见的是"状态机管形态,序列管演出":Boss 进入二阶段时,先播登场演出(序列),演出结束再交割状态(状态机)。

2新版 vs 参考实现(动作序列执行系统)

2.1 总览

维度原体系本框架
序列怎么配Unity Inspector 拖组件(enter/exit/stay/listenEvent 四个列表,列表顺序即执行顺序)纯代码 DSL:RevSequence.Create(...).Do(...).Wait(...).Build()
序列是什么挂在 GameObject 上的一堆 MonoBehaviour 引用不可变的 RevSequenceDefinition(可 static readonly、可跨场景复用)
谁驱动DimensionTriggerService:每物理帧 × 每个 actor × 每个触发器宿主自己 Tick(dt)(RevSequencePlayer 可零配置代劳)
触发方式碰撞进入/离开/停留 + 系统事件 + 自定义字符串事件触发源契约(区域/事件/定时/手动)+ 手动 Play
节点数量15 个(其中 6 个实现已失效)3 个内置 + 3 个等待 + 4 个组合 + 1 个事件;业务能力靠服务注入
核心代码SimpleTrigger 约 500 行(触发+调度+执行三合一)24 文件 / 2561 行(注释 757 + 净代码 1383),拆成 Facade(接入入口)/ Core(数据与契约)/ Implementation(引擎内部)/ Implementation\Steps(步骤库)/ Interfaces(契约)/ Support(宿主适配)
取消无(没有句柄概念)handle.Stop() / StopAll() + OnCancel 收尾
等待只有"等玩家输入",没有等秒数/等条件Wait / WaitFrames / WaitUntil(带超时)/ WaitTask
并行 / 重复 / 嵌套纯线性序列,都没有Parallel / Repeat / Sequence
进度查询无("卡在哪一步"靠猜)Progress01 / CurrentStepName / Status / IsSuspended
事件字符串名 + 5 个通用参数槽(GameObject×4 + string×1)事件类型即键 + 泛型负载(强类型)
单测必须进 PlayMode(节点是 MonoBehaviour、时间靠 Time.time)内核可手动 Tick + 注入假服务

2.2 原体系那些"真实的疼"(本框架的设计动机)

#原体系的疼本框架怎么解
①状态写在节点组件字段上:SimpleActionWaitInput.m_inputTriggered 被多 actor 并发触发时互相覆盖(07:27)定义(不可变)与运行实例(状态槽)分离;状态按运行实例隔离
②多 actor 并发:谁后触发谁覆盖并发策略:Free / ReplacePerSource / RejectPerSource
③没有句柄:不能取消、不能查询(07:29)Play 返回 RevSequenceHandle(可 Stop / IsValid / 查进度)
④纯线性:无并行、无条件跳转、无等待秒数(02:204)Parallel / Repeat / Sequence + 四种等待
⑤弱类型胶水:param1~param5 + 字符串事件名,改名即断链(07:28)事件类型即键 + 泛型负载(重构时编译器报错)
⑥节点与业务硬耦合:SimpleActionPlaySound 直接 CSoundManager.GetInstance()(06:21)业务依赖走 context.Get<IService>();框架零业务引用
⑦绑死场景与物理帧:DimensionBaseWorld 硬编码在基类签名里(07:30)Tick(dt) 由调用方决定;框架不认识场景/世界
⑧触发器三职责合一(条件+调度+执行),生命周期耦合深(00:38)职责拆开:触发源(喊一声)/ 引擎(推进)/ 定义(内容)
⑨序列会卡死:MoveActorTo 的移动代码被注释后 m_moveCompleted 永不置真(05:113)阻塞判据由你写,但等待类步骤带超时保护(超时 = 放行 + 告警)
⑩空壳/失效节点:ChangeCamera/ShakeCamera/DollyZoom/SetActive/PlayTimelineOrAnimator 实现被注释或直接抛异常(README:57-67)步骤库只放确定可用的通用步骤;业务能力走服务注入(没实现一眼可见)
⑪每帧乘积复杂度:每物理帧 × 每 actor × 每触发器(00:84)只遍历活跃序列;区域检测留在业务侧
⑫不可版本化/不可 CI:序列是场景里的组件列表,改参数就是改资产序列是代码 → 可 Code Review、可 diff、可 CI
一句话总结这次改造 原体系是"编辑器 + 组件"驱动的演出系统,内核(顺序执行 / 阻塞即挂起 / 跨帧续跑)很值钱, 但把状态、生命周期、业务调用都压在了组件上,于是并发、取消、复用、单测全都难受。 本框架保留那三条内核,把"状态 / 生命周期 / 业务依赖"分别交给运行实例 / 句柄契约 / 服务注入。

3七个关键设计决策,以及为什么这么定

3.1 决策一:定义与运行分离(Definition / Run)

RevSequenceDefinition  ←  你写的"剧本":不可变、可缓存、可被无限次播放
        ↓ runner.Play(def, source)
RevSequenceRun         ←  这一次播放的"现场":步骤进度、状态槽、取消标记、句柄

原体系的组件既是定义又是状态(既描述"等输入",又用字段存"这次等到了没"),所以"同一条序列被两个 actor 同时触发"必然互相踩。拆开后:Definition 可以放进 static readonly,每次 Play 拿到的 Run 完全独立 —— "同一条序列并发跑两份"从一个 bug 变成了一件自然的事。

3.2 决策二:状态槽 —— 并发安全的落点

// RevStepBase 给自定义步骤的状态 API(业务侧唯一需要关心的)
protected T GetState<T>(RevSequenceRun run);              // 读
protected void SetState(RevSequenceRun run, object state); // 写
protected T GetOrCreateState<T>(RevSequenceRun run);      // 取不到就 new(池化友好)
微动画状态槽隔离:同一条序列并发跑两份,状态互不影响
Run A(玩家甲触发)
步骤① 状态槽飞到一半:t=0.18s
步骤② 状态槽(未使用)
Run B(玩家乙触发)
步骤① 状态槽飞到一半:t=0.08s
步骤② 状态槽(未使用)
两个运行实例共享同一个不可变的步骤对象,但各自持有一份 object[] 状态数组 —— 甲的状态变化不会动到乙。原体系把状态写在组件字段上,这种情况下必然互相覆盖。

3.3 决策三:并发策略是"配置",不是"约定"

原体系"多 actor 并发触发互相覆盖"是缺陷;这里把同一件事变成每条序列自己声明的策略:

动画 2同一个触发者重复触发同一条序列,三种策略三种结果
Free 自由并发 默认
触发 A两条并存
触发 B
来几次跑几次,互不影响 —— 每个怪各播各的受击表现
ReplacePerSource 同源顶替
触发 A✗ 被取消(作废)
触发 B✓ 新的跑完
连点采集只留最新一次;同一 NPC 换目标,旧演出立刻让位
RejectPerSource 同源拒绝
触发 A✓ 继续跑完
触发 B✗ 本次触发被忽略
宝箱只能开一次;演出期间忽略重复请求;防连点
关键点:判定"同一个触发者"靠的是 context.Source(Play(def, source) 传进去的那个对象)—— 所以传对 source 很重要:传玩家对象 = 每个玩家各跑一份;传 null = 所有人共用同一个"空触发者"(通常不是你想要的)。

3.4 决策四:时间注入(Tick(deltaTime)),而不是 Time.time

runner.Tick(Time.deltaTime);        // 正式运行:宿主每帧调
runner.Tick(0.016f);                // 单测:手动推进,精确到帧
runner.Tick(0f);                    // 暂停:一行就能做"时间静止"

换来三件事:① 可暂停/慢放/快进(不改业务代码);② 单测里"等 3 秒"= 循环调 30 次 Tick(0.1f);③ 与渲染帧率脱钩。

代价:谁 Tick 谁负责 —— 所以额外给了 RevSequencePlayer.Default(自动创建驱动者,零配置)来兜底"我 Play 了怎么没反应"这个最烦人的问题。

3.5 决策五:框架零业务依赖(服务容器 + 事件总线)

// 业务侧:定义接口 + 实现(框架完全不知道这些类型)
public interface IDemoSoundService { void PostEvent(string eventName, object emitter = null); }

// 序列里:只面向接口
.Do("播放出现音效", ctx => ctx.Get<IDemoSoundService>()?.PostEvent("Play_Box_Appear"))

// 组装时:显式注册(★ 泛型参数要写接口类型)
runner = new RevSequenceRunner(RevDemoServiceHub.CreateServices(hub));
依赖边界检查项实测结果
全框架真正的引擎调用0 行 —— Core\ / Implementation\ / Implementation\Steps\ / Interfaces\ 一个字都不碰引擎;只有宿主适配层 Support\RevSequencePlayer.cs 用 MonoBehaviour + 2 条 Debug 告警
框架里出现的业务类型0 个(不认识音效 / 特效 / 相机 / Timeline / 奖励)
LINQ / 反射实例化0 处(无 Activator.CreateInstance;typeof() 只作类型键)
引擎依赖目录只有 Support\(驱动组件 1 个文件)—— 其余目录完全不碰引擎

3.6 决策六:取消与收尾是契约,不是"自觉"

.OnCancel(finallySteps => finallySteps          // 被取消时必然执行(同步、一次性)
    .Do("回收生成物", ctx => ctx.Get<IEffectService>()?.Recycle(node))
    .Do("恢复镜头",   ctx => ctx.Get<ICameraService>()?.Restore()))
动画 3a两条结束路径:正常跑完 / 被取消 —— 都保证收尾
正常跑完
Do 音效› Wait 0.6s ✓ OnCompleted
所有步骤放行 → 完成回调(收尾步骤不执行)
跑到一半被取消(切场景 / 掉线 / StopAll)
Do 音效› Wait 0.6s› OnCancel 收尾
取消请求在下个 Tick 的步骤边界生效(不打断正在执行的步骤);嵌套子序列会被一起取消

原体系对应的是"靠节点 OnDisable 自觉兜底"(00:38)—— 漏一处就是资源泄漏或"镜头还推着"。

3.7 决策七:组合优于内建,以及刻意不做的东西

内建写法原体系
并行.Parallel("音乐与演出同时", p => p.Do(...).Wait(1f))❌ 没有
重复.Repeat("闪三次", 3, r => r.Do(...).WaitFrames(6))❌ 没有
嵌套.Sequence("先开宝箱", ChestOpen)❌ 没有
事件.Publish(new MyEvent(1)) / BindEvents<T>(...)字符串事件 + 5 个参数槽

刻意不做的三件事(保持轻量,避免变成"配置语言的方言"):条件分支/跳转步骤(分支就是步骤里的 if;要两条流程就构建两条定义);步骤间数据管道(用局部变量或业务服务);区域/碰撞数学(只给触发源契约)。

微动画Build 期静态校验:错误在启动时炸出来,而不是运行中途沉默失败
✗ 空序列 ✗ 步骤名为空 ✗ 并行分组为空 ✗ 子构建器单独 Build → Build() 直接抛异常,带上"哪条序列、第几步"
原体系是"把已注释掉的节点(例如 PlayTimelineOrAnimator)排进序列",运行到那一步才抛 NotImplementedException(05:114)。代码驱动 + 构建期校验,把这类问题提前到了启动阶段。

4通用性论证:能不能经得起真实业务的考验

4.1 六条判据(逐条给证据)

#判据判定证据
①零业务依赖优秀全框架 0 个业务类型引用;业务能力全部经 context.Get<T>() 注入
②脱离场景/编辑器优秀序列是代码:可 diff、可 Code Review、可 CI;不需要往场景里摆任何东西
③表达能力优秀原体系的超集:顺序 / 阻塞 / 秒·帧·条件·异步四种等待 / 并行 / 重复 / 嵌套 / 事件 / 取消 / 收尾
④并发安全优秀不可变定义 + 状态槽隔离 + 三种并发策略(修掉原体系的真实缺陷)
⑤可诊断良好步骤异常 / 等待超时 / 异步任务失败 / 收尾异常都经 RevLog(tag = ActionSequence)输出,并写明「哪条序列、第几步、步骤名」;另有三个回调(OnCompleted / OnCancelled / RunFinished)+ handle.IsValid + runner.ActiveCount
⑥性能可控良好只遍历活跃序列;运行实例与上下文池化 → 稳态 Tick 零分配;无 LINQ、无装箱

4.2 能力对照:原体系 15 个动作节点 → 现在怎么写

原体系节点本框架写法
SimpleActionPlaySound.Do("音效", ctx => ctx.Get<ISound>().PostEvent("Play_Box_Appear"))
SimpleActionSpawnObject服务 Spawn(...) + OnCancel 里 Recycle(...)
SimpleActionWaitInput.Step(new RevWaitClickStep()) 或 .WaitUntil("等点击", ctx => clicked, 15f)
SimpleActionSendEvent.Publish(new ChestOpenedEvent(...)) 或 .Do(ctx => ctx.Events.Publish(...))
SendCustomEvent / 滑门开关业务步骤里调业务服务(ctx.Get<IScene>().SetSlidingDoor(true))
ChangeCamera / DollyZoom / ShakeCamera.Do(ctx => ctx.Get<ICamera>().PushIn(...)) + .Wait(0.4f);镜头数学在业务系统里
MoveActorTo.DoBlocking("移动", ctx => StartMove(), ctx => MoveDone) 或自定义步骤
SetActive / PlayTimelineOrAnimator.Do(ctx => target.SetActive(true)) / .Do(ctx => tl.Play(path)) + 等演出步骤
原体系里 6 个失效节点(README:57-67 点名)一律不迁移 —— 本框架只内建确定可用的通用步骤, 业务能力用"服务 + 自定义步骤"表达,哪个系统没实现一眼就能看出来(不会出现"节点排进去了但运行期什么都不做")。

4.3 八类业务场景的适配度

业务类型适配度说明
场景 / 次元演出(原体系主场)★★★★★一一对应,且补了取消 / 并行 / 超时保护
新手引导(等点击 / 等进入 / 等条件)★★★★★WaitUntil + 超时正是引导要的;埋点在步骤里自己写(.Do)
登录 / 加载 / 预热流程★★★★★WaitTask 等异步 + Parallel 并行预热
自动化测试编排★★★★★时间可注入 → 手动 Tick 精确复现(限不用 RevTask 的序列)
结算 / 领奖 / 抽奖演出★★★★☆顺序 + 等演出完 + 收尾回收;与 UI 系统配合
大厅 UI 序列(拍脸、弹窗串)★★★★☆与弹窗队列系统配合最佳(框架不内建队列 / 频控)
战斗 / 技能表现序列★★★★☆线性表现没问题;分支网络多时该用状态机
服务端指令流 / GM / 断线重连回放★★★★☆代码驱动天然适配;可手动 Tick 逐条执行
局内 AI / 战斗决策★★☆☆☆不该用它 —— 这是状态机 / 行为树的活

4.4 三条硬边界(诚实清单)

  1. 不内置业务动作库 —— 播特效 / 音效 / 切镜头 / 播 Timeline 都要业务自己写服务或步骤。原体系那些 SimpleAction* 恰恰是"业务实现",硬塞回框架就又把业务和框架绑回去了(原体系 ⑥ 号疼点)。
  2. 不做状态网络 —— 没有条件跳转、没有"回退到第 3 步"这类图结构。序列管一条路走到底;要复杂决策请用状态机。
  3. 异步那条线仍依赖 RevTask,而它是引擎相关的(RevTask.cs 2 处、RevTaskScheduler.cs 5 处引擎调用)。准确表述:不用 PlayAsync / WaitTask 时内核是纯 C#;用了就把 RevTask 的引擎依赖带上。

4.5 三个压力测试("会不会被业务打穿")

压力场景结论
① 并发压力两个玩家同时触发同一条序列定义共享且只读,状态在各自的 Run.StepStates 槽里 → 不互踩。扛得住
② 取消压力玩家切场景 / 掉线,此时 5 条序列正跑着(含嵌套、含生成的演出物件)Dispose() / StopAll() → 每条走 OnCancel,嵌套子序列一起取消。扛得住(前提:清理写进了 OnCancel)
③ 长流程 + 高频几十步流程、上百条并发、每帧 Tick稳态 Tick 零分配;但有 3 处 O(活跃数) 线性开销(FindRun、非 Free 策略的每次 Play 扫描)→ 几百条以内无压力,上千条要改这三处
那么,"通用性够不够"的最终回答 够用,而且是原体系能力的超集 —— 表达能力更强(并行/重复/嵌套/四种等待/取消收尾)、依赖边界更干净(零业务引用、内核零引擎调用)、可诊断可测试(句柄 + 进度 + 手动 Tick + 服务注入)。
它不是一个"全功能流程引擎" —— 刻意不碰状态网络、不内置业务动作、不接管区域数学。这三条边界不是缺陷,而是"轻量化"的代价与选择。
判断标准一句话:能用"先 A 后 B、B 要等条件、中途可能取消"说清楚的,它合适;必须用"如果…就回到第 3 步"说清楚的,请用状态机。

5怎么用(从"能跑"到"写对")

5.1 三个概念,三种驱动方式

RevSequenceDefinition(剧本,不可变)  ←  用 RevSequence.Create(...).Build() 构建一次
        │  runner.Play(def, source)          source = 触发者(判定并发策略、日志定位)
        ▼
RevSequenceRun(这次播放的现场,池化复用)  ←  步骤进度 / 状态槽 / 取消标记 / 句柄
        │  runner.Tick(deltaTime)            每帧推进一步(谁 Tick 谁说了算)
        ▼
RevSequenceHandle(钥匙,值类型)          ←  handle.Stop() / IsValid(只答「还在不在跑」)
// ① 最省事(Unity,零配置):内部自动创建隐藏驱动者 + 全局默认引擎
RevSequencePlayer.Default.Play(MyFlow, source: gameObject);

// ② 场景级引擎:勾选组件上的"使用自己的引擎",随场景卸载一起收掉

// ③ 自己 new + 自己 Tick(单元测试 / 特殊时间源 / 多套隔离)
var runner = new RevSequenceRunner(services, events);
runner.Tick(0.016f);        // ← 时间由你决定:快进、慢放、暂停都是一行

5.2 步骤全集(DSL 一览)

写法是否阻塞用途
.Do("名字", ctx => ...)立即执行一句业务代码(音效、开面板、发奖励)
.DoBlocking("名字", execute, isCompleted)阻塞执行一次,之后每帧问 isCompleted
.Step(new MyStep())看实现自定义步骤(继承 RevStepBase);需要跨帧私有状态时用它
.Wait(0.6f)阻塞等固定秒数(Tick 的 dt 累计)
.WaitFrames(6)阻塞等固定帧数(= 至少再等 N 个 Tick)
.WaitUntil("名字", ctx => 条件, timeoutSeconds: 5f)阻塞等条件成立;带超时保护(超时 = 放行 + 告警)
.WaitTask(ctx => 任务, "名字")阻塞等异步任务(资源加载 / 网络回包)—— 传工厂,任务运行时创建
.Parallel("名字", p => ...)阻塞组内并行推进,全部完成才算这步完成
.Repeat("名字", 3, r => ...)阻塞组内步骤重复 N 轮
.Sequence("名字", otherDefinition)阻塞嵌套另一条序列(父被取消 → 子一起取消)
.Publish(new MyEvent(1))立即发强类型事件(载荷构建期固定)
.OnCancel(f => f.Do(...))收尾取消时执行的清理步骤(只允许立即步骤)
.OnCompleted / .OnCancelled回调结束时同步状态 / 上报
关于"等异步"的两种写法(都推荐,按业务形态挑)
场景写法
等"业务服务的状态"(pending / 结果由自己管).DoBlocking("等服务器确认", ctx => 发起请求(), ctx => 服务已完成()) 或 .WaitUntil(...)
等"一个异步任务本身"(资源加载 / 网络 API 的返回值).WaitTask(ctx => 那个任务, "等服务器回包")
两者都能缓存清单、都能让并发策略生效。关键点只有一个:任务 / 状态要在运行时获取, 所以 WaitTask 收的是工厂 ctx => RevTask,而不是任务本身。
动画 1序列推进:立即步骤同帧连放、阻塞步骤挂起、跨帧续跑
Do 音效立即
›
Do 开面板立即
›
Wait 0.6s阻塞
›
WaitUntil 条件阻塞
›
Do 发奖励立即

① 第 1 步 Do 执行完,引擎同一帧就放行了下一步(不是"一步一帧")。

② 第 2 步也是立即步骤 → 同帧连放;蓝色高亮 = 本帧刚执行过。

③ 第 3 步 Wait 是阻塞步骤:进度条 = 等待进度,序列挂起在此,每帧回来问一次"到了吗"。

④ 第 4 步 WaitUntil 同样挂起(条件是外部状态);条件成立 → 放行 → 第 5 步在本帧连放 → 完成。

这就是原体系那三条内核的现代版本:顺序执行 / 阻塞即挂起 / 跨帧续跑; 差别是"时间从哪来" —— 这里由 Tick(dt) 注入,所以能暂停、快进、脱离引擎单测。

5.3 自定义步骤:什么时候必须写

情况用什么
状态在业务服务里(播放器是否在播、加载是否完成、点击是否发生).WaitUntil(...) / .DoBlocking(...) 就够
需要本次运行私有的状态(本次飞行起点、本次计时、本次随机数)必须写 RevStepBase,用状态槽
// Demo 里的真实例子:灵果抛物线飞向玩家
public sealed class RevFlyParabolaStep : RevStepBase
{
    private sealed class State { public float Duration; }      // ← 本次运行私有的状态

    private readonly float _duration;

    public RevFlyParabolaStep(float duration = 0.35f) => _duration = duration;

    public override string Name => "抛物线飞向玩家";

    public override void Execute(RevSequenceRun run, RevSequenceContext context)
    {
        State state = GetOrCreateState<State>(run);            // ★ 状态进槽位,按运行实例隔离
        state.Duration = _duration;
    }

    public override bool IsCompleted(RevSequenceRun run, RevSequenceContext context)
        => run.Elapsed >= GetState<State>(run).Duration;        // ★ 时间从 run 取,不读 Time.time
}
反例(会踩并发坑) float _start; .DoBlocking("飞", ctx => _start = ctx.Elapsed, ctx => ctx.Elapsed - _start > 0.35f)
这里的 _start 是构建期闭包字段,两个玩家同时采集会互相覆盖 —— 这正是原体系 m_inputTriggered 的老毛病。

5.4 触发源与绑定

// ① 区域触发(对应原体系 enterActions):让 MonoBehaviour 实现触发源契约 —— 8 行接上 Unity 物理
public sealed class MyZone : MonoBehaviour, RevITriggerSource
{
    public string Name => "宝箱区域";
    public event Action<RevTriggerSignal> Triggered;
    public void Dispose() => Triggered = null;                    // 断开所有订阅
    private void OnTriggerEnter(Collider other)
        => Triggered?.Invoke(new RevTriggerSignal(other.gameObject, Name));
}
runner.Bind(zone, ChestOpenSequences.ChestOpen);      // 返回值 Dispose 即解绑

// ② 事件触发(对应原体系 listenEventActions)—— 一行绑定
runner.BindEvents<ChestOpenedEvent>(runner.Events, SlidingDoorOpen, e => e.Opener);

// ③ 手动触发(最直接,永远可用)
runner.Play(LingGuoDemoSequences.Gather, source: player);
动画 3b事件串联:序列 A 发事件 → 序列 B 被触发(强类型替代字符串事件)
宝箱序列:Do 音效 → 等点击 → 开箱 Publish(ChestOpenedEvent) → BindEvents<ChestOpenedEvent> → 滑门序列:音效 → 开门
原体系是 SendEvent("chest_opened")(字符串)+ 门触发器监听字符串(05:39-55)—— 字符串一改名就断链。 这里事件类型就是键,且 sourceSelector(e => e.Opener)决定"触发者是谁",从而决定并发策略按谁判定。

5.5 生命周期回调与三种结束路径

RevSequence.Create("带埋点的流程")
    .Do("第一步", ctx => ...)
    .OnCompleted(run => Analytics.End(run.ToString(), true))
    .OnCancelled(run => Analytics.End(run.ToString(), false))
    .OnCancel(f => f.Do("清理", ctx => ...))
    .Build();
结束方式触发条件是否执行 OnCancel 收尾
正常跑完所有步骤放行否(走 OnCompleted)
被取消handle.Stop() / StopAll() / 宿主 Dispose()是(走 OnCancelled)
步骤抛异常记异常日志(带序列名与步骤)→ 跑 OnCancel / Finally 收尾 → 按"取消"结束(PlayAsync 得到 false);调试包与正式包行为一致是

5.6 五个必须避开的陷阱

  1. 在 DoBlocking 谓词里闭包捕获字段(并发互踩)→ 用 Step 自定义步骤,或把状态放进业务服务。
  2. 把运行期数据塞进 .Publish(evt):它的载荷是构建期给定的,只适合常量事件;要带 ctx.Source / uid 就用 .Do("广播", ctx => ctx.Events.Publish(new X(ctx.Source, uid)))。
  3. 创建了资源但不写 OnCancel:生成的演出物件、音频 Bank、推近的镜头都要在收尾里回收。
  4. 忘了 Tick / Tick 了两次:忘了 → "我 Play 了没反应";两次(场景里两个驱动全局引擎的组件)→ 所有序列速度翻倍、等待时间减半。框架会自动关闭第二个驱动者并告警。
  5. 反复 Build():构建器是一次性的,重复调用会抛异常 —— 正确姿势是构建一次缓存到 static readonly。

5.7 可复用性:把序列做成"库"(四个做法)

做法怎么做收益
① 定义缓存private static readonly RevSequenceDefinition Flow = ...Build();零构建成本;并发策略才有效(策略判定"同一个定义实例")
② 子序列嵌套常用积木(开箱 / 开门 / 结算演出)各做成定义,用 .Sequence("先开箱", ChestOpen) 拼改一处积木,所有引用它的流程同时生效
③ 服务注入换实现序列只调 ctx.Get<IXxx>();真实现 / Fake / 录播各注册一套同一份序列能在单测、离线工具、真机上跑
④ 触发源与序列分离同一条定义既能被区域 Bind、也能被事件 BindEvents、也能手动 Play一条流程多处复用,不用复制步骤
// 一座"序列库":全部是 static readonly(加载期构建一次,运行期零成本)
internal static class GameSequences
{
    public static readonly RevSequenceDefinition ChestOpen = BuildChestOpen();
    public static readonly RevSequenceDefinition DoorOpen  = BuildDoorOpen();

    public static readonly RevSequenceDefinition BattleIntro =
        RevSequence.Create("战斗\\入场")
            .Sequence("先开箱", ChestOpen)          // ← 复用积木(父被取消时子一起取消)
            .Sequence("再开门", DoorOpen)
            .Do("播战斗音乐", ctx => ctx.Get<ISoundService>()?.PostEvent("Battle_BGM"))
            .Build();
}
⚠️ 反面写法(框架会告警):runner.Play(RevSequence.Create("X", ReplacePerSource).Do(...)) —— 每次触发都新建定义,于是 ① 每次都付构建成本;② ReplacePerSource / RejectPerSource 永远不命中(判定的是"同一个定义实例",而每次都是新的)。 开发期框架会给出一次告警;同理,声明了同源策略却用无 source 的 Play(def) 也会告警(策略会退化成"全局唯一")。

6参考实现真实业务场景 Demo(Assets\Revolution.Demo\RevActionSequence.Demo\)

6.1 目录与怎么跑

RevActionSequence.Demo\              10 文件 / 1181 行
├── RevDemoServices.cs              业务服务接口 + 假实现(音效/特效/相机/Timeline/输入/奖励/场景)
├── RevDemoEvents.cs                强类型事件(替代 SendEvent + param1~param5)
├── RevDemoSteps.cs                 三个自定义步骤(等待点击 / 抛物线飞行 / 等演出播完)
├── RevDemoTriggerSource.cs         区域触发源(对应 enterActions / exitActions)
├── ChestOpenSequences.cs           场景① 宝箱 → 等待点击 → 开门(原体系唯一的完整样例)
├── LingGuoGatherSequences.cs       场景② 灵果采集(进出区域 + 并行表现 + 发奖 + 收尾回收)
├── FarmGiftBoxSequences.cs         场景③ 农场礼盒进入/离开(双向 + 防擦边抖动)
├── NpcCloseupSequence.cs           场景④ NPC 特写(推近 → 等对白 → 恢复;取消也恢复)
├── RocketTimelineSequence.cs       场景⑤ 火箭演出(预加载 → 并行 → 等播完 → 降级 → 回收)
└── RevActionSequenceDemo.cs        入口:挂到场景任意物体上,按键体验
按键演示内容
1玩家进入「宝箱区域」→ 宝箱序列开始(播出现音效 → 等点击)
空格模拟玩家点击 → 开箱 → 广播事件 → 自动触发「滑门打开」序列
2 / 4灵果进入 / 离开区域(对应 OnGatherZoneEnter / Exit)
3采集灵果 —— 连按体验 ReplacePerSource;Parallel 里音效与飞行同时进行
5 / 6农场礼盒进入 / 离开(离开带 0.2s 缓冲,防擦边闪烁)
7 / 8NPC 特写开始 / 取消特写(看 OnCancel 如何强制恢复镜头与 HUD)
9火箭演出(Inspector 勾 Low Quality Device 即走简化演出)
A / Q取消所有序列(都走收尾)/ 打印活跃序列数量
Pawait PlayAsync(...) 体验"等一条序列跑完"
O等"服务器回包"再发奖 —— 推荐写法(请求状态放业务服务 + 每帧问 → 定义可缓存复用)
L等异步任务(WaitTask(ctx => 任务):任务运行时创建,清单可缓存、策略也有效)

6.2 五个场景,逐个对照原体系

场景① 宝箱 → 等待点击 → 开门(原体系《05 配置与架构解析》39~55 行)

原体系本框架
触发器球半径 2000mm + 触发次数 1new RevDemoZoneTriggerSource("宝箱区域", radius: 2f) + RejectPerSource
enterActions = [PlaySound → WaitInput → SendEvent("chest_opened")].Do(音效).Step(new RevWaitClickStep()).Do(ctx => ctx.Events.Publish(...))
门触发器 listenEventActions = [SlidingDoorOpen]runner.BindEvents<ChestOpenedEvent>(runner.Events, SlidingDoorOpen, e => e.Opener)

演示的机制:触发源绑定;强类型事件替代字符串事件;事件触发另一条序列;嵌套写法(父被取消时子一起取消)。

场景② 灵果采集(原体系 DimensionObjectService.cs:585-666)

RevSequence.Create("次元\\灵果采集", RevSequenceConcurrency.ReplacePerSource)
    .Do("播放采集音效", ctx => Sound(ctx)?.PostEvent("Play_CiYuan_UI_LingGuo_Get"))
    .Parallel("消失表现与飞行动画同时进行", p => p                      // ← 原体系做不到(纯线性)
        .Do("播放消失音效", ctx => Sound(ctx)?.PostEvent("Play_CiYuan_UI_LingGuo_Disappear"))
        .Step(new RevFlyParabolaStep(duration: 0.35f)))
    .Do("发放灵果奖励", ctx => ctx.Get<IDemoRewardService>()?.Grant("LingGuo", 1, ctx.Source))
    .Do("广播采集事件", ctx => ctx.Events.Publish(new LingGuoGatheredEvent(ctx.Source, uid, id)))
    .OnCancel(f => f.Do("回收灵果物件", ctx => ...))                    // ← 原体系靠 OnDisable 自觉兜底
    .Build();

演示的机制:三种并发策略一起用;Parallel 并行;自定义步骤的状态隔离;取消收尾回收物件。

场景③ 农场礼盒进入/离开(原体系 DimensionObjectService.cs:360-431、1938-2011)

原体系在代码里 AddComponent<SimpleTrigger>() + 组两个 SimpleActionSendEvent(enter/exit)。 本框架两条序列 + 两个触发源事件。重点在"防擦边抖动":离开序列先 Wait(0.2f) 再关面板 —— 这类"时间上的缓冲"用序列表达最自然,用 if/else 写就会散成"延迟调用 + 取消调用"。

场景④ NPC 特写(原体系 DimensionNpcCamera.cs:44-143)

原体系用 LeanTween 推近 + 隐藏 HUD/Player/Doll,靠外部 onComplete 回调串联;中途切场景就容易留下"镜头还推着"的状态。 本框架:隐藏 HUD → 推近 → 等 0.35s → 播对白 → WaitUntil(等点击,超时 15s) → 恢复 → OnCancel 里再恢复一遍。

演示的机制:WaitUntil 的超时保护(对应原体系 MoveActorTo 永久阻塞事故 05:113);取消收尾是契约;两条结束路径各自埋点。

场景⑤ 火箭演出(原体系 DimensionSceneMgr.cs:2268-2403)

原体系:预加载 → 取用 → 播 Timeline → 回收,散在几个方法里;低画质设备替换材质走简化演出。 本框架一条序列从上到下读完,另有 ShortShow 走简化流程 —— 降级不是序列里的 if,而是"两条定义二选一"(更好读、也更好测)。

演示的机制:WaitUntil 等预加载(带超时);Parallel(音乐 ∥ 演出);自定义步骤等演出播完;OnCancel 停止并回收;业务判定(设备等级)留在业务侧。

6.3 Demo 刻意演示的 8 个机制

#机制出现在哪
1触发源绑定(区域 / 事件 / 手动三种入口)RevDemoTriggerSource + 入口 Awake
2三种并发策略的业务语义宝箱(Reject)/ 灵果采集(Replace)/ 提示(Free)
3强类型事件替代字符串事件 + 事件串联序列RevDemoEvents + BindEvents
4自定义步骤 + 状态槽隔离RevDemoSteps(抛物线飞行是重点)
5并行 / 嵌套灵果采集的 Parallel;ChestOpenAndDoorInline
6取消 + 收尾(含取消嵌套子序列)特写 / 火箭 / 灵果采集的 OnCancel
7等异步的两种写法:等业务服务状态(DoBlocking / WaitUntil)+ 等一个异步任务(WaitTask(ctx => 任务))—— 两种都能缓存清单GatherWithServerConfirm / WaitTaskDemo(按键 O / L)
8可 await 一条序列 + 手动 TickPlayAsync / new RevSequenceRunner() + Tick(dt)

7API 速查

按"学哪一层"看,见〇章的上手地图:核心 8(RevSequence / RevSequenceBuilder / RevSequenceDefinition / RevSequenceRunner / RevSequenceHandle / RevSequenceContext / RevSequenceConcurrency / RevStepBase)、 扩展 8(RevSequenceRun / RevSequenceStatus / RevISequenceStep / RevSequenceServices / RevSequenceEventBus / RevSequenceSubscription / RevITriggerSource / RevSequenceTriggerExtensions)、 工具 2(RevSequencePlayer / RevTriggerSignal)。 下面按功能分组列出全部成员。

7.1 构建:RevSequence / RevSequenceBuilder

分类成员
入口RevSequence.Create(name, concurrency = Free)
立即Do(name, body)、Log(message)、Publish<TEvent>(evt, name = null)
阻塞DoBlocking(name, execute, isCompleted)、Wait(seconds)、WaitFrames(frames)、WaitUntil(name, predicate, timeoutSeconds = 0)、WaitTask(ctx => 任务, name)
组合Parallel(name, build)、Repeat(name, times, build)、Sequence(name, nestedDefinition)
自定义Step(step) ← 加入继承 RevStepBase 的自定义步骤
回调OnCompleted、OnCancelled
收尾OnCancel(buildFinally)(只允许立即步骤)
产出Build() → RevSequenceDefinition(一次性,重复调用抛异常)

7.2 引擎:RevSequenceRunner

分类成员
构造new RevSequenceRunner(services = null, events = null)
播放Play(def, source = null)、Play(builder, source = null)、PlayAsync(def, source = null) → RevTask<bool>
控制Stop(handle, runFinally = true)、StopAll(runFinally = true)、Tick(deltaTime)、Clear()、Dispose()
查询无(不提供状态 / 进度查询);只有 Services、Events 两个引用
事件event RunFinished(任意序列结束时触发,用于统计 / 采样)

7.3 句柄 / 运行实例 / 上下文

类型公开成员
RevSequenceHandle(值类型)IsAssigned、IsValid、Stop(runFinally = true)、==/!=、ToString()
RevSequenceRunHandle、Definition、Context、Status、StepIndex、StepCount、CurrentStepName、Elapsed、Progress01、IsFinished、IsCancellationRequested
RevSequenceContextSource、Services、Events、Run、DeltaTime、Elapsed、IsCancellationRequested、Handle、Get<T>()
枚举RevSequenceStatus{Idle,Running,Completed,Cancelled}、RevSequenceConcurrency{Free,ReplacePerSource,RejectPerSource}

7.4 触发源 / 事件总线

类型说明
RevITriggerSource触发源契约(Name + event Triggered)—— 只有这一种写法:实现它(MonoBehaviour 也能实现)
RevTriggerSignal触发信号:Source(成为序列的 context.Source)+ Tag(日志用)
Bind(runner, source, definition)一行绑定"触发 → 播放",返回 IDisposable
BindEvents<TEvent>(runner, bus, definition, sourceSelector)一行绑定"事件 → 播放"(对应原体系 listenEventActions)
RevSequenceEventBusSubscribe<TEvent>(handler) → IDisposable;Publish<TEvent>(evt)(直接委托调用,不分配数组)

7.5 自定义步骤:RevStepBase / 配套类型

类型成员
RevStepBase必须实现 Name / Execute;可选重写 IsCompleted;状态槽 GetState<T> / SetState / GetOrCreateState<T>;LogStep(message)
RevSequencePlayerDefault(全局默认引擎,零配置)、静态 Play(...) / PlayAsync(...)
RevTask / RevCancellationToken(Source)异步与取消(来自 Runtime\RevTask\,WaitTask / PlayAsync 用它)

8验证结果(实测,含未做的部分)

验证项结果
Revolution.Runtime —— Debug 配置(临时补入未进 csproj 的动作序列文件后整包编译)0 错误 / 0 警告
同上 —— Release 配置(走异常隔离分支、[Conditional] 生效)0 错误(2 条 CS0436 来自既有 RevTask.cs 的 netstandard2.0 兼容补丁,与本模块无关)
Demo 独立程序集编译(用编译好的 Revolution.Runtime.dll + Unity 引用单独编译 Demo,等价于 Unity 里 Assembly-CSharp 的处境:只吃 public API)0 错误 / 0 条代码警告(另有 14 条 MSB3245 是引用清单里的 Plastic / VisualScripting 等第三方程序集未落地,与本项目无关)
静态检查(IDE 语言服务,整个 RevActionSequence\)无 error / warning

编译器验证覆盖了什么:DSL 全链路、泛型约束与类型键事件、async RevTask 的自定义 AsyncMethodBuilder 接入、#if UNITY_5_3_OR_NEWER 两条分支、自定义步骤的抽象成员与状态槽 API、触发源契约与绑定扩展、以及 public/internal 边界(Demo 用独立程序集编译,误用 internal 成员会立刻报错)。

还没做的验证(诚实说明) 工程外(无 UnityEngine)编译没有跑:内核自身引擎调用为 0,但 PlayAsync / WaitTask 依赖的 RevTask 是引擎相关的。
没有运行时行为断言,下面这些目前是"设计保证 + 代码审查",建议后续补工程外断言逐条验证:
  • 状态槽按运行实例隔离(两个运行实例同时跑同一条序列不串味);
  • 并发策略实际生效(Replace 取消旧的 / Reject 拒绝新的);
  • 取消路径一定执行 OnCancel(含 StopAll / Dispose / 嵌套父被取消);
  • 池化归还后状态被清空;
  • 稳态 Tick 零分配(可用 GC 计数断言)。

9关键文件索引

文件职责
README.md(代码目录内)3 分钟上手:三步走 + 读我顺序 + 三条铁律 + 排障清单(非编译文件)
Facade\RevSequence.cs ★唯一入口:RevSequence.Create(...) + 三条铁律 —— 24 个文件里,你只需要读 Core\ 的前 3 个
Facade\RevSequenceBuilder.cs常用 DSL 6 个:Do / Wait / WaitFrames / WaitUntil / OnCancel / Build
Facade\RevSequenceBuilder.Advanced.cs进阶 DSL(同一 partial 类,用到才查):并行 / 重复 / 嵌套 / 事件 / 等异步 / 自定义步骤 / 埋点
Core\RevSequenceContext.cs上下文:触发者 / 服务 / 事件 / 时间(步骤与宿主的唯一通道)
Core\RevSequenceStatus.cs状态枚举 + 并发策略枚举
Interfaces\RevISequenceStep.cs / RevStepBase.cs步骤契约;自定义步骤基类(状态槽 API)
以下都不用读:Implementation\ 引擎内部(要调优排查时再进) · Implementation\Steps\ 内置步骤库 · Interfaces\ 触发源契约 · Support\ 宿主适配
Implementation\RevSequenceRunner.cs执行引擎:Play / Stop / Tick / 池化 / 并发策略 / 异常边界(最大文件,470 行)
Implementation\RevSequenceDefinition.cs不可变蓝图:步骤数组、并发策略、生命周期回调、状态槽分配
Implementation\RevSequenceRun.cs运行实例:进度、状态槽数组、取消标记、句柄、子序列句柄
Implementation\RevSequenceHandle.cs句柄(值类型):可查询、可取消,结束后自动失效
Implementation\RevSequenceServices.cs服务容器:把业务依赖从框架里赶出去
Implementation\RevSequenceEventBus.cs强类型事件总线 + 订阅句柄
Implementation\Steps\RevWaitSteps.cs / RevCompositeSteps.cs / RevNestedStep.cs四种等待 / 并行与重复(含子步骤槽位分配)/ 嵌套序列
Interfaces\RevITriggerSource.cs / RevEventTriggerSource.cs触发源契约(唯一写法:实现接口);事件触发源 + Bind / BindEvents 一行绑定(可选层,可整块删)
Support\RevSequencePlayer.csUnity 驱动组件(全局 / 场景级引擎)

配套材料

材料位置
参考实现真实业务场景 Demo(本文第六章)Assets\Revolution.Demo\RevActionSequence.Demo\
异步与取消(WaitTask / PlayAsync 的底座)Runtime\RevTask\
状态机官方指南(同为"当前实现 vs 早期版本"对照文档)Revolution.Document\状态机\状态机架构解析.md / .html

10本期没做的 / 已知边界

项说明
运行时行为断言见第八章;建议下一步就补(并发隔离 / 取消收尾 / 池化不串味 / 零分配)
可视化编排
(明确不做)
代码是唯一入口:不提供编辑器编排、也不提供配置表编排。理由:原体系那套 Inspector 拖组件的代价是"序列不可 diff、不可 CI、状态压在组件上"(见 2.2 ①⑤⑫),这比"策划能自己配"更贵。
→ 策划要改表现时的替代路径:① 参数外置(步骤里读服务 / 配置表的参数,改数值不改代码);② 复用库(常用演出做成积木,用 .Sequence 拼装,新增流程不动步骤实现);③ 改代码走正常的 Code Review / 热更。
(本框架连"可视化查看"都没有:要看某条序列还在不在跑,用 handle.IsValid)
条件分支 / 跳转 / 数据管道刻意不做;需要图结构请用状态机。若未来有高频需求,优先做"子序列库 + 入口处选择",而不是给序列加 if
WaitTask<T>(带返回值的任务)RevTask 与 RevTask<T> 是两个独立结构体(无隐式转换),要等带返回值的结果需自己包一层 RevTaskCompletionSource(Demo 有标准写法)
内置区域检测(空间网格)只给契约;"高频穿越小触发器漏检"这类问题由业务的检测方案决定
全局时间缩放 / 暂停开关现在靠调用方传 Tick(dt * scale);可以加统一便捷入口
步骤级耗时统计框架不打日志:要定位"哪一步最耗时",请在步骤里自己采样(记 ctx.Elapsed 差值)
一句话总结:动作序列把"一段有先后、要等条件、可能被取消的流程"从"散落的字段与回调"变成了一个可读、可取消、可复用、可测试的对象。
它继承原体系最值钱的内核(顺序执行 / 阻塞即挂起 / 跨帧续跑),修掉了原体系的四个结构性毛病(状态压在节点上 / 没有句柄 / 弱类型胶水 / 绑死场景与物理帧), 并把"业务"彻底赶了出去(服务注入 + 事件总线,框架零业务引用)。
它的边界也很清楚:管一条路走到底,不管状态之间的网络 —— 后者请交给状态机。