Skip to content

PySTG 编辑器产品愿景(持续讨论稿) ​

状态:Living Draft / 持续讨论稿

首版日期:2026-08-09

当前修订:v0.3(2026-08-09)

目的:固定我们对“预想中的 PySTG 编辑器”的当前共识,并在后续讨论中持续修订。

1. 文档定位 ​

本文描述 PySTG 编辑器希望最终提供的产品体验、作者工作流和概念边界。

它回答的是:

  • 不同经验层级的作者怎样完成一段弹幕、一场 Boss 战和一整关;
  • 状态图、时间线、行为图、预设和脚本分别解决什么问题;
  • 内容、行为、工具怎样共享生命周期、类型系统和调试器;
  • Safe API、Runtime API、Engine API 怎样形成真正的能力边界;
  • 编辑器怎样通过渐进式展开服务新人和高级开发者。

本文不是:

  • 当前功能清单;
  • 实施顺序或里程碑承诺;
  • schema、ABI 或插件 API 的冻结规范;
  • 当前版本验收结论;
  • 对已有路线图完成记录的替代。

实施顺序、依赖关系和验收证据仍以 EDITOR_IMPLEMENTATION_TODO.md 为准;不可破坏的工程边界仍以 EDITOR_ARCHITECTURE.md 为准。本文中的未来概念只有在进入路线图、形成测试契约并通过相应验收后,才能被称为已交付能力。

2. 一句话愿景 ​

PySTG 应成为一套面向 STG 创作的渐进式 authoring platform:

新人从关卡骨架、时间线和可调预设开始,不写 Python 也能完成结构完整的道中与关底;高级作者可以逐级进入曲线、表达式、行为图和脚本;玩法与工具开发者可以添加新的可视化行为、调试信息和编辑器工具,而不要求普通项目直接接触引擎内部。

产品模型可以概括为:

text
关卡与阶段大纲
    + 时间线编排
    + 配方优先的弹幕创作
    + 可组合、可查看内部结构的预设
    + 类型化变量、事件与激活规则
    + 同一资源上的曲线 / 行为图 / 脚本扩展
    + 正式运行时预览与分层调试
    + Runtime 插件和编辑器工具扩展

3. 产品成功标准 ​

3.1 对新人 ​

一个第一次接触 PySTG 的作者应当能够:

  1. 从完整关卡、道中、Boss 战或单张符卡模板创建内容;
  2. 用阶段卡片组织关卡结构;
  3. 用时间线安排敌人波次、Boss 移动、弹幕、背景、音频和演出;
  4. 用预设和 Inspector 调整弹量、速度、间隔、颜色、路径和难度;
  5. 分别预览 Pattern、波次、阶段和完整关卡;
  6. 保存、关闭、重新打开并继续编辑;
  7. 在不打开行为图、不编写 Python 的情况下完成第一整关。

3.2 对进阶内容作者 ​

进阶作者应当能够在不推倒已有资源的前提下:

  • 把固定参数改成曲线、变量或受限表达式;
  • 查看预设的内部组成;
  • 局部替换某个行为;
  • 将配方展开为类型化行为图;
  • 封装自己的项目预设;
  • 使用统一的事件、取消、预览和调试能力。

3.3 对高级开发者 ​

高级开发者应当能够:

  • 添加新的行为类型、弹道、组件、碰撞能力或道具机制;
  • 为自定义行为提供 Inspector、画布控制柄、时间线片段和预览器;
  • 添加受约束的 Renderer Pass;
  • 添加资源类型、编译器贡献和外部事件适配器;
  • 让普通作者通过预设和参数使用这些能力,而不必理解其实现。

3.4 反向成功标准 ​

以下现象说明产品设计失败:

  • 新人必须先理解一张空白蓝图才能做出第一段弹幕;
  • 普通常见弹幕需要几十或上百个节点;
  • 稍复杂的需求只能放弃可视化资源并重写成脚本;
  • 时间线、行为图和脚本拥有互不兼容的生命周期或调试器;
  • 模板只会复制一坨不可维护的数据;
  • 编辑器预览与正式游戏运行时走不同的行为路径;
  • 为了扩展能力而把高密度子弹展开成节点或逐弹 Python 回调。

4. 目标用户与默认工作面 ​

用户角色主要任务默认工作面不应默认暴露
顶层内容作者道中、Boss 阶段、弹幕参数、演出与难度阶段大纲、时间线、预设、Inspector、正式预览引擎对象、渲染后端、底层池、插件注册
进阶内容作者自定义局部结构、变量关系、事件响应配方、曲线、表达式、行为图、调试器调度器内部、逐弹回调、裸渲染状态
玩法开发者新组件、新行为、新机制、新弹道Runtime API、行为 SDK、插件清单、性能工具随意修改调度器和后端内部
工具开发者Inspector、控制柄、轨道、预览器、导入器Tool 插件 API、注册表、命令系统绕过文档和 Undo/Redo 的直接修改
引擎开发者调度器、渲染后端、资源系统、编译器核心Engine API 和仓库内部模块面向普通项目的稳定兼容承诺

“顶层内容作者”不是能力不足的作者,而是选择只处理作品结构与表现、不开发新引擎语义的人。产品不应通过迫使他写脚本来弥补常用创作能力的缺失。

5. 两条正交的架构轴 ​

最终设计包含两条互相独立的轴。

5.1 产品职责轴:内容、行为、工具 ​

内容(Content) ​

内容由版本化数据和标准组件构成,可以通过阶段大纲、场景画布、时间线、配方和领域图编辑。

内容描述“作品是什么”,例如:

  • 一关有哪些阶段;
  • 某个阶段有哪些敌人波次;
  • 某张符卡引用哪个 Pattern;
  • 背景在什么时候过渡;
  • 某个参数绑定哪个变量。

行为(Behavior) ​

行为描述“某个效果怎样运行”,由数据化动作、协程式动作、编译后的批量运行数据和脚本扩展构成。

所有行为应共享:

  • 类型化输入和输出;
  • start / update / event / stop / cancel 生命周期;
  • 所有权和取消传播;
  • 变量、事件和资源引用协议;
  • 正式预览和调试协议;
  • 结构化错误和性能统计。

工具(Tool) ​

工具由编辑器插件构成,为内容和行为提供:

  • Inspector 编辑器;
  • 画布控制柄;
  • 专用预览器;
  • 时间线轨道和片段;
  • 搜索项和创建命令;
  • 导入器、验证器和诊断视图。

工具只能通过文档命令和注册协议修改作者资源,不能绕过 Undo/Redo 直接改内部状态。

5.2 信任轴:Safe API、Runtime API、Engine API ​

职责轴和信任轴不能混成一条继承链。一个自定义行为可以属于 Runtime 权限,同时由 Tool 插件提供编辑器控制柄,并以 Content 资源实例出现在普通作者的项目里。

Safe API:内容创作者 ​

Safe API 只提供经过授权、可验证、可取消的能力:

  • 批量发射 Pattern 或子弹;
  • 移动自己拥有或被显式授予的对象句柄;
  • 播放已注册的 res:// 资源;
  • 读取只读游戏状态快照;
  • 等待类型化事件;
  • 设置类型化局部变量;
  • 发出受 schema 约束的事件;
  • 创建受配额限制的稀疏行为实例。

Safe API 不返回 Player、Pool、Renderer、Manager 等引擎活对象,不允许任意文件、网络、进程和模块访问。

Runtime API:玩法开发者 ​

Runtime API 面向受信任项目代码,可以:

  • 创建自定义稀疏组件;
  • 注册事件和行为;
  • 定义新弹道或批量运动内核;
  • 查询碰撞系统或注册碰撞组件;
  • 创建自定义道具与机制;
  • 注册受约束的 Renderer Pass;
  • 为自定义行为提供编译器和调试快照。

Runtime 扩展可以是进程内 Python,但必须明确标记为受信任代码,并遵守版本化 SDK、资源所有权、线程和清理约定。

