docs: reorganize docs into guide/ and electron/, add 6 new guide docs, update README
This commit is contained in:
147
README.md
147
README.md
@@ -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×720,30fps,2-5Mbps |
|
||||
| 背景音乐 | `public/audio/` | MP3 |
|
||||
| 缩略图 | `public/images/` | JPG/PNG,320×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
138
docs/guide/BRANCHING.md
Normal 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
159
docs/guide/I18N.md
Normal 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
123
docs/guide/INTERACTIONS.md
Normal 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
116
docs/guide/PUBLISHING.md
Normal 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
87
docs/guide/QUICK_START.md
Normal 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×720,30fps,2-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` 前缀使用。
|
||||
255
docs/guide/SCENE_JSON_SPEC.md
Normal file
255
docs/guide/SCENE_JSON_SPEC.md
Normal 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[]
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user