iT邦幫忙

2026 iThome 鐵人賽

DAY 13
1
Modern Web

《九重燼》Vue3 + PixiJS + Ink.js 視覺小說遊戲開發全紀錄系列 第 13

Day13 遊戲 UI 設計:打造沉浸式文字冒險介面

  • 分享至 

  • xImage
  •  

前面幾天談的是「資料怎麼流動」,這篇要談玩家實際看到、點到的介面本身。

文字冒險的 UI 很容易被低估。它看起來只是對話框、選項、幾個按鈕,但玩家其實是透過 UI 在判斷「現在能做什麼」。介面沒有講清楚,玩家不會覺得是系統複雜,只會覺得自己卡住。


元件拆分:每個系統一個獨立元件

src/components/ 底下目前有 21 個 Vue 元件。切分原則是「一個系統一個元件」而不是「一個畫面一個元件」:

元件 負責的系統
DialogueBox 對話框與打字機效果
ChoiceMenu 選項(含四種特殊模式)
FreeActionMap 第一章自由行動地圖
EvidenceModal 證據彈窗
DebatePlanner 文辯小遊戲
LogisticsPlanner 算籌小遊戲
DuelArena 武鬥小遊戲
SystemPanel 遊戲中系統選單(存讀檔、設定、歷史等入口)
SettingsPanel 遊戲設定面板
GalleryPanel 圖鑑
EndingScreen 結局畫面
StatsPanel 數值面板
MainMenu 主選單
LoadingScreen 載入畫面
OrientationOverlay 手機方向提示
ChapterLockScreen 章節密碼鎖
CharacterPortrait 角色立繪
GameCanvas PixiJS 畫布容器
ConfirmModal 通用確認對話框
AdminPanel 開發者面板(生產環境隱藏)
SystemToasts Toast 通知

每個元件主要讀寫自己負責的那塊 Pinia 狀態,互動盡量走 store,而不是讓元件彼此互相呼叫。這延續 Day8 提過的單向資料流:劇情推進改變 store,Vue 元件再依照 reactive state 更新畫面。


GameView:真正的 UI 殼層

如果 src/components/ 是零件庫,src/views/GameView.vue 就是把零件裝起來的殼層。它同時掛載 PixiJS 畫布、角色立繪、對話框、選項、自由行動地圖、小遊戲面板、系統面板、Toast、證據彈窗、結局畫面與 loading 畫面。

這個檔案的重點不是畫面長相,而是「誰該在什麼狀態出現」。例如:

  • 還沒開始遊戲時只顯示 MainMenu
  • 遊戲開始後才顯示 GameCanvasCharacterPortraitDialogueBox
  • 有滿版 CG 時隱藏角色立繪,避免立繪蓋到 CG
  • 自由行動地圖開啟時,不顯示一般選項
  • 算籌、文辯、武鬥啟動時,改用各自的小遊戲 UI
  • 章節被鎖定時,用 ChapterLockScreen 擋住底層互動

這些條件如果分散在各個元件裡,debug 會很痛苦。集中在 GameView.vue 的好處是很直觀:要查「為什麼某個 UI 沒出現」,先看殼層條件,再看元件內部邏輯。


z-index 分層:有規律,不是隨便填

21 個元件疊在一起,層次要清楚。src/components/ 裡出現的 z-index 值是刻意設計的分層:

z-index 元件 說明
0–30 畫布、對話框、選項、立繪、證據彈窗等 基礎遊戲 UI
100 ConfirmModal 通用確認視窗
1000 LoadingScreen 蓋住一般遊戲內容
9999 ChapterLockScreen 章節鎖定時擋住底層遊戲
10000 SystemToasts Toast 高於面板與鎖定畫面
99999 OrientationOverlay 手機方向提示不可被其他元件蓋掉

Toast 通知設在 10000,確保不管系統面板、確認視窗或章節鎖定畫面開著,都還能看到關係變化、存檔錯誤這類短提示。OrientationOverlay 設在 99999,是整個堆疊的天花板,因為手機直向提示出現時,底層遊戲本來就不該再被操作。


Responsive:不是一套版型縮放,是逐元件調整

src/components/ 裡共有 15 個 @media 規則,分散在各個元件的 <style scoped> 裡。其中 14 個是寬度斷點,另 1 個是 hover 裝置判斷。寬度斷點主要落在 720px,小遊戲與自由行動地圖還有 920px640px,系統面板則多一個 480px 微調。

這裡沒有靠一個全域 RWD 框架縮放整個畫面,原因是文字冒險遊戲的介面元素密度差很多:

  • 對話框在手機直向要縮小字級(1.15rem → 1.05rem)跟內距
  • 選項清單在窄螢幕換成較緊湊的排版
  • 角色立繪在小螢幕要限制高度
  • 系統面板在小螢幕要調整欄寬
  • 小遊戲面板在平板尺寸就要先改成比較窄的 grid

