iT邦幫忙

2026 iThome 鐵人賽

DAY 14
0

從一個模糊的靈感,走到選項系統、存讀檔、UI 分層全部到位,Day14 終於進到一個比較像作品的節點:第一個可以玩的 Demo。

這裡的「可以玩」不是指單一場景能跑,也不是指某個元件 demo 成功,而是玩家可以從標題畫面開始,讀序章、進第一章自由行動、打完第二章三關,最後看到 Demo 結局或 Bad End。那一刻會很明顯地感覺到,前面做的所有系統不再只是散裝零件。


Demo 範圍:刻意縮小的第一版

照 PRD 定下的第一版目標,當時的 Demo 範圍刻意收得很小:

  • 序章〈夢醒九重宮〉+ 第一章〈藏鋒〉+ 第二章〈使臣叩關〉
  • 三個可續玩結局 + 一個死亡 Bad End
  • 基礎存讀檔、自由行動、證據蒐集
  • 文辯、算籌、武鬥三種輕量小遊戲
  • 桌面與行動裝置支援

Ink 腳本的結構:三章 + 獨立結局

首版 Demo 時,Ink 的核心結構是把章節和結局拆開管理。

ink
INCLUDE chapters/chapter_00.ink   // 序章〈夢醒九重宮〉
INCLUDE chapters/chapter_01.ink   // 第一章〈藏鋒〉
INCLUDE chapters/chapter_02.ink   // 第二章〈使臣叩關〉
INCLUDE chapters/endings_demo.ink // Demo 結局 fallback

現在的結局判定分成兩層。前端的 EndingResolver.ts 是權威判定,story.ts 裡的 _resolveEnding() 會讀取 Ink/Pinia 狀態,算出 ending id 後寫回 resolved_ending_idchapters/endings_demo.ink 則保留本地 fallback:如果前端沒有先寫入結果,Ink 會用 envoy_winsdisguise 做最基本的分流。

ink
=== demo_ending_resolution ===
  # ending:resolve

  { resolved_ending_id == "":
      - envoy_wins >= 2 && disguise < 60:  ~ resolved_ending_id = "DEMO_END_A"
      - envoy_wins >= 1 && disguise >= 60: ~ resolved_ending_id = "DEMO_END_B"
      - else:                              ~ resolved_ending_id = "DEMO_END_C"
  }

  - resolved_ending_id == "DEMO_END_A": -> demo_end_a
  - resolved_ending_id == "DEMO_END_B": -> demo_end_b
  - resolved_ending_id == "DEMO_END_C": -> demo_end_c
  - else:                               -> demo_bad_end   // DEMO_BAD_END

EndingResolver.ts 裡的判定,它會先檢查 male_lead_dead,再檢查「沒有證據又攻擊侍衛、帝心過低」這種直接 Bad End 的狀況;接著才判斷 DEMO_END_ADEMO_END_B,最後落到 DEMO_END_C


這個階段代表什麼

到這一步,前面幾週講的每個系統第一次真的接在一起:

  • StoryRuntime 執行 Ink 劇本
  • StoryCommandRouter 分派畫面指令
  • Pinia store 驅動 UI 狀態
  • SaveManager 負責存讀檔
  • ChoiceMenu 和三個小遊戲元件承接第二章玩法
  • EndingResolver 判斷 Demo 結局

這些東西各自測通,不代表作品能玩。真正的驗收是從標題畫面一路走到第二章結尾,中間存一次檔、讀一次檔、打開證據、觸發小遊戲,再看結局判定有沒有照玩家選擇反映出來。Demo 的價值就在這裡:它逼我驗證整條技術管線,而不是只驗證單一模組。

這也是為什麼企劃階段要先做序章+兩章的 Demo,而不是直接鋪開八章。先驗證整條管線走得通,後面再擴章節,心裡會踏實很多。


整合期真實踩到的 bug

這個階段最有價值的不是「內容量」,而是暴露整合問題。記錄幾個實際踩到的:

角色 id 顯示問題:真正從頭玩到結局之前,DialogueBox 的名牌有一段時間會顯示英文 id(例如 noble_son)而不是「權貴公子」。根因是 findCharacter() 找不到角色時,fallback 邏輯是「直接顯示原始 id 字串」。這個 fallback 在單元測試裡看不出問題,要實際跑完 CH01 醫館這段劇情才會觸發。後來把缺漏角色補進 characters-scenes.json,現在 noble_son 會正確顯示成「權貴公子」。

Ink 變數與 Pinia 同步的時序問題_syncVariables() 原本主要跟著 advance() 走,但選項和小遊戲會走 store.choose()。這條路徑如果沒有同步,結局判定就可能讀到上一拍的舊值。現在 choose() 也會在選完後呼叫 _syncVariables(),避免選項改了 Ink 變數,Pinia 還停在上一個狀態。

小遊戲結束沒有交回控制權:文辯、算籌、武鬥的 store.minigame 在某些分支路徑下沒有正確清空,玩家點到選項後畫面像是卡住。現在 choose() 會在處理選項後把 minigame 清成 null,讓控制權回到一般對話流程。這個 bug 只在完整跑過某一段特定分支路線時才出現,局部測試很難看出來。

三個 bug 的共同點:各自獨立測試時「看起來都對」,串成完整路徑之後才暴露。Demo 的意義就是把這些「各自都對、串起來才錯」的問題提前抓出來。


本週成果

✅ 完成第一個可遊玩的 MVP

Day14 的成果不是「所有功能都完成」,而是證明最小閉環成立:劇本能跑、畫面能更新、玩家能選、狀態能存、結局能判斷。

下週開始要往技術深化篇走。PixiJS 的圖層管理策略、Pinia 跟 Ink 狀態的同步機制,這些都是讓 Demo 從「能跑」變成「跑得穩」的關鍵。Demo 跑起來只是第一步,真正麻煩的事情通常都藏在第二輪、第三輪遊玩裡。
https://ithelp.ithome.com.tw/upload/images/20260814/20183394n7s1jScEua.jpg

https://ithelp.ithome.com.tw/upload/images/20260814/201833942p9EpZlcyT.jpg

https://ithelp.ithome.com.tw/upload/images/20260814/201833946sSioioT19.jpg


上一篇
Day13 遊戲 UI 設計:打造沉浸式文字冒險介面
下一篇
Day15 PixiJS 進階:Sprite、Container 與圖層管理策略
系列文
《九重燼》Vue3 + PixiJS + Ink.js 視覺小說遊戲開發全紀錄15
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言