Engine API:引擎开发者 ​

Engine API 允许:

  • 修改调度器;
  • 修改渲染后端和渲染图;
  • 修改资源生命周期;
  • 添加底层资源类型;
  • 定义编辑器核心插件点;
  • 增加或修改编译器节点和运行时 IR。

Engine API 是内部接口,可以随引擎版本迁移;普通项目不应直接依赖它。

关于 Python 沙箱 ​

仅仅给 Python 脚本一个较小的 ctx 对象,不能构成安全沙箱。任意进程内 Python 仍可能导入模块、访问文件或接触内部对象。

因此未来的 Safe 行为应优先采用:

  • 数据化动作;
  • 白名单表达式;
  • 状态图和可暂停行为计划;
  • 经过验证的行为字节码或 DSL。

如果 Safe 层必须运行任意 Python,原则上应使用独立进程和能力代理;进程内 Python 默认属于受信任的 Runtime 层。

6. 作者看到的核心层级 ​

text
Project
└─ SceneDocument(保存、Undo/Redo 与编译边界)
   └─ Stage / Boss / Spell(语义所有者)
      └─ StateGraphSpec(分层状态结构)
         └─ StateSpec / Section
            ├─ Local Variables
            ├─ Timeline(编排)
            │  └─ Clip Definition
            │     └─ Pattern / Behavior / Resource
            ├─ Child StateGraph(可选)
            └─ Transitions(条件与事件)

StageFlow 和 PhaseFlow 只是在不同语义所有者下显示同一种 StateGraphSpec 的编辑器名称,不做成两种互不兼容的资源或运行时。ActivationSpec 等字段名仍是概念名,最终 schema 需要另行版本化。

6.1 状态图负责“现在处于哪个阶段” ​

状态图适合表达:

  • Intro、Normal、Spell、Enrage、End;
  • Boss 血量达到阈值后切换阶段;
  • 收到完成事件后转移;
  • 超时、击破、失败或玩家选择产生的分支;
  • 父状态退出时统一取消其内部行为。

状态图不负责描述每一发子弹,也不负责表现所有时间关键帧。

6.2 时间线负责“这个阶段内部怎样编排” ​

时间线适合表达:

  • 已知顺序、持续时间和重叠关系;
  • Boss 移动;
  • Pattern、音频、背景、UI 和演出片段;
  • 参数曲线;
  • 相对于标记或事件的延迟;
  • 某个条件满足后实例化一个预先创作的片段。

时间线不直接进入 Pattern 内部连线,也不修改作者文档来表示运行时重复实例。

6.3 行为图负责“一个效果内部怎样运行” ​

行为图适合表达:

  • 发射源、形状、瞄准、调度、运动和修饰器的组合;
  • 一个效果内部可复用的数据流和稀疏控制流;
  • 局部条件、事件响应和可替换实现;
  • 插件提供的高级行为节点。

行为图不是整个关卡的万能图。UI、背景、状态、时间线和弹幕保留各自的领域视图,只共享文档、类型、命令、变量、事件、预览和调试基础设施。

6.4 状态图与 SceneDocument 的持久化边界 ​

愿景层固定以下方向:

  • SceneDocument 继续作为一个 Stage、Boss 战或 Spell 的作者真源、保存边界、Undo/Redo 边界和编译入口;
  • StateGraphSpec 是 SceneDocument 内的版本化子结构,不另建一份与 Scene 竞争的关卡文档;
  • StateSpec 是带稳定 UUID 的内嵌子资源,默认不是散落在资源浏览器中的独立文件;
  • 每个 State 直接拥有自己的局部变量、局部时间线和转移,删除、复制、Undo/Redo 可以形成一个符合作者直觉的事务;
  • Composite State 可以拥有子状态图;进入父状态时进入其初始子状态,退出父状态时取消整个子树;
  • 转移默认只连接同级 State。子图通过声明的完成结果或类型化事件与父级交流,不跨层拉线;
  • Pattern、Behavior、UI、Background 和预设继续使用项目相对资源引用,State 只实例化或绑定它们;
  • 跨项目复用阶段时,保存为 PhasePreset,而不是让另一个 Scene 直接引用本 Scene 内部 State 的 UUID。

现有 SceneDocument.tracks 在未来迁移到该结构时,应进入一个隐式的默认 State 或父级时间线,并保留 Track、Clip、Keyframe 的稳定 ID。没有实际文件大小、协作或加载性能证据前,不增加独立 FlowDocument、StateDocument 或 SequenceDocument 三套文件类型。以后若确实需要拆分,也应通过显式“提取为资源”完成,而不是改变默认新手流程。

7. 状态图、时间线和行为图怎样互相影响 ​

三者不直接跨画布拉长线,而通过三个中立协议交互:

text
类型化变量(数据)
类型化事件(离散信号)
激活与停止规则(生命周期)
text
State / Phase
    ├─ owns Timeline
    ├─ reads Variables
    └─ transitions on Conditions / Events

Timeline
    ├─ writes or animates declared Variables
    ├─ instantiates Behavior / Pattern resources
    └─ emits typed Events

Behavior Instance
    ├─ reads bound inputs and snapshots
    ├─ publishes declared outputs / Events
    └─ stops when its token or owner is cancelled

7.1 类型化变量表 ​

共享变量必须声明:

  • 名称和类型;
  • 默认值;
  • 作用域:项目、关卡、状态、片段或行为实例;
  • 谁可以写;
  • 是否可动画、是否进入回放记录;
  • 调试显示方式。

示例:

text
boss.hp_ratio          float    运行时只读
phase.enrage           bool     状态级
mapping.c              complex  状态级,可被时间线动画
difficulty.rank        float    关卡级
emitter.generated      int      行为输出,只读

时间线可以动画变量,行为可以读取绑定,状态图可以用变量判断转移。任何一方都不应通过取得另一方的内部对象来修改状态。

变量的运行所有权固定为:

作用域生命周期典型用途
Project Config启动时读取,运行期间默认只读难度、玩家选项、目标平台配置
Stage进入关卡时创建,跨 State 保留,关卡结束销毁rank、累计击破、分支选择
State每次进入 State 创建,退出时销毁阶段进度、局部计数、Enrage 标记
Clip / Reaction每个运行实例独立,完成或取消时销毁循环序号、触发计数、局部等待状态
Behavior每个行为实例独立生成进度、算法内部记忆
Engine Snapshot引擎每帧发布,只读HP、位置、碰撞结果、性能计数

作者文档只保存声明和默认值,不在停止预览时把运行值写回文档。跨局持久化属于 Save/Profile 系统,不伪装成普通内容变量。跨 State 传值必须使用 Stage 作用域变量或显式输出映射,不能依赖上一个 State 已销毁的局部内存。

写权限遵循“默认单一权威写入者”:

  • Engine Snapshot 只能读取;
  • 时间线只能写声明为 animatable 的变量或属性;
  • Safe Action 只能通过声明式 set / add / toggle / reset 命令写入获准目标;
  • Behavior 只能发布已声明的输出,不能取得全局字典后任意改值;
  • 同一帧可能存在多个写入者时,默认产生编译诊断,作者必须显式选择优先级或该类型支持的 reducer/blend;
  • 变量名、类型或作用域不兼容时,热重载不得猜测迁移。正式预览默认重建并确定性重放到当前帧。

7.2 类型化事件 ​

事件适合表达离散事实:

text
wave.completed
enemy.all_defeated
spell.declared
spell.started
mapping.finished
boss.defeated
background.transition_finished

事件需要 schema、来源、帧号和生命周期所有者。外部适配器也必须先规范化为类型化事件,不能从网络线程直接改游戏系统。

事件只描述“刚才发生了什么”,不携带对消费者的命令。例如对象行为可以发布 decoy.hit,但不应发布 start_timeline_clip("counter_burst")。事件生产者不需要知道状态图、时间线或背景系统将如何响应;声明式反应规则负责在它们之间完成匹配。对象与高密度弹幕的生命周期事件和反应模型见第 17 节。

