645 lines
30 KiB
Markdown
645 lines
30 KiB
Markdown
# 交互式电影游戏引擎 — Roadmap
|
||
|
||
## 技术栈
|
||
|
||
- **框架**: Vue 3 (Composition API + `<script setup>`)
|
||
- **构建**: Vite
|
||
- **状态管理**: Pinia
|
||
- **可视化编辑器**: Vue Flow
|
||
- **存储**: IndexedDB (Dexie.js)
|
||
- **语言**: TypeScript
|
||
- **视频**: 原生 `<video>` + A/B 双缓冲
|
||
|
||
## 项目结构
|
||
|
||
```
|
||
moviegame/
|
||
├── engine/ # 框架无关的核心引擎(纯 TS)
|
||
│ ├── core/
|
||
│ │ ├── Engine.ts # 主循环,驱动各子系统
|
||
│ │ ├── SceneManager.ts # 剧情节点图遍历
|
||
│ │ ├── VideoManager.ts # A/B 双缓冲视频播放
|
||
│ │ └── StateManager.ts # 全局状态、条件求值
|
||
│ ├── systems/
|
||
│ │ ├── ChoiceSystem.ts # 选择 UI + 倒计时
|
||
│ │ ├── QTESystem.ts # QTE 触发、判定、超时
|
||
│ │ └── SaveSystem.ts # IndexedDB 存取
|
||
│ └── types.ts # 场景数据类型定义
|
||
├── src/ # Vue 应用(播放器端)
|
||
│ ├── components/
|
||
│ │ ├── GamePlayer.vue # 主播放器(挂载 video 元素)
|
||
│ │ ├── ChoicePanel.vue # 选项面板
|
||
│ │ ├── QTEOverlay.vue # QTE 遮罩
|
||
│ │ ├── SaveLoadMenu.vue # 存档界面
|
||
│ │ └── Subtitles.vue # 字幕显示
|
||
│ ├── composables/
|
||
│ │ ├── useGameEngine.ts # 引擎实例 + 生命周期
|
||
│ │ ├── useVideoPlayer.ts # 视频状态响应式封装
|
||
│ │ └── useGameState.ts # 状态响应式封装
|
||
│ ├── stores/
|
||
│ │ └── gameStore.ts # 游戏全局状态(Pinia)
|
||
│ ├── App.vue
|
||
│ └── main.ts
|
||
├── editor/ # Vue Flow 可视化编辑器(独立入口)
|
||
│ ├── components/
|
||
│ │ ├── SceneGraph.vue
|
||
│ │ ├── NodeEditor.vue
|
||
│ │ └── PreviewPanel.vue
|
||
│ ├── composables/
|
||
│ │ └── useGraphEditor.ts
|
||
│ └── App.vue
|
||
├── public/
|
||
│ ├── videos/ # 视频资源
|
||
│ └── scenes/
|
||
│ └── demo.json # 示例剧情数据
|
||
├── vite.config.ts
|
||
├── tsconfig.json
|
||
├── package.json
|
||
└── index.html
|
||
```
|
||
|
||
## 场景数据格式
|
||
|
||
```typescript
|
||
// engine/types.ts
|
||
|
||
interface SceneNode {
|
||
id: string;
|
||
videoUrl: string;
|
||
subtitleUrl?: string;
|
||
choices?: Choice[];
|
||
qte?: QTEDefinition;
|
||
nextScene?: string;
|
||
onEnter?: Effect[];
|
||
}
|
||
|
||
interface Choice {
|
||
text: string;
|
||
targetScene: string;
|
||
conditions?: Condition[];
|
||
effects?: Effect[];
|
||
timeLimit?: number; // 限时选择(秒),0=不限时
|
||
}
|
||
|
||
interface Condition {
|
||
variable: string;
|
||
op: '>' | '<' | '>=' | '<=' | '==' | '!=' | 'hasFlag';
|
||
value: number | string | boolean;
|
||
}
|
||
|
||
interface Effect {
|
||
type: 'set' | 'add' | 'toggleFlag' | 'triggerEvent';
|
||
target: string;
|
||
value?: number | string | boolean;
|
||
}
|
||
|
||
interface QTEDefinition {
|
||
triggerTime: number;
|
||
prompt: string;
|
||
keys: string[];
|
||
timeLimit: number;
|
||
successScene: string;
|
||
failScene: string;
|
||
effects?: {
|
||
success: Effect[];
|
||
fail: Effect[];
|
||
};
|
||
}
|
||
|
||
interface GameData {
|
||
scenes: Record<string, SceneNode>;
|
||
startScene: string;
|
||
variables: Record<string, number>;
|
||
}
|
||
|
||
interface SaveData {
|
||
slot: number;
|
||
timestamp: number;
|
||
currentScene: string;
|
||
variables: Record<string, number>;
|
||
flags: string[];
|
||
history: ChoiceRecord[];
|
||
thumbnail?: string;
|
||
}
|
||
```
|
||
|
||
## 实现路线
|
||
|
||
### P0 MVP — 最小可玩原型(3-5 天)✅ 已完成 2026-06-07
|
||
|
||
目标:能播放一段视频 → 弹出选项 → 跳到下一段视频
|
||
|
||
- [x] 项目脚手架:Vite + Vue3 + TypeScript + Pinia
|
||
- [x] `engine/core/Engine.ts` — 主循环骨架(加载场景 → 播放 → 等选择 → 切换)
|
||
- [x] `engine/core/SceneManager.ts` — 加载 JSON,按 ID 查找场景节点
|
||
- [x] `engine/core/VideoManager.ts` — 单 video 元素播放,监听 ended 事件
|
||
- [x] `engine/core/StateManager.ts` — 变量存取、条件求值、效果执行
|
||
- [x] `engine/types.ts` — 类型定义
|
||
- [x] `src/components/GamePlayer.vue` — 挂载 video,控制播放
|
||
- [x] `src/components/ChoicePanel.vue` — 渲染选择按钮,触发引擎切换
|
||
- [x] `public/scenes/demo.json` — 编写一段简单剧情(7 个场景节点)
|
||
- [x] 验证:从 demo.json 加载场景,能走通 开始→选择→分支播放→结束 流程
|
||
|
||
### P1 核心 — 无缝切换 + 条件分支 + 存档(1-2 周)✅ 已完成 2026-06-07
|
||
|
||
- [x] `engine/core/VideoManager.ts` 升级 — A/B 双缓冲,预加载候选视频,CSS 交叉淡化
|
||
- [x] `engine/core/SceneManager.ts` 升级 — 支持条件分支(根据 variables/flags 过滤选项)
|
||
- [x] `engine/systems/SaveSystem.ts` — Dexie.js IndexedDB 存取,多槽位
|
||
- [x] `engine/systems/ChoiceSystem.ts` — 限时选择倒计时,超时默认选择
|
||
- [x] `src/components/SaveLoadMenu.vue` — 存档/读档 UI
|
||
- [x] `src/stores/gameStore.ts` — Pinia 全局状态管理(含计时器、存档列表)
|
||
- [x] `src/composables/useGameEngine.ts` — 桥接层(双 video、存档、计时器)
|
||
- [x] `src/components/GamePlayer.vue` — 双 video 元素 + 交叉淡化 CSS
|
||
- [x] `src/components/ChoicePanel.vue` — 倒计时进度条 + 计时文字
|
||
- [x] `src/App.vue` — 整合 SaveLoadMenu、双 video、计时器
|
||
- [x] 验证:条件分支走通,存档读档正常,视频切换交叉淡化
|
||
|
||
### P2 进阶 — QTE + 字幕 + 多存档槽(1 周)✅ 已完成 2026-06-07
|
||
|
||
- [x] `engine/systems/QTESystem.ts` — QTE 触发、键盘监听(支持多键匹配)、超时判定
|
||
- [x] `src/components/QTEOverlay.vue` — SVG 倒计时环 + 按键提示 + 成功/失败动画
|
||
- [x] `src/components/Subtitles.vue` — WebVTT 解析 + 字幕同步渲染
|
||
- [x] `engine/core/Engine.ts` — 集成 QTE(timeupdate 检测 + 条件跳转 + 效果应用)
|
||
- [x] 多存档槽位 + 存档缩略图(canvas 截图当前视频帧,320x180 JPEG)
|
||
- [x] `engine/core/VideoManager.ts` — 新增 `getActiveVideoElement()` 供截图
|
||
- [x] `engine/systems/SaveSystem.ts` — DB 版本升级 v2(支持 thumbnail 字段)
|
||
- [x] `src/components/SaveLoadMenu.vue` — 存档缩略图预览
|
||
- [x] 完整事件总线(sceneChange, choiceRequest, choiceTimer, choiceTimeout, videoEnd, qteTrigger, qteTimer, qteResult, gameEnd)
|
||
- [x] 验证:QTE 正常触发与判定(ArrowLeft/ArrowRight/A/D 躲石块),字幕同步,存档缩略图正常
|
||
|
||
### P3 编辑器 — 可视化剧情编辑(2-3 周)✅ 已完成 2026-06-07
|
||
|
||
- [x] 编辑器入口:独立 `editor/index.html` + `editor/main.ts`(Vite 多入口构建)
|
||
- [x] `editor/components/SceneGraph.vue` — Vue Flow 节点图(场景节点 + 分支/默认/QTE 连线)
|
||
- [x] `editor/components/NodeEditor.vue` — 右侧面板(视频/字幕路径、nextScene、选项增删改、QTE 参数编辑)
|
||
- [x] `editor/components/PreviewPanel.vue` — 嵌入播放器实时预览选中场景视频
|
||
- [x] `editor/composables/useGraphEditor.ts` — 图数据与 JSON 双向同步
|
||
- [x] JSON 导出/导入(文件下载 + 文件选择)
|
||
- [x] 工具栏:新建场景、导入 JSON、导出 JSON、加载示例、起始场景选择
|
||
- [x] `vite.config.ts` — 多页面构建(main + editor)
|
||
- [x] 验证:编辑器能产出合法 JSON,引擎能正确加载并运行
|
||
|
||
### P4 视频/图片热点 — 点击画面区域触发分支 ✅ 已完成 2026-06-08
|
||
|
||
目标:在视频或图片上定义可点击热区(Hotspot),玩家点击画面不同位置触发不同分支。
|
||
热区既可覆盖在静态图片上(调查/解谜场景),也可覆盖在播放中的视频上(根据时间轴淡入淡出)。
|
||
|
||
**视频热点 vs 图片热点(架构统一,差异仅两点):**
|
||
|
||
| | 图片热点 | 视频热点 |
|
||
|------|----------|----------|
|
||
| 底层内容 | `<img>` 元素 | `<video>` 元素(已经在播) |
|
||
| 热点出现时机 | 始终可见 | 按时间轴出现/消失(`showAt`/`hideAt`) |
|
||
|
||
**场景数据设计:**
|
||
|
||
```json
|
||
{
|
||
"id": "investigation",
|
||
"type": "video",
|
||
"videoUrl": "/videos/investigation.mp4",
|
||
"subtitleUrl": "/subtitles/investigation.vtt",
|
||
"hotspots": [
|
||
{
|
||
"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" }]
|
||
},
|
||
{
|
||
"id": "hs_window",
|
||
"label": "靠近窗户",
|
||
"targetScene": "window_look",
|
||
"x": 0.70, "y": 0.10, "width": 0.20, "height": 0.40,
|
||
"showAt": 5.0,
|
||
"hideAt": 10.0
|
||
},
|
||
{
|
||
"id": "hs_door",
|
||
"label": "离开房间",
|
||
"targetScene": "leave_room",
|
||
"x": 0.30, "y": 0.60, "width": 0.40, "height": 0.30,
|
||
"timeLimit": 15
|
||
}
|
||
],
|
||
"choices": [
|
||
{ "text": "放弃调查", "targetScene": "give_up" }
|
||
]
|
||
}
|
||
```
|
||
|
||
**字段约定:**
|
||
- `x/y/width/height` — 热区坐标,使用**相对比例**(0~1),自适应屏幕尺寸
|
||
- `showAt`/`hideAt` — 视频热点的时间轴(秒),未设置时热区始终可见(兼容图片场景和始终可见的视频热点)
|
||
- `hotspots` 支持 `conditions`(条件显隐)、`effects`(点击后效果)、`timeLimit`(限时热区)
|
||
- 热点场景仍可同时附带底部 `choices`(如"放弃调查"按钮)
|
||
- `type` 字段区分 `"video"`(默认)和 `"image"`(静态图,此时 `imageUrl` 替代 `videoUrl`)
|
||
|
||
**实现清单:**
|
||
|
||
- [x] `engine/types.ts` — `SceneNode.type` 字段、`Hotspot` 接口(含 `showAt`/`hideAt`)
|
||
- [x] `src/components/HotspotLayer.vue` — 通用热区覆盖层:叠加在视频或图片之上,render 热区矩形 + hover 高亮 + label 浮动提示
|
||
- [x] `engine/core/Engine.ts` — 视频模式下监听 timeupdate,按时显隐热区;点击热区触发分支跳转
|
||
- [ ] `editor/components/NodeEditor.vue` — 场景类型切换(视频/图片)+ 热区列表编辑 + 时间轴参数(showAt/hideAt)
|
||
- [x] `public/images/` — 示例图片目录
|
||
- [x] `public/scenes/demo.json` — 新增图片热点场景 `investigation_site` + 视频热点场景 `corridor`
|
||
- [x] 验证:图片热区点击触发、视频热区按时出现/消失、条件过滤、hover 高亮
|
||
|
||
### P5 选择等待循环 — 单文件内时间锚点无缝循环 ✅ 已完成 2026-06-08
|
||
|
||
目标:视频结束后画面不暂停,而是在同一文件内通过 `loopStart`/`loopEnd` 时间锚点实现无切换循环,
|
||
选项浮在循环画面之上。和《底特律:变人》《The Dark Pictures Anthology》等商业游戏的做法一致。
|
||
|
||
**为什么不用单独 loop 文件做 cross-fade:**
|
||
- 任何文件切换(硬切或淡入)都会产生可感知的割裂感
|
||
- 商业游戏的循环效果本质上就是同一帧内 `video.currentTime = loopStart`,完全透明
|
||
- 同一文件内 seek 只在下一个 timeupdate 触发(~250ms),但对 ≥2 秒的循环区间来说误差 <5%,肉眼无感
|
||
|
||
**做法对比:**
|
||
|
||
| 方案 | 体验 |
|
||
|------|------|
|
||
| ~~主视频 → cross-fade → loopVideo 文件~~ | 两画面重叠 300ms,**割裂** |
|
||
| ~~主视频 → 硬切 → loopVideo 文件~~ | 一帧黑/闪,依赖浏览器 |
|
||
| **同一文件内 `loopStart/loopEnd` seek** | **完全无缝,AAA 游戏标准** |
|
||
|
||
**场景数据设计:**
|
||
|
||
```json
|
||
{
|
||
"id": "tense_moment",
|
||
"videoUrl": "/videos/tense_full.mp4",
|
||
"loopStart": 8.0,
|
||
"loopEnd": 10.0,
|
||
"choices": [
|
||
{ "text": "冒险救人", "targetScene": "rescue" },
|
||
{ "text": "悄悄离开", "targetScene": "flee" }
|
||
]
|
||
}
|
||
```
|
||
|
||
素材制作流程:导演剪辑时将主剧情段 + 循环段合成为一个 MP4 文件,比如:
|
||
```
|
||
0:00 ~ 8:00 正常剧情演绎
|
||
8:00 ~ 10:00 循环片段(角色呼吸、张望)── 循环起点 loopStart=8, loopEnd=10
|
||
```
|
||
|
||
**工作流程:**
|
||
|
||
```
|
||
┌─ 主视频正常播放(0s → loopStart)
|
||
│
|
||
├─ time >= loopStart → 标记"已到达循环区间"
|
||
│ └─ timeupdate 持续检测:time >= loopEnd → video.currentTime = loopStart(无任何过渡)
|
||
│ └─ 无限循环中...
|
||
│ │
|
||
│ ├─ 用户选择 ──→ break loop → switchTo(nextScene)
|
||
│ │
|
||
│ ├─ 视频 ended → 自动触发循环区间
|
||
│ │
|
||
│ └─ 选项面板在循环开始时浮出
|
||
│
|
||
└─ 无 loopStart 的场景 → 保持现有行为(结束后暂停,等待选择)
|
||
```
|
||
|
||
**关键设计细节:**
|
||
|
||
- 检测循环完全依赖 `timeupdate` 事件,无需 `requestAnimationFrame` 或额外定时器——浏览器 ~250ms 的 timeupdate 间隔对 ≥2s 的循环段误差可忽略
|
||
- 循环中 A/B 预加载仍然工作:inactive slot 加载第一个候选目标场景,用户选择后 cross-fade 过去
|
||
- `loopStart` 既是触发选项显示的时机(视频到达此处时 emit `choiceRequest`)也是循环起点
|
||
- `loopEnd` 为循环终点,到达后 seek 回 `loopStart`
|
||
- 若只设 `loopStart` 不设 `loopEnd`,则循环区间为 `loopStart → 视频结尾`
|
||
|
||
**实现清单:**
|
||
|
||
- [x] `engine/types.ts` — `SceneNode.loopStart?: number`, `loopEnd?: number`
|
||
- [x] `engine/core/VideoManager.ts` — 新增 `seekTo(time)` 方法
|
||
- [x] `engine/core/Engine.ts` — `checkLoop(time)` 在 timeupdate 中检测循环区间;`onVideoEnd` 循环活跃时跳过;`goToScene` 重置 `loopActive`
|
||
- [x] `public/scenes/demo.json` — `stay` 场景添加 loopStart=3, loopEnd=6, 循环中显示选项
|
||
- [x] `public/videos/stay_loop.mp4` — 6s 测试视频(0-3s 蓝色正文 + 3-6s 绿色循环段)
|
||
- [x] 验证:正文播放完毕 → 进入循环 → 选项浮现 → 画面无缝来回 → 选择后跳转
|
||
|
||
### P6 独立背景音乐 + Ducking — 画面循环不打断 BGM ✅ 已完成 2026-06-08
|
||
|
||
目标:将 BGM 从视频中剥离,由独立 AudioSystem 驱动。视频循环/切换时 BGM 保持连贯,场景间交叉淡化衔接。
|
||
QTE 和选择面板出现时 BGM 自动闪避(ducking)以确保提示音不被淹没。
|
||
|
||
**技术选型**
|
||
|
||
- **Web Audio API** — `GainNode.exponentialRampToValueAtTime()` 实现指数渐变(听感均匀,Wwise/FMOD/UE 同款做法)
|
||
- **MP3** — 全浏览器支持(含 Safari),解码快。OGG 暂不采用(Safari 不支持),P14 短循环音效需要 OGG 时单独处理
|
||
- **预加载** — `fetch(url) → decodeAudioData() → 缓存 AudioBuffer`,已解码 buffer 最多 3 个,LRU 淘汰
|
||
- **视频不自动静音** — `videoMuted` 字段由制作者手动设置,引擎不做自动静音
|
||
|
||
**架构变更:**
|
||
|
||
```
|
||
Engine
|
||
├── VideoManager(A/B 双缓冲,只管画面和视频内音轨)
|
||
│ └── loopStart/loopEnd 循环 → BGM 不受影响
|
||
└── AudioSystem(Web Audio API)
|
||
├── AudioContext → GainNode(BGM) → destination
|
||
│ └── 多个 BufferSourceNode(新旧 BGM 交叉淡化,指数 ramp)
|
||
└── ducking 控制:QTE/选择/热点触发 → GainNode ramp 降 → 事件结束 → ramp 恢复
|
||
```
|
||
|
||
**BGM 切换策略:**
|
||
|
||
```
|
||
goToScene(Scene B)
|
||
├── bgmUrl 相同?→ 什么都不做,继续播(bgmVolume 变化 → ramp 调整)
|
||
├── bgmUrl 为 null?→ 当前 BGM 指数 fade out(bgmCrossFade 秒)
|
||
└── bgmUrl 不同?
|
||
├── fetch + decode BGM B(若未缓存)
|
||
├── AudioBufferSourceNode 播 BGM B,gain 从 0.001 ramp 到 bgmVolume
|
||
├── 同时 BGM A 的 gain ramp 到 0.001,耗时 bgmCrossFade 秒
|
||
├── ramp 完成后 stop BGM A 的 source(释放)
|
||
└── 画面交叉淡化照常(画面和 BGM 各自独立过渡)
|
||
```
|
||
|
||
**Ducking 自动闪避策略:**
|
||
|
||
| 触发事件 | duck 目标值 | 进入耗时 | 恢复耗时 |
|
||
|----------|------------|---------|---------|
|
||
| QTE 触发 | bgmDuckLevel × bgmVolume | 0.3s | bgmDuckFade |
|
||
| 选择面板出现 | bgmDuckLevel × bgmVolume | bgmDuckFade | bgmDuckFade |
|
||
| 视频热点出现 | bgmDuckLevel × bgmVolume | bgmDuckFade | bgmDuckFade |
|
||
|
||
实现方式:AudioSystem 内部维护一个"当前 duck 等级"计数器(允许多个事件重叠)。
|
||
GainNode 的 ramp 目标值 = `Math.min(bgmVolume, bgmDuckLevel × bgmVolume)`。
|
||
最后一个事件结束时恢复为 `bgmVolume`。
|
||
|
||
**场景数据设计:**
|
||
|
||
```json
|
||
{
|
||
"id": "tense_moment",
|
||
"videoUrl": "/videos/tense_no_bgm.mp4",
|
||
"loopStart": 8.0,
|
||
"loopEnd": 10.0,
|
||
"bgmUrl": "/audio/tense_bgm.mp3",
|
||
"bgmVolume": 0.8,
|
||
"bgmCrossFade": 2.0,
|
||
"bgmDuckLevel": 0.35,
|
||
"bgmDuckFade": 0.5,
|
||
"videoMuted": false,
|
||
"choices": [...]
|
||
}
|
||
```
|
||
|
||
**字段说明:**
|
||
|
||
| 字段 | 类型 | 默认 | 说明 |
|
||
|------|------|------|------|
|
||
| `bgmUrl` | string | null | BGM 文件路径(MP3),null/falsy 表示静默并 fade out 当前 BGM |
|
||
| `bgmVolume` | number | 0.8 | 目标音量(0~1) |
|
||
| `bgmCrossFade` | number | 2.0 | BGM 切换交叉淡化时长(秒) |
|
||
| `bgmDuckLevel` | number | 0.35 | QTE/选择/热点时 duck 到 bgmVolume 的百分比 |
|
||
| `bgmDuckFade` | number | 0.5 | duck 进入和恢复的渐变时长(秒) |
|
||
| `videoMuted` | bool | false | 制作者手动设置,引擎不自动静音 |
|
||
|
||
**实现清单:**
|
||
|
||
- [x] `engine/systems/AudioSystem.ts` — Web Audio API:fetch+decode 缓存、BufferSourceNode 创建、GainNode 指数 ramp 交叉淡化、同源继续/不同 crossFade/静音 fade out、ducking 事件接口
|
||
- [x] `engine/core/Engine.ts` — 集成 AudioSystem;`goToScene` 对比 `bgmUrl` 调度切换;QTE/choice/hotspot 触发时调用 `audioSystem.duckOn()`/`duckOff()`
|
||
- [x] `engine/types.ts` — `SceneNode` 加 `bgmUrl`、`bgmVolume`、`bgmCrossFade`、`bgmDuckLevel`、`bgmDuckFade`、`videoMuted`
|
||
- [x] `engine/core/VideoManager.ts` — 根据 `videoMuted` 设置 `<video>.muted`(手工字段,不自动)
|
||
- [x] `public/audio/` — BGM 测试 MP3(calm_bgm.mp3, tense_bgm.mp3)
|
||
- [x] `public/scenes/demo.json` — intro/stay/right_door 配置 BGM + cross-fade + ducking 示例
|
||
- [ ] `editor/components/NodeEditor.vue` — BGM 字段编辑面板(6 个字段)
|
||
- [x] 验证:BGM 跨视频循环连续、场景切换交叉淡化、ducking 降/恢复、同源不中断、指数曲线听感均匀
|
||
|
||
**远期功能(不纳入 P6):**
|
||
|
||
| 功能 | 说明 |
|
||
|------|------|
|
||
| 自适应 BGM | 按 StateManager 变量值切换变奏(如 suspicion < 50 放安静版,>= 50 放紧张版) |
|
||
| 水平分段编排 | BGM 前奏/主体/变奏/尾奏自动串联 |
|
||
| 分层 Stems | 多轨独立 GainNode 动态叠加,按变量增减层数 |
|
||
| Stingers | 短乐句事件音(发现线索的"叮"、惊悚弦乐刺音) |
|
||
| BGM 弧线 | 一条 BGM 覆盖多个连续场景而不被切换打断 |
|
||
|
||
### P7 全屏模式 — 沉浸式浏览器体验 ✅ 已完成 2026-06-08
|
||
|
||
目标:一键进入全屏播放模式,播放中自动隐藏 UI(选项/菜单等浮层除外),提供 F11 级别的沉浸感。
|
||
|
||
**实现清单:**
|
||
|
||
- [x] `src/composables/useFullscreen.ts` — Fullscreen API 封装(`toggle` + `isFullscreen` + `fullscreenchange` 监听)
|
||
- [x] `src/App.vue` — 右上角全屏按钮,与"菜单"按钮并排;`fullscreenchange` 同步图标状态
|
||
- [x] `FUTURE.md` — 远期扩展笔记(Pointer Lock、自动全屏、UI 自动隐藏、移动端适配等)
|
||
|
||
### P8 章节选择 — 通关后可跳转(待实现)
|
||
|
||
目标:玩家通关后,可从中途任意章节起始点重新开始。每个章节记录一个入口场景 ID,展示章节缩略图和标题。
|
||
|
||
**数据结构设计:**
|
||
|
||
```json
|
||
{
|
||
"chapters": [
|
||
{ "id": "ch1", "label": "第一章:醒来", "startScene": "intro", "thumbnail": "/images/ch1.jpg" },
|
||
{ "id": "ch2", "label": "第二章:抉择", "startScene": "left_door", "thumbnail": "/images/ch2.jpg" }
|
||
]
|
||
}
|
||
```
|
||
|
||
**实现清单:**
|
||
|
||
- [ ] `engine/types.ts` — `GameData.chapters` 字段、`ChapterInfo` 接口
|
||
- [ ] `engine/systems/SaveSystem.ts` — 章节解锁状态持久化(已通关章节记录)
|
||
- [ ] `src/components/ChapterSelect.vue` — 章节选择界面:缩略图网格 + 标题 + 锁定/解锁状态
|
||
- [ ] `engine/core/Engine.ts` — `startChapter(chapterId)` 方法,跳转到指定章节起始场景
|
||
- [ ] `src/App.vue` — 主菜单:新游戏 / 章节选择 / 继续
|
||
- [ ] 验证:通关后章节解锁、从章节入口跳转正确、未解锁章节灰显
|
||
|
||
### P9 跳过已看 + 倍速播放(待实现)
|
||
|
||
目标:玩家重玩时,可以跳过已看过的场景或加速播放(2x/4x),避免重复等待。
|
||
|
||
**实现清单:**
|
||
|
||
- [ ] `engine/systems/SkipSystem.ts` — 记录已观看的场景 ID 列表(持久化到 IndexedDB)
|
||
- [ ] `src/components/SkipIndicator.vue` — "按住 Space 跳过" 进度环指示器
|
||
- [ ] `engine/core/VideoManager.ts` — `setPlaybackRate(rate)` 方法(原生 `video.playbackRate`)
|
||
- [ ] `src/App.vue` — 跳过按钮/快捷键(Space 长按 → 进度环走满 → 触发 skip),倍速切换按钮(1x/2x/4x)
|
||
- [ ] `engine/core/Engine.ts` — 跳过逻辑:跳过 → 直接触发 `onVideoEnd` 流程;倍速不触发跳过
|
||
- [ ] `public/scenes/demo.json` — 添加 `skippable: true/false` 字段(关键场景禁止跳过)
|
||
- [ ] 验证:已看场景可跳过、未看不可跳过、倍速切换正常、关键场景不可跳过
|
||
|
||
### P10 键盘/手柄导航(待实现)
|
||
|
||
目标:支持纯键盘或手柄操作整个游戏流程(选择选项、确认、QTE、菜单),适配"躺沙发"体验。
|
||
|
||
**实现清单:**
|
||
|
||
- [ ] `engine/systems/InputSystem.ts` — 统一输入抽象层:键盘(方向键/WASD)+ 手柄(Gamepad API)+ 鼠标
|
||
- [ ] 选项高亮导航:↑↓ 移动焦点,Enter/Space 确认,有视觉高亮指示器
|
||
- [ ] QTE 键位整合到 InputSystem(目前 QTE 直接监听 `keydown`)
|
||
- [ ] `src/components/ChoicePanel.vue` — 键盘焦点环样式(`focus-visible`)
|
||
- [ ] `src/App.vue` — 菜单键(Esc 打开/关闭菜单)
|
||
- [ ] 验证:纯键盘完成一次完整流程(开始→选择→QTE→存档→读档→结束)、手柄连接时自动切换
|
||
|
||
### P11 多语言字幕(待实现)
|
||
|
||
目标:支持多语言字幕切换,UI 文本国际化,同一个场景可有中/英/日等多个字幕文件。
|
||
|
||
**数据结构设计:**
|
||
|
||
```json
|
||
{
|
||
"id": "intro",
|
||
"videoUrl": "/videos/intro.mp4",
|
||
"subtitles": {
|
||
"zh": "/subtitles/intro_zh.vtt",
|
||
"en": "/subtitles/intro_en.vtt",
|
||
"ja": "/subtitles/intro_ja.vtt"
|
||
},
|
||
"choices": [...]
|
||
}
|
||
```
|
||
|
||
**实现清单:**
|
||
|
||
- [ ] `engine/types.ts` — `SceneNode.subtitles` 改为 `Record<lang, url>`,`Choice.text` 支持多语言 key
|
||
- [ ] `engine/systems/I18nSystem.ts` — 语言切换、当前语言持久化到 localStorage
|
||
- [ ] `src/components/Subtitles.vue` — 监听语言切换,动态加载对应 VTT
|
||
- [ ] `src/components/ChoicePanel.vue` — 选项文字支持 i18n 映射
|
||
- [ ] `src/components/LanguageSwitch.vue` — 语言选择下拉菜单(顶部或菜单中)
|
||
- [ ] `public/scenes/demo.json` — 中英双语字幕示例
|
||
- [ ] 验证:语言切换后字幕/UI 即时更新、刷新保持语言偏好
|
||
|
||
### P12 场景过渡特效(待实现)
|
||
|
||
目标:场景切换不再只有 opacity 交叉淡化,支持更多电影转场语言:淡黑(fade to black)、白闪(fade to white)、模糊(blur)、滑动等。
|
||
|
||
**实现清单:**
|
||
|
||
- [ ] `src/components/TransitionOverlay.vue` — 全屏遮罩层:CSS transition 控制颜色和透明度
|
||
- [ ] `engine/core/Engine.ts` — `SceneNode.transition?: TransitionDef`,在执行视频切换前先播放过渡
|
||
- [ ] `engine/types.ts` — `TransitionDef { type: 'fadeBlack' | 'fadeWhite' | 'dissolve' | 'cut' | 'blur'; duration: number }`
|
||
- [ ] `engine/core/VideoManager.ts` — 过渡时序:过渡遮罩覆盖 → 切换视频 → 过渡遮罩消失
|
||
- [ ] 验证:淡黑转场、白闪转场、不同 duration 参数生效
|
||
|
||
### P13 重玩驱动系统 — 成就 + 统计 + 结局画廊 + 关键选择提示(待实现)
|
||
|
||
目标:增加重玩驱动力。告知玩家"还有未发现的内容",激发探索欲望。涵盖 Telltale 的"[某人]会记住"、
|
||
Quantic Dream 的结局流程图、Steam 式成就系统、通关后的全局统计。
|
||
|
||
**子功能清单:**
|
||
|
||
**13a. 关键选择提示:**
|
||
- [ ] `Choice.prompt?: string` — 选择前弹出的提示文字(如 "[某人] 会记住你的选择")
|
||
- [ ] `src/components/ChoicePanel.vue` — 选项确认前展示提示动画(短暂放大/光效)
|
||
|
||
**13b. 成就系统:**
|
||
- [ ] `engine/systems/AchievementSystem.ts` — 成就定义:`{ id, title, description, icon, condition(scene, state) }`
|
||
- [ ] 成就触发检测(场景切换时匹配条件),解锁持久化到 IndexedDB
|
||
- [ ] `src/components/AchievementToast.vue` — 解锁时弹出提示(底部 toast + 音效)
|
||
- [ ] `src/components/AchievementPanel.vue` — 成就列表页面(全部/已解锁/未解锁)
|
||
|
||
**13c. 结局画廊:**
|
||
- [ ] `GameData.endings` — 结局定义:`{ id, label, thumbnail, unlockCondition }`
|
||
- [ ] `src/components/EndingGallery.vue` — 结局缩略图网格:已解锁显示画面,未解锁显示?剪影 + 提示
|
||
- [ ] 通关后自动跳转到结局画廊(或主菜单入口)
|
||
|
||
**13d. 全局统计:**
|
||
- [ ] `engine/systems/StatsSystem.ts` — 计数:线索收集数、总死亡/失败次数、各结局达成率
|
||
- [ ] `src/components/StatsPanel.vue` — 通关后展示统计面板:"73% 的玩家选择了救人"、"你收集了 3/5 个线索"
|
||
- [ ] 注:全局百分比需要后端聚合数据(`/api/stats`),纯本地可展示个人计数
|
||
|
||
**13e. 章节剧情回顾:**
|
||
- [ ] `src/components/ChapterRecap.vue` — 章节结束后显示本分支的简化流程图(已走过的路径高亮)
|
||
- [ ] 基于 SceneGraph 现有的 Vue Flow 节点图实现,只读模式
|
||
|
||
**验证:** 成就解锁弹出 toast、结局画廊正确显示解锁状态、章节结束后显示回顾、统计面板数据正确
|
||
|
||
### P14 沉浸感提升 — SFX 音效 + 对话轮 UI + 氛围特效 + 动态字幕(待实现)
|
||
|
||
目标:提升视听表现力,从"视频播放器"进化到"电影级游戏"。
|
||
|
||
**子功能清单:**
|
||
|
||
**14a. 独立音效层 (SFX):**
|
||
- [ ] `engine/systems/AudioSystem.ts` 升级 — 支持多轨道:BGM 轨道 + SFX 轨道(可叠加),各自独立音量
|
||
- [ ] `SceneNode.sfx` — 场景音效定义:`[{ url, trigger: 'enter' | 'choice_a' | 'qte_success', volume }]`
|
||
- [ ] `engine/core/Engine.ts` — 事件触发对应 SFX 播放(进入场景、做选择、QTE 结果)
|
||
|
||
**14b. 对话轮 UI:**
|
||
- [ ] `src/components/DialogueWheel.vue` — 替代/补充底部选项列表:圆环布局,选项按弧度分布,中心显示倒计时
|
||
- [ ] 键盘/手柄方向键对应轮盘位置(↑=上方选项,↓=下方选项)
|
||
- [ ] `Choice.wheelPosition?: number` — 手动指定在轮盘中的角度(0~360)
|
||
|
||
**14c. 氛围特效:**
|
||
- [ ] `src/components/AmbientEffects.vue` — 全屏 CSS filter 叠加(`brightness`/`contrast`/`saturate` 渐变)
|
||
- [ ] 画面震动:`CSS transform: translate(random)` 关键帧动画
|
||
- [ ] `SceneNode.effects?: AmbientEffect[]` — 场景级特效声明(震动、暗角、色彩偏移、闪光)
|
||
|
||
**14d. 动态字幕:**
|
||
- [ ] `engine/types.ts` — `SubtitleCue.speaker?: string`, `SubtitleCue.color?: string`, `SubtitleCue.position?: 'left' | 'center' | 'right'`
|
||
- [ ] `src/components/Subtitles.vue` — 升级:说话人标签前缀 + 颜色区分 + 根据 position 调整水平偏移
|
||
|
||
**验证:** SFX 不打断 BGM、对话轮方向键选择、震动特效不卡顿、说话人颜色区分清晰
|
||
|
||
### P15 平台化 — 云存档 + 可访问性 + 自适应码率(待实现)
|
||
|
||
目标:面向分发和用户多样性的补全功能。
|
||
|
||
**子功能清单:**
|
||
|
||
**15a. 云存档(需后端):**
|
||
- [ ] 存档上传/下载 API 接口设计(REST: `PUT /saves/:slot`, `GET /saves`)
|
||
- [ ] `engine/systems/SaveSystem.ts` 升级 — save/load 支持 `remote: boolean` 参数
|
||
- [ ] 登录态管理(可选:不强制登录,本地存档为主,云存档为可选项)
|
||
|
||
**15b. 可访问性设置:**
|
||
- [ ] 字幕样式自定义:字体大小、背景透明度、颜色(白/黄/绿)
|
||
- [ ] 高对比度模式(`filter: contrast(1.3)` 全局应用)
|
||
- [ ] QTE 辅助模式:放宽时间限制、简化按键(如单键替代组合键)
|
||
- [ ] 色盲模式:选项颜色不依赖红/绿区分
|
||
- [ ] `src/components/AccessibilitySettings.vue` — 可访问性设置面板
|
||
|
||
**15c. 自适应码率:**
|
||
- [ ] `engine/core/VideoManager.ts` — 支持 HLS(`.m3u8`)和 DASH(`.mpd`)流媒体源
|
||
- [ ] `SceneNode.videoUrl` 支持多码率:`{ auto: '/videos/hls/scene.m3u8', hd: '/videos/scene_1080p.mp4' }`
|
||
- [ ] 网络质量检测(`navigator.connection` API),自动降级
|
||
|
||
**15d. 其余补充:**
|
||
- [ ] 主菜单界面:新游戏 / 继续 / 章节选择 / 成就 / 结局画廊 / 设置
|
||
- [ ] 设置持久化到 localStorage(语言、音量、可访问性偏好)
|
||
- [ ] 导演评论音轨开关(`SceneNode.commentaryUrl?: string`,作为可选第二音轨)
|
||
|
||
**验证:** 云存档跨设备同步、字幕大小调整生效、码率自适应切换无卡顿
|
||
|
||
## 依赖清单
|
||
|
||
```json
|
||
{
|
||
"dependencies": {
|
||
"vue": "^3.4",
|
||
"pinia": "^2.1",
|
||
"@vue-flow/core": "^1.x",
|
||
"@vue-flow/background": "^1.x",
|
||
"@vue-flow/controls": "^1.x",
|
||
"dexie": "^4.0"
|
||
},
|
||
"devDependencies": {
|
||
"@vitejs/plugin-vue": "^5.0",
|
||
"typescript": "^5.3",
|
||
"vite": "^5.0",
|
||
"vue-tsc": "^2.0"
|
||
}
|
||
}
|
||
```
|
||
|
||
## 关键架构决策记录
|
||
|
||
1. **引擎与 UI 分离**: `engine/` 下纯 TS 类,不 import Vue。UI 层通过 composables 桥接。
|
||
2. **A/B 双缓冲**: 两个 `<video>` 元素轮换,一个播放时另一个预加载候选视频。
|
||
3. **JSON 驱动**: 所有剧情数据放在 JSON 中,编辑器本质是 JSON 的可视化读写工具。
|
||
4. **IndexedDB 存档**: 比 localStorage 容量大,可存储截屏缩略图。
|