Интерфейс: 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.

Проверьте себя
1. Почему интерфейс кладут в CanvasLayer, а не делают потомком игрового мира?
AТак красивее в редакторе
BЧтобы UI рисовался поверх мира и не двигался вместе с камерой
CCanvasLayer работает быстрее
DИначе игра не запустится
2. Как правильно собрать текст надписи со счётом 50 в GDScript?
Atext = "Очки: " + 50
Btext = "Очки: " + str(50)
Ctext = score
Dtext = 50