7.3 激活规则 ​

一个时间线片段不应只有固定 start_frame,还应能声明概念上的激活规则:

  • 固定时间;
  • 相对父状态进入时间;
  • 相对某个标记或事件;
  • 条件从假变真的上升沿;
  • 条件保持为真期间持续运行;
  • 手动或事件触发;
  • 启动时计算一次的动态延迟。

条件触发不使用含糊的单个布尔开关,而提供明确模式:

  • on_rise:条件从假变真时启动一次,是新建条件片段的默认值;
  • while_true:条件为真时保持一个实例运行,变假时停止,不代表每帧重启;
  • on_fall:条件从真变假时启动一次;
  • on_change:值发生变化时启动,属于高级选项;
  • 类型化 Event 天然是离散脉冲,不再额外选择边沿模式。

默认条件规则是 on_rise + once_per_scope。需要重复命中反击、循环生成或保持运行时,作者必须从可读的策略字段中明确选择,而不是依靠图的连线形状猜测语义。

对应的停止规则包括:

  • 固定持续时间;
  • 条件变假;
  • 收到事件;
  • 行为自行完成;
  • 父状态退出;
  • 显式取消。

7.4 运行时实例策略 ​

“条件产生一个时间线内容”不意味着运行时改写作者文档。作者保存片段定义;条件满足时,调度器创建运行时实例。

片段应能声明:

  • once:只启动一次;
  • retrigger:每次上升沿重新启动;
  • restart:新触发替换旧实例;
  • parallel:允许并行实例;
  • max_instances:限制并发数;
  • cooldown:限制重触发频率。

作者片段 ID 和运行实例 ID 必须分开,调试器需要同时显示二者。

安全默认值是 once_per_scope、max_instances = 1,活动期间的新触发使用 ignore_while_running。restart 和 parallel 必须由作者或预设显式打开;允许并行时必须同时声明 max_instances。立即完成的无状态批量 Action 可以逐事件处理,但仍受同帧聚合与生成预算约束。

7.5 动态时间的确定性 ​

动态时间必须说明求值时刻:

  • delay = difficulty * 30:父状态进入或片段被调度时计算一次;
  • playback_rate <- rank:运行期间按明确边界持续采样;
  • start_when hp_ratio < 0.5:监听条件上升沿;
  • 已经发生的开始时间不能因变量改变而追溯移动。

默认情况下不允许一个表达式每帧重写已经过去的 start_frame,避免时间旅行和不可复现行为。

7.6 每帧执行顺序与事件可见性 ​

愿景层固定“事件不重入、下一固定帧可见”的语义:

  1. 在帧边界收集外部输入和上一帧发布的 Event Outbox,形成本帧只读 Inbox;
  2. 更新正式运行状态与 Engine Snapshot;
  3. 在同一快照上计算状态转移、激活条件和反应匹配;
  4. 先提交状态退出/进入与取消传播,再启动仍然有效的 Clip、Reaction 和 Behavior 实例;
  5. 采样时间线曲线、变量和属性轨道;
  6. 执行稀疏行为、Pattern 和批量运行数据;
  7. 将本帧新事实写入 Event Outbox,记录调试轨迹,供下一固定帧读取。

事件处理器发布的新事件不能在当前 dispatch 中再次触发处理器。这样一次死亡开花至少跨一个确定的调度边界,不会产生无法终止的同栈事件递归。若同一个事件同时导致状态转移和旧 State 内的反应,状态生命周期优先:退出 State 的新反应不会启动;需要保留的击破演出应写成 Exit Action 或下一 State 的 Entry Timeline。

外部输入只在帧边界规范化;网络线程不能获得“同帧立即修改游戏”的特殊通道。能静态识别的反应环在编译期报告;动态事件携带因果链信息并受目标运行配置的最大深度约束。

7.7 动态实例和冲突的可视化 ​

时间线始终区分作者定义与运行时实例:

  • 尚未触发的 ReactiveClip 显示为空心或虚线的武装区间;
  • 触发后在定义上叠加实际触发帧、原因和实例数量,不创建可保存的新 Clip;
  • restart 显示旧实例被取消和新实例接管的 trace;
  • parallel 以堆叠实例行或 ×N 徽标显示,选中后展开各自的开始帧、所有者和状态;
  • 被 cooldown、max_instances、Gate 或预算抑制的触发保留可检查标记,不能无声消失。

属性和变量冲突按 target + property 分组显示。未来新建内容的默认规则是“重叠写入需要显式解决”,编辑器在轨道重叠处显示条纹和诊断;作者可选择有顺序的 override、数值类型支持的 add / multiply / weighted blend,或调整作用域。现有 Scene 的确定性 last-wins 顺序作为兼容语义保留,但不作为新人新建内容的隐式默认值。

8. 新人的完整创作流程 ​

8.1 创建项目或资源 ​

新建入口首先询问作者想做什么:

  • 一段道中;
  • 一张符卡;
  • 一场 Boss 战;
  • 一整关;
  • 一个 Pattern 实验;
  • 一个背景或 UI 资源。

编辑器应从可运行骨架创建内容,而不是只提供空场景。

8.2 完整关卡模板 ​

标准关卡骨架可以是:

text
Opening
→ Wave A
→ Wave B
→ Midboss
→ Transition
→ Wave C
→ Boss Entrance
→ Boss Battle
→ Ending

作者先删减、重命名、重排阶段并选择默认背景和 BGM,而不是先连接底层节点。

8.3 道中开发 ​

作者点开一个 Wave 阶段后,看到局部时间线:

text
敌人波次    [左右妖精编队────────]
移动轨道    [入场]        [离场]
主要弹幕          [自机狙连射────]
背景        [云层加速────────────]
音效             [出现音效]

拖入“左右妖精编队”后,Inspector 暴露:

  • 敌人资源;
  • 数量和编队形状;
  • 入场与离场路径;
  • 生成间隔;
  • 攻击预设;
  • 掉落配置;
  • 启动与完成条件;
  • 难度缩放。

常见关系如“上一波全灭后等待 0.8 秒”应由激活面板完成,不要求打开行为图。

8.4 关底与符卡开发 ​

Boss 模板可以产生:

text
Entrance
→ Nonspell 1
→ Spell 1
→ Nonspell 2
→ Spell 2
→ Enrage
→ Defeat

每个阶段先编辑:

  • HP、时间限制和奖励;
  • 符卡名称;
  • 击破、超时与消弹策略;
  • Boss 初始位置;
  • 背景、BGM 和宣言;
  • 进入、完成和退出条件。

阶段内部时间线安排 Boss 移动、符卡宣言、主要弹幕、辅助弹幕、背景和音效。Pattern 先在配方模式编辑子弹、形状、瞄准、间隔、速度、旋转和修饰器;只有局部结构超出配方能力时才展开行为图。

8.5 预览阶梯 ​

作者应能从不同层级启动同一正式运行时:

  1. Pattern 预览;
  2. 单个时间线片段预览;
  3. 波次或阶段预览;
  4. Boss 多阶段预览;
  5. 从关卡标记开始;
  6. 完整关卡运行。

每种预览都应记录初始变量、随机种子、实际触发帧和资源版本,以便复现。

时间轴拖动和 Stage 级 seek 的正式语义固定为:

text
重置预览作用域
→ 从作用域入口按固定帧确定性重放
→ 到目标帧暂停
→ 用正式运行时快照更新编辑器

它不尝试从目标帧反推出此前的变量、随机数、事件、子弹或 Task 状态,也不要求作者维护另一套快照初始化数据。Pattern 或单片段预览从自身局部入口开始;阶段、Boss 和整关预览若依赖父级状态,就从相应父级入口重放。

重放期间,内部状态改变、事件和反应必须照常计算;音频播放、网络发送、文件写入等由引擎代理的外部副作用只更新逻辑状态而不实际重复执行,到目标帧后再恢复需要持续存在的 BGM/背景状态。Runtime 插件必须声明 replay policy;绕过能力代理自行产生外部副作用的插件被标记为不可重放。依赖真实外部输入的片段只有在输入已被记录时才能精确重放,否则调试器明确标记“不可复现输入”。

