iT邦幫忙

2026 iThome 鐵人賽

DAY 18
0
Modern Web

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

Day18 打造跨裝置體驗:對話框 RWD 與 UI 響應式設計

  • 分享至 

  • xImage
  •  

Day13 談過整體 UI 設計原則:元件拆分、Z-index 管理、各元件自己處理響應式。
Day18 往下鑽一層,看實際的斷點數字,以及手機、平板、桌面到底怎麼分流。

先講一個我後來才比較確定的判斷:文字冒險遊戲的 RWD,不是把桌面畫面等比例縮小。它更像在保護閱讀節奏。對話框不能太小,選項不能擠成一團,系統面板不能讓按鈕跑出畫面,手機直向也不能硬撐到玩家看不清字。

範圍先講清楚:這裡看的是遊戲 runtime

repo 裡不只一套介面。src/portal/style.css 是入口/展示頁,有自己的斷點;Day18 這篇主要看遊戲本體,也就是 src/components/*.vuesrc/views/GameView.vuesrc/App.vue 這些 runtime 畫面。

在 runtime Vue 元件裡,目前 @media (max-width: ...) 主要集中在四個數字:

斷點 用在哪裡 主要用途
920px FreeActionMap.vueLogisticsPlanner.vueDuelArena.vue 卡片型介面從 3 欄改 2 欄
720px DialogueBox.vueChoiceMenu.vueGameView.vueCharacterPortrait.vueSystemPanel.vue 核心遊戲 UI 縮小留白、字級或欄位
640px 地圖、小遊戲規劃器、文辯/武鬥介面 卡片型介面改成單欄
480px SystemPanel.vue 存檔格從 2 欄再縮成 1 欄

另外還有一個 768px,不是一般排版斷點,而是 App.vue 用來判斷手機直向是否要顯示方向提示:

window.matchMedia('(max-width: 768px) and (orientation: portrait)').matches

所以如果只說「專案有四個斷點」會不夠精準。比較正確的說法是:遊戲元件的版面斷點集中在 920 / 720 / 640 / 480;裝置方向判斷另用 768。

不用全域 RWD 框架,因為每個畫面的壓力不同

這個專案沒有建立一套像 Bootstrap 那樣的全域斷點系統,也沒有在 style.css 放一堆共用 RWD class。大部分響應式規則都寫在各元件自己的 <style scoped> 裡。

這不是因為全域設計系統不好,而是文字冒險的畫面差異太大:

  • 對話框的壓力是「每行字數」。
  • 選項清單的壓力是「矮螢幕會不會爆出視口」。
  • 地圖與小遊戲卡片的壓力是「卡片欄數」。
  • 系統面板的壓力是「表單和存檔按鈕能不能操作」。
  • 角色立繪的壓力是「不能擋住對話,也不能消失到太小」。

把這些全塞進同一個 RWD 規則,會很快變成互相牽制。這也是 Day13 提到「逐元件響應式」的原因:讓每個元件照自己的內容密度調整。

viewport:保留瀏覽器原生縮放

index.html 的 viewport meta 只有最基本的一行:

<meta name="viewport" content="width=device-width, initial-scale=1.0" />

它沒有加 user-scalable=no。這個小細節我覺得很重要。很多遊戲網頁會想把縮放鎖住,讓畫面看起來更像 app;但文字冒險是閱讀量很高的遊戲,玩家如果需要放大,瀏覽器原生縮放不應該被拿掉。

這跟視覺完整性有一點拉扯。允許縮放,代表畫面可能不是每次都維持設計稿裡那個比例;但可讀性比「畫面永遠不變形」重要。尤其《九重燼》這種文字密度高的專案,字看不舒服,其他演出做再多都沒用。

手機直向:不硬塞,直接提示旋轉

App.vue 負責判斷手機直向是否要擋住遊戲:

function updateOrientationBlocked() {
  if (typeof window === 'undefined') return
  orientationBlocked.value = window.matchMedia('(max-width: 768px) and (orientation: portrait)').matches
}

onMounted(() => {
  updateOrientationBlocked()
  window.addEventListener('resize', updateOrientationBlocked)
  window.addEventListener('orientationchange', updateOrientationBlocked)
})

這裡不是用 matchMedia(...).addEventListener('change')。目前做法是監聽 resizeorientationchange,每次事件發生時再查一次 media query 的 matches

orientationBlocked 會同時做兩件事。第一,顯示 OrientationOverlay;第二,把底下的遊戲 shell 加上 inertaria-hidden

vue
<div
  class="game-shell"
  :inert="orientationBlocked ? true : undefined"
  :aria-hidden="orientationBlocked ? 'true' : undefined"
>
  <GameView />
</div>
<OrientationOverlay :active="orientationBlocked" />

這裡不是只蓋一塊黑幕而已。inert 讓底下按鈕不能被鍵盤或滑鼠操作,aria-hidden 讓輔助工具不要讀到底層遊戲內容。overlay 本身則用 role="dialog"aria-modal="true"aria-labelledbyaria-describedby,並在啟用時把焦點移到 overlay。

手機直向要完整支援當然可以,但成本會很高。與其讓玩家勉強在窄直螢幕裡看被壓扁的文字,不如明確告訴他「這個遊戲需要橫向」。

對話框:不重排,只調字級和留白

DialogueBox.vue 在桌面狀態下,對話框最小高度是 9rem,內距是 1.5rem 2rem,正文 font-size1.15rem

.dialogue-box {
  min-height: 9rem;
  padding: 1.5rem 2rem;
}
.text {
  font-size: 1.15rem;
  line-height: 1.85;
}

720px 以下,它只做很小的調整:

@media (max-width: 720px) {
  .dialogue-box {
    padding: 1.2rem 1.5rem;
    min-height: 8rem;
  }
  .text { font-size: 1.05rem; }
  .name-tag { font-size: 1.05rem; }
}

注意它沒有改 DOM 結構,也沒有把說話者名牌、游標、推進提示拆到別的位置。原因很簡單:對話框是玩家最常看的東西,結構一變,閱讀肌肉記憶就會被打斷。

小螢幕的問題通常不是「放不下對話框」,而是單行字數太少。一句話被切成太多行,讀起來會碎。把正文從 1.15rem 降到 1.05rem,再稍微縮內距,比重做版面更穩。

底部 UI:寬度用 min(),高度用內部捲動

GameView.vue 的底部 UI 不是固定寬度,而是:

.bottom-ui {
  width: min(56rem, calc(100% - 2rem));
}

桌面最多 56rem,窄螢幕則保留左右各 1rem 的安全空間。這比寫死 width: 900px 好很多,也比每個斷點都改一次寬度乾淨。

選項清單還處理了另一個常見問題:手機橫向高度很矮,選項如果太多,很容易往上爆出畫面。ChoiceMenu.vue 直接限制高度並啟用內部捲動:

.choice-menu {
  max-height: calc(100vh - 13rem);
  overflow-y: auto;
  scrollbar-width: thin;
}

調查模式的 .investigation-menu 也用同樣策略。這其實比縮小所有選項還好,因為按鈕仍然保有可點擊面積;多出來的內容交給捲動處理。

選項與調查:720px 從雙欄改單欄

ChoiceMenu.vue 裡的調查模式預設是兩欄:

.investigation-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

720px 以下改成單欄,卡片最小高度也從 5rem 降到 4.5rem

@media (max-width: 720px) {
  .investigation-grid {
    grid-template-columns: 1fr;
  }
  .investigation-choice {
    min-height: 4.5rem;
  }
}

這裡的取捨跟地圖卡片不同。調查選項是文字判斷,不是瀏覽型卡片;窄螢幕硬塞雙欄會讓每張卡的文字太窄,反而更難讀。改單欄雖然變長,但配合內部捲動,閱讀和點擊都比較穩。

地圖與小遊戲:卡片介面走 3 / 2 / 1 欄

FreeActionMap.vue 是比較典型的卡片式 RWD。桌面是三欄:

.map-grid {
  grid-template-columns: repeat(3, minmax(0, 1fr));
}

920px 以下改兩欄,640px 以下改單欄:

@media (max-width: 920px) {
  .map-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}
@media (max-width: 640px) {
  .map-grid {
    grid-template-columns: 1fr;
  }
}

LogisticsPlanner.vueDuelArena.vue 也是同一種節奏:桌面三欄、920px 兩欄、640px 單欄。DebatePlanner.vue 則從兩欄開始,到 640px 改單欄。

這種卡片型介面適合用欄數處理,因為每張卡片都有標題、說明、狀態或效果 chip。卡片太窄時,不只是不好看,資訊層級也會亂掉。

系統面板:不是縮小,而是把操作區拆行

SystemPanel.vue 比對話框麻煩,因為它裡面有存檔、讀檔、設定、證據、事件等不同內容。它在 720px 以下做幾個調整:

@media (max-width: 720px) {
  .evidence-list {
    grid-template-columns: 1fr;
  }
  .manual-slots-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  .save-slot {
    flex-wrap: wrap;
    gap: 0.75rem;
  }
  .slot-actions {
    flex: 1 1 100%;
    flex-direction: row;
    justify-content: flex-end;
  }
}

480px 以下,手動存檔格再降成一欄:

@media (max-width: 480px) {
  .manual-slots-grid {
    grid-template-columns: 1fr;
  }
}

這裡沒有把按鈕縮到很小,而是讓 .save-slot 換行,讓操作按鈕自己佔一整排。小螢幕最怕的是按鈕看得到但點不到,尤其存讀檔這種高風險操作,寧可佔空間,也不要擠。

角色立繪:小螢幕不是等比例縮小

CharacterPortrait.vue 的處理也很有趣。桌面寬度是:

width: min(47rem, 62vw);

720px 以下變成:

@media (max-width: 720px) {
  .portrait-stage {
    top: 3rem;
    bottom: 1.6rem;
    width: 86vw;
    opacity: 0.78;
  }
  .portrait-stage.is-right { right: -12vw; }
  .portrait-stage.is-left { left: -12vw; }
}

它不是單純縮小,而是讓立繪寬到 86vw,再往左右推出一點,透明度降到 0.78。這樣角色仍然有存在感,但不會跟底部對話框搶得太兇。

文字冒險的立繪很容易陷入兩難:太小,角色像背景裝飾;太大,玩家讀字會被干擾。這段 CSS 其實是在找中間值。

測試跨裝置時,看的是操作手感

這幾組斷點不是「設計稿上看起來差不多」就結束。真正要看的,是幾個操作場景:

  • 手機橫向時,對話框能不能舒服讀完一行。
  • 選項很多時,內部捲動會不會把底部對話框擠走。
  • 自由行動地圖從 3 欄切到 2 欄、1 欄後,卡片文字還能不能掃讀。
  • 系統面板在 480px 寬度附近,存檔按鈕是否還有足夠點擊空間。
  • 手機直向 overlay 出現時,底層遊戲是否真的不能被鍵盤或輔助工具操作。

這些都不是單看桌面瀏覽器就能抓完的。尤其手機橫向,高度常常比寬度更麻煩;如果只看寬度斷點,很容易漏掉選項清單爆出視口這種問題。

小結

Day18 做完後,我對 RWD 的想法更保守了一點。不是每個畫面都需要一套宏大的響應式系統;有時候更好的做法,是先搞清楚每個元件真正痛的是什麼。

對話框痛在閱讀節奏,所以只微調字級和留白。卡片介面痛在欄寬,所以用 3 / 2 / 1 欄。系統面板痛在操作安全,所以讓按鈕換行。手機直向成本太高,先用明確的方向提示擋住。

這些決定都不炫,但玩家不會因為 RWD 炫而留下來。玩家只會在某個小螢幕上按下一個選項,發現它剛好看得清、點得到,然後繼續讀下一句。
https://ithelp.ithome.com.tw/upload/images/20260818/20183394caBchGoSlC.png


上一篇
Day17 動畫系統實作:轉場、淡入淡出
下一篇
Day19 AI 如何加速我的美術、CG、BGM 製作
系列文
《九重燼》Vue3 + PixiJS + Ink.js 視覺小說遊戲開發全紀錄19
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言