iT邦幫忙

2026 iThome 鐵人賽

DAY 5
0

前四天談的是內容——世界觀、角色、選項系統。今天要退一步,談「怎麼把這些內容變成一份可以被執行的文件包」。
這件事聽起來很像行政作業,但《九重燼》能一個人在短時間內做完,關鍵就在這裡。

PRD 撰寫:先定義系統邊界,再談內容

建立PRDGAME_SYSTEM_PRD.md 先劃邊界:Ink 只管劇情節點、對話、選項、變數、跳轉,不碰 DOM/Pixi Sprite/存檔介面;PixiJS 只管畫面渲染,不碰劇情判定。

文件用途:定義《九重燼》Web 版互動敘事遊戲的系統需求、技術架構、模組邊界、資料流、存讀檔、渲染、UI、測試與部署規格。
技術架構:Ink + inkjs、PixiJS、Vue 3、Pinia、Vite、Vercel。
搭配文件:GAME_STORY_BIBLE.mdCHARACTERS.mdCHARACTER_RELATIONSHIP.mdCHAPTER_FLOW.mdCHOICE_BRANCHES.mdENDING_CONDITIONS.mdDEMO_SCRIPT.mdSCENE_ASSET_LIST.md


一、產品定位

  1. 遊戲類型
    《九重燼》是一款以古代宮廷、皇子奪嫡、角色關係與多結局為核心的 Web 互動敘事遊戲。
    主要玩法包括:
  • 視覺小說式對話與選項
  • 好感、信任、立場、恐懼與敵意系統
  • 自由行動與地點選擇
  • 推理蒐證
  • 文辯
  • 糧運算籌
  • 武鬥選擇
  • 朝堂與政治資源管理
  • 多結局

  1. 首版目標
    首版以 Demo 為目標,完成:
  • 序章〈夢醒九重宮〉
  • 第一章〈藏鋒〉
  • 第二章〈使臣叩關〉
  • 三個可續玩 Demo 結局
  • 一個死亡 Bad End
  • 基礎存讀檔
  • 自由行動
  • 證據蒐集
  • 文辯、算籌、武鬥三種輕量玩法
  • 角色立繪、背景、CG、BGM、音效
  • 桌面與行動裝置支援

二、技術架構

劇情邏輯層
→ Ink (.ink 腳本) + inkjs runtime

渲染層
→ PixiJS
  - 背景
  - 角色
  - CG
  - 特效
  - 場景轉場

UI 層
→ Vue 3
  - 對話框
  - 選項
  - 主選單
  - 設定
  - 存讀檔
  - 地圖
  - 證據
  - 文辯
  - 算籌
  - 武鬥介面

狀態管理
→ Pinia

建置工具
→ Vite

部署
→ Vercel

三、架構原則

1. Ink 管劇情,不直接管畫面

Ink 負責:

  • 劇情節點
  • 對話內容
  • 選項
  • 條件判定
  • 劇情旗標
  • 劇情變數
  • 跳轉
  • 結局導向
  • 標籤輸出

Ink 不負責:

  • DOM
  • PixiJS Sprite
  • BGM 實際播放
  • 存檔介面
  • Vue 元件
  • 動畫細節
  • 資產載入
  • 玩家設定

2. PixiJS 只負責畫面渲染

PixiJS 負責:

  • 背景層
  • 後景特效
  • 角色立繪
  • 角色前景特效
  • CG
  • 畫面轉場
  • 雨、煙、灰塵、閃白等 VFX

PixiJS 不負責:

  • 劇情條件
  • 角色好感判定
  • 存檔資料
  • 選項邏輯
  • UI 表單

3. Vue 3 管 UI 與操作

Vue 負責:

  • 對話框
  • 角色名稱
  • 選項
  • 主選單
  • 設定選單
  • 存讀檔
  • 對話回顧
  • 地圖
  • 證據面板
  • 結局圖鑑
  • 系統提示
  • 觸控與鍵盤操作

4. Pinia 是全域狀態的唯一來源

Pinia 管理:

  • 劇情執行狀態
  • 玩家數值
  • 角色關係
  • 資產狀態
  • UI 狀態
  • 音訊設定
  • 存檔資料
  • 系統設定
  • 解鎖內容

5. 劇情資料與顯示資料分離

所有 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 劇情架構

1. Ink 檔案拆分

main.ink
├── globals.ink
├── characters.ink
├── chapter_00.ink
├── chapter_01.ink
├── chapter_02.ink
└── endings_demo.ink

2. main.ink

  • include 其他腳本
  • 初始化
  • 章節入口
  • Demo 結局入口
  • 全域流程控制

範例:

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

3. 全域變數

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

4. 角色關係

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

5. 旗標

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 才有東西可以對照,而不是自己腦補劇情。

任務拆解:67 個 TASK,切成 14 個 Phase

最後一步是把整包企劃拆成可以逐一勾掉的任務。
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 劃邊界、世界觀拆成可交叉引用的規格書、劇本正本唯一化、任務拆成可勾選的清單
一個人開發最大的風險(今天寫的東西,三週後自己也記不清邏輯)就被文件結構本身擋掉了大半。


上一篇
Day4 如何設計玩家選擇與劇情分支
系列文
《九重燼》Vue3 + PixiJS + Ink.js 視覺小說遊戲開發全紀錄5
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言