选对弹窗
弹窗会打断玩家。选用之前,先回答一个问题:玩家必须先作答,游戏才能继续吗? 是,就拦住;不是,就绝不拦——为了玩家并没有问过的消息把游戏停下来的卡片,是让玩家关掉你的游戏最快的办法。
- 有答案的问题——购买、删除、离开、覆盖 →
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 底板的胶囊按钮 |
| 儿童 | 带柔和阴影的果冻糖卡片,完全不透明 | 阳光色的标题彩带(夜间为薄荷色)、圆形的糖果关闭按钮、按下会被压扁的按钮 |
| 漫画 | 粗墨线描边和硬阴影,完全不透明 | 黄色的标题框、描了墨线的圆形关闭按钮、按下时阴影消失的按钮 |
| 街机 | 厚实的面板——一圈彩色边框围着浅色内芯——完全不透明 | 金色标题横幅上是带墨线描边的白色标题、圆形红色关闭键、按下时会弹一下的上色按键 |
用皮肤装扮弹窗
皮肤决定 Theme 够不着的那些部分。下面这些钩子是弹窗会调用的——在你自己的
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()。 - 绝不在消息条里提问,绝不用对话框发通知。问题被忽略就消失了;消息则不该让游戏停下。