Интерфейс: Control и Label
Интерфейс живёт отдельно от игрового мира: он не двигается вместе с камерой и всегда висит поверх экрана.
Суть: UI-узлы (Control, Label) рисуются в особом слое и привязаны к экрану, а не к игровому миру.
Счёт, полоска здоровья, кнопка «пауза» должны оставаться на месте, даже когда камера летит за персонажем по огромному уровню. Поэтому интерфейс строят не на Node2D, а на узлах Control — это семья UI-узлов с собственной системой расположения (якоря, отступы). А чтобы интерфейс точно рисовался поверх мира и не ехал с камерой, его кладут в CanvasLayer — отдельный слой отрисовки.
UI (CanvasLayer) <- слой поверх мира, не едет с камерой ├── ScoreLabel (Label) <- очки ├── HealthLabel (Label) <- здоровье └── PauseButton (Button) <- кнопка
Самый частый UI-узел — Label, показывающий текст. У него есть свойство text. Чтобы обновить счёт, ты меняешь это свойство из кода. Числа надо превращать в строку через str():
extends Label
var score: int = 0
func add_points(amount: int) -> void:
score += amount
text = "Очки: " + str(score)Стоит привыкнуть мыслить интерфейс как отдельный мир со своими законами. У игрового мира есть камера, пиксельные единицы и физика; у интерфейса — якоря, контейнеры и реакция на размер окна. Контейнеры особенно полезны: они сами расставляют дочерние элементы рядами, колонками или сеткой, и тебе не нужно вручную выставлять каждую кнопку в пикселях. Это спасает, когда игру запускают на телефоне, планшете и большом мониторе: один и тот же интерфейс аккуратно подстраивается под любой экран, если ты доверил расстановку контейнерам и якорям, а не хардкоду.
Как работает под капотом
Движок рисует мир и интерфейс в разных проходах. Мировые узлы (Node2D) рисуются с учётом камеры: сдвинул камеру — сдвинулась картинка. Узлы в CanvasLayer рисуются в координатах экрана и камеру игнорируют, поэтому остаются на месте. Control-узлы используют якоря: «прижми меня к левому верхнему углу», «растяни на всю ширину» — и интерфейс сам подстраивается под разный размер окна.
Логика счёта проста и хорошо моделируется на Python: копим очки и форматируем строку для надписи.
score = 0
def add_points(label_text, score, amount):
score += amount
label_text = "Очки: " + str(score)
return label_text, score
label = "Очки: 0"
for coin in [10, 10, 25, 50]:
label, score = add_points(label, score, coin)
print("Надпись на экране:", label)
print("Итоговый счёт:", score)Та же логика на Python ▶. Счёт — это число, а надпись — строка, которую мы пересобираем при каждом изменении. В GDScript ты так же присваиваешь label.text новую строку.
Полезно различать два подхода к расположению интерфейса. Можно ставить элементы вручную по координатам — быстро для прототипа, но хрупко при смене размера окна. А можно доверить расстановку якорям и контейнерам, и тогда интерфейс сам тянется и перестраивается. Для серьёзной игры почти всегда выбирают второй путь: он чуть дольше настраивается, зато один и тот же интерфейс одинаково хорошо смотрится и на широком мониторе, и на узком экране телефона. Привыкай мыслить интерфейс гибким с самого начала — переделывать жёсткую вёрстку потом очень муторно.
Частые ошибки
Первая ошибка — строить интерфейс на Node2D: тогда он едет вместе с камерой и уплывает за край экрана. UI — на Control в CanvasLayer. Вторая — складывать число и строку напрямую ("Очки: " + score): GDScript ругнётся, число надо обернуть в str(). Третья — каждый кадр в _process пересобирать текст, даже когда счёт не менялся: обновляй надпись только при изменении, через сигнал или прямой вызов. Четвёртая — забыть про якоря и хардкодить позиции в пикселях: на другом размере окна интерфейс разъедется.
Best practices
Держи весь интерфейс в одном CanvasLayer, отдельно от игрового мира. Используй Control-узлы и якоря, чтобы UI подстраивался под размер экрана. Обновляй надписи событийно (по сигналу «счёт изменился»), а не каждый кадр. Превращай числа в строки через str(). Давай UI-узлам понятные имена: ScoreLabel, HealthBar — потом легче к ним обращаться.
Итоги: интерфейс строят на Control-узлах в CanvasLayer, чтобы он висел поверх мира и не ехал с камерой. Label показывает текст через свойство text; числа переводи в строку через str(). Обновляй надписи по событию, используй якоря для адаптивности и не путай UI-узлы с мировыми Node2D.