Files
story-edit-web/IR_SCHEMA.md
邓雨鹏 021080dd56 feat(timeline): P2 并行编排——scene 多轨编辑器 + 白模重叠预览
剧情 Timeline P2 前端 + 共享内核(与 SGame 源真同步):
- ir_core/IR_SCHEMA/样张:scene v0.3 + scene 校验 + 导出 gate(D3),与 SGame 仓逐字一致
- timeline.js:appendScene 按 authored start 铺多轨 clip(自然重叠预览),move from 同 actor 跨轨续连(D4);
  drawStage 改逐 actor 查对话→多人气泡同时计时;导出 _clipDur 纯函数;show() 加 startId 参;常量加 CAMERA_DUR
- scene_edit.js(新):演出段编辑模态——拖 clip 改 start(吸附 0.1s)、拖右缘改 dur、增删 clip/轨道、
  选中属性条精确编辑、客户端轻量 lint(镜像 validate.py)、▶ 预览此段(复用播放核)
- graph.js:scene 节点(KIND_CN/summary/nodeInner 列轨道)+双击进编辑模态
- form.js:右栏 renderScene 精确数值编辑(轨道/clip 的 start/dur/kind/目标)+打开编辑器按钮
- app.py export:捕获 CompileError 并入 report(scene 被拦时不再 500)
- test_scene.js:离线 10 断言全过(重叠确凿/晚 1.5s 起步/from 续连);gitignore 忽略本地 _localdemo.db

待浏览器目测拖拽编辑落 IR + 白模重叠演出。
2026-06-13 22:34:29 +08:00

13 KiB
Raw Blame History

Story IR Schemav0.3P2 扩展)

v0.3 变更(剧情 Timeline P2新增 scene 节点(多轨时间线容器,承载并行+时间偏移演出)+ track + clip 模型(见 §4.2)。 scene 与现有线性节点并存D1scene 内只放演出、不含分支,有单一 next 出口D2导出 gateD3:含 scene 的事件暂不可编译导出(需 P3 引擎支持),compile_ir 遇 scene 报错拦下;校验/白模预览不受限。 v0.2 变更M4新增 sequences/out_ref子序列复用、Option skip(押注跳过)、 stage.point_set(点位集引用 + 坐标校验);条件/奖励词典外置到 ir_dictionary.json。 校验+编译内核收敛进 ir_core/CLI 与 M5 Web 后端共用)。

Story IR 是"讲故事"与"编译成配置"两段之间的唯一交接棒。它只描述语义层 (语义角色 / 语义场景 / 叙事节点),把真实 ID、prefab 点位、step 号、nextStepId 前缀拼接全部留给编译器。

配套:

  • 样张:tools/event_authoring/samples/yuye_koumen.ir.json
  • 降级目标:Assets/Scripts/Base/Config/Data/GameEventData.cs
  • 设计依据:docs/plans/2026-06-03-event-config-automation-design.md

1. 顶层结构

{
  "id":    "QY_YYKM",          // group 名(唯一,不与现有内置 group 冲突)
  "title": "雨夜叩门",
  "theme": "正统武侠·道德抉择",
  "scale": "标准奇遇",          // 仅元信息,不参与编译
  "roles":     [ Role, ... ],
  "stage":     Stage,
  "sequences": [ Sequence, ... ],   // 可选;可复用子序列(被 out_ref 引用)
  "nodes":     [ Node, ... ],
  "endings":   [ Ending, ... ]
}

2. Role语义角色

{ "slot": "NP1", "name": "神秘剑客", "archetype": "负伤外门高手", "camp": 0 }
  • slot:编译后写进 points/camp2Fighters 的语义点位名。P1=玩家固定。
  • archetype:原型标签,编译期→真实 NPC ID匹配 npc_data)。
  • camp0 中立 / 1 玩家方 / 2 敌方(战斗时用,可省略)。

3. Stage语义场景

{ "type": "门派入口·夜", "reuse_hint": "K3_A", "point_set": "QY_YYKM" }
  • type:场景语义;编译器据此 + 所需角色数从点位库挑结构匹配的 prefab。
  • reuse_hint:可选,显式指定复用哪个现有 prefab仅作 BDTree host
  • point_set:可选,引用的点位集名(默认 = id)。对应 Assets/StreamingAssets/Story/PointSets/{point_set}.points.jsonM1 取点工具产出。编译期校验IR 里实际站位的点位名speaker/actor/move.to/fight 阵营/camera必须都在该点位集中存在点位集文件不存在则降级为警告--strict-points 下视为错)。

4. Node叙事节点

通用字段:id(组内唯一)、kind。各 kind 字段如下:

kind 字段 → GameEventData
narration speaker?, text, next type0挂角色(默认 P1)用气泡实景呈现,不走全屏黑幕content=text
dialogue speaker, text, camera?, next type0points=[speaker]contentcameraPoint
move actor, to, mode?(walk/teleport/remove), speed?, ani?, next type0points/movePoint/moveStatus
anim actor, ani, angle?, next type0anis=[ani,angle]
choice options:[Option] type1每 option 一行
choice_once options:[Option] type2编译器保证有恒满足兜底项
random branches:[{weight,goto}] type3weight+nextStepId
fight fight_type(1 击倒/2 死斗), camp2:[slot], camp1?:[slot], win, lose type0camp2Fighters,fightStatus=[type,win,lose]
reward grants:[Grant], next type0resultRewardIdsroleActionCode
out_ref ref(子序列 id), next 编译前预展开成普通节点(见 §4.1);本节点不直接产行
scene tracks:[Track], duration?, next P2 多轨演出段(见 §4.2);并行+时间偏移容器。导出 gateD3编译期报错暂不产行需 P3 引擎)

4.1 Sequence / out_ref子序列复用对应 OutRefStoryNodeData

// 顶层 sequences可复用子序列
{ "id": "seq_cheer", "nodes": [ {Node}, ... ] }
// 引用它的节点
{ "id": "ref_a", "kind": "out_ref", "ref": "seq_cheer", "next": "end_win" }
  • 语义:把一段公共流程(庆功/过场/结算)抽成子序列,多处 out_ref 复用,避免重复手写。
  • 编译(对齐 StorylineMgr.ReplaceStoryEventData 的展开):编译前把每个 out_ref 节点 r 摊平—— 克隆 ref 子序列的节点id 加 {r.id}__ 前缀,内部跳转同步加前缀;子序列出口(next 为空的尾节点) 的 next 接到 r.next;指向 r 的跳转改写为子序列入口 {r.id}__{首节点}
  • 多处复用不撞 id:不同 out_ref 节点前缀不同 → 各自一份独立行。
  • 限制M4 仅支持一层(子序列内不得再含 out_ref),违者校验报错;子序列内跳转目标须落在子序列内或共享结局。

4.2 scene / Track / Clip多轨演出段P2 新增)

scene 是一段被分支夹着的连续并行演出(对应影视「一镜到底的演出段」):多条轨道同时推进, 每个 clip 有场景内绝对起始秒 start,因此「角色 A 走到点 11.5 秒后角色 B 才开始走、两者时间重叠」 这类语义成为一等公民。scene 内不含分支D2有单一 next 出口;需要选择/战斗/随机仍用图级节点。

{
  "id": "sc_intro",
  "kind": "scene",
  "next": "choice_1",        // 单一出口D2
  "duration": 8.5,           // 可选;缺省 = 所有 clip end 的最大值
  "tracks": [ Track, ... ]
}

Track轨道

{ "id": "tk_p1",  "actor": "P1", "clips": [ Clip, ... ] }      // 演员轨actor=角色 slotP1/NP1…
{ "id": "tk_cam", "role": "camera", "clips": [ Clip, ... ] }   // 镜头轨role="camera"
  • 演员轨绑定一个 slot承载该角色的 move/anim/dialogue/narration。
  • 镜头轨 role:"camera",承载 camera clip不校验 actor
  • 推荐单 actor 单轨;一个 actor 多轨亦可(校验不强制,但同 actor 的 move 跨轨也不得时间重叠)。

Clip片段:通用字段 idscene 内唯一)、kindstart场景内绝对秒≥0dur(可选,按 kind 派生)。

kind 字段 dur 来源
move to(点位名), speed?, dur? dur > dist/speed > dist/默认速度
dialogue text, dur? dur > 字数×打字速度+尾停
narration text, dur? 同 dialogue
anim ani, angle?, dur? dur > 缺省 1.0sP3 由引擎动画时长表回填)
camera focus(点位名), dur? dur > 缺省 2.0s
wait (仅占位), dur 显式 dur
  • D4 move 时长派生、位置隐式clip 的 start 由作者编排move 的 dur 默认由 dist(from→to)/speed 派生(可显式 dur 覆盖)。 move 的 from 不存,运行期由「同 actor 轨上一段 move 的 to」推出(无则取点位集初始锚点)。
  • 校验约束:同一 actor 的 move 之间时间上不得重叠一个人不能同时走两处dialogue/anim 可与本人 move 重叠(边走边说)。
  • 时长/打字速度常量与白模预览 timeline.js 共享CHAR_TIME/TAIL_PAUSE/MIN_DLG/MOVE_SPEED/ANIM_DUR/CAMERA_DUR
  • 样张:samples/scene_demo.ir.jsonA 走、1.5s 后 B 走,两者重叠)。

P3 衔接契约scene/track/clip 即 P3 StoryTimelineData 的序列化源。引擎进入 scene 时对每 clip 按 start 排程 UniTask.Delaymove 用现有多 agent 并发寻路dialogue 走打字机camera 设 otherTargetwait 纯延时; 在 max(clip.end) 或显式 duration 后推进 next。故 IR 不写 move 的 from/绝对坐标(引擎自点位集+轨道续连推)。

Option选项

