挑對彈出視窗
彈出視窗會打斷玩家。挑選之前,先回答一個問題:玩家必須先回答,遊戲才能繼續嗎? 是,就擋住;不是,就絕對不要擋 — 為了玩家根本沒問過的消息把遊戲停下來的卡片,是讓玩家關掉你的遊戲最快的方法。
- 有答案的問題 — 購買、刪除、離開、覆寫 →
GoDialogs.confirm()。只需要知道的事 →alert()。從幾個選項中挑一個 →choose()。 - 自成一頁的畫面 — 設定、角色面板、商店裡的一件物品 → 畫面正中央的
GoSurface。 - 長清單或管理頁面 — 背包、郵件、好友 → 手機上從底部升起的
GoSheet,寬螢幕上從側邊滑入的GoDrawer。 - 關於剛按下那個東西的資訊 — 物品屬性、可以做的動作 → 在它旁邊彈出的
GoPopover或GoContextMenu。 - 遊戲可以照常進行的消息 — 已存檔、已丟棄、收到邀請 →
GoSnackbar、GoNotice或GoPromptCard。它們從不佔據畫面。 - 教玩家怎麼操作 →
GoCoachMark。開發時用的作弊 →GoConsole。
所有彈出視窗一覽
| 彈出視窗 | 是否擋住遊戲 | 返回鍵 / Esc | 點按外面 | 用途 |
|---|---|---|---|---|
GoDialogs confirm · alert | 是 | 關閉 — confirm 回答 false,alert 返回 | 只有開啟 dismiss_on_scrim 時才關閉 | 需要回答的問題 |
GoDialogs.choose | 是 | 關閉 — 回答 -1 | 只有開啟 dismiss_on_scrim 時才關閉 | 從幾個當中挑一個 |
GoSurface | 是 | 要求關閉最上層的視窗 | 開啟 dismiss_on_scrim 時要求關閉 | 你自己的視窗 |
GoSheet | 是 | 關閉整個底部面板 | 關閉 | 背包、商店清單、郵件 |
GoDrawer | 是 | 只回應 Android 返回鍵 | 關閉 | 寬螢幕上的側邊清單 |
GoPopover | 關閉之前都會擋住 | 關閉 | 關閉 — 而且這次點按不會再往下傳 | 物品與技能的詳細資訊 |
GoCombobox 清單 | 關閉之前都會擋住 | 關閉 | 關閉 | 可以搜尋的選擇清單 |
GoContextMenu · dropdown · select | 依引擎選單的行為 | 依引擎選單的行為 | 關閉 | 動作、短清單 |
GoSnackbar | 否 | — | 傳給遊戲 | 已存檔 · 復原 · 重試 |
GoPromptCard | 否 | — | 傳給遊戲 | 邀請、交易請求 |
GoNotice | 從不 | — | 傳給遊戲 | HUD 上固定的一行字 |
GoBanner | 否 — 它就在頁面裡 | — | — | 「你已離線。重試」 |
GoCoachMark | 只擋它的卡片 | 結束導覽 | 傳給遊戲 | 第一次啟動時的導覽 |
GoConsole | 只擋它的面板 | 按 Esc 鍵關閉 | 傳給遊戲 | 除錯指令 |
每一種開在哪個圖層上、為什麼是這個順序,請看圖層、返回鍵與輸入。

GoDialogs
GoSurface
GoSheet
GoDrawer
GoPopover
GoContextMenu
GoSnackbar
GoPromptCard
GoCoachMark
GoConsole本頁的每張圖都是同一個小遊戲 — 左上角是生命與魔力,右上角是金幣,底部是搖桿與三個快捷格 — 用的是
default_dark,上面開著一個彈出視窗。同一個彈出視窗在全部十四種外觀下的樣子 →
彈出視窗的構造
gohud 裡幾乎每一個會擋住遊戲的彈出視窗 — 對話方塊、底部面板、錨定卡片、組合方塊的清單,以及你自己的
視窗 — 都是同一個節點:GoSurface。把它的各個部分認識一遍,就全部都認識了。

| 部分 | 是什麼 | 用什麼控制 |
|---|---|---|
| 圖層 | 彈出視窗所在的 CanvasLayer。它的編號決定它能蓋住什麼。 | GoSurface 由你提供;其他彈出視窗已內建(見表格) |
| 遮罩 | 蓋在後面一切之上的那層布幕。沒點中卡片的點按由它接住。 | scrim_transparent、dismiss_on_scrim、佈景主題的 scrim 顏色 |
| 卡片 | 視窗本身 — 佈景主題的面板表面,依外觀不同而是圓角、切角、鍛造或果凍。 | card、alpha |
| 頁首 | 可選的返回按鈕、標題(最多兩行,超過就顯示「…」)、關閉按鈕。 | set_title() · set_title_key() · set_back() · close_enabled · show_header |
| 工具列 | 頁首下方固定的一列 — 搜尋欄、篩選用的方塊標籤。沒用到時一直隱藏。 | toolbar |
| 本文 | 會捲動的部分。內容放在這裡。 | body · scroll_body · clear() |
| 狀態列 | 固定在本文與頁尾之間的一行 — 「名稱檢查失敗」、「儲存中…」。 | set_status_text() · set_status_key() · clear_status() |
| 頁尾 | 固定在底部的按鈕。沒用到時一直隱藏。 | footer |
toolbar、狀態列與 footer;本文捲動時它們會留在原地。
可以 await 的對話方塊
GoDialogs 負責發問與等待。一個節點,三種問法 — 答案就在同一行回來,
所以程式碼的順序就是玩家經歷的順序。
var dialogs := GoDialogs.new()
add_child(dialogs)
if await dialogs.confirm("Buy the starter pack?", "300 gems for a sword, a shield and 10 potions.",
"Buy", "Not now"):
buy_starter_pack()

