源设计起源 · 为什么要做这个模块
- 没它会怎样:代码会慢慢烂掉:一开始只是"播个音效"这种还能接受的样子。接着需求一个个来 —— 要等玩家点击(于是需要跨帧状态,只能存字段)、要加超时、要能取消、取消后还要恢复现场、还要并行做两件事……最后状态散在 5 个字段里,没人能一眼说清"现在走到哪、谁负责在什么时机清理"。
- 为什么不用协程:协程轻量,但它时间绑死引擎、取消没有收尾契约、没有并行与重复的语义,参数还只能靠闭包捕获 —— 多实例共享正是并发坑的源头。一旦流程要复用、要取消、要诊断、要被别的系统驱动,就需要更结实的结构。
- 早期(原体系)真实的疼:状态写在节点组件的字段上,同一节点被多个对象并发触发时互相覆盖;没有句柄,一条正在等待的序列既不能取消也不能查进度;纯线性,没有并行、没有等待秒数;节点还和具体业务硬耦合,没法单测。
- 换来了什么:把"定义"和"运行"分开、把跨帧状态收进每次运行自己的槽位、把取消与收尾变成必然会执行的契约 —— 于是"同一条序列并发跑两份"从一个 bug 变成了一件自然的事。
本篇只讲为什么;怎么用见同目录《使用说明》。
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 自动创建驱动者(零配置)
第三步:记住三条铁律
- 状态不存在步骤里,存在"本次运行"里 —— 自定义步骤用
GetOrCreateState<T>(run),不要在 lambda 里闭包捕获字段(并发会互踩,这正是原体系的老毛病); - 时间只认
Tick(deltaTime)—— 不读Time.time,所以能暂停、快进、脱离引擎单测; - 取消必须走收尾 —— 被
Stop/StopAll/ 宿主销毁时,OnCancel里的步骤会执行;耗资源的序列都该在这里回收。
第四步:上手地图(学的时候只需要认识第一层)
| 层 | 类型 | 什么时候会用到 |
|---|---|---|
| 核心 8 必学 |
RevSequence | 入口门面:RevSequence.Create("名字") |
RevSequenceBuilder | DSL:把步骤串起来(.Do / .Wait / .Parallel …) | |
RevSequenceDefinition | Build() 的产物 → 缓存到 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(池化友好)
- 为什么不用
Dictionary<步骤, 状态>:哈希查找 + 装箱;而"槽位号"是构建期常量,运行时一次数组下标访问(O(1)、零哈希); - 为什么归还池时要清空:否则下一条复用本实例的序列会读到上一条的状态对象("第一次跑对、第二次跑用了上次的数据",极难查)。
object[] 状态数组 ——
甲的状态变化不会动到乙。原体系把状态写在组件字段上,这种情况下必然互相覆盖。
3.3 决策三:并发策略是"配置",不是"约定"
原体系"多 actor 并发触发互相覆盖"是缺陷;这里把同一件事变成每条序列自己声明的策略:
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()))
原体系对应的是"靠节点 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;要两条流程就构建两条定义);步骤间数据管道(用局部变量或业务服务);区域/碰撞数学(只给触发源契约)。
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)) + 等演出步骤 |
README:57-67 点名)一律不迁移 —— 本框架只内建确定可用的通用步骤,
业务能力用"服务 + 自定义步骤"表达,哪个系统没实现一眼就能看出来(不会出现"节点排进去了但运行期什么都不做")。
4.3 八类业务场景的适配度
| 业务类型 | 适配度 | 说明 |
|---|---|---|
| 场景 / 次元演出(原体系主场) | ★★★★★ | 一一对应,且补了取消 / 并行 / 超时保护 |
| 新手引导(等点击 / 等进入 / 等条件) | ★★★★★ | WaitUntil + 超时正是引导要的;埋点在步骤里自己写(.Do) |
| 登录 / 加载 / 预热流程 | ★★★★★ | WaitTask 等异步 + Parallel 并行预热 |
| 自动化测试编排 | ★★★★★ | 时间可注入 → 手动 Tick 精确复现(限不用 RevTask 的序列) |
| 结算 / 领奖 / 抽奖演出 | ★★★★☆ | 顺序 + 等演出完 + 收尾回收;与 UI 系统配合 |
| 大厅 UI 序列(拍脸、弹窗串) | ★★★★☆ | 与弹窗队列系统配合最佳(框架不内建队列 / 频控) |
| 战斗 / 技能表现序列 | ★★★★☆ | 线性表现没问题;分支网络多时该用状态机 |
| 服务端指令流 / GM / 断线重连回放 | ★★★★☆ | 代码驱动天然适配;可手动 Tick 逐条执行 |
| 局内 AI / 战斗决策 | ★★☆☆☆ | 不该用它 —— 这是状态机 / 行为树的活 |
4.4 三条硬边界(诚实清单)
- 不内置业务动作库 —— 播特效 / 音效 / 切镜头 / 播 Timeline 都要业务自己写服务或步骤。原体系那些
SimpleAction*恰恰是"业务实现",硬塞回框架就又把业务和框架绑回去了(原体系 ⑥ 号疼点)。 - 不做状态网络 —— 没有条件跳转、没有"回退到第 3 步"这类图结构。序列管一条路走到底;要复杂决策请用状态机。
- 异步那条线仍依赖
RevTask,而它是引擎相关的(RevTask.cs2 处、RevTaskScheduler.cs5 处引擎调用)。准确表述:不用PlayAsync/WaitTask时内核是纯 C#;用了就把 RevTask 的引擎依赖带上。
4.5 三个压力测试("会不会被业务打穿")
| 压力 | 场景 | 结论 |
|---|---|---|
| ① 并发压力 | 两个玩家同时触发同一条序列 | 定义共享且只读,状态在各自的 Run.StepStates 槽里 → 不互踩。扛得住 |
| ② 取消压力 | 玩家切场景 / 掉线,此时 5 条序列正跑着(含嵌套、含生成的演出物件) | Dispose() / StopAll() → 每条走 OnCancel,嵌套子序列一起取消。扛得住(前提:清理写进了 OnCancel) |
| ③ 长流程 + 高频 | 几十步流程、上百条并发、每帧 Tick | 稳态 Tick 零分配;但有 3 处 O(活跃数) 线性开销(FindRun、非 Free 策略的每次 Play 扫描)→ 几百条以内无压力,上千条要改这三处 |
它不是一个"全功能流程引擎" —— 刻意不碰状态网络、不内置业务动作、不接管区域数学。这三条边界不是缺陷,而是"轻量化"的代价与选择。
判断标准一句话:能用"先 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 执行完,引擎同一帧就放行了下一步(不是"一步一帧")。
② 第 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);
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 五个必须避开的陷阱
- 在
DoBlocking谓词里闭包捕获字段(并发互踩)→ 用Step自定义步骤,或把状态放进业务服务。 - 把运行期数据塞进
.Publish(evt):它的载荷是构建期给定的,只适合常量事件;要带ctx.Source/ uid 就用.Do("广播", ctx => ctx.Events.Publish(new X(ctx.Source, uid)))。 - 创建了资源但不写
OnCancel:生成的演出物件、音频 Bank、推近的镜头都要在收尾里回收。 - 忘了 Tick / Tick 了两次:忘了 → "我 Play 了没反应";两次(场景里两个驱动全局引擎的组件)→ 所有序列速度翻倍、等待时间减半。框架会自动关闭第二个驱动者并告警。
- 反复
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 / 8 | NPC 特写开始 / 取消特写(看 OnCancel 如何强制恢复镜头与 HUD) |
9 | 火箭演出(Inspector 勾 Low Quality Device 即走简化演出) |
A / Q | 取消所有序列(都走收尾)/ 打印活跃序列数量 |
P | await PlayAsync(...) 体验"等一条序列跑完" |
O | 等"服务器回包"再发奖 —— 推荐写法(请求状态放业务服务 + 每帧问 → 定义可缓存复用) |
L | 等异步任务(WaitTask(ctx => 任务):任务运行时创建,清单可缓存、策略也有效) |
6.2 五个场景,逐个对照原体系
场景① 宝箱 → 等待点击 → 开门(原体系《05 配置与架构解析》39~55 行)
| 原体系 | 本框架 |
|---|---|
| 触发器球半径 2000mm + 触发次数 1 | new 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 一条序列 + 手动 Tick | PlayAsync / new RevSequenceRunner() + Tick(dt) |
7API 速查
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() |
RevSequenceRun | Handle、Definition、Context、Status、StepIndex、StepCount、CurrentStepName、Elapsed、Progress01、IsFinished、IsCancellationRequested |
RevSequenceContext | Source、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) |
RevSequenceEventBus | Subscribe<TEvent>(handler) → IDisposable;Publish<TEvent>(evt)(直接委托调用,不分配数组) |
7.5 自定义步骤:RevStepBase / 配套类型
| 类型 | 成员 |
|---|---|
RevStepBase | 必须实现 Name / Execute;可选重写 IsCompleted;状态槽 GetState<T> / SetState / GetOrCreateState<T>;LogStep(message) |
RevSequencePlayer | Default(全局默认引擎,零配置)、静态 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 成员会立刻报错)。
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.cs | Unity 驱动组件(全局 / 场景级引擎) |
配套材料
| 材料 | 位置 |
|---|---|
| 参考实现真实业务场景 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 差值) |
它继承原体系最值钱的内核(顺序执行 / 阻塞即挂起 / 跨帧续跑),修掉了原体系的四个结构性毛病(状态压在节点上 / 没有句柄 / 弱类型胶水 / 绑死场景与物理帧), 并把"业务"彻底赶了出去(服务注入 + 事件总线,框架零业务引用)。
它的边界也很清楚:管一条路走到底,不管状态之间的网络 —— 后者请交给状态机。