420 lines
15 KiB
Markdown
420 lines
15 KiB
Markdown
# 场景 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(不支持注释、尾逗号)。用编辑器导出可保证格式正确 |
|