confirm()
confirm(…, true) — 破壞性動作
alert()
choose()
ActionLayout.HORIZONTAL三種問法
confirm(title, body, ok_text := "", cancel_text := "", extra := "",
args := {}, destructive := false) -> bool
alert(title, body, ok_text := "", extra := "", args := {}) # 用 await 等它結束
choose(title, options: Array, cancel_text := "", translate := false) -> int
confirm_key(…) · alert_key(…) # 翻譯鍵
| 呼叫 | 回傳 | 用途 |
|---|---|---|
confirm() | true 或 false | 是或否 — 購買、刪除、離開、覆寫。 |
alert() | 沒有 — 用 await 等它結束 | 玩家一定要看到的事:獎勵、連線中斷。 |
choose() | 選項的索引,或 -1 | 從幾個當中挑一個。選項可以是 String,也可以是 {text, icon, danger, subtitle}。 |
confirm_key() · alert_key() | 同上 | 同樣的呼叫,只是改用翻譯鍵 — 對話方塊開著時切換語言,文字也會跟著變。 |
按鈕標籤留空時,會退回套件自己的用字(GoConfig.text_keys 裡的 confirm、cancel)。
extra 會另起一行接在本文後面。args 會填入標題與本文裡的
{placeholders}:confirm("Buy {item}?", "It costs {price} gold.", "Buy", "", "", {"item": name, "price": 300})。
每一種離開方式都有答案
| 玩家…… | confirm | alert | choose |
|---|---|---|---|
| 按下確定/某個選項 | true | 返回 | 該選項的索引 |
| 按下取消 | false | — | -1 |
| 按下 ×、Esc 或 Android 返回鍵 | false | 返回 | -1 |
點按遮罩(只有開啟 dismiss_on_scrim 時) | false | 返回 | -1 |
GoDialogs 節點離開場景樹 | false | 返回 | -1 |
所以 await 之後的程式碼一定會執行 — 不會有哪個回答一直懸著,就算對話方塊底下的場景已經換掉也一樣。
破壞性動作
無法復原的動作請傳入 destructive = true。確定按鈕會變成實心的
Tone.DANGER_SOLID 按鈕 — 在每一種外觀下都是紅底白字;而著色的危險標籤在淺色佈景主題上
必須一路變深到接近黑色才讀得清楚。因為視窗會被重複使用,色調在每次呼叫時都會重設。
if await dialogs.confirm("Delete this save?", "Slot 2 — Aria, level 12. This cannot be undone.",
"Delete", "Keep it", "", {}, true):
delete_save(2)
按鈕排列
action_layout | 作用 |
|---|---|
VERTICAL(預設) | 按鈕上下堆疊、佔滿整個寬度。對每種語言都安全 — 德文或俄文的長標籤永遠不會被擠壓。 |
HORIZONTAL | 排成一列,各佔一半寬度。 |
AUTO | 只有兩個標籤都能在卡片一半寬度內單行放下時才排成一列,否則上下堆疊。切換語言或改變大小時會重新判斷。 |
action_gap 是按鈕之間的間距,body_gap 是按鈕上方的間距 — 把後者調得比按鈕間距更寬,
問題與選項才會讀起來像兩個部分。只想改某一個對話方塊時,在開啟之前呼叫
set_next_action_layout()。提示對話方塊唯一的那顆按鈕永遠佔滿整個寬度。
一次只開一個
已經開著一個時再呼叫 confirm(),會立刻回傳 false — 對話方塊絕不會意外疊在對話方塊上。
打開 queue_when_busy,它就會改成排隊等候;alert() 一律排隊等候。
queue_limit(8)限制隊伍的長度,滿了的時候最早的請求會得到 false。
pending() 計算正在等候的請求數,clear_pending() 讓它們全部得到 false。
choose() 從不排隊:已經有對話方塊開著時,它回傳 -1。
手機與桌機
在 Android 與 iOS 上,choose() 會以底部面板的形式從下方升起 — 那裡正是拇指所在的位置;在桌機上則是
畫面正中央的一張卡片。確認對話方塊的寬度上限是 max_width(420dp),並且置中顯示。layer_index、
max_width 與其他匯出屬性請在 add_child 之前設定;它們在節點進入場景樹時才會套用。
var picked := await dialogs.choose("Sort the bag by", [
{"text": "Newest first", "icon": GoIconSet.CLOCK},
{"text": "Rarity", "icon": GoIconSet.STAR},
{"text": "Drop everything", "icon": GoIconSet.TRASH, "danger": true}])
if picked >= 0:
sort_bag(picked)
GoSurface — 你自己的視窗
當彈出視窗不只是一個問題 — 設定、角色面板、要購買的物品 — 就在
GoSurface 上打造。安全區域、鍵盤、固定列、捲動、返回鍵、焦點與改變大小都已經處理好了。
var layer := CanvasLayer.new()
layer.layer = 50
add_child(layer)
var window := GoSurface.new()
window.set_title("Settings")
window.close_requested.connect(layer.queue_free)
window.body.add_child(GoStyle.toggle("Music", false))
window.body.add_child(GoStyle.slider(0.0, 1.0, 0.05))
var row := GoStyle.row()
row.add_child(GoStyle.button("Cancel", window.request_close))
row.add_child(GoStyle.button("Save", save_and_close, GoStyle.Tone.PRIMARY))
window.footer.add_child(row)
window.footer.visible = true
layer.add_child(window)
close_requested。要隱藏還是釋放
視窗,是持有它的那個畫面的責任 — 只有它知道「關閉」代表儲存、捨棄,還是先問一聲。
三種擺放方式

