Files
tianshu-engine/docs/SCENE_JSON_SPEC.md

420 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 场景 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<string, number>` | ✅ | 全局变量初始值(如好感度、勇气值等,制作者自定义) |
| `scenes` | `Record<string, SceneNode>` | ✅ | 所有场景节点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<string, string>` | — | 多语言字幕映射,如 `{"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<string, number>` | 否 | 从章节入口跳转时套用的变量初始值。未设时 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不支持注释、尾逗号。用编辑器导出可保证格式正确 |