docs: reorganize docs into guide/ and electron/, add 6 new guide docs, update README

This commit is contained in:
2026-06-10 17:01:48 +08:00
parent 99f80147c7
commit 686b1b45ea
11 changed files with 950 additions and 75 deletions

147
README.md
View File

@@ -1,7 +1,7 @@
# 交互式电影游戏引擎
基于 Vue 3 + TypeScript 的浏览器端交互式电影游戏引擎。
**零代码门槛**:你只需要会剪视频和写 JSON,不需要前端知识
**零代码门槛**:你只需要会剪视频和写 JSON。
## 快速开始
@@ -12,33 +12,35 @@ npm install
npm run dev
```
打开 `http://localhost:5173/`,你会看到示例剧情。
打开 `http://localhost:5173/`示例剧情。
打开 `http://localhost:5173/editor/` 使用可视化剧情编辑器。
## 制作你的游戏
### 1. 准备素材
| 素材 | 目录 | 格式要求 |
|------|------|---------|
| 视频文件 | `public/videos/` | MP4 (H.264)1280×72030fps2-5Mbps |
| 背景音乐 | `public/audio/` | MP3 |
| 缩略图 | `public/images/` | JPG/PNG320×180 |
| 字幕 | `public/subtitles/` | WebVTT (.vtt) |
按场景组织素材,每个场景一个文件夹:
> `public/videos/` 已在 `.gitignore` 中,视频文件不需要提交到 Git。
```
public/my_story/
scene_1/video.mp4 ← 视频文件 (MP4 H.264, 1280×720)
scene_2/video.mp4
shared/bgm.mp3 ← 跨场景共享素材
```
### 2. 写剧情 JSON
### 2. 写剧情 JSON
编辑 `public/scenes/demo.json`,定义你的场景和分支。**最小可玩示例只需 20 行 JSON**
创建 `public/scenes/my_story.json`
```json
{
"startScene": "intro",
"assetBase": "my_story/",
"startScene": "scene_1",
"variables": { "trust": 50 },
"scenes": {
"intro": {
"id": "intro",
"videoUrl": "/videos/intro.mp4",
"scene_1": {
"id": "scene_1",
"videoUrl": "scene_1/video.mp4",
"choices": [
{ "text": "帮助他", "targetScene": "help_end" },
{ "text": "离开", "targetScene": "leave_end" }
@@ -46,105 +48,100 @@ npm run dev
},
"help_end": {
"id": "help_end",
"videoUrl": "/videos/help.mp4",
"videoUrl": "scene_2/video.mp4",
"choices": []
},
"leave_end": {
"id": "leave_end",
"videoUrl": "/videos/leave.mp4",
"videoUrl": "shared/video.mp4",
"choices": []
}
}
}
```
完整字段参考见 **[docs/SCENE_JSON_SPEC.md](docs/SCENE_JSON_SPEC.md)**。
### 3. 实时预览
### 3. 运行
```bash
npm run dev
```
Vite 会启动热重载服务器,修改 JSON 或视频后自动刷新
### 4. 可视化编辑剧情
浏览器打开 `http://localhost:5173/editor/`,可以用节点图编辑器拖拽编辑场景分支。
## 打包发布
```bash
npm run pack:html # Web 版 → release/mygame.zip → 上传 itch.io / Netlify
npm run pack:mac # macOS → release/MyGame-darwin-arm64/
npm run pack:win # Windows → release/MyGame-win32-x64/
```
打包前会自动验证 JSON 合法性。
浏览器打开 `http://localhost:5173/?scene=my_story.json`
## 引擎功能
| 功能 | 说明 |
|------|------|
| 视频分支播放 | A/B 双缓冲无缝切换300ms 交叉淡化 |
| 选择系统 | 限时选择、条件分支(根据变量显示/隐藏选项 |
| QTE 快速反应事件 | 视频中插入限时按键挑战,成功/失败跳转不同场景 |
| 图片/视频热点 | 点击画面区域触发分支,视频热点按时间轴显隐 |
| 循环等待 | 视频结束后自动循环指定片段,保持画面动态 |
| 独立 BGM | 背景音乐独立驱动,场景切换时交叉淡化,不受画面循环影响 |
| BGM Ducking | 选择/QTE/热点出现时 BGM 自动降低音量 |
| 字幕系统 | WebVTT 解析,多语言字幕切换 |
| 章节系统 | 分章节管理剧情,到达即解锁,可跳转 |
| 成就系统 | 变量满足条件时自动解锁,底部弹窗提示 |
| 结局画廊 | 所有结局缩略图展示,已解锁/未解锁状态 |
| 章节回顾 | 每章完成度百分比 + 未解锁分支条件提示 |
| 关键选择提示 | 重要选项前置金色标识 + 选后浮现提示文字 |
| 跳过已看 + 倍速 | 已看场景可跳过1x/2x/4x 倍速播放 |
| 全屏模式 | 一键全屏沉浸式浏览器体验 |
| 键盘导航 | 方向键选选项Esc 菜单Space 暂停 |
| 多语言 i18n | UI + 字幕支持中英文切换 |
| 可访问性 | 字幕字号/背景、QTE 时限放宽/按键简化、防误触延迟 |
| 选择系统 | 限时选择、条件分支(根据变量显示/隐藏) |
| 选择提示 (Prompt) | 重要选项前置金色标识 + 选后浮现提示文字 |
| QTE 快速反应事件 | 视频中限时按键挑战,成功/失败导向不同分支 |
| 图片/视频热点 | 像素级精准定位,视频热点按时间显隐 |
| 循环等待 | 指定片段反复播放直到玩家做选择 |
| 独立 BGM | 背景音乐独立轨道,场景切换交叉淡化 |
| BGM Ducking | 选择/QTE/热点出现时 BGM 自动压低 |
| 字幕系统 | WebVTT 解析,多语言字幕一键切换 |
| 多语言 i18n | UI + 数据层双语体系,支持动态扩展语言 |
| 章节系统 | 分章节管理,到达即解锁 |
| 成就系统 | 条件触发自动解锁,底部 Toast 弹窗 |
| 故事进度总览 | 章节完成度 + 结局解锁状态 + 条件提示 |
| 存档系统 | IndexedDB 多槽位,跨会话持久化 |
| 开场视频 + 菜单背景 | 电影级启动流程 |
| 暂停菜单 | Esc 呼出暂停面板:继续/存档/设置/主菜单 |
| 可访问性 | 字幕字号/背景、QTE 时限放宽/按键简化、防误触 |
| 跳过/倍速 | 已看场景可跳过1x/2x/4x 倍速 |
| 全屏模式 | 一键全屏沉浸式体验 |
| 键盘导航 | 方向键选选项Esc 暂停/菜单P 暂停 |
| 自动隐藏 UI | 3s 无操作隐藏顶栏和光标 |
| Electron 打包 | macOS / Windows 桌面应用 |
## 文档索引
| 文档 | 内容 | 适合 |
|------|------|------|
| [**快速开始**](docs/guide/QUICK_START.md) | 5 分钟做一个游戏 | 新手 |
| [**JSON 字段参考**](docs/guide/SCENE_JSON_SPEC.md) | 全部字段 + 类型说明 | 必读 |
| [**分支叙事**](docs/guide/BRANCHING.md) | 选项/条件/变量/效果/Prompt | 进阶 |
| [**交互指南**](docs/guide/INTERACTIONS.md) | QTE / Hotspot / Loop | 进阶 |
| [**国际化**](docs/guide/I18N.md) | 多语言 UI + 数据层 | 进阶 |
| [**打包发布**](docs/guide/PUBLISHING.md) | Web + macOS + Windows | 分发 |
| [**编辑器**](editor/README.md) | 可视化剧情编辑 | 可选 |
| [**开发路线图**](ROADMAP.md) | 已实现/待实现功能 | 参考 |
## 目录结构
```
mygame/
├── engine/ # 引擎核心(纯 TS不依赖 Vue
│ ├── core/ # Engine / VideoManager / StateManager / SceneManager
│ ├── core/ # Engine / VideoManager / SceneManager
│ ├── systems/ # QTE / Choice / Audio / Achievement / Save
│ └── types.ts # 类型定义
│ └── types.ts
├── src/ # Vue UI 层
│ ├── components/ # 所有界面组件
│ ├── composables/ # 引擎 ↔ UI 桥接
│ ├── stores/ # Pinia 状态管理
│ └── locales/ # 翻译文件zh.json / en.json
│ ├── stores/ # Pinia 状态
│ └── locales/ # UI 翻译(开发者维护
├── editor/ # 可视化剧情编辑器
├── electron/ # 桌面应用打包(Electron
├── public/ # 你的素材
│ ├── videos/ # 视频文件(.mp4
│ ├── audio/ # 背景音乐(.mp3
├── images/ # 缩略图
│ ├── subtitles/ # 字幕(.vtt
── scenes/demo.json # 剧情定义
├── electron/ # Electron 桌面打包
├── public/ # 你的素材(唯一需要编辑的目录)
│ ├── demo/ # 示例数据(参考用
│ ├── scenes/demo.json
│ └── locales/{zh,en,ja}.json
│ ├── your_story/ # 你的游戏
│ ├── scene_1/video.mp4
│ │ └── locales/zh.json
│ └── scenes/config.json # 默认加载哪个 JSON
├── docs/ # 文档
── SCENE_JSON_SPEC.md # JSON 完整字段参考
── guide/ # 游戏制作引导
│ └── electron/ # 打包配置
├── scripts/ # 打包脚本
└── ROADMAP.md # 开发路线图
└── ROADMAP.md
```
## 视频制作建议
| 参数 | 建议值 |
|------|--------|
| 视频不包含 BGM | 背景音乐用独立 .mp3 文件 + `bgmUrl` 字段,画面循环时 BGM 不中断 |
| 循环片段 | 正文段 + 循环段合成为一个文件,用 `loopStart`/`loopEnd` 标记 |
| 字幕 | WebVTT 格式,时间轴精确到毫秒 |
## 命令参考
## 命令
```bash
npm run dev # 启动开发服务器(实时预览)
npm run dev # 启动开发服务器
npm run build # 构建生产版本
npm run preview # 预览构建结果
npm run pack:html # 打包 Web 版

138
docs/guide/BRANCHING.md Normal file
View File

@@ -0,0 +1,138 @@
# 分支叙事指南
## 基本分支
最简单的分支:一个场景 → 多个选项 → 不同目标场景。
```json
"scene_1": {
"id": "scene_1",
"videoUrl": "scene_1/video.mp4",
"choices": [
{ "text": "帮助他", "targetScene": "help" },
{ "text": "离开", "targetScene": "leave" }
]
}
```
## 条件分支
选项可以根据变量条件显示/隐藏:
```json
"choices": [
{
"text": "高信任路线",
"targetScene": "trust_path",
"conditions": [
{ "variable": "trust", "op": ">=", "value": 80 }
]
},
{ "text": "普通路线", "targetScene": "normal_path" }
]
```
`op` 支持:`>`, `<`, `>=`, `<=`, `==`, `!=`
## 变量与效果
全局变量在 JSON 顶层定义初始值:
```json
{
"variables": { "trust": 50, "courage": 0, "investigation": 0 }
}
```
选项选择后应用效果:
```json
{
"text": "与陌生人握手",
"targetScene": "trust_ending",
"effects": [
{ "type": "add", "target": "trust", "value": 30 }
]
}
```
效果类型:
- `"set"` — 设置变量为指定值
- `"add"` — 增加(负数=减少)
## 场景进入效果
进入场景时自动触发:
```json
"ending": {
"id": "ending",
"videoUrl": "ending/video.mp4",
"onEnter": [
{ "type": "set", "target": "completed_game", "value": 1 }
]
}
```
## 限时选择
```json
{
"text": "快速决定!",
"targetScene": "timeout_scene",
"timeLimit": 10
}
```
10 秒内不选,自动选这个选项。`timeLimit: 0` 或省略 = 不限时。
## 默认跳转
无选项或有选项但都不满足条件时,自动跳转:
```json
"scene": {
"id": "scene",
"videoUrl": "scene/video.mp4",
"choices": [], // 无选项时自动跳
"nextScene": "auto_next"
}
```
优先级choices > nextScene > 什么都没配(游戏结束)。
## 关键选择提示Prompt
重要选项可以配置前置金色标识 + 选后浮现提示:
```json
{
"text": "与陌生人握手",
"textKey": "left_door.choice.handshake",
"prompt": "陌生人会记住你的善意",
"promptKey": "left_door.prompt.handshake",
"targetScene": "trust_ending"
}
```
- 前置:选项按钮左侧显示金色竖线 + 淡金边框
- 后置:选择确认后,画面中央浮现 prompt 文字2 秒淡出
`promptKey` 支持 i18n配置方法见 [国际化指南](I18N.md)。
## 多国语言选项
选项文案支持中/英/日等多语言,使用 `textKey` 机制:
```json
{
"text": "继续前进",
"textKey": "qte_success.choice.continue",
"targetScene": "continue_ending"
}
```
- `text` 是回退值(翻译找不到时使用)
- `textKey` 指向 `public/locales/zh.json` 中的翻译
详细配置见 [国际化指南](I18N.md)。

159
docs/guide/I18N.md Normal file
View File

@@ -0,0 +1,159 @@
# 国际化指南i18n
## 双源体系
| 类别 | 位置 | 加载 | 维护者 |
|------|------|------|--------|
| **UI 文案**(按钮、菜单、设置) | `src/locales/{zh,en,ja}.json` | `import` 打包进 bundle | 引擎开发者 |
| **故事文案**(选项、提示、章节名) | `public/demo/locales/{zh,en,ja}.json` | `fetch()` 动态加载 | 故事创作者 |
**核心理念:** 游戏制作者只需编辑 `public/` 下的 JSON刷新页面即生效不需要重新打包。
## 数据层 i18n故事文案
### 架构
```json
// public/scenes/demo.json
{
"locales": { "path": "locales/", "languages": ["zh", "en", "ja"] },
"scenes": {
"intro": {
"choices": [
{ "text": "走向左边那扇发光的门", "textKey": "intro.choice.left_door", ... }
]
}
}
}
```
```json
// public/demo/locales/zh.json
{
"intro": {
"choice": {
"left_door": "走向左边那扇发光的门"
}
}
}
```
```json
// public/demo/locales/en.json
{
"intro": {
"choice": {
"left_door": "Walk toward the glowing door on the left"
}
}
}
```
### 所有支持 i18n 的数据字段
| 数据对象 | 字段 | key 字段 | 示例 key |
|---------|------|---------|----------|
| Choice | `text` | `textKey` | `"intro.choice.left_door"` |
| Choice | `prompt` | `promptKey` | `"left_door.prompt.handshake"` |
| QTE | `prompt` | `promptKey` | `"right_door.qte.dodge"` |
| Hotspot | `label` | `labelKey` | `"investigation_site.hotspot.desk"` |
| Chapter | `label` | `labelKey` | `"chapter.ch1"` |
| Ending | `label` | `labelKey` | `"ending.trust_end"` |
| Achievement | `title` | `titleKey` | `"achievement.qte_master.title"` |
| Achievement | `description` | `descKey` | `"achievement.qte_master.desc"` |
**规律:** 每个可国际化的数据字段都有对应的 `xxxKey` 版本。引擎先查 key找不到时回退原始字段值。
### 回退逻辑
```
t('intro.choice.left_door')
→ 查 public/locales/{lang}.json
→ 找到了 → 返回翻译文本
→ 找不到 → 返回 choice.text"走向左边那扇发光的门"
```
### Locale JSON 结构示例
```json
{
"intro": {
"choice": {
"left_door": "...",
"right_door": "...",
"search": "...",
"stay": "..."
}
},
"left_door": {
"choice": {
"handshake": "...",
"reject": "..."
},
"prompt": {
"handshake": "..."
}
},
"right_door": {
"qte": { "dodge": "..." }
},
"chapter": {
"ch1": "第一章:醒来",
"ch2": "第二章:调查"
},
"ending": {
"trust_end": "信任的伙伴",
"alone_end": "独行之路"
},
"achievement": {
"qte_master": {
"title": "反应达人",
"desc": "成功完成一次 QTE"
}
}
}
```
## 新增语言的方法
### 1. 注册到引擎
`src/locales/` 新增语言文件(复制 `en.json` 翻译):
```bash
cp src/locales/en.json src/locales/ko.json
# 编辑 src/locales/ko.json → 翻译 UI 文案
# 编辑 src/composables/useI18n.ts → 新增 `import ko from '@/locales/ko.json'` + 加入 `uiMessages`
```
### 2. 注册到故事数据
`public/demo/locales/` 新增语言文件:
```bash
cp public/demo/locales/en.json public/demo/locales/ko.json
# 编辑 → 翻译故事文案
```
`demo.json``locales.languages` 数组加 `"ko"`
```json
"locales": { "path": "locales/", "languages": ["zh", "en", "ja", "ko"] }
```
### 3. 刷新页面
语言切换按钮自动新增韩语选项,无需重新构建。
## 配置语言目录
所有 locale 文件放在 `assetBase` 指定的基础路径下。JSON 中配置:
```json
{
"assetBase": "my_story/",
"locales": { "path": "lang/", "languages": ["zh", "en"] }
}
```
则语言文件路径为 `my_story/lang/zh.json`

123
docs/guide/INTERACTIONS.md Normal file
View File

@@ -0,0 +1,123 @@
# 交互指南 — QTE / Hotspot / Loop
## QTE 快速反应事件
在视频播放到特定时间点时弹出按键提示,玩家在倒计时内按下指定按键。
```json
"qte": {
"triggerTime": 1.0,
"prompt": "躲避飞来的石块!",
"promptKey": "right_door.qte.dodge",
"keys": ["ArrowLeft", "ArrowRight", "a", "d"],
"timeLimit": 3.0,
"successScene": "qte_success",
"failScene": "qte_fail",
"effects": {
"success": [{ "type": "add", "target": "courage", "value": 15 }],
"fail": [{ "type": "add", "target": "trust", "value": -20 }]
}
}
```
| 字段 | 说明 |
|------|------|
| `triggerTime` | 触发时间(秒) |
| `prompt` / `promptKey` | 提示文字 / i18n key |
| `keys` | 有效按键(键盘 key 名,不区分大小写) |
| `timeLimit` | 倒计时(秒) |
| `successScene` / `failScene` | 成功/失败目标场景 |
| `effects` | 成功/失败分别触发效果 |
**注意:** QTE 是模态交互。视频播放到 QTE 触发时暂停场景流程QTE 期间视频结束事件被忽略。
**可访问性:** 玩家可在设置中开启"QTE 按键简化"(所有 QTE 统一为空格键)和"QTE 时限放宽 ×1.5"。
**禁止跳过:** QTE 场景建议设 `"skippable": false`,防止玩家跳过 QTE。
## 图片/视频热点Hotspot
在画面中划定可点击区域,玩家点击后触发分支。
### 图片热点
```json
"investigation_site": {
"type": "image",
"imageUrl": "investigation_site/scene.jpg",
"contentSize": { "w": 1280, "h": 720 },
"hotspots": [
{
"id": "hs_desk",
"label": "查看书桌",
"labelKey": "investigation_site.hotspot.desk",
"targetScene": "desk_detail",
"x": 154, "y": 144, "width": 230, "height": 101
}
]
}
```
### 视频热点(按时间显隐)
```json
"corridor": {
"videoUrl": "corridor/video.mp4",
"contentSize": { "w": 1280, "h": 720 },
"hotspots": [
{
"id": "hs_left",
"label": "走向左边通道",
"targetScene": "left_door",
"x": 26, "y": 216, "width": 384, "height": 324,
"showAt": 1.5
},
{
"id": "hs_right",
"label": "走向右边通道",
"targetScene": "alone_ending",
"x": 870, "y": 216, "width": 384, "height": 324,
"showAt": 5.0
}
]
}
```
| 字段 | 说明 |
|------|------|
| `showAt` | 视频播放到此秒数后才显示热点(可选) |
| `hideAt` | 视频播放到此秒数后隐藏热点(可选) |
| `conditions` | 条件满足时才显示(可选) |
### 坐标系统
Hotspot 坐标使用**绝对像素**,基于 `contentSize` 指定的基准分辨率:
- `x, y` — 左上角像素坐标
- `width, height` — 热点区域像素尺寸
引擎自动处理 `object-fit: contain` 的黑边偏移和屏幕缩放。
**制作建议:** 在 Photoshop 中测量坐标,直接写入 JSON。
## 循环等待Loop
视频播放到指定区间后自动循环,适合"等待玩家做决定"的桥段。
```json
"stay": {
"videoUrl": "stay/loop.mp4",
"loopStart": 3.0,
"loopEnd": 6.0,
"choices": [
{ "text": "站起来离开", "targetScene": "alone_ending" }
]
}
```
- 视频 0-3s 正常播放(正文段)
- 到 3s 时循环开始,选项面板同时弹出
- 视频在 3.0-6.0s 之间反复播放,直到玩家做出选择
- BGM 不受循环影响(独立音频轨道)
**视频制作技巧:** 正文段和循环段合成为一个文件。循环段首尾画面应自然衔接。

116
docs/guide/PUBLISHING.md Normal file
View File

@@ -0,0 +1,116 @@
# 打包发布指南
## 准备工作
```bash
npm install
npm run build # 先构建,自动校验 JSON 合法性
```
构建产物在 `dist/`,是纯静态文件,可直接部署到任意 HTTP 服务器。
## Web 版发布
### 一键打包
```bash
npm run pack:html
```
生成 `release/mygame.zip`,上传到任意平台:
| 平台 | 上传方式 |
|------|---------|
| **itch.io** | 直接上传 zip |
| **Netlify** | 拖拽 `dist/` 文件夹到 Drop |
| **GitHub Pages** | 推送 `dist/``gh-pages` 分支 |
| **自有服务器** | 上传 `dist/` 到任意静态文件服务Nginx, Apache, Caddy |
### 域名/CDN
如果把素材放到 CDN只需改一行
```json
{
"assetBase": "https://cdn.example.com/mygame/"
}
```
所有 `videoUrl: "scene_1/video.mp4"` 自动拼为 `https://cdn.example.com/mygame/scene_1/video.mp4`
## 桌面版发布
### macOS
```bash
npm run pack:mac
```
生成 `release/MyGame-darwin-arm64/`。将整个文件夹打包为 `.dmg` 或直接分发文件夹。用户双击 `MyGame.app` 即可运行。
### Windows
```bash
npm run pack:win
```
生成 `release/MyGame-win32-x64/`。运行 `MyGame.exe`
## 桌面版命令行参数
打包后的应用支持 `--scene` 参数指定剧情文件:
```bash
# macOS
./MyGame.app/Contents/MacOS/MyGame --scene=./scenes/my_story.json
# Windows
MyGame.exe --scene=./scenes/my_story.json
```
## 素材管理
### 本地开发
所有素材放在 `public/`Vite 自动 serve。
### 生产发布
- 视频/音频通常较大(几百 MB建议单独分发或放 CDN
- `.gitignore` 中已排除 `public/videos/`,视频不提交到 Git
- 打包脚本自动复制 `public/``dist/`,无需手动处理
### 目录规范
```
public/demo/ ← 示例数据
scenes/demo.json
locales/{zh,en,ja}.json
intro/video.mp4
shared/bgm.mp3
public/your_story/ ← 你的游戏(复制此结构)
scenes/main.json
locales/{zh,en}.json
scene_1/video.mp4
```
## 常见问题
### Q: 构建时提示 "JSON 不合法"
检查 JSON 文件是否有多余逗号(最后一项不能有逗号)。
### Q: 打包后视频不加载
检查 `assetBase` 配置,确保路径拼接正确。开发模式 `assetBase: ""`,发布到 CDN 时改为完整 URL。
### Q: 打包体积太大
- 视频是最大的文件建议单独制作低码率预览版2Mbps用于小体积分发
- 音频用 MP3 128kbps 即可
- 缩略图 JPG 质量 60% 足够
### Q: 如何制作安装包(.dmg / .exe 安装程序)
参考 `docs/electron/packaging-guide.md`

87
docs/guide/QUICK_START.md Normal file
View File

@@ -0,0 +1,87 @@
# 快速开始 — 5 分钟制作你的第一个交互式电影游戏
## 1. 准备视频
用任何方式制作三个短视频MP4 H.264
```
my_story/
scene_1/video.mp4 ← 开场视频
scene_2/video.mp4 ← 分支 A 视频
ending/video.mp4 ← 结局视频
```
推荐参数1280×72030fps2-5Mbps。
## 2. 写剧情 JSON
创建 `public/scenes/my_story.json`
```json
{
"assetBase": "my_story/",
"startScene": "scene_1",
"variables": {},
"scenes": {
"scene_1": {
"id": "scene_1",
"videoUrl": "scene_1/video.mp4",
"choices": [
{ "text": "选择 A", "targetScene": "scene_2" },
{ "text": "选择 B", "targetScene": "ending" }
]
},
"scene_2": {
"id": "scene_2",
"videoUrl": "scene_2/video.mp4",
"nextScene": "ending"
},
"ending": {
"id": "ending",
"videoUrl": "ending/video.mp4",
"choices": []
}
}
}
```
## 3. 启动
```bash
npm install
npm run dev
```
打开 `http://localhost:5173/?scene=my_story.json`
## 4. 发生了什么
- 场景 1 播放 → 视频结束后弹出两个选项
- 选 A → 场景 2 → 自动跳转到 ending
- 选 B → 直接到 ending
- ending 无选项 → 游戏结束,返回主菜单
## 下一步
- 想加限定条件的选择?→ [分支叙事指南](BRANCHING.md)
- 想加 QTE 或热点?→ [交互指南](INTERACTIONS.md)
- 想加背景音乐或字幕?→ [场景 JSON 参考](SCENE_JSON_SPEC.md)
- 想支持多语言?→ [国际化指南](I18N.md)
- 想打包发布?→ [发布指南](PUBLISHING.md)
## 素材组织建议
```
my_story/
scene_1/video.mp4 ← 每个场景一个文件夹
scene_2/video.mp4
ending/video.mp4
shared/ ← 跨场景共享素材
bgm.mp3
thumb.jpg
locales/ ← 多语言翻译文件(可选)
zh.json
en.json
```
JSON 中所有路径都是相对于素材根目录的相对路径,配合 `assetBase` 前缀使用。

View File

@@ -0,0 +1,255 @@
# Scene JSON 完整字段参考
## 顶层结构
```json
{
"assetBase": "",
"locales": { "path": "locales/", "languages": ["zh", "en"] },
"startScene": "intro",
"variables": { "trust": 50, "courage": 0 },
"introVideo": "__intro__/logo.mp4",
"menuVideo": "__intro__/menu_bg.mp4",
"scenes": { ... },
"chapters": [ ... ],
"achievements": [ ... ],
"endings": [ ... ]
}
```
| 字段 | 类型 | 必需 | 说明 |
|------|------|------|------|
| `assetBase` | string | 否 | 所有资源路径前缀,默认 `""`。设 `"demo/"``videoUrl: "intro/video.mp4"` 自动拼成 `demo/intro/video.mp4`。改 CDN 只需改这一行 |
| `locales` | object | 否 | 多语言配置。`path` 为 locale 文件目录(相对于 `assetBase``languages` 为支持的语言列表 |
| `startScene` | string | 是 | 开始场景的 ID |
| `variables` | object | 否 | 全局变量初始值 |
| `introVideo` | string | 否 | 开场视频路径 |
| `menuVideo` | string | 否 | 主菜单背景视频路径(自动循环播放) |
| `scenes` | object | 是 | 所有场景的集合key 为场景 ID |
| `chapters` | array | 否 | 章节列表 |
| `achievements` | array | 否 | 成就列表 |
| `endings` | array | 否 | 结局列表 |
---
## SceneNode
```typescript
interface SceneNode {
id: string
type?: 'video' | 'image'
videoUrl: string
imageUrl?: string
contentSize?: { w: number; h: number }
subtitleUrl?: string
subtitles?: Record<string, string>
choices?: Choice[]
hotspots?: Hotspot[]
qte?: QTEDefinition
nextScene?: string
onEnter?: Effect[]
loopStart?: number
loopEnd?: number
bgmUrl?: string
bgmVolume?: number
bgmCrossFade?: number
bgmDuckLevel?: number
bgmDuckFade?: number
videoMuted?: boolean
skippable?: boolean
}
```
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | string | 场景唯一标识 |
| `type` | string | `"video"` (默认) 或 `"image"`。image 类型展示图片+热点,不播放视频 |
| `videoUrl` | string | 视频文件路径。image 场景可为空 |
| `imageUrl` | string | 图片场景的图片路径 |
| `contentSize` | object | 内容基准分辨率 `{ w: 1280, h: 720 }`Hotspot 坐标基于此计算。用于 object-fit: contain 的偏移补偿 |
| `subtitleUrl` | string | 回退字幕路径。优先使用 `subtitles` |
| `subtitles` | object | 多语言字幕 `{ "zh": "...", "en": "..." }`。语言切换时自动选择 |
| `choices` | Choice[] | 选项列表 |
| `hotspots` | Hotspot[] | 可点击热点区域 |
| `qte` | QTEDefinition | QTE 定义 |
| `nextScene` | string | 无选项时的默认跳转场景 |
| `onEnter` | Effect[] | 进入场景时触发的效果 |
| `loopStart` | number | 循环起始时间(秒) |
| `loopEnd` | number | 循环结束时间(秒)。视频播放到 loopEnd 时跳回 loopStart |
| `bgmUrl` | string | 背景音乐路径 |
| `bgmVolume` | number | 背景音乐音量0-1默认 0.8 |
| `bgmCrossFade` | number | 背景音乐交叉淡化时长(秒),默认 2.0 |
| `bgmDuckLevel` | number | BGM Duck 压低比例0-1默认 0.35 |
| `bgmDuckFade` | number | BGM Duck 过渡时长(秒),默认 0.5 |
| `videoMuted` | boolean | 视频静音(配合独立 BGM 使用) |
| `skippable` | boolean | `false` = 禁止跳过(用于 QTE 等关键场景) |
---
## Choice
```typescript
interface Choice {
text: string
textKey?: string
prompt?: string
promptKey?: string
targetScene: string
conditions?: Condition[]
effects?: Effect[]
timeLimit?: number
}
```
| 字段 | 说明 |
|------|------|
| `text` | 选项文本(回退值) |
| `textKey` | i18n key优先于 `text`。如 `"intro.choice.left_door"` |
| `prompt` | 选择后浮现的提示文字(回退值) |
| `promptKey` | prompt 的 i18n key。如 `"left_door.prompt.handshake"` |
| `targetScene` | 目标场景 ID |
| `conditions` | 显示条件,不满足的选项隐藏 |
| `effects` | 选择后触发的效果 |
| `timeLimit` | 限时秒数0=不限时 |
---
## Hotspot
```typescript
interface Hotspot {
id: string
label: string
labelKey?: string
targetScene: string
x: number
y: number
width: number
height: number
showAt?: number
hideAt?: number
conditions?: Condition[]
effects?: Effect[]
}
```
| 字段 | 说明 |
|------|------|
| `id` | 热点唯一标识 |
| `label` | 显示标签(回退值) |
| `labelKey` | i18n key |
| `x, y` | 左上角坐标(像素,基于 contentSize |
| `width, height` | 尺寸(像素) |
| `showAt` | 视频时间(秒),此时间后显示。不设则始终可见 |
| `hideAt` | 视频时间(秒),此时间后隐藏 |
| `targetScene` | 点击后跳转的场景 |
| `conditions` | 显示条件 |
| `effects` | 点击后触发的效果 |
---
## QTEDefinition
```typescript
interface QTEDefinition {
triggerTime: number
prompt: string
promptKey?: string
keys: string[]
timeLimit: number
successScene: string
failScene: string
effects?: {
success: Effect[]
fail: Effect[]
}
}
```
| 字段 | 说明 |
|------|------|
| `triggerTime` | 触发时间(秒),视频播放到此时间弹出 QTE |
| `prompt` | 提示文字(回退值) |
| `promptKey` | i18n key。如 `"right_door.qte.dodge"` |
| `keys` | 有效按键列表,如 `["ArrowLeft", "ArrowRight", "a", "d"]` |
| `timeLimit` | 限时秒数 |
| `successScene` | 成功跳转场景 |
| `failScene` | 失败/超时跳转场景 |
| `effects` | 成功/失败分别触发效果 |
---
## 其他类型
### Condition
```typescript
interface Condition {
variable: string
op: '>' | '<' | '>=' | '<=' | '==' | '!='
value: number | string | boolean
}
```
### Effect
```typescript
interface Effect {
type: 'set' | 'add'
target: string
value?: number | string | boolean
}
```
- `set` — 设置变量值
- `add` — 变量增加/减少
### ChapterInfo
```typescript
interface ChapterInfo {
id: string
label: string
labelKey?: string
startScene: string
thumbnail?: string
defaultVariables?: Record<string, number>
}
```
### AchievementDef
```typescript
interface AchievementDef {
id: string
title: string
titleKey?: string
description: string
descKey?: string
icon?: string
hidden?: boolean
condition: Condition
}
```
### EndingDef
```typescript
interface EndingDef {
id: string
label: string
labelKey?: string
sceneId: string
chapterId?: string
thumbnail?: string
}
```
### LocalesConfig
```typescript
interface LocalesConfig {
path: string
languages: string[]
}
```