未来可以用确定性检查点缓存加速,但缓存必须与从头重放逐帧等价,失效时回退到完整重放。它只是透明性能优化,不改变 authoring 语义,也不成为当前短期要解决的结构问题。拖动过程中可以先显示作者曲线和静态姿态;只有收到正式运行时目标帧快照后,界面才能标记为 Runtime Preview。

9. 什么时候使用哪一种工具 ​

作者的问题首选工具
什么先发生、持续多久、怎样重叠时间线
当前处于哪个阶段、何时转移状态图
一次攻击内部怎样生成和变化Pattern 配方或行为图
参数随时间怎样变化曲线或属性轨道
参数怎样依赖游戏状态变量绑定或受限表达式
等待 HP、事件或上一波完成激活条件
多处复用一组结构预设或子行为
创造引擎还不认识的新算法语义Runtime 脚本或行为插件
创造新渲染阶段、资源或编译器能力Runtime / Engine 插件

时间线功能不足时,不应逼用户用行为图补洞;行为图存在也不意味着所有时间和状态关系都应塞进图里。

10. 渐进式展开 ​

同一资源应提供以下深度,而不是五套互不兼容的模式:

text
L0  预设卡片
L1  常用参数
L2  高级参数、曲线、变量和表达式
L3  内部时间线或行为图
L4  局部脚本 / Runtime 扩展源码
L5  Engine 内部实现

用户进入下一层时,不应丢失:

  • 资源 UUID;
  • 引用关系;
  • Undo/Redo 历史边界;
  • 正式预览入口;
  • 诊断定位;
  • 调试实例身份。

配方、曲线、行为图和可选 ScriptBehavior 仍是同一个资源的不同深度,不应变成互相漂移的副本。

11. 预设系统 ​

预设比继续增加节点数量更重要。大部分新人希望“找一个差不多的效果再调整”,而不是从原子节点开始编程。

11.1 预设层级 ​

Pattern 预设 ​

  • 自机狙;
  • 奇数弹、偶数弹;
  • 圆形开花;
  • 扇形扫射;
  • 单螺旋、双螺旋、交错螺旋;
  • 延迟转向、子弹分裂、速度层叠;
  • 波纹、米弹墙。

Wave 预设 ​

  • 左右夹击;
  • 编队入场;
  • 环绕入场;
  • 道中墙;
  • 精英敌人;
  • 中 Boss 过渡。

Phase 预设 ​

  • 普通攻击阶段;
  • 标准符卡阶段;
  • 狂暴阶段;
  • 耐久或超时阶段;
  • 击破演出阶段。

Stage 骨架 ​

  • 短道中;
  • 标准完整关卡;
  • Boss Rush;
  • 符卡练习;
  • 耐久关。

Background / Presentation 预设 ​

  • 道中滚动背景;
  • 符卡背景过渡;
  • 雾、扭曲、镜头推进;
  • 符卡宣言与 UI 演出。

11.2 预设的外部接口 ​

预设不是不可拆的模板。它应声明:

  • 稳定 ID 和版本;
  • 参数 schema、单位和默认值;
  • 资源插槽;
  • 启动与停止接口;
  • 输入变量;
  • 输出变量和事件;
  • 取消策略;
  • 内部行为图、时间线片段或资源引用;
  • 来源、标签、本地化名称和性能等级。

11.3 查看、覆盖与展开 ​

正式语义区分:

  1. 查看内部结构:显示虚拟展开,仍与预设关联;
  2. 覆盖参数或局部行为:保持预设身份,只替换公开插槽;
  3. 转为本地结构:物化内部节点和片段,明确脱离上游更新;
  4. 保存为项目预设:把本地结构重新封装为可复用资源。

无论折叠还是展开,最终都必须进入相同编译和正式运行时路径。不能出现“模板模式”和“展开模式”行为不一致。

“查看内部结构”和“转为本地结构”具有不同的文件语义:

  • 查看内部结构是只读虚拟视图,由“精确预设版本 + 实例参数 + 插槽覆盖”解析得到;预设内部节点不会复制进当前 Scene,编辑器只允许修改公开参数和插槽;
  • 虚拟节点使用由实例 ID 与预设内部 ID 派生的稳定调试身份,因此折叠和展开仍能对应同一个运行实例;
  • 转为本地结构是一个明确、可撤销的文档事务:把当前已解析结构物化到本地,生成当前文档命名空间中的稳定 ID,保留来源和 ID 映射作为 provenance,然后解除上游更新关系;
  • 转为本地后不会因为预设升级而偷偷重写。重新绑定预设属于另一个需要预览差异的显式操作;
  • 保存为项目预设则把本地结构重新封装为新的稳定预设 ID,不覆盖原上游包。

11.4 预设版本与迁移 ​

预设实例必须记录稳定预设 ID、精确解析版本、参数覆盖和插槽覆盖。项目依赖锁定到精确版本;即使是内置预设,也不因编辑器升级而静默改变已有弹幕。

升级流程固定为:

  1. 编辑器发现可用新版本并读取该预设声明的迁移链;
  2. 在临时副本上迁移公开参数 ID、类型和插槽,不依赖容易变化的内部节点位置;
  3. 显示结构差异、参数差异、诊断和正式预览对比;
  4. 作者确认后,以一个 Undo/Redo 事务更新实例与项目依赖锁;
  5. 迁移失败时继续保留旧版本,不能半升级或静默丢弃覆盖。

插件或预设包应能被项目 vendoring,以保证旧项目可重放。精确版本缺失时,文档以带诊断的降级模式打开并保留原数据,不自动转为本地结构,也不拿“最接近的新版本”猜测替代。

11.5 首发最小预设库与新手测试 ​

首发库不追求堆数量,而覆盖一条可以学习的组合路径:

  • Pattern:自机狙、奇偶数扇形、圆形开花、扇形扫射、单双螺旋、延迟转向、子弹分裂;
  • Wave:单编队入场、左右交替、编队自机狙、精英/中 Boss;
  • Phase:普通攻击、标准符卡、耐久、击破演出;
  • Stage 骨架:Pattern 实验、短道中、标准 Boss 战;
  • Background:滚动道中背景、符卡背景转场。

每个预设必须可直接运行、参数少而有意义、可以查看内部结构,并至少教会一个新概念。高级效果可以作为后续包增加,但不能挤占首次选择界面。

每轮可用性验证至少邀请 5 名没有 PySTG 经验的目标用户,不由维护者口头带做。首轮门槛建议为:至少 4 人能在 10 分钟内从模板得到并修改一个正式预览 Pattern;在 30 分钟内完成一个带“上一波全灭后继续”的短道中;在 60 分钟内完成两阶段 Boss、一次背景转场和一次事件反应,全程不写脚本。还要观察他们能否找到搜索入口、理解状态图与时间线边界、通过 Undo 恢复误操作,并从调参数自然进入一次局部展开。

测试记录完成时间、求助次数、无效点击、撤销次数、首次脚本冲动、诊断理解和正式预览性能。任何任务需要讲解“内部对象 ID”“运行时池”或插件注册,说明顶层流程仍然泄漏了引擎概念。

12. 快速搜索与创建入口 ​

编辑器的主要创建入口应是上下文感知搜索,而不是巨大菜单。

在任意作者上下文轻按空格,可以搜索:

text
开花
  圆形开花
  多层开花
  延迟开花
  自定义开花生成器

等待
  等待时间
  等待移动结束
  等待血量条件
  等待事件

搜索结果由统一的 Action Catalog 提供,每个条目声明:

  • 可出现的上下文;
  • 输入和输出类型;
  • 所需能力;
  • 创建或修改命令;
  • 中文名、别名、拼音和标签;
  • 来源:内置、项目或插件;
  • 帮助和性能提示。