CENTER — 確認、設定
BOTTOM — 清單、管理頁
ANCHOR — 釘在控制項旁邊
CENTER 視窗裡的 GoDatePickerANCHOR 需要設定 anchor_control。右側放得下 anchor_min_width 時,卡片會放在它的右邊;
否則放在比較寬的那一側;再不行就放在畫面內;上方空間比較大時則往上展開。錨點移動時,它每一格都會跟上。
anchor_width(320)與 anchor_max_height(520)決定它的大小。
能長到多大
| 尺度 | 規則 | 用什麼改 |
|---|---|---|
| 可用空間 | 安全區域,扣掉鍵盤,再扣掉畫面邊距(16dp,而且絕不超過短邊的 10%)。 | — |
| 寬度 | 直向時是可用空間的 94%,橫向時是 72%,最多 480dp。 | max_width · GoConfig.surface_width_ratio_* |
| 高度 | 和內容一樣高(fit_content),最多到可用空間的 68%,上限 72%。不能改變大小的置中卡片可以長到 94%,免得出現捲動。 | height_ratio · max_height_ratio · max_height |
| 密度 | 設定了 compact、畫面高度不到 420dp(橫拿的手機)或內容放不下時,就變得緊湊:內距更小、間距更小、標題更小。兩種排法都會量,保留放得下的那個比較寬鬆的 — 不會來回跳動。 | compact |
| 改變大小 | 開啟 resizable 後,標題就是拖曳把手:可以在 30% 與上限之間拖曳。height_changed(ratio) 回報結果。 | resizable |
超過上限的比例會被裁回,並在除錯建置中發出一次警告。鍵盤會把卡片往上推,並把取得焦點的輸入欄 捲到看得見的位置;在 Android 與 iOS 上,安全區域讓它避開瀏海與手勢列。
屬性
| 屬性 | 預設值 | 作用 |
|---|---|---|
placement | CENTER | CENTER、BOTTOM 或 ANCHOR |
dismiss_on_scrim | GoConfig.dismiss_on_scrim(false) | 點按遮罩會要求關閉。從卡片裡面開始的拖曳不算。 |
scrim_transparent | false | 讓遊戲保持看得見 — 遮罩仍然會接住外面的點按。 |
close_enabled | true | 顯示 ×,並讓 Esc 與返回鍵可以要求關閉。 |
show_header · scroll_body · resizable | true · true · false | 頁首列、捲動的本文、拖曳把手。 |
fit_content · compact | true · false | 依內容決定大小;一開始就用緊湊排法。 |
fade_in | GoConfig.surface_fade_in(false) | 打開時讓卡片淡入。在 reduce_motion 下會立即出現。 |
alpha | -1(佈景主題的值) | 卡片表面的不透明度,以比例表示。只有表面會變淡 — 文字絕不會。 |
initial_focus | 無 | 打開時取得焦點的控制項。 |
add_child 之前設定:scrim_transparent、show_header、resizable 與
scroll_body 在視窗進入場景樹時套用。dismiss_on_scrim、close_enabled、
fade_in 與 initial_focus 則是即時讀取的。
訊號與呼叫
| 名稱 | 是什麼 |
|---|---|
close_requested | ×、Esc、Android 返回鍵或點按遮罩 — 以及 request_close()。只有最上層的視窗會發出。 |
back_requested · set_back(action) | 頁首的返回按鈕,給有多個頁面的視窗用。傳入空的 Callable 會把它隱藏。 |
height_changed(ratio) | 拖曳把手之後發出。 |
is_top() · GoSurface.is_any_open() | 這是不是接收返回鍵的那個視窗 — 現在到底有沒有任何視窗開著。 |
relayout() | 立刻重新排版。改動與大小有關的屬性時,已經會自動執行。 |
底部面板與抽屜
需要空間的清單,在直拿的手機上從底部升起 — 那裡正是拇指所在的位置;在平板或桌機上則從 側邊滑入,因為在那裡底部面板會把遊戲蓋住。