{ "text": "收留他", "condition": Condition?, "reward": {"grants":[Grant]}?,
  "skip": { "node": "end_bet", "reward": {"grants":[Grant]} }?, "goto": "end_ally" }
  • textchooseconditionconditionrewardresultRewardIds(选项被选后自身发的奖励/扣费),gotonextStepId
  • skip(押注跳过,对应 skipNodeId+skipReward:选该项时skip.reward 注入 skip.node 那行的奖励,常配合 goto 越过中间战斗直达结算行(押对了→不打了直接领彩头)。
    • 编译:选项行写 skipNodeId = skip.node(裸节点 id运行时 ChooseTaskUI 自动拼 {group}_ 查行)、skipReward = compile(skip.reward)
    • 注意:运行时是覆盖 skip.node 行的 resultRewardIds(非追加)→ 让 skip.node 的自带 grants 为空、彩头全由 skip.reward 给。
    • 校验:skip.node 须可解析;skip.reward.grants 非空且合法。

5. Ending结局

{ "id": "end_ally", "summary": "结义同盟", "grants": [Grant], "result": "success" }
  • result:可选,success(默认,→gameTaskStatus1) / fail(→2) / end(→3)。决定收尾分支。
  • 编译为两行:① 结算行(missionText=summary + resultRewardIds/roleActionCodegameTaskStatus=0 nextStepId→终结行);② 裸终结行(仅 gameTaskStatus)。
  • 为何拆两行EventRefreshStepActionRefreshStep() 返回 false即下一步 gameTaskStatus>0)时 返回 Failure,会中断该步 Sequence——若把奖励与 gameTaskStatus 放同一行,发奖励/入门的 Action 被跳过。 终结行触发 DirectStoryEnd(复位镜头 otherTarget=null / 恢复玩家控制 / 结束剧情)。与可视化编辑器 及内置奇遇 Qiyu/QY10_A.json(末节点 taskStatus:1、内容为空、奖励在前置节点)一致。

6. Condition / Grant 词典(外置 ir_dictionary.json

M4 起词典外置到 tools/event_authoring/ir_dictionary.json,编译器(ir_core)与 M5 Web 前端共读。 新增查表类 kind 只改该 JSON、零代码需引擎特例的 kind 标 "engine": true。 编译器对未登记 kind 报错不静默。下表为当前登记项(随推进扩充)。

Conditioncondition 字段)

{ "kind": "银两", "op": ">=", "value": 500 }   // → "V19,2,500"   (op: >=→2)
kind 真实ID 说明
银两 V19 门派金钱(RewardItems 特判 schoolMoney

GrantresultRewardIds 或行为码)

{ "kind": "银两",   "value": 200 }                  // → "V19,200"(正=给,负=扣)
{ "kind": "道具",   "item": "P6", "value": 3 }       // → "P6,3"
{ "kind": "友好度", "target": "NP1", "value": 30 }   // → "NP1,V15,30"
{ "kind": "入门",   "target": "NP1" }                // → roleActionCode "JoinToPlayerSch=NP1"
kind 真实ID/机制
银两 V19
道具 item 指定drop_item_data ID
友好度 V15target NPC slot
入门 roleActionCode JoinToPlayerSchNPC 加入玩家门派)

词典随 MVP 推进扩充;编译器对未登记的 kind 报错,不静默产出。

7. 编译器降级不变量(与 StorylineMgr 现有逻辑一致)

  • 自动生成 step(按节点拓扑序)。
  • 自动给 id/nextStepId/fightStatus[1..2]{group}_ 前缀。
  • 所有跳转目标(next/goto/win/lose/branches[].goto)必须可解析到某节点或结局,否则编译失败。
  • 每个 choice/choice_once 至少一个无 condition(或恒满足)的兜底项,否则编译失败。

8. 多语言 / 本地化

游戏本地化走 SGameText(重写 Text.text setter。剧情对话气泡 TaskDialogUI.contentText 已是 SGameText,链路可用。规则:

  • 文本一律用简体中文撰写content/choose/summary),作为 canonical key
    • 简体↔繁体SGameText 字符级机械转换,自动覆盖,无需额外工作。
    • 韩文(及未来语言)SGameText 用中文整句作 key 查 StreamingAssets/i18n/ko.tsv
  • 编译器附带产出 {group}.i18n.tsv(中文 key 列 + 空韩文列),翻译填好后合并进 ko.tsv 新剧情文本即纳入本地化,不会成为漏网。
  • 占位符:需动态插入角色名时用游戏既有约定(如 {NP1}{A5,NP1}),编译器原样保留。 注意:含占位的整句韩文查表会 miss(拿到的是替换后的完整串)→ 简繁不受影响,韩文 fallback 中文。 这是项目级已知限制(待 SGameText.SetFormat 模板方案解决),故重要台词尽量少用占位。
  • 运行时前提:剧情文本的显示组件必须是 SGameText。对话气泡已满足;黑屏旁白/选项 UI 若发现是原生 Text,需在游戏侧补挂 SGameText(属游戏侧改造,不在编译器范围)。