# 场景 JSON 规范文档 电影游戏引擎的场景数据全部定义在 JSON 文件中。本文档是完整字段参考手册。 --- ## 目录 1. [顶层结构 (GameData)](#1-顶层结构-gamedata) 2. [场景节点 (SceneNode)](#2-场景节点-scenenode) 3. [选项 (Choice)](#3-选项-choice) 4. [图片/视频热点 (Hotspot)](#4-图片视频热点-hotspot) 5. [快速反应事件 (QTEDefinition)](#5-快速反应事件-qtedefinition) 6. [条件和效果 (Condition & Effect)](#6-条件和效果-condition--effect) 7. [背景音乐 (BGM 字段)](#7-背景音乐-bgm-字段) 8. [章节 (ChapterInfo)](#8-章节-chapterinfo) 9. [成就 (AchievementDef)](#9-成就-achievementdef) 10. [结局 (EndingDef)](#10-结局-endingdef) 11. [完整示例](#11-完整示例) 12. [验证规则](#12-验证规则) --- ## 1. 顶层结构 (GameData) ```json { "startScene": "intro", "variables": { "trust": 50, "courage": 0 }, "scenes": { ... }, "chapters": [ ... ], "achievements": [ ... ], "endings": [ ... ] } ``` | 字段 | 类型 | 必填 | 说明 | |------|------|:--:|------| | `startScene` | string | ✅ | 游戏开始的场景 ID | | `variables` | `Record` | ✅ | 全局变量初始值(如好感度、勇气值等,制作者自定义) | | `scenes` | `Record` | ✅ | 所有场景节点,key 为场景唯一 ID | | `chapters` | ChapterInfo[] | 否 | 章节定义,用于章节选择功能 | | `achievements` | AchievementDef[] | 否 | 成就定义,满足条件时自动解锁 | | `endings` | EndingDef[] | 否 | 结局定义,用于结局画廊 | --- ## 2. 场景节点 (SceneNode) ```json { "id": "intro", "type": "video", "videoUrl": "/videos/intro.mp4", "imageUrl": "/images/room.jpg", "subtitleUrl": "/subtitles/intro.vtt", "subtitles": { "zh": "/subtitles/intro_zh.vtt", "en": "/subtitles/intro_en.vtt" }, "choices": [ ... ], "hotspots": [ ... ], "qte": { ... }, "nextScene": "auto_next", "onEnter": [ { "type": "set", "target": "visited_room", "value": 1 } ], "loopStart": 8.0, "loopEnd": 10.0, "bgmUrl": "/audio/tense.mp3", "bgmVolume": 0.8, "bgmCrossFade": 2.0, "bgmDuckLevel": 0.35, "bgmDuckFade": 0.5, "videoMuted": false, "skippable": true } ``` | 字段 | 类型 | 默认 | 说明 | |------|------|------|------| | `id` | string | — | **唯一标识**。任意字符串,建议用有意义的英文名 | | `type` | `"video"` \| `"image"` | `"video"` | `"image"` 时显示静态图片和热点,不播放视频 | | `videoUrl` | string | `""` | 视频文件路径,推荐 `/videos/xxx.mp4` | | `imageUrl` | string | — | 图片场景的图片路径 | | `subtitleUrl` | string | — | 字幕 VTT 文件路径(单语言,向后兼容) | | `subtitles` | `Record` | — | 多语言字幕映射,如 `{"zh": "...", "en": "..."}`。优先级高于 `subtitleUrl` | | `choices` | Choice[] | — | 选项列表。为空或不存在时场景播放完毕后自动结束(或走 `nextScene`) | | `hotspots` | Hotspot[] | — | 可点击热区。视频热点按 `showAt`/`hideAt` 时间轴显隐 | | `qte` | QTEDefinition | — | QTE 事件定义 | | `nextScene` | string | — | 无选项时的默认下一场景 ID(自动跳转) | | `onEnter` | Effect[] | — | 进入场景时执行的效果(如设置变量) | | `loopStart` | number | — | 循环起点(秒)。到达后弹出选项并开始循环。需配合 `loopEnd` | | `loopEnd` | number | — | 循环终点(秒)。播到时 seek 回 `loopStart` | | `bgmUrl` | string | — | 背景音乐 MP3 路径。null/省略时静默 | | `bgmVolume` | number | `0.8` | BGM 音量(0~1) | | `bgmCrossFade` | number | `2.0` | BGM 切换时的交叉淡化时长(秒) | | `bgmDuckLevel` | number | `0.35` | QTE/选项出现时 BGM 降低到的百分比 | | `bgmDuckFade` | number | `0.5` | Ducking 的渐变时长(秒) | | `videoMuted` | boolean | `false` | 是否静音视频自带音轨 | | `skippable` | boolean | `true` | `false` 时禁止跳过此场景(即使已看过) | --- ## 3. 选项 (Choice) ```json { "text": "帮助陌生人", "textKey": "scene.intro.choice.help", "prompt": "你的善意将被记住", "targetScene": "help_ending", "conditions": [ { "variable": "trust", "op": ">=", "value": 50 } ], "effects": [ { "type": "add", "target": "trust", "value": 20 } ], "timeLimit": 10 } ``` | 字段 | 类型 | 默认 | 说明 | |------|------|------|------| | `text` | string | — | 选项显示文字(默认语言) | | `textKey` | string | — | 多语言 key。有值时用翻译文件查文字,否则用 `text` | | `prompt` | string | — | 选后浮现提示文字(Telltale 式 "某人会记住这件事")。有值时按钮前置金色标识 | | `targetScene` | string | — | 点击后跳转的目标场景 ID | | `conditions` | Condition[] | — | 显示条件。所有条件都满足时选项才显示 | | `effects` | Effect[] | — | 选择后的效果(修改变量值等) | | `timeLimit` | number | — | 限时选择(秒)。0 或不设表示不限时 | --- ## 4. 图片/视频热点 (Hotspot) ```json { "id": "hs_desk", "label": "查看书桌", "targetScene": "desk_detail", "x": 0.15, "y": 0.30, "width": 0.25, "height": 0.35, "showAt": 2.0, "hideAt": 8.0, "conditions": [ { "variable": "investigation", "op": ">=", "value": 1 } ], "effects": [ { "type": "setFlag", "target": "checked_desk" } ], "timeLimit": 10 } ``` | 字段 | 类型 | 默认 | 说明 | |------|------|------|------| | `id` | string | — | 热区唯一标识 | | `label` | string | — | 鼠标悬停时显示的提示文字 | | `targetScene` | string | — | 点击后跳转的目标场景 ID | | `x` | number | — | 热区左上角 X 坐标,**相对比例(0~1)**,自适应屏幕 | | `y` | number | — | 热区左上角 Y 坐标,**相对比例(0~1)** | | `width` | number | — | 热区宽度,**相对比例(0~1)** | | `height` | number | — | 热区高度,**相对比例(0~1)** | | `showAt` | number | — | 视频模式下的出现时间(秒)。未设时始终显示 | | `hideAt` | number | — | 视频模式下的消失时间(秒)。未设时始终显示 | | `conditions` | Condition[] | — | 显示条件 | | `effects` | Effect[] | — | 点击后的效果 | | `timeLimit` | number | — | 限时热区(秒)。超时后热区消失,不触发跳转 | --- ## 5. 快速反应事件 (QTEDefinition) ```json { "triggerTime": 2.5, "prompt": "躲避飞来的石块!", "keys": ["ArrowLeft", "ArrowRight", "a", "d"], "timeLimit": 3.0, "successScene": "qte_win", "failScene": "qte_lose", "effects": { "success": [ { "type": "add", "target": "courage", "value": 10 } ], "fail": [ { "type": "add", "target": "health", "value": -20 } ] } } ``` | 字段 | 类型 | 默认 | 说明 | |------|------|------|------| | `triggerTime` | number | — | QTE 触发时间(视频播到第几秒时触发) | | `prompt` | string | — | QTE 提示文字 | | `keys` | string[] | — | 需按下的键,大小写不敏感。如 `["Space"]` | | `timeLimit` | number | — | 限时(秒) | | `successScene` | string | — | 成功时跳转的场景 ID | | `failScene` | string | — | 失败/超时时跳转的场景 ID | | `effects` | object | — | 成功/失败各自的效果(`effects.success` 和 `effects.fail`) | --- ## 6. 条件和效果 (Condition & Effect) ### Condition(条件) ```json { "variable": "trust", "op": ">=", "value": 80 } ``` | 字段 | 类型 | 说明 | |------|------|------| | `variable` | string | 变量名 | | `op` | string | 比较操作符:`>` / `<` / `>=` / `<=` / `==` / `!=` | | `value` | number | 比较值 | > **注意:** 条件中变量的值都是 number 类型。布尔值用 `== 1` 表示 true,`== 0` 表示 false。 ### Effect(效果) ```json { "type": "set", "target": "trust", "value": 80 } { "type": "add", "target": "courage", "value": 10 } ``` | `type` | 说明 | |--------|------| | `set` | 设置变量为目标值 | | `add` | 变量增加指定值(负数为减) | | 字段 | 类型 | 说明 | |------|------|------| | `target` | string | 变量名(与 Condition 的 `variable` 对应) | | `value` | number | 值 | --- ## 7. 背景音乐 (BGM 字段) BGM 字段定义在 SceneNode 中,由独立 AudioSystem 驱动,不受视频循环/切换影响。 | 字段 | 类型 | 默认 | 说明 | |------|------|------|------| | `bgmUrl` | string | — | BGM MP3 路径。**两个相邻场景的 `bgmUrl` 相同时 → BGM 不中断继续播放。** 不同时 → 交叉淡化切换。null/省略时 → fade out | | `bgmVolume` | number | `0.8` | BGM 音量(0~1) | | `bgmCrossFade` | number | `2.0` | 交叉淡化时长(秒) | | `bgmDuckLevel` | number | `0.35` | QTE 触发 / 选项出现 / 热点出现时 BGM 自动降到 `bgmVolume × bgmDuckLevel` | | `bgmDuckFade` | number | `0.5` | Ducking 渐变时长(秒) | | `videoMuted` | boolean | `false` | 是否静音视频自带音轨。引擎不自动设置,需制作者手动指定 | **制作建议:** BGM 使用独立的 .mp3 文件,视频导出时不要嵌入背景音乐。这样 BGM 在场景切换和画面循环时保持连贯。 --- ## 8. 章节 (ChapterInfo) ```json { "id": "ch1", "label": "第一章:相遇", "startScene": "intro", "thumbnail": "/images/ch1.jpg", "defaultVariables": { "trust": 50, "courage": 0 } } ``` | 字段 | 类型 | 必填 | 说明 | |------|------|:--:|------| | `id` | string | ✅ | 章节唯一标识 | | `label` | string | ✅ | 章节显示名称(支持多语言) | | `startScene` | string | ✅ | 章节起始场景 ID。**玩家到达此场景时 → 章节自动解锁** | | `thumbnail` | string | 否 | 章节缩略图路径(320×180 推荐) | | `defaultVariables` | `Record` | 否 | 从章节入口跳转时套用的变量初始值。未设时 fallback 到全局 `variables` | --- ## 9. 成就 (AchievementDef) ```json { "id": "helper", "title": "善良的人", "description": "选择帮助陌生人", "icon": "/images/ach_helper.png", "hidden": false, "condition": { "variable": "trust", "op": ">=", "value": 70 } } ``` | 字段 | 类型 | 必填 | 说明 | |------|------|:--:|------| | `id` | string | ✅ | 成就唯一标识 | | `title` | string | ✅ | 成就标题 | | `description` | string | ✅ | 成就描述 | | `icon` | string | 否 | 成就图标路径(可选) | | `hidden` | boolean | 否 | `true` 时未解锁不显示标题和描述(显示 ??? ) | | `condition` | Condition | ✅ | 解锁条件。变量满足时自动解锁并弹出 toast | --- ## 10. 结局 (EndingDef) ```json { "id": "help_end", "label": "伸出援手", "sceneId": "help_ending", "thumbnail": "/images/end_help.jpg" } ``` | 字段 | 类型 | 必填 | 说明 | |------|------|:--:|------| | `id` | string | ✅ | 结局唯一标识 | | `label` | string | ✅ | 结局显示名称 | | `sceneId` | string | ✅ | 结局场景 ID。**玩家到达此场景时 → 结局自动标记已解锁** | | `thumbnail` | string | 否 | 结局缩略图路径(320×180 推荐) | --- ## 11. 完整示例 以下示例覆盖所有功能: ```json { "startScene": "intro", "variables": { "trust": 50, "courage": 0, "investigation": 0 }, "scenes": { "intro": { "id": "intro", "videoUrl": "/videos/intro.mp4", "subtitles": { "zh": "/subtitles/intro_zh.vtt", "en": "/subtitles/intro_en.vtt" }, "bgmUrl": "/audio/calm.mp3", "bgmVolume": 0.6, "loopStart": 5.0, "loopEnd": 8.0, "choices": [ { "text": "帮助陌生人", "textKey": "scene.intro.choice.help", "prompt": "你的善意将被记住", "targetScene": "help_ending", "effects": [{ "type": "add", "target": "trust", "value": 20 }] }, { "text": "调查房间", "textKey": "scene.intro.choice.investigate", "targetScene": "crime_scene" } ] }, "crime_scene": { "id": "crime_scene", "type": "image", "imageUrl": "/images/crime_scene.jpg", "hotspots": [ { "id": "hs_desk", "label": "查看书桌", "targetScene": "desk_detail", "x": 0.15, "y": 0.30, "width": 0.25, "height": 0.35, "effects": [{ "type": "add", "target": "investigation", "value": 1 }] } ], "choices": [ { "text": "离开房间", "targetScene": "corridor" } ] }, "corridor": { "id": "corridor", "videoUrl": "/videos/corridor.mp4", "bgmUrl": "/audio/tense.mp3", "bgmVolume": 0.7, "bgmCrossFade": 1.5, "qte": { "triggerTime": 2.0, "prompt": "躲避飞来的石块!", "keys": ["Space"], "timeLimit": 3.0, "successScene": "qte_win", "failScene": "qte_lose", "effects": { "success": [{ "type": "add", "target": "courage", "value": 10 }], "fail": [{ "type": "add", "target": "trust", "value": -10 }] } } }, "help_ending": { "id": "help_ending", "videoUrl": "/videos/help.mp4", "bgmUrl": "/audio/calm.mp3", "bgmVolume": 0.6, "onEnter": [{ "type": "set", "target": "completed_game", "value": 1 }], "choices": [] } }, "chapters": [ { "id": "ch1", "label": "第一章", "startScene": "intro", "thumbnail": "/images/ch1.jpg", "defaultVariables": { "trust": 50, "courage": 0, "investigation": 0 } } ], "achievements": [ { "id": "helper", "title": "善良的人", "description": "选择帮助陌生人", "hidden": false, "condition": { "variable": "trust", "op": ">=", "value": 70 } } ], "endings": [ { "id": "help_end", "label": "伸出援手", "sceneId": "help_ending", "thumbnail": "/images/end_help.jpg" } ] } ``` --- ## 12. 验证规则 | 规则 | 说明 | |------|------| | **ID 唯一性** | `scenes` 的 key、`Choice.targetScene`、`Hotspot.targetScene`、`QTEDefinition.successScene`/`failScene`、`ChapterInfo.startScene`、`EndingDef.sceneId` 必须引用存在的场景 ID | | **循环引用** | 场景 A 的 targetScene 指向 A 自身 → 允许(重玩同一场景) | | **死路检测** | 有 `conditions` 的 choice 如果条件永远无法满足 → 该分支永久不可达(建议制作者验证) | | **videoUrl 与 imageUrl** | `type: "image"` 时可省略 `videoUrl`;`type: "video"`(默认)时必须提供 `videoUrl` | | **loopStart / loopEnd** | 两者必须同时出现或同时不出现。`loopStart < loopEnd` | | **JSON 格式** | 严格 JSON(不支持注释、尾逗号)。用编辑器导出可保证格式正确 |