GoSheet
GoDrawer · LEFT
GoDrawer · RIGHTGoSheet — 從底部升起的頁面
var sheet := GoSheet.new()
add_child(sheet)
func show_bag(items: Array) -> void:
sheet.open("Bag")
var search := GoStyle.line_edit("Search the bag…")
search.text_changed.connect(filter_bag)
sheet.toolbar().add_child(search)
sheet.toolbar().visible = true
for item in items:
sheet.body.add_child(GoStyle.list_button(GoIconSet.BOX, item.name, show_item.bind(item),
Color.TRANSPARENT, item.description, false, GoIconSet.CHEVRON_RIGHT))
sheet.add_footer(GoStyle.button("Close", sheet.close, GoStyle.Tone.PRIMARY))
- 它是一個
CanvasLayer(圖層 10),裡面裝著一個BOTTOM、可以改變大小的GoSurface— 所以它待在 HUD 之上,玩家還能把它拉高。 open(title)會開始一個全新的頁面:清空本文、工具列與返回按鈕,並釋放add_footer()加進來的東西。再呼叫一次,就能在同一個底部面板裡顯示另一頁;set_back()會顯示一顆 用來回到首頁的返回按鈕。open_key()收的是翻譯鍵。close()只是把它隱藏 — 並不釋放,所以下一次open()會立刻完成。closed與page_changed會告訴你什麼時候發生。height_ratio(0.6)是它打開時的高度;max_height_ratio提高上限;dismissable(true)讓點按遮罩時關閉它。
set_back() 進入的子頁面 — 只有畫面上的返回按鈕
才會退回上一頁。如果返回鍵應該退回一頁,請自己在 sheet.surface 上處理 close_requested。
GoDrawer — 從側邊滑入
var drawer := GoDrawer.new()
drawer.side = GoDrawer.Side.LEFT
drawer.follow_text_direction = true # 起始那一側:一般在左邊,阿拉伯文底下在右邊
add_child(drawer)
drawer.body.add_child(GoNavBar.drawer_list([{"icon": GoIconSet.HOME, "text": "Town"},
{"icon": GoIconSet.MAP, "text": "World map"}], 0, go_to))
drawer.open("Menu")
- 圖層 80。寬度是畫面的 42%(
width_ratio),介於min_width(320dp)與max_width(420dp)之間 — 在手機上它會蓋住大半個畫面,所以在那裡底部面板是比較好的選擇。 - 面板一路延伸到畫面邊緣;只有裡面的內容會從安全區域往內縮,所以圓角絕不會切掉第一列。
- 就算在阿拉伯文底下,
LEFT也是指畫面左側。你要的是「起始那一側」時,請打開follow_text_direction;effective_side()會告訴你它最後在哪一側。 open(title)會保留本文 — 填一次就夠了。set_title("")會隱藏頁首。motion_seconds(0.2)控制滑入;reduce_motion讓它立即出現。
drawer.close() — 另外注意,GoSurface.is_any_open() 不會把抽屜算進去;
要問 drawer.is_open()。
控制項旁邊的卡片與選單
玩家剛按下的東西,答案應該出現在手指旁邊,而不是畫面正中央。

GoPopover
GoContextMenu
GoStyle.dropdown()
GoStyle.select()
GoComboboxGoPopover — 釘在被按下之物旁的卡片
slot.pressed.connect(func() -> void:
var card := GoStyle.item_card({"icon": GoIconSet.POTION, "title": "Health potion",
"subtitle": "Heals 50 HP · 12 left"}, false)
GoPopover.open(slot, card, {"width": 280}))
一個 ANCHOR 擺放的視窗,圖層、錨點與善後都已經接好 — 就是每個遊戲都要一再重寫的那十行。
一次只開一張:打開第二張會關掉第一張,因為卡片疊起來會把畫面埋掉,誰也分不出哪張屬於哪一格。
遮罩是透明的 — 為了比較裝備而打開的卡片,不可以把裝備變暗 — 但它仍然會接住外面的第一次點按:
卡片隨之關閉,這次點按也不會再往下傳。
| 選項 | 預設值 | 作用 |
|---|---|---|
title | "" | 附 × 的頁首。空字串代表沒有頁首。 |
width · max_height | 320 · 520 | 卡片的大小。 |
dismissable | true | 點按外面會關閉它。 |
compact · alpha · translate | false · -1 · false | 緊湊間距、表面不透明度、把標題當成翻譯鍵。 |
layer | 95 | 它打開時所在的 CanvasLayer。 |
GoPopover.close() 與 GoPopover.is_open() 是靜態方法。open() 回傳這個視窗 — 錨點不在場景樹裡時則回傳
null,所以在 await shown.close_requested 之前請先檢查。
dismissable = false 又沒有標題時,畫面上沒有任何東西能關閉它 — 只有 Esc、返回鍵或你自己的呼叫。
GoContextMenu — 長按與右鍵
var menu := [
{"text": "Use", "icon": GoIconSet.POTION, "action": use},
{"separator": true},
{"text": "Drop", "icon": GoIconSet.TRASH, "action": drop, "danger": true}]
GoContextMenu.attach(slot, menu)
var more := GoStyle.icon_button(GoIconSet.MORE, Callable(), -1, &"more")
more.pressed.connect(func() -> void: GoContextMenu.open_at(more, menu))
- 按右鍵會立刻打開。手指按住半秒(
HOLD_SECONDS)會打開;移動超過 12dp 就取消 — 所以捲動清單絕不會跳出選單。 - 項目是
{text, action, icon, disabled, checked, separator, danger, translate}。項目要看按到的是什麼才能決定時, 傳一個Callable來代替陣列;每次打開選單時都會呼叫它。 danger會在文字前面加上「⚠ 」 — 引擎的選單只能一次替所有項目上色。- 它就是引擎的
PopupMenu,由GoStyle.style_popup()打扮。它不算是開著的視窗。
⋯ 按鈕
(open_at(),如上),或用導覽標記教一次。
下拉選單、選擇器與分割按鈕
var sort := GoStyle.dropdown("Sort", ["Highest score", "Most wins", {"text": "Friends only", "disabled": true}],
func(index: int) -> void: sort_board(index))
var quality := GoStyle.select(["Low", "Medium", "High", "Ultra"], "Quality")
quality.item_selected.connect(func(index: int) -> void: settings.quality = index)
var send := GoSplitButton.make("Send", send_now, ["Send later", "Save as draft"])
三者打開的都是引擎的彈出選單:dropdown() 是執行動作的 MenuButton,
select() 是保存一個值的 OptionButton,GoSplitButton 是一個主要動作,再加上它箭頭上的選單
(chosen(index))。選單刻意保持完全不透明 — 顯示在獨立作業系統視窗裡的選單,會把半透明的像素變成黑色。
GoCombobox — 可以搜尋的清單
var friend := GoCombobox.make(friend_names, -1, "Find a friend")
friend.picked.connect(func(index: int) -> void: invite(friend_names[index]))
按下後,它會在圖層 96 上打開一份錨定的清單,遮罩是透明的。項目達到 search_threshold(8)個時,
會出現搜尋欄並取得焦點;它在文字或提示的任何位置比對,不分大小寫,找不到結果時也會明白告知。
list_width(0 = 按鈕的寬度,至少 180dp)決定清單的寬度。
從不打斷遊戲的彈出視窗
玩家可以邊玩邊讀的消息。這些都沒有遮罩;點按卡片本身以外的任何地方, 都會傳給遊戲。