上下文过滤示例:

  • 从 PointSet 端口拉线时,只显示接受点集的行为;
  • 在时间线中,只显示轨道、片段和编排动作;
  • 在场景画布中,只显示当前父节点允许创建的对象;
  • 在 Inspector 中,显示绑定、转曲线、重置和暴露为参数等操作。

所有搜索动作都必须生成正式 Command,并进入对应文档的 Undo/Redo 栈。

空格与画布平移的交互可以采用“轻按打开搜索、按住拖动平移”,或提供可配置替代快捷键;最终行为需要通过原生交互 QA 冻结。

13. 短期非目标:自然语言和 Agentic 生成 ​

短期与中期产品规划不考虑用自然语言或 Agentic Coding 生成弹幕、时间线或行为图,也不把模型服务作为编辑器依赖。

弹幕创作依赖精确帧数、角度、速度、数量、随机种子、生命周期和性能预算。自然语言容易隐藏默认值与歧义,生成后的大块结构还会增加审查成本,弱化“作者知道当前资源为何这样运行”的可调试性。

本阶段只保留确定性的上下文搜索:中文名、别名、拼音、标签、输入输出类型和当前位置共同筛选 Action、Preset、Track 与资源。搜索负责快速找到结构,Inspector、曲线、时间线和行为图负责精确编辑。

未来若重新评估自然语言,也必须建立在结构化 authoring 模型已经稳定、确有用户证据且生成结果能逐字段确认的前提上;这不是当前愿景对应路线图的承诺。

14. 行为扩展协议 ​

未来需要一个统一的 Behavior Descriptor 概念,把运行时行为和编辑器工具连接起来。概念上至少描述:

  • 稳定 ID 和版本;
  • 输入、输出和端口类型;
  • 参数 schema、单位和可绑定性;
  • 所需能力;
  • 执行类型:数据内核、稀疏控制器、Renderer Pass 等;
  • 生命周期和取消语义;
  • 编译器;
  • 调试快照 schema;
  • 可选编辑器贡献。

具体类名、字段和注册方式尚未冻结。

14.1 ComplexMapEmitter 示例 ​

高级开发者提供一个复映射发射器插件:

text
ComplexMapEmitter
输入:采样域、函数参数、采样数、颜色映射、触发与取消
输出:started、progress、completed、generated_count
Runtime:批量计算或合适的数据内核
Tool:采样区域控制柄、Inspector、预览叠层、搜索项、预设
Debug:状态、生成进度、CPU/GPU 耗时、当前函数参数

普通作者看到:

text
函数:z² + c
采样域:圆盘
采样数:256
映射方式:位置映射
颜色:arg(z)

高级作者可以进入源码,但两者操作的是同一个行为实例和同一套生命周期、类型与调试协议。

14.2 Runtime、Tool 与 schema 的插件包装 ​

一个扩展功能使用一个可安装、可锁定版本的插件包,避免作者分别寻找“运行插件”“编辑器插件”和“schema 插件”并自行匹配版本。包内贡献按职责分区:

text
Plugin Package
├─ manifest + dependency/version constraints
├─ schema + migrations
├─ runtime/compiler contributions
├─ editor/tool contributions(可选)
└─ assets/examples/presets(可选)

“一个包”不等于“一个拥有全部权限的模块”:

  • headless 游戏运行只加载 schema、compiler 和 Runtime 部分,绝不能因此导入 Qt;
  • 编辑器按需加载 Inspector、控制柄、轨道、搜索项和调试视图;
  • schema 与 migration 是纯数据/纯转换层,不依赖 Renderer 或编辑器实例;
  • Runtime 与 Tool 分别获得不同的注册上下文和能力声明,不能从 Tool 贡献取得 Engine 内部对象;
  • 各分区激活均为事务式,失败时回滚自己的贡献,卸载时按所有权清理;
  • 缺少 Tool 部分时可以退化为通用 Inspector;缺少必需 Runtime/Compiler 时必须产生可定位的编译错误,不能假装预览成功。

资源记录插件 ID、资源类型和兼容版本范围;项目依赖锁记录实际解析版本。这样 ComplexMapEmitter 的运行算法、参数 schema、迁移、控制柄和调试器可以一起发布,同时继续保持运行时与编辑器进程边界。

15. 脚本的使用边界 ​

顶层作者完成第一整关时,原则上不应写脚本。

以下需求不应要求脚本:

  • 等待敌人全灭;
  • 按 HP、时间、难度或事件触发;
  • 安排敌人波次;
  • 移动 Boss;
  • 播放音频;
  • 调整背景、摄像机、雾和滚动;
  • 用曲线或变量改变弹量和速度;
  • 组合已有分裂、转向、旋转和波纹行为。

脚本适用于:

  • 新的数学或生成算法;
  • 有内部记忆的稀疏控制器;
  • 新行为、新机制或新组件;
  • 新事件适配器;
  • 新背景算法、Shader 或 Renderer Pass;
  • 暂时无法由标准行为协议表达、但边界明确的高级扩展。

如果作者只是因为图太乱而写脚本,优先检查是否缺少子图、预设、表达式或领域工具;脚本不应成为修复编辑器表达力不足的默认出口。

15.1 Safe 层不新增通用 DSL ​

Safe 行为由版本化数据动作、状态图、时间线、预设、曲线、只包含 Safe 节点的类型化行为图、类型化变量/事件和受限表达式共同构成。短期不再设计一门能表达任意控制流的通用 Safe DSL,因为它会复制行为图和脚本的类型系统、调试器、迁移与学习成本。

若作者需要文本输入,文本只能是结构化属性或受限表达式的另一种编辑视图,不能成为第六套运行语义。新的常见需求应优先沉淀成 Action、Preset、Reaction 或领域专用控件;真正的新算法进入 Runtime Behavior。

15.2 不提供进程内 Safe Python ​

当前和未来默认都把进程内 Python 视为受信任的 Runtime API。限制一个 ctx 对象不能阻止模块导入、文件访问、全局状态或进程级副作用,因此不能称为沙箱。

短期不交付 Safe Python。若未来出现真实的非受信代码需求,只能采用独立进程、版本化 IPC、能力代理、时间/内存限制和批量命令协议;其热重载默认重建进程与局部状态,不承诺任意 Python 对象迁移。该隔离执行器是独立安全项目,不是顶层内容创作的必经路径。

16. 背景工作流与钩子 ​

普通作者看到的背景应是可编排资源,而不是裸 renderer callback。

16.1 三种常见关系 ​

时间片段 ​

text
0–2 秒:从道中背景过渡到符卡背景
2–20 秒:缓慢旋转和推进
20–22 秒:恢复并淡出

连续绑定 ​

text
fog.density       <- boss.enrage_level
camera.rotation   <- phase_progress * 8
warp.intensity    <- 1 - boss.hp_ratio

一次性事件 cue ​

text
spell.started     -> 播放符卡背景转场
spell.ended       -> 恢复默认背景
boss.defeated     -> 停止扭曲并淡出

16.2 生命周期事件 ​

可选择的正式事件可以包括:

text
stage.enter
section.enter
section.exit
boss.enter
spell.declare
spell.start
spell.end
boss.defeated

作者应在激活面板中选择这些事件,而不是默认编写 on_spell_start()。只有创造新的渲染算法、Shader、程序化几何或 Renderer Pass 时,才进入 Runtime 插件层。

16.3 Renderer Pass 边界 ​

普通内容只实例化 Pass 预设、绑定输入并动画公开参数。Runtime Renderer Pass 通过后端中立的 PassDescriptor 进入 Render Graph,而不是接收一个可以任意修改全局 GL 状态的裸回调。

Descriptor 至少声明:

  • 输入和输出附件;
  • 格式、尺寸、采样和 load/store 要求;
  • 执行阶段、依赖、排序和是否允许与其他 Pass 合并;
  • 参数 schema、可动画字段和默认值;
  • 临时资源所有权、生命周期和清理;
  • 支持的后端、所需 capability 和可选显式 fallback;
  • CPU/GPU 性能预算和调试标签。

