前四天談的是內容——世界觀、角色、選項系統。今天要退一步,談「怎麼把這些內容變成一份可以被執行的文件包」。
這件事聽起來很像行政作業,但《九重燼》能一個人在短時間內做完,關鍵就在這裡。
建立PRDGAME_SYSTEM_PRD.md 先劃邊界:Ink 只管劇情節點、對話、選項、變數、跳轉,不碰 DOM/Pixi Sprite/存檔介面;PixiJS 只管畫面渲染,不碰劇情判定。
文件用途:定義《九重燼》Web 版互動敘事遊戲的系統需求、技術架構、模組邊界、資料流、存讀檔、渲染、UI、測試與部署規格。
技術架構:Ink + inkjs、PixiJS、Vue 3、Pinia、Vite、Vercel。
搭配文件:GAME_STORY_BIBLE.md、CHARACTERS.md、CHARACTER_RELATIONSHIP.md、CHAPTER_FLOW.md、CHOICE_BRANCHES.md、ENDING_CONDITIONS.md、DEMO_SCRIPT.md、SCENE_ASSET_LIST.md。
一、產品定位
二、技術架構
劇情邏輯層
→ Ink (.ink 腳本) + inkjs runtime
渲染層
→ PixiJS
- 背景
- 角色
- CG
- 特效
- 場景轉場
UI 層
→ Vue 3
- 對話框
- 選項
- 主選單
- 設定
- 存讀檔
- 地圖
- 證據
- 文辯
- 算籌
- 武鬥介面
狀態管理
→ Pinia
建置工具
→ Vite
部署
→ Vercel
三、架構原則
Ink 負責:
Ink 不負責:
PixiJS 負責:
PixiJS 不負責:
Vue 負責:
Pinia 管理:
所有 Ink 腳本不得直接寫死完整資產路徑。
Ink 應輸出語意標籤,例如:
ink
# bg:chenghua_bedroom_night_rain
# char:xiao_chengyuan:drunk:center
# bgm:rain_palace
# sfx:door_hit
前端再透過 Asset Registry 將語意 ID 映射到實際檔案。
四、系統架構圖
┌──────────────────────────────┐
│ Vue 3 UI │
│ 對話/選項/選單/存讀檔 │
└──────────────┬───────────────┘
│ actions / state
▼
┌──────────────────────────────┐
│ Pinia │
│ game / story / ui / audio │
│ save / asset / relation │
└───────┬───────────┬──────────┘
│ │
│ ├────────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Ink Runtime │ │ Pixi Renderer│ │ Audio Manager│
│ inkjs │ │ Background │ │ BGM / SFX │
│ story state │ │ Character │ │ Ambience │
└───────┬──────┘ │ CG / VFX │ └──────────────┘
│ └──────────────┘
▼
┌──────────────────────────────┐
│ Compiled Ink JSON │
│ Asset Manifest │
│ Character / Ending Config │
└──────────────────────────────┘
五、專案目錄
jiuchongjin/
├── public/
│ ├── assets/
│ │ ├── characters/
│ │ ├── backgrounds/
│ │ ├── cg/
│ │ ├── props/
│ │ ├── ui/
│ │ ├── audio/
│ │ └── vfx/
│ ├── story/
│ │ └── story.json
│ └── manifests/
│ ├── assets.json
│ ├── characters.json
│ ├── endings.json
│ └── chapters.json
├── scripts/
│ ├── ink/
│ │ ├── main.ink
│ │ ├── globals.ink
│ │ ├── characters.ink
│ │ ├── chapter_00.ink
│ │ ├── chapter_01.ink
│ │ ├── chapter_02.ink
│ │ └── endings_demo.ink
│ ├── build-ink.mjs
│ ├── validate-story.mjs
│ └── validate-assets.mjs
├── src/
│ ├── app/
│ │ ├── App.vue
│ │ └── router.ts
│ ├── components/
│ │ ├── dialogue/
│ │ ├── choices/
│ │ ├── menu/
│ │ ├── save-load/
│ │ ├── map/
│ │ ├── evidence/
│ │ ├── debate/
│ │ ├── logistics/
│ │ └── duel/
│ ├── pixi/
│ │ ├── PixiStage.ts
│ │ ├── SceneRenderer.ts
│ │ ├── LayerManager.ts
│ │ ├── TransitionManager.ts
│ │ ├── CharacterRenderer.ts
│ │ ├── BackgroundRenderer.ts
│ │ ├── CgRenderer.ts
│ │ └── VfxRenderer.ts
│ ├── story/
│ │ ├── StoryRuntime.ts
│ │ ├── InkTagParser.ts
│ │ ├── StoryEventBus.ts
│ │ ├── StoryCommandRouter.ts
│ │ └── StoryVariableBridge.ts
│ ├── stores/
│ │ ├── game.ts
│ │ ├── story.ts
│ │ ├── relations.ts
│ │ ├── ui.ts
│ │ ├── audio.ts
│ │ ├── assets.ts
│ │ ├── saves.ts
│ │ └── settings.ts
│ ├── services/
│ │ ├── AssetLoader.ts
│ │ ├── AudioManager.ts
│ │ ├── SaveManager.ts
│ │ ├── StorageAdapter.ts
│ │ └── EndingResolver.ts
│ ├── config/
│ │ ├── assets.ts
│ │ ├── characters.ts
│ │ ├── game.ts
│ │ └── storage.ts
│ ├── types/
│ │ ├── story.ts
│ │ ├── save.ts
│ │ ├── character.ts
│ │ ├── asset.ts
│ │ └── ending.ts
│ ├── utils/
│ │ ├── clamp.ts
│ │ ├── deepMerge.ts
│ │ ├── version.ts
│ │ └── logger.ts
│ └── main.ts
├── tests/
│ ├── unit/
│ ├── integration/
│ ├── story/
│ └── e2e/
├── package.json
├── vite.config.ts
├── tsconfig.json
├── vercel.json
└── README.md
六、Ink 劇情架構
main.ink
├── globals.ink
├── characters.ink
├── chapter_00.ink
├── chapter_01.ink
├── chapter_02.ink
└── endings_demo.ink
範例:
ink
INCLUDE globals.ink
INCLUDE characters.ink
INCLUDE chapter_00.ink
INCLUDE chapter_01.ink
INCLUDE chapter_02.ink
INCLUDE endings_demo.ink
-> chapter_00_start
ink
VAR power = 0
VAR popular_support = 0
VAR emperor_favor = 20
VAR emperor_fear = 0
VAR ambition = 20
VAR compassion = 50
VAR suspicion = 20
VAR reform = 0
VAR military_power = 0
VAR scholar_support = 0
VAR merchant_support = 0
VAR western_alliance = 0
VAR palace_intel = 0
VAR mother_case_progress = 0
VAR crown_prince_case_progress = 0
VAR disguise = 60
VAR public_reputation = 10
Ink 可保存主要角色關係數值。
範例:
ink
VAR liu_affection = 35
VAR liu_trust = 30
VAR liu_alignment = 40
VAR liu_fear = 5
VAR liu_ideal = 10
VAR liu_hostility = 0
為避免變數過多,正式版可採命名規則:
rel_<character>_<metric>
範例:
rel_liu_affection
rel_gu_trust
rel_shen_hostility
ink
VAR flag_hidden_jade = false
VAR flag_emperor_has_jade = false
VAR flag_checked_qinghe_wound = false
VAR flag_found_sedative_incense = false
VAR flag_brother_pact = false
VAR flag_entered_court_politics = false
這條分工線看起來理所當然,寫劇本的人不用管畫面怎麼跑,寫渲染的人不用管好感度怎麼算,兩邊都改不到對方的地雷。
PRD 裡也要明確寫了第一版 Demo 的範圍,先劃清楚「這次不做什麼」,比只列「要做什麼」更重要。
世界觀文件:一份「故事聖經」+ 延伸規格
GAME_STORY_BIBLE.md:世界觀與完整主線(作品定位、故事主題、核心衝突)CHARACTERS.md:人物設定與關係CHAPTER_FLOW.md:章節流程CHOICE_BRANCHES.md:選項與條件ENDING_CONDITIONS.md:結局判定DIALOGUE_STYLE_GUIDE.md:古風對白規範DEMO_SCRIPT.md:Demo 完整劇本SCENE_ASSET_LIST.md:背景、立繪與 CG 清單這樣拆分的好處是:改角色設定不會動到結局判定邏輯,調整選項數值也不用翻整本故事聖經,各文件之間用交叉引用互相對照。
劇本管理:正本只有一份,草稿不算數
這是我覺得最重要的一條紀律:docs/FULL_SCRIPT/ 底下逐章的劇本檔,才是唯一的劇情正本CHAPTER_01_SCRIPT.md〈藏鋒〉一路到 CHAPTER_08_SCRIPT.md〈煙雨故人〉,加上 ENDINGS_SCRIPT.md,每一份都寫死場景/選項 ID、數值旗標效果、CG 與 BGM 清單。DEMO_SCRIPT.md 只是序章到第二章的早期草稿,一旦正本寫出來,草稿就不再是依據。
轉寫成 .ink 檔時,一律照正本,不是憑印象發揮——這條規則直接寫進了專案根目錄的 AGENTS.md,也是後來能讓 AI 協作寫劇本而不跑題的前提。
正本夠具體,AI 才有東西可以對照,而不是自己腦補劇情。
最後一步是把整包企劃拆成可以逐一勾掉的任務。DEVELOPMENT_TASKS.md 把整個開發流程切成 Phase 0 到 Phase 13,一共 67 個 TASK:
| 階段 | 內容 |
|---|---|
| Phase 0 | 專案規範與初始化 |
| Phase 1 | Ink 劇情執行核心 |
| Phase 2 | PixiJS 渲染核心 |
| Phase 3 | Vue 對話與選項 UI |
| Phase 4 | Pinia 狀態與同步 |
| Phase 5 | 存讀檔與設定 |
| Phase 6~9 | 序章、第一章自由行動、第二章三種玩法、結局與圖鑑 |
| Phase 10~12 | 音訊特效整合、測試效能、Vercel 部署 |
| Phase 13 | 通關後番外(刻意排在主線全部完成之後才開始) |
值得一提的是 Phase 13 的排序邏輯——番外故事的解鎖旗標依賴第七、八章的結局判定,所以企劃書刻意把番外轉寫排到最後,避免番外開發跟主線結局邏輯還在變動時互相打架。
每個 TASK 也有固定欄位(前置依賴、驗收標準),跟前面選項系統的「固定欄位規格」是同一套思維的延伸。
這四步走完——PRD 劃邊界、世界觀拆成可交叉引用的規格書、劇本正本唯一化、任務拆成可勾選的清單
一個人開發最大的風險(今天寫的東西,三週後自己也記不清邏輯)就被文件結構本身擋掉了大半。