알맞은 팝업 고르기
팝업은 흐름을 끊는다. 하나를 고르기 전에 질문 하나에만 답한다: 게임이 이어지기 전에 플레이어가 반드시 답해야 하는가? 그렇다면 막는다. 아니라면 절대 막지 않는다 — 플레이어가 묻지도 않은 소식으로 게임을 멈추는 카드는 플레이어가 게임을 꺼 버리게 만드는 가장 빠른 길이다.
- 답이 있는 질문 — 구매, 삭제, 나가기, 덮어쓰기 →
GoDialogs.confirm(). 확인만 받으면 되는 일 →alert(). 몇 가지 중 하나 →choose(). - 그 자체로 한 화면 — 설정, 캐릭터 정보, 상점 아이템 → 화면 가운데의
GoSurface. - 긴 목록이나 관리 페이지 — 가방, 우편, 친구 → 폰에서는 아래에서 올라오는
GoSheet, 넓은 화면에서는 옆에서 나오는GoDrawer. - 방금 누른 것에 관한 것 — 아이템의 능력치, 할 수 있는 동작 → 그 옆에 뜨는
GoPopover나GoContextMenu. - 게임을 멈추지 않아도 되는 소식 — 저장됨, 버림, 초대받음 →
GoSnackbar,GoNotice또는GoPromptCard. 이것들은 화면을 차지하지 않는다. - 조작 가르치기 →
GoCoachMark. 개발 중에 쓰는 치트 →GoConsole.
모든 팝업 한눈에 보기
| 팝업 | 게임을 멈추나 | 뒤로 가기 / Escape | 바깥 탭 | 쓰는 곳 |
|---|---|---|---|---|
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 · 드롭다운 · 셀렉트 | 엔진 메뉴를 따른다 | 엔진 메뉴를 따른다 | 닫는다 | 동작, 짧은 목록 |
GoSnackbar | 아니요 | — | 게임에 닿는다 | 저장됨 · 되돌리기 · 다시 시도 |
GoPromptCard | 아니요 | — | 게임에 닿는다 | 초대, 거래 요청 |
GoNotice | 절대 안 멈춘다 | — | 게임에 닿는다 | HUD 위에 고정된 한 줄 |
GoBanner | 아니요 — 페이지 안에 있다 | — | — | "오프라인입니다. 다시 시도" |
GoCoachMark | 자기 카드만 | 투어를 끝낸다 | 게임에 닿는다 | 첫 실행 투어 |
GoConsole | 자기 패널만 | Escape 키로 닫는다 | 게임에 닿는다 | 디버그 명령 |
각 팝업이 어느 레이어에서 열리는지, 왜 그 순서인지는 레이어, 뒤로 가기, 입력에 있다.

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 |
| ×, Escape, 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 을 돌려준다.
폰과 데스크톱
choose() 는 Android 와 iOS 에서는 엄지가 닿는 아래쪽에서 시트로, 데스크톱에서는 가운데 카드로
열린다. 확인창은 폭이 max_width(420dp)로 제한되고 가운데에 놓인다. layer_index,
max_width 와 다른 export 값은 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 | × 를 보이고, Escape 와 뒤로 가기로 닫기를 요청할 수 있게 한다. |
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 | ×, Escape, 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))
BOTTOM배치에 크기 조절이 되는GoSurface를 담은CanvasLayer(레이어 10)다 — 그래서 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 에 제목도 없으면 화면 위의 어떤 것도 그것을 닫지 못한다 — Escape, 뒤로 가기, 직접 부르는 호출뿐이다.
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))
- 오른쪽 클릭은 곧바로 연다. 손가락을 0.5초(
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)). 메뉴는 일부러 완전히 불투명하게 둔다 — 자체 OS 창으로 뜨는 메뉴는 반투명
픽셀을 검게 만든다.
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가 하나라도 열려 있는 동안 투어는 숨어서 기다린다. 뒤로 가기나 Escape 는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 이다 — 모든 것 위에 있으므로 대화상자 위에서도 디버그할 수 있다 — 그리고 트리가 일시 정지되어도 계속 돈다. 치는 동안 명령을 제안하는 목록이 뜨고, 위·아래 키로 기록을 오가며, Escape 로 닫는다. 기본 키는 없다: 직접 정한다.
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 |
이 순서는 의도한 것이다: 스낵바는 방금 일어난 일을 알리므로 서랍 위에서 읽힌다. 팝오버는 방금 누른 것에 딸려 있으므로 그 위에 놓인다. 대화상자는 질문을 하므로 둘 다 덮는다. 콘솔은 모든 것을 덮는다 — 나머지가 다 잘못되었을 때 여는 것이기 때문이다.
뒤로 가기는 누구의 것인가
- 가장 위 창만 답한다. 열린 표면 가운데 가장 높은 레이어의 것(같으면 트리에서 가장 나중 것)이
Escape, 게임패드 취소(
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% 불투명 | 평범한 제목과 ×. 강조 버튼은 떠 있다가 누르면 가라앉는다 |
| Sci-fi | 네온 테두리와 발광이 있는 잘린 모서리, 테두리는 강조색으로 물든다 | 꺾쇠 모양의 포커스 표식 |
| 중세 | 단조 프레임 — 철과 가죽, 또는 양피지 — 에 리벳과 모서리 장식 | 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 | Escape, 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 가 곧 확인이다. 되돌릴 수 없는 대화상자에서도 확인 버튼이 포커스를 가진다.
- 시트에서 뒤로 가기를 누르면 시트가 닫힌다. 하위 페이지만 닫히는 것이 아니다.
- Escape 는 서랍을 닫지 않고,
is_any_open()도 서랍을 보지 못한다. - 팝오버나 콤보박스 목록은 닫힐 때까지 게임을 막는다. 스크림이 투명해도 그렇다 — 그리고
dismissable = false에 제목도 없는 팝오버는 화면에 그것을 닫을 것이 없다. - 프롬프트 카드는 숨은 채로 시작하고 시간 제한도 없다.
seconds: 0에 버튼도 없는 스낵바는 영영 사라지지 않는다. 게다가 기본으로 자기 카드의 탭을 받는다.- 아무도 모르는 길게 누르기는 아무도 쓰지 않는 기능이다.
⋯도 함께 보인다. - 일시 정지된 트리는 팝업도 멈춘다. 그 레이어에
PROCESS_MODE_ALWAYS를 쓴다. - 폭을 정하지 않은 알림은 가장 좁게 접힌다 —
preferred_width()를 쓴다. - 스낵바로 묻지 않고, 대화상자로 알리지 않는다. 무시하면 질문이 사라져 버리고, 소식이 게임을 멈춰서는 안 된다.