编译器验证 Render Graph 的依赖环、读写 hazard、格式兼容和能力要求,再由各后端 adapter 生成实际命令。插件只获得受限 Pass Context、Command Encoder 和作用域化资源 lease;不能访问或污染全局 GL 状态,也不能持有超出生命周期的引擎资源。

不支持目标后端时必须在编译/导出阶段报错。只有插件显式声明且效果语义可接受时,才允许 pass-through 或替代实现,不能静默关闭效果。Tool 贡献可以为同一 Descriptor 增加 Inspector、缩略预览和画布控制柄,但不能另建一条与正式 Render Graph 不同的 Qt 效果实现并宣称 parity。

17. 事件反应、生命周期、所有权与取消 ​

时间线与行为图的边界固定为:

行为、碰撞和对象生命周期发布已经发生的事实;反应规则描述事实发生后要做什么;时间线决定某条阶段性反应规则在什么时候有效;任务作用域负责反应启动后的等待、循环、子任务和收尾。

因此,时间线上可以放置“等待触发的生命周期内容”,但作者文档保存的是一个处于特定时间窗口的反应定义,而不是等待蓝图直接调用的片段入口。行为图不通过片段 ID 操纵时间线;它发布类型化事实,调度器再匹配当前已武装的声明式反应。

17.1 四个核心概念 ​

LifecycleEvent:刚才发生了什么 ​

LifecycleEvent 是运行时在确定调度边界产生的不可变事实。它至少需要携带事件类型、来源、所有者、正式帧、有效载荷和必要的终止原因。

例如:

text
bullet.terminated(reason=expired, position=...)
emitter.completed(generated=256)
decoy.hit(source=player_bullet, damage=3)
enemy.defeated(enemy_tag=midboss_guard)

事件本身没有动作,也不知道谁会处理它。高密度子弹的生命周期事件在语义上仍是事件,但其运行表示必须允许批量化,不能等同于为每发子弹创建一个 Python Event 对象。

ReactionSpec:发生后怎样反应 ​

ReactionSpec 是可序列化、可检查、可调试的声明式规则:

text
Trigger + Filter + Gate + Policy + Action + Scope
  • Trigger:监听哪一种事实;
  • Filter:限定来源、标签、终止原因、攻击来源或其他载荷;
  • Gate:当前变量、状态或作用域条件是否允许反应;
  • Policy:逐个、同帧首次、同帧计数、去抖、冷却和并发限制;
  • Action:启动哪个 Pattern、行为、预设、背景 cue 或变量命令;
  • Scope:反应归谁所有,何时随父级取消。

如果一种反应是对象无论在哪里复用都具有的固有能力,例如“这种子弹自然到期时一定开花”,ReactionSpec 应随子弹行为资源或预设保存。如果反应只属于某一符卡、阶段或时间窗口,则由时间线上的 ReactiveClip 持有。如果事件意味着整个关卡阶段发生变化,则由状态图声明转移,而不是把转场偷偷藏在敌人蓝图里。

ReactiveClip:什么时候允许这条规则生效 ​

ReactiveClip 是时间线上的“武装反应区域”。它不规定事件必然在某个绝对时间发生,而是声明:在这个阶段或时间窗口内,如果指定事件到来且过滤与门控通过,就按给定策略启动动作。

text
8.0s  [──────── 假 Boss 受击反击:Armed ────────]  20.0s
              监听 decoy.hit
              过滤 target=decoy, source=player_bullet
              策略 count_per_frame, cooldown=6f
              动作 CounterBurst

它可以覆盖固定区间,也可以覆盖整个父状态,或由另一个事件/条件武装和解除。作者文档始终只保存定义;真正触发时,调度器创建运行时反应实例并在时间线上留下 trace 标记,不把重复实例写回文档。

这正是“可触发生命周期”的推荐形态,但需要把三件事分开显示:

  1. 片段是否已武装;
  2. 哪个事实真正触发了它;
  3. 启动后的行为实例现在运行到哪里。

TaskScope:启动后谁负责收尾 ​

TaskScope 管理一次反应启动后的跨帧工作,包括等待、循环、订阅、子行为、临时对象和取消传播。它相当于把 LuaSTG 风格 Task 中有价值的顺序表达保留下来,同时补上明确的所有权和结构化取消。

text
PhaseScope
└─ ClipScope
   └─ ReactionScope
      └─ BehaviorScope

立即完成的单帧动作不需要常驻协程;只要稀疏编排动作跨越多个调度边界,就必须进入可追踪的 TaskScope。父级退出后,所有等待和子任务都应被统一取消,不能继续在已结束的符卡之外发弹或切换背景。

TaskScope 不是“每发子弹一个协程”。大量子弹的延迟转向、寿命和分裂倒计时应编译为池内批量状态或批量调度命令,TaskScope 只拥有这一批行为。以 LuaSTG 的常见概念作近似映射:Task 对应稀疏的 TaskBehavior / TaskScope,宏精灵对应可复用的 ActorPreset,宏子弹对应编译到正式弹池的 BulletArchetype;三者共享事件和所有权协议,但运行密度不同。

四者的最短判断口诀是:

概念回答的问题作者通常在哪里看到
LifecycleEvent刚才发生了什么?事件监视器、对象/行为输出
ReactionSpec发生后怎样反应?预设的反应槽、Inspector、行为资源
ReactiveClip什么时候这条规则有效?阶段时间线
TaskScope启动后谁负责等待和收尾?运行时层级与调试器

17.2 具体弹幕与关卡例子 ​

例一:子弹自然结束后死亡开花 ​

一个“死亡开花弹”预设可以声明:

text
Trigger: bullet.terminated
Filter:  reason == expired
Policy:  each
Action:  在终止位置生成 12 发小环
Scope:   bullet owner / current phase

如果这种子弹在任何地方自然结束都应该开花,规则随预设保存,作者只调整数量、速度和颜色。如果只有符卡后半段允许开花,子弹仍然只发布 bullet.terminated,时间线后半段的 ReactiveClip 再按 owner tag、子弹类型和终止原因监听它。不要让子弹蓝图查找并启动“后半段开花片段”。

阶段统一消弹产生的是 phase_cleared,默认不会被“自然结束”过滤器接受,因此不会在符卡结束时意外产生全屏二次开花。

例二:假 Boss 被玩家子弹命中后过量反击 ​

碰撞系统或假 Boss 行为发布:

text
decoy.hit(target=decoy_1, source=player_bullet, position=..., damage=...)

假 Boss 所在阶段的时间线上放置一个 ReactiveClip:

  • 只在假 Boss 可见且可受击的区间武装;
  • 过滤 target=decoy_1 和 source=player_bullet;
  • 将同一帧的多次命中聚合为一次或一个计数;
  • 应用冷却、并发数和每帧生成预算;
  • 启动 CounterBurst Pattern,并把它放入当前 ReactionScope。

这样,碰撞与对象行为只负责报告“假 Boss 被打了”,阶段编排负责“这个时期是否反击、反击多凶”。退出该阶段时,尚未完成的延迟反击随 PhaseScope 一起取消。

例三:击破敌人后切换背景 Scene ​

这里要先判断它是局部演出,还是阶段语义:

  • 只是这一小段战斗的视觉 cue:时间线上的 ReactiveClip 监听 enemy.defeated 或 encounter.cleared,在背景轨道启动转场;
  • 击破意味着进入下一阶段:状态图监听该事件并发生状态转移,新状态自己的时间线负责背景、BGM、Boss 移动和下一轮弹幕;
  • 敌人蓝图只发布击破事实,不直接调用 set_background_scene(),也不持有背景轨道或片段 ID。

前者属于阶段内编排,后者属于阶段边界。两种做法都能在顶层视图看到原因和实际触发帧。

例四:三段延迟开花 ​

一次终止事件触发 ReactionSpec 后,TaskScope 可以表达:

text
立即生成第一圈
等待 12 帧
生成第二圈
等待 12 帧
生成第三圈
完成

“为什么开始”由事件与反应规则回答,“开始后怎样按顺序运行”由行为和 TaskScope 回答。若符卡在第二次等待期间结束,取消沿所有权链传播,第三圈不会脱离阶段继续生成。

17.3 预设怎样承载和展开反应 ​

预设不应把钩子封死在不可见脚本中,而应暴露类型化的反应槽。例如 DeathBloomBullet 可以向顶层作者提供:

text
死亡效果:圆形开花
触发原因:自然到期
数量:12
延迟:0f
仅在时间线片段内启用:可选

顶层作者可以只调这些参数;选择“仅在时间线片段内启用”时,编辑器可在当前时间线创建或绑定一个 ReactiveClip。展开预设后,进阶作者看到的是:

text
BulletArchetype
└─ ReactionSlot: on_terminated
   ├─ Filter: reason == expired
   ├─ Policy: each
   └─ Action: BloomPattern

作者可以局部替换过滤器、策略或动作,而不必把整个预设推倒重写。高级开发者也可以用 Runtime API 提供新的 Action 或可视化反应类型,但它们仍遵守同一事件、作用域和调试协议。

17.4 终止原因必须是正式语义 ​

“死亡”不能只有一个含糊的布尔值。愿景层至少必须区分下列语义;最终枚举名和 payload 由实现时的 schema 契约冻结:

终止原因含义默认死亡效果是否应响应
hit_destroyed因命中或受击规则被销毁由预设明确选择
expired寿命或自身流程自然结束通常响应
out_of_bounds离开有效世界边界通常不响应
bomb_cancelled被玩家 Bomb 或消弹机制取消默认不响应
phase_cleared阶段结束时统一清场默认不响应
owner_cancelled所有者被取消或销毁默认不响应
replaced因重启、热重载或实例替换而终止默认不响应

反应必须按明确原因过滤。尤其不能让符卡结束、热重载或清屏弹幕触发面向自然死亡设计的二次生成链。

17.5 同帧批处理、递归与预算边界 ​

稀疏的 boss.defeated、wave.completed 等事件可以逐条进入高层事件总线;成百上千发子弹的 bullet.terminated 必须走数据化批处理路径。正式 authoring 路径不得扩大为逐弹 Python callback 或逐弹通用事件对象。

高密度反应至少需要支持以下策略:

  • each:逐个响应,仅在明确选择且预算允许时使用;
  • first_per_frame:同一规则每帧只响应第一次;
  • count_per_frame:聚合同帧数量,再把计数交给一次动作;
  • debounce:在给定窗口内合并触发;
  • cooldown / max_instances:限制频率和并发实例。

事件 schema 应声明 sparse 或 batch 密度等级。新建稀疏事件反应默认 each;新建批量事件反应默认 count_per_frame。如果 Action 明确需要每个位置,例如每发自然到期子弹各自开花,预设可以选择 each,但它仍编译为批量过滤和批量生成,而不是逐个 Python 调用。假 Boss 受击这类可能同帧多次命中的模板默认使用 count_per_frame;作者需要“只认第一次”时再显式切换为 first_per_frame,Inspector 必须直接显示该选择。

编译后的过滤与聚合应尽量按 archetype、owner、tag 和 reason 批量执行,结果写入生成命令缓冲,而不是回到 Python 逐发调用。调度器还必须具备:

  • 最大反应链深度;
  • 每帧或每作用域生成预算;
  • 新生成对象不能重新进入当前这一轮死亡收集;
  • 预算超限、链深超限和事件溢出时产生结构化诊断,不能静默失控;
  • 调试器能够把批量事实还原为“哪条规则处理了多少个、抑制了多少个”的可读摘要。

本帧批量事实只写入 Outbox,下一固定帧才进入反应匹配;新生成对象不能重新进入产生它的死亡收集轮次。最大因果深度、每帧生成数、并发实例数和队列容量属于目标运行配置,不作为每份内容文档各自发明的魔法常量。实现阶段必须根据目标硬件基准冻结默认配置,并把它随导出 profile、trace 和诊断记录下来;内容作者可以选择更低的局部预算,但 Safe 内容不能移除引擎硬上限。

静态可估算的超预算内容在编译或预览时直接报告。动态超限时,编辑器预览暂停 offending scope 并定位到 ReactionSpec;发行运行时按配置确定性抑制该 scope 的超额命令并记录结构化诊断,不能随机丢弃、跨帧摊薄而悄悄改变弹幕节奏,或让整个进程无界增长。

17.6 统一所有权规则 ​

所有行为、时间线片段、反应和状态都应遵循统一规则:

  • 父状态拥有其时间线实例;
  • 时间线实例拥有包括 ReactiveClip 在内的片段实例;
  • ReactiveClip 拥有每次触发形成的 ReactionScope;
  • 行为拥有自己创建的稀疏对象和弹幕 owner tag;
  • 父级停止或退出时,取消向下传播;
  • stop 和 cancel 必须幂等;
  • 失败不能跳过清理;
  • 热重载必须明确保留、迁移或重建哪些局部状态;
  • 停止一个实例不能清理其他实例的资源。

“等待事件”“等待移动结束”等动作必须绑定取消令牌,不能留下脱离所有者的协程或订阅。

18. 分层调试器 ​

调试器需要同时服务普通作者和高级开发者。

18.1 顶层视图 ​

  • 当前 Stage / State;
  • 当前本地时间和正式帧;
  • 活跃时间线片段;
  • 已武装和已触发的 ReactiveClip;
  • 实际触发帧和触发原因;
  • 主要变量;
  • 最近事件与批量生命周期事件摘要;
  • 弹量和性能预算;
  • 暂停、步进、跳转和重置。

18.2 行为实例视图 ​

  • 作者资源和运行实例 ID;
  • 生命周期状态;
  • 输入和输出值;
  • 所有者与取消令牌;
  • 已生成数量;
  • CPU/GPU 耗时;
  • 当前子行为;
  • 错误与能力违规;
  • 对应的预设内部节点、行为图节点或脚本位置。

18.3 事件与变量追踪 ​

调试器应能回答:

  • 谁写了这个变量;
  • 谁读取了它;
  • 哪个条件在什么值下变为真;
  • 哪个事件启动或停止了片段;
  • 哪条 ReactionSpec 接受、聚合或抑制了事件;
  • 一个 ReactiveClip 是未武装、过滤失败、门控失败、冷却中还是预算受限;
  • 为什么这个片段没有触发;
  • 同一属性存在多少个竞争写入者;
  • 热重载前后保留了哪些状态。

19. 正式预览原则 ​

  • 所有可宣称运行正确的预览都必须使用正式编译和运行路径;
  • Qt 画布可以显示作者几何、控制柄和只读诊断叠层;
  • 高密度子弹继续留在正式渲染器和数据化运行时;
  • 结构测试、正式运行时 trace、性能测试和原生视觉验收必须分别记录;
  • 预设、配方、行为图和脚本最终应汇合到同一正式运行路径;
  • Stage/Timeline seek 以重置后确定性重放为正式语义,不通过状态反推伪造目标帧;
  • 无法正式执行的实验性预览必须明确标记,不得冒充 runtime parity。

20. 非目标与不可破坏边界 ​

当前愿景不追求:

  • 把每发子弹变成场景节点;
  • 给每发子弹安装 Python 每帧回调;
  • 给每发子弹安装 Python 死亡回调或创建通用 Python 事件对象;
  • 用一张万能图取代状态图、时间线、UI、背景和弹幕领域编辑器;
  • 反向解析任意 Python 并承诺无损恢复为可视化图;
  • 让生成的 Python 成为唯一可运行真源;
  • 在短期或中期引入自然语言/Agentic 弹幕生成入口;
  • 用 Qt 近似绘制替代正式 GLFW/ModernGL 运行时;
  • 在没有测量依据时引入 Redis、远程协作或二进制资源打包;
  • 在插件 ABI 和安全边界稳定前构建公共插件市场。