GoSnackbar
GoNotice
GoPromptCard
GoStyle.tooltip_node()GoSnackbar — 附「復原」的訊息
var snack := GoSnackbar.new()
add_child(snack)
if await snack.post({"text": "Dropped {name}", "args": {"name": item.name}, "icon": GoIconSet.TRASH,
"actions": ["Undo"], "seconds": 5.0}) == 0:
restore(item)
post() 的鍵 | 作用 |
|---|---|
text · title · args · translate | 訊息、可選的粗體行、{placeholders}、翻譯鍵。 |
tone · icon | 強調色(GoTheme.SUCCESS、WARNING……)與圖示。 |
actions · closable | 按鈕 — String 或 {text, action} — 以及一個 ×。 |
seconds | 停留多久。負數:使用佈景主題的 notice_duration_ms。0:直到被關掉 — 這時一定要給它一顆按鈕。 |
post() 回傳被按下的按鈕的索引;逾時或被關掉時回傳 -1。訊息會排隊
(queue_limit 為 4,最早的一則以 -1 被丟掉),正在顯示時再送一次相同的文字,只會重設它的計時。
它待在底部(或頂端,見 placement),在安全區域與鍵盤之上,圖層 90 — 蓋在抽屜上也讀得到。
開啟 tap_to_dismiss(預設開啟)時,點按沒有按鈕的卡片會關閉它;點按卡片以外的地方則一律
傳給遊戲。show_text() 與 show_key() 是一行的簡寫。
GoNotice — HUD 上的一行字
var notice := GoNotice.new()
hud_corner.add_child(notice)
notice.show_text("Game saved", GoTheme.SUCCESS)
notice.custom_minimum_size.x = notice.preferred_width(340)
一塊由你自己擺放的普通面板 — 放進 GoHudAnchor、放在角落,放哪裡都行。它既不接收輸入也不取得焦點,
所以隔著它按下去,按到的是底下的按鈕;新訊息會取代舊訊息。記得給它一個寬度:
preferred_width(limit) 對短文字回傳它的自然寬度,對長文字則在 limit 之內換行。
set_content() 可以用任何控制項取代文字來顯示。
GoPromptCard — 可以等一等的提問
var spot := GoHudAnchor.new()
spot.spot = GoHudAnchor.Spot.TOP_CENTER
spot.reserve_space = false
spot.avoid_peers = true # 退到生命條下方,而不是蓋住它們
hud.add_child(spot)
var invite := GoPromptCard.new()
spot.add_child(invite)
invite.set_icon(GoIconSet.USERS, Color.TRANSPARENT, true)
invite.set_title("Party invite from Ann")
invite.set_subtitle("Dragon's Lair · 3 / 4 members")
invite.set_actions([{"text": "Accept", "action": accept, "primary": true},
{"text": "Decline", "action": invite.hide}])
invite.fit_width(340)
invite.show()
get_tree().create_timer(15.0).timeout.connect(invite.hide)
- 它一開始是隱藏的 — 要呼叫
show()。它沒有自己的計時器;如果提問會過期,請自己加一個。 - 它的按鈕不會關閉它 — 由動作決定。×(
set_closable(true))會發出closed。 fit_width(width)設定寬度,並依字型預測高度,所以依它排版的東西不會在下一格跳一下。- 它的表面跟隨
hud_alpha— 它是 HUD 的一部分,不是視窗。
GoBanner 與工具提示
GoBanner.make("You're offline.", [{"text": "Retry", "action": reconnect}], GoIconSet.WARNING) 就放在頁面裡、
內容的上方,從不逾時;按下按鈕就會關閉它,除非那顆按鈕帶有 "keep": true。
GoStyle.tooltip_node(text) 是 _make_custom_tooltip() 應該回傳的東西 — 底板由引擎繪製
(TooltipPanel),文字由 gohud 繪製,超過 260dp 就換行。工具提示是給滑鼠用的;玩家需要的東西絕不要藏在它後面。
導覽與主控台