用單一縮放規則套所有元件,會讓其中某幾個在特定裝置上看起來過大或過小,逐元件調整雖然程式碼比較多,但每個元件可以為自己的內容密度量身訂做。

這件事在文字冒險裡很明顯:對話框太小,玩家讀起來累;選項太大,畫面一次只看得到一兩個;小遊戲格子太擠,又會變成誤觸。RWD 不只是讓畫面「塞得下」,還要讓玩家能舒服地讀和點。


手機直向:不只是「請旋轉裝置」的提示

OrientationOverlay.vue 處理手機直向持握時的提示畫面,這裡有兩個容易被忽略的細節:

焦點管理:元件出現時用 watch 監聽 active prop,一變成 true 就在 nextTick 之後把焦點移到 overlay 本身(overlayEl.value?.focus())。這代表鍵盤使用者或螢幕閱讀器的焦點不會停在底層遊戲的某個按鈕上——不主動移焦點的話,鍵盤仍然在操作被蓋住的遊戲內容。

inert 屬性:提示顯示的同時,App.vue 會在底層的 .game-shell 加上 inert 屬性跟 aria-hidden="true"

<!-- App.vue -->
<div
  class="game-shell"
  :inert="orientationBlocked ? true : undefined"
  :aria-hidden="orientationBlocked ? 'true' : undefined"
>

inert 是 HTML 原生屬性,會讓元素(及其所有子元素)完全無法被點擊、聚焦、或被輔助工具感知——這比 pointer-events: nonevisibility: hidden 更徹底,連鍵盤 Tab 鍵都繞過去。aria-hidden="true" 則讓螢幕閱讀器直接略過底層內容。
https://ithelp.ithome.com.tw/upload/images/20260813/201833941pp8ca7aVg.jpg


系統面板:不是打開就好,還要管焦點

SystemPanel.vue 是另一個容易低估的元件。表面上它只是存檔、讀檔、設定、歷史、證據、角色事件、圖鑑與後台的入口,但實作上它還負責一件很重要的事:焦點管理。

面板打開時,程式會記住原本的 document.activeElement,等 DOM 更新後把焦點移到關閉按鈕。面板關閉時,再把焦點還回原本的位置。這個細節對滑鼠玩家幾乎沒有存在感,但鍵盤玩家會很有感:按 Esc 關掉面板後,焦點不會迷路。

SystemPanel 也攔截 Tab 鍵,把焦點圈在面板裡。使用者一路按 Tab 時,焦點會在面板內的可操作元素之間循環,不會跑到底層的對話框、存檔按鈕或其他被遮住的 UI。這和前面 OrientationOverlay 的 inert 是同一個思路:畫面上不可操作的東西,鍵盤和輔助工具也不應該碰得到。

GameView.vue 還有一層全域快捷鍵:遊戲進行中按 A 可以切自動模式,沒有面板時按 Escape 會開設定;如果面板已經開了,Esc 關閉則交給 SystemPanel 處理。這樣分工後,快捷鍵不會互相打架。

https://ithelp.ithome.com.tw/upload/images/20260813/20183394Pas8wUDt6s.jpg

UI 不只負責漂亮,也負責降低理解成本

這次做 UI 時,我一直把「玩家現在該看哪裡」放在第一順位。對話框永遠是閱讀焦點,選項只在需要決策時出現;證據、地圖、小遊戲這些資訊量比較大的介面,則用 modal 或獨立面板承接,不混進主對話區。這樣做的好處是玩家不需要同時理解太多系統:讀劇情時就讀劇情,做選擇時才看選項,蒐證時才進入證據介面。

另一個小原則是,不把教學文字寫得太重。像存檔、設定、圖鑑這些功能,盡量靠固定位置、清楚按鈕狀態、以及元件本身的命名讓玩家理解,而不是每個畫面都塞一段說明。文字冒險已經有大量正文,如果 UI 再一直說明自己,玩家會很快疲乏。好的介面應該把注意力還給故事,而不是一直提醒玩家「這裡有一個系統」。

Day13 回頭看 UI,最重要的收穫其實不是某個按鈕長得好不好看,而是介面規則要能被維護。哪些元件負責閱讀、哪些元件負責決策、哪些元件會蓋住其他東西、焦點該去哪裡,這些都寫清楚之後,後面加功能才不會把畫面堆成一團。

https://ithelp.ithome.com.tw/upload/images/20260813/20183394mB4nluy6EI.jpg


上一篇
Day12 存檔與讀檔機制:LocalStorage 還是 IndexedDB?
下一篇
Day14 完成第一個可以玩的 Demo!
系列文
《九重燼》Vue3 + PixiJS + Ink.js 視覺小說遊戲開發全紀錄15
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言