21. 与当前仓库的关系 ​

截至本文首版日期,历史路线图已记录 M0–M7 与 R5–R7 在当时 checkout 的验收闭环;该历史清单现已由下一代实施 TODO 取代。已有基础包括版本化作者资源、正式 Pattern 运行时与预览、可编辑时间线、配方与行为图、表达式与脚本生命周期、UI/背景资源、类型化事件、插件注册和恢复机制。

v0.3 的决策有意沿现有边界演进:当前 SceneDocument 已拥有稳定 UUID 的 Track/Clip/Keyframe、迁移和 Undo/Redo,因此状态结构扩展它而不另造平行真源;当前 PreviewController 的 seek 已采用 reset 后从 0 固定帧重放,愿景把它确认为长期正式语义。另一方面,当前高层 EventBus 会在一次 dispatch() 中持续排空处理器新加入的事件,尚不具备 v0.3 的逐帧 Inbox/Outbox 隔离;当前 ScriptContext 继承完整 StageContext,也不能被称为 Safe 沙箱。这两点属于未来实施迁移,本文不把愿景写成现有交付声明。

本文在这些基础上继续讨论的主要是产品层目标形态:

  • StageFlow / PhaseFlow 的作者体验;
  • 条件、事件和变量驱动的片段激活;
  • 一等预设资源和分层预设库;
  • 上下文 Action Catalog 与快速搜索;
  • Safe / Runtime / Engine 的可执行能力隔离;
  • 统一 Behavior Descriptor;
  • 更完整的背景编排和生命周期 cue;
  • 跨状态、时间线、行为和插件的分层调试器。

这些条目在进入路线图前一律视为“愿景或待设计”,不能仅凭本文描述为已实现。

22. 已冻结决策索引 ​

本轮原有待讨论问题已按“便于开发、渐进学习、运行性能、人机交互直觉、编辑器扩展性和充分解耦”收敛:

原则对架构的直接约束
便于开发一个作者真源、一条编译/预览路径;不平行增加 Flow 文件族、通用 Safe DSL 或第二套渲染器
新手循序渐进可运行骨架和预设先行;参数覆盖、虚拟展开、本地物化、Runtime 扩展逐层开放
引擎性能高密度事实与行为批量化;预算由目标 profile 管理;稀疏 Task 不渗入逐弹路径
人机交互直觉状态图回答阶段、时间线回答顺序、行为图回答局部实现;默认策略可读且运行实例不污染作者文档
再开发与扩展性稳定 ID、版本、迁移、Descriptor、分区插件包和通用降级 Inspector 构成扩展协议
功能充分解耦各域只通过类型化变量、事件、资源引用和生命周期所有权交流,不跨编辑面持有内部对象或片段 ID
原问题结论
StageFlow / PhaseFlow 边界都是 SceneDocument 内同一种分层 StateGraphSpec 的上下文视图,不新增两类文件
State 的文件语义带稳定 UUID 的内嵌子资源,直接拥有局部时间线;复用通过 PhasePreset
变量作用域和持久化Project Config、Stage、State、Clip/Reaction、Behavior、Engine Snapshot 六类;运行值不写回作者文档,默认单写入者
条件触发默认值on_rise + once_per_scope + max_instances=1;保持、下降沿、重启和并行均显式选择
同帧事件和递归本帧 Outbox、下一固定帧 Inbox,不允许 dispatch 重入;状态生命周期优先于退出作用域的新反应
聚合策略和预算稀疏事件默认 each,批量事件默认 count_per_frame;硬预算属于经基准冻结的目标运行配置
动态实例与冲突显示作者定义不被运行实例改写;用 trace、堆叠实例和抑制标记显示;新内容的重叠写入必须显式解决
预设升级项目锁定精确版本,迁移在临时副本预览并由作者以一个事务确认,不自动升级
查看与转为本地查看是只读虚拟解析;转本地是物化、记录 provenance、解除上游关系的显式 Undo/Redo 事务
Safe DSL不新增通用 DSL;使用数据动作、状态图、时间线、预设、受限表达式和领域控件
Safe Python短期不提供;进程内 Python 一律属于受信 Runtime,未来如需不可信执行只能独立进程
插件包装一个版本化包共同交付 schema/runtime/tool,但三个分区使用不同能力和加载生命周期
Renderer Pass使用后端中立 Descriptor + Render Graph + backend adapter,不暴露裸全局渲染状态
自然语言短期与中期完全不进入产品规划,只保留确定性的上下文搜索
任意位置预览重置并从作用域入口确定性重放到目标帧,不建设状态反推系统;缓存只可作为等价优化
新手模板与测试采用第 11.5 节的最小组合库;每轮至少 5 名新用户,以无脚本完成 Pattern、道中和两阶段 Boss 为门槛

最大事件因果深度、每帧生成上限、队列容量和具体性能目标不需要由内容作者或本文凭感觉指定;它们属于实施阶段在目标硬件上测量后冻结的运行 profile。除这些需要性能证据的数值外,本表当前没有遗留的产品方向选择。

23. 后续修改约定 ​

后续讨论修改本文时:

  • 已达成共识的内容写入正文;
  • 尚未决定的内容保留“概念名”“建议”或“待讨论”标记;
  • 不把愿景描述改写成当前实现声明;
  • 实施决策需要同步进入路线图并定义验收证据;
  • schema/API 变更必须另行形成版本、迁移和 round-trip 契约;
  • 重大取舍在变更记录中注明原因,不静默覆盖历史方向。

24. 变更记录 ​

2026-08-09 — v0.3 愿景收敛 ​

  • 固定 SceneDocument 内嵌分层 StateGraphSpec,StageFlow / PhaseFlow 不成为两套新资源;
  • 固定 State、变量、条件触发、动态实例、冲突写入和事件下一帧可见语义;
  • 固定 Stage/Timeline seek 使用重置后确定性重放,不建设任意状态反推系统;
  • 固定预设精确版本锁、显式迁移、虚拟查看与本地物化的文件语义;
  • 固定 Safe 层不新增通用 DSL,短期不提供 Safe Python;
  • 固定插件单包分区、后端中立 Renderer Pass 和目标 profile 性能预算边界;
  • 将自然语言与 Agentic 生成移出短期、中期规划,只保留上下文搜索;
  • 固定首发最小预设集合和面向真实新用户的可用性验证门槛;
  • 将原 16 项待讨论问题全部收敛为正文决策或需基准测量的实施参数。

2026-08-09 — v0.2 事件反应与生命周期模型 ​

  • 固定 LifecycleEvent / ReactionSpec / ReactiveClip / TaskScope 四个概念及其职责边界;
  • 固定“行为发布事实、声明式规则决定反应、时间线决定规则何时有效”的蓝图与时间线交互方式;
  • 记录死亡开花、假 Boss 受击反击、击破切背景和三段延迟开花的具体归属;
  • 记录预设通过可展开的反应槽承载钩子,并允许时间线绑定与局部覆盖;
  • 固定终止原因必须区分自然结束、清场、取消、越界和替换等语义;
  • 固定高密度生命周期事件走批量路径,不扩展为逐弹 Python 回调;
  • 保留同帧可见边界、聚合默认值和具体预算为后续确定性契约。

2026-08-09 — v0.1 初稿 ​

  • 固定“关卡大纲 → 状态 → 时间线 → 预设/配方 → 行为图 → 脚本”的渐进式创作路径;
  • 固定状态图、时间线和行为图通过变量、事件、激活/停止规则交互,不直接形成万能跨域蓝图;
  • 记录内容/行为/工具职责轴和 Safe/Runtime/Engine 信任轴;
  • 记录新人道中、关底、预览与脚本升级流程;
  • 记录分层预设、上下文搜索和自然语言结构化编辑方向;
  • 记录背景编排、Behavior Descriptor、生命周期、取消和分层调试器方向;
  • 汇总当前仍需讨论的关键问题。

Released under the MIT License.