GoCoachMark
GoConsoleGoCoachMark — 帶玩家認識真正的控制項
var tour := GoCoachMark.new()
tour.keep_clear = [top_bar]
tour_layer.add_child(tour)
tour.finished.connect(func(completed: bool) -> void: save.tutorial_done = true)
tour.start([{"target": bag_button, "title": "Bag", "body": "Everything you pick up lands here."},
{"target": map_button, "title": "Map", "body": "Tap to open the full map."}])
- 真正的控制項周圍一個圈,再加上旁邊的一張卡片。按下目標就會往下一步(靠它的
pressed訊號,或步驟裡的"signal");卡片上有「下一步」與「略過」。隱藏的目標會被跳過。 - 卡片會避開畫面中間(
avoid_center_band)、HUD 錨點與keep_clear。 - 只要有任何
GoSurface開著,導覽就會隱藏並等待。返回鍵或 Esc 會以finished(false)結束它。 - 它沒有自己的圖層 — 請最後才加入它,或放在 HUD 之上的圖層裡。
GoConsole — 開發時用的作弊
var console := GoConsole.new()
add_child(console)
console.register("give", "give <item> <count>", func(args: PackedStringArray) -> String:
return "gave %s x%s" % [args[0], args[1]] if args.size() >= 2 else "give <item> <count>")
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed(&"dev_console"): console.toggle()
圖層 200 — 在所有東西之上,所以你可以蓋在對話方塊上除錯 — 而且場景樹暫停時它照樣執行。輸入時,指令面板會 提示指令;上下方向鍵瀏覽歷史紀錄;Esc 關閉它。沒有內建的快速鍵:請自己綁一個。
debug_only(預設開啟)會在發行版本中擋住它 — open() 與 run() 都會被擋住。作弊指令一旦
落到玩家手上,遊戲的經濟系統第一天就結束了。
圖層、返回鍵與輸入
有東西開著時玩家碰得到什麼,由三條規則決定:誰在最上面、誰握有返回鍵、 誰接住點按。
誰在最上面
| 圖層 | 放著什麼 | 用什麼設定 |
|---|---|---|
| 5 | 你的 HUD(由你決定) | — |
| 10 | GoSheet | CanvasLayer.layer |
| ~50 | 你自己的 GoSurface 視窗、暫停選單 | 你的 CanvasLayer |
| 80 | GoDrawer | CanvasLayer.layer |
| 90 | GoSnackbar | layer_index |
| 95 | GoPopover | "layer" 選項 |
| 96 | GoCombobox 清單 | — |
| 100 | GoDialogs | layer_index |
| 200 | GoConsole | layer_index |
這個順序是刻意安排的:訊息條回報的是剛剛發生的事,所以蓋在抽屜上也要讀得到;錨定卡片屬於剛按下的東西, 所以待在它上面;對話方塊提出問題,把兩者都蓋住;主控台蓋住一切,因為它是其他東西都出問題時你要打開的那一個。
誰握有返回鍵
- 只有最上層的視窗會回應。在開著的視窗當中,圖層最高的那一個(同一層時取場景樹中最晚加入的)
接收 Esc、遊戲手把的取消鍵(
ui_cancel)、Android 返回鍵與遮罩點按。疊起來的視窗一次關一個, 絕不會一起關掉。 - Android 返回鍵會延後一格才處理,所以按一下絕不會關掉兩個視窗。
- 只要有任何握有返回鍵的東西開著,Android 返回鍵就不會結束應用程式(
GoBackPolicy計算持有者, 並在最後一個關閉後還原quit_on_go_back)。你自己的彈出視窗如果接管了返回鍵,應該呼叫GoBackPolicy.acquire()與release()— 在_exit_tree裡也要呼叫。 - 有視窗開著時,抽屜、導覽與表單都會讓路 — 返回鍵先交給視窗。
GoConfig.close_on_back(預設開啟)是視窗這一項的開關。
誰接住點按
- 視窗蓋住整個畫面並擋下輸入 — 透明遮罩也一樣。它開著時,較低圖層上的任何東西都按不到; 它上面的圖層(例如訊息條)仍然按得到。
- 滑鼠滾輪與雙指縮放手勢同樣停在視窗上:捲過清單的盡頭也絕不會捲動到遊戲。
- 不經過控制項的遊戲輸入 — 用來移動的
_unhandled_input、拖曳鏡頭 — 應該暫停:if GoSurface.is_any_open(): return。這涵蓋了對話方塊、底部面板、錨定卡片與組合方塊;抽屜則要另外問它的is_open()。 - 訊息條、提示條、提示卡片與導覽標記只在它們自己的卡片上接收輸入;其他地方的輸入都交給遊戲。
焦點
用指標打開的視窗不會把焦點放在任何東西上 — 觸控時不會閃出焦點框。用鍵盤或遊戲手把打開時,關閉按鈕
(或 initial_focus)取得焦點。視窗開著時焦點留在視窗裡,關閉後回到原本握有
焦點的那個控制項。對話方塊每次都會把焦點放在確定按鈕上。
暫停遊戲
暫停場景樹也會暫停彈出視窗 — 只有主控台照常運作。做暫停選單時,請替它的圖層設定
process_mode = PROCESS_MODE_ALWAYS;這樣遊戲停下來時,視窗、它的按鈕與返回鍵都會繼續運作。
同一個彈出視窗,每一種外觀
本頁開頭的那個確認對話方塊,蓋在同一個遊戲上,只換了預設集。程式碼一模一樣 — 其餘都交給佈景主題、外觀與圖示。

default_dark
default_light
scifi_dark
scifi_light
medieval_dark
medieval_light
material_light 新
material_dark 新
kids_light 新
kids_dark 新
comic_light 新
comic_dark 新
arcade_light 新
arcade_dark 新每種外觀改了什麼
| 外觀 | 視窗 | 標題與按鈕 |
|---|---|---|
| 預設 | 圓角(18dp)、柔和的陰影,蓋在遊戲上 80% 不透明 | 樸素的標題與 ×;主要按鈕會浮起,按下時沉下去 |
| 科幻 | 帶霓虹邊與光暈的切角,邊的顏色隨強調色變化 | 括號狀的焦點記號 |
| 中世紀 | 鍛造框 — 鐵與皮革,或羊皮紙 — 附鉚釘與角落裝飾 | 標題用 Cinzel |
| Material 3 | 28dp 圓角、24dp 內距、完全不透明 | Roboto 的 24sp 標題;48dp 觸控目標裡附 40dp 底板的膠囊按鈕 |
| 兒童 | 帶柔和陰影的果凍糖卡片,完全不透明 | 陽光色的標題緞帶(夜間是薄荷色)、圓形的糖果關閉按鈕、按下時會被壓扁的按鈕 |
| 漫畫 | 粗墨線描邊與硬陰影,完全不透明 | 黃色的標題框、描了墨線的圓形關閉按鈕、按下時陰影消失的按鈕 |
| 街機 | 厚實的面板 — 一圈彩色外框圍著淺色內芯 — 完全不透明 | 金色標題橫幅上是帶墨線描邊的白色標題、圓形紅色關閉鍵、按下時會彈一下的上色按鍵 |
用外觀打扮彈出視窗
外觀決定佈景主題碰不到的那些部分。下面這些掛鉤是彈出視窗會呼叫的 — 在你自己的
GoSkin 子類別裡覆寫它們;預設它們什麼都不做。
| 掛鉤 | 打扮什麼 |
|---|---|
title_plate_box() · title_plate_ink() · title_plate_min_size() | 視窗標題後面的底板。標題在底板上置中,並會縮小(最小到 15)以保持在一行內。 |
dress_close_button(button) | 視窗的 × — 兒童外觀把它做成一顆圓糖果,漫畫外觀是一枚描了墨線的圓片,街機外觀則是一顆圓形的紅色按鍵。 |
dress_title(label) | 視窗的標題標籤 — 街機外觀替白色標題描上墨線。 |
press_feedback(control) | 手指落在 gohud 建立的按鈕上的那一刻執行 — 兒童外觀會把按鈕壓扁,街機外觀會讓它彈一下。 |
floating_box() · notice_box() · drawer_box() · banner_box() · overlay_box() | 提示卡片與導覽卡片、訊息條與提示條、抽屜、橫幅、主控台的表面。 |
coach_ring_box() · draw_coach_pointer() | 導覽的圈與指標。 |
要透明到什麼程度
每一類彈出視窗各自依循自己的不透明度,所以 HUD 卡片可以保持通透,而對話方塊保持實心:視窗、底部面板、對話方塊、
錨定卡片與組合方塊依循 panel_alpha;抽屜與導覽卡片依循 card_alpha;提示卡片與
主控台依循 hud_alpha;訊息條與提示條依循 notice_alpha;選單依循 popup_alpha。Material 與兒童外觀
讓它們全部不透明;漫畫外觀也一樣,只有提示卡片與主控台是 92%;街機外觀同樣如此,只是這兩者是 94%。用 GoConfig.container_alpha_overrides 覆寫某一類,用
container_alpha(一個比例 — 0.95)覆寫全部,或用某個視窗自己的 alpha 覆寫單一視窗。只有表面會變淡;文字
絕不會變成半透明。不透明度詳解 →
影響每個彈出視窗的設定
GoConfig(GoUi.config)的欄位。其中幾個在視窗建立時就會被複製,所以請在
建立 UI 之前設定 — 或在改動之後呼叫 GoUi.refresh()。
| 欄位 | 預設值 | 作用 |
|---|---|---|
surface_max_width · surface_max_height | 480 · 700 | 視窗最大能長到多大。 |
surface_height_ratio · surface_max_height_ratio | 0.68 · 0.72 | 平常的高度與上限(也是拖曳的極限)。 |
surface_fit_max_height_ratio | 0.94 | 依內容決定高度的置中卡片,在開始捲動之前最多能長多高。 |
surface_width_ratio_portrait · …_landscape | 0.94 · 0.72 | 寬度佔可用空間的比例。 |
dismiss_on_scrim | false | 點按遮罩會關閉視窗與對話方塊。在視窗建立時複製。 |
surface_fade_in · fade_seconds | false · 0.14 | 視窗是否淡入,以及淡入多快。 |
close_on_back | true | Esc、ui_cancel 與 Android 返回鍵關閉最上層的視窗。 |
close_button_visual | 36 | × 看起來多大 — 它的觸控目標維持 48dp。 |
suppress_pointer_focus_ring | true | 用觸控或滑鼠打開視窗時不顯示焦點框。 |
container_alpha · container_alpha_overrides | -1 · {} | 所有表面的不透明度,或依類別分別設定(GoTheme.BOX_*)。 |
reduce_motion | false | 拿掉淡入淡出、滑動、脈動與壓扁效果。 |
min_touch_size | 48 | 提示卡片與導覽標記裡按鈕的高度。 |
respect_safe_area | true | 讓視窗避開瀏海與手勢列。 |
text_keys · text_overrides | — | 套件自己的用字:關閉、返回、確認、取消、下一步、完成、略過、搜尋、空白…… |
sound_cues · 觸覺回饋 | — | GoFeedback 在打開、關閉、確認、取消與點按時播放什麼。 |
常用寫法
複製、貼上、改改文字就好。這裡的每一個呼叫都對照過原始碼。
把價格寫進去的購買確認
func buy(item_name: String, price: int) -> void:
if await dialogs.confirm("Buy {item}?", "It costs {price} gold.", "Buy", "", "",
{"item": item_name, "price": price}):
purchase(item_name)
附固定「儲存」按鈕的設定視窗
func open_settings() -> void:
var layer := CanvasLayer.new()
layer.layer = 50
add_child(layer)
var window := GoSurface.new()
window.set_title("Settings")
window.dismiss_on_scrim = true
window.close_requested.connect(layer.queue_free)
window.body.add_child(GoStyle.section("Sound", false))
window.body.add_child(GoStyle.toggle("Music", false))
window.body.add_child(GoStyle.slider(0.0, 1.0, 0.05))
var row := GoStyle.row()
row.add_child(GoStyle.button("Cancel", window.request_close))
row.add_child(GoStyle.button("Save", func() -> void:
save_settings()
layer.queue_free(), GoStyle.Tone.PRIMARY))
window.footer.add_child(row)
window.footer.visible = true
layer.add_child(window)
說得出哪裡錯的表單
var field := GoField.make("Name", GoStyle.line_edit(), "3–12 letters")
window.body.add_child(field)
create_button.pressed.connect(func() -> void:
if name_is_taken():
field.set_error("That name is taken")
window.set_status_text("Name check failed", GoTheme.DANGER)
else:
window.clear_status()
create_character())
在底部面板裡選日期
sheet.open("Delivery date")
var picker := GoDatePicker.make({"year": 2026, "month": 10, "day": 14}, func(date: Dictionary) -> void:
delivery = date
sheet.close())
picker.min_date = GoDatePicker.today()
sheet.body.add_child(picker)
暫停選單
func pause() -> void:
get_tree().paused = true
var layer := CanvasLayer.new()
layer.layer = 50
layer.process_mode = Node.PROCESS_MODE_ALWAYS # 遊戲暫停時也繼續執行
add_child(layer)
var menu := GoSurface.new()
menu.set_title("Paused")
menu.close_requested.connect(func() -> void:
get_tree().paused = false
layer.queue_free())
menu.body.add_child(GoStyle.button("Resume", menu.request_close, GoStyle.Tone.PRIMARY))
menu.body.add_child(GoStyle.button("Settings", open_settings))
menu.body.add_child(GoStyle.button("Quit to title", quit_to_title, GoStyle.Tone.DANGER))
layer.add_child(menu)
等彈出視窗關閉後才回應的遊戲操作
func _unhandled_input(event: InputEvent) -> void:
if GoSurface.is_any_open() or drawer.is_open():
return
handle_gameplay(event)
常見陷阱
GoSurface從不自己釋放。請連接close_requested,否則按 × 不會有任何反應。- 在
add_child之後才設定的選項 —scrim_transparent、show_header、resizable、scroll_body,以及GoDialogs的layer_index與max_width— 都不會套用。 - 第二個
confirm()會立刻回傳false。如果兩個問題可能撞在一起,請打開queue_when_busy。 - Enter 會確認。焦點在確定按鈕上,破壞性對話方塊也一樣。
- 底部面板上的返回鍵會關閉整個底部面板,而不是退出子頁面。
- Esc 不會關閉抽屜,而且
is_any_open()也看不到它。 - 錨定卡片或組合方塊的清單會擋住遊戲,直到它關閉為止,即使遮罩是透明的 — 而一張
dismissable = false又沒有標題的錨定卡片,畫面上沒有任何東西能關閉它。 - 提示卡片一開始是隱藏的,而且沒有逾時。
seconds: 0又沒有按鈕的訊息條永遠不會離開。而且預設情況下,它會接住落在卡片上的點按。- 沒有人知道的長按,就是沒有人用的功能。請同時顯示一個
⋯。 - 場景樹暫停時,你的彈出視窗也會暫停。請替它的圖層設定
PROCESS_MODE_ALWAYS。 - 沒設寬度的提示條會縮到最窄 — 請用
preferred_width()。 - 絕不在訊息條裡發問,絕不用對話方塊發通知。問題被忽略就消失了;消息則不該讓遊戲停下來。