LEARN X · ЗА 17 МИН

Godot

Основы движка Godot 4 за 17 минут: узлы, сцены, сигналы, жизненный цикл, ввод, физика и автозагрузка — на закомментированном GDScript.

Godot — открытый игровой движок. Здесь не про язык GDScript (это отдельный тур), а про сам ДВИЖОК: как из узлов собираются сцены, как они общаются сигналами и оживают скриптами. Почти без прозы — всё объяснение спрятано в комментариях рабочего кода Godot 4.

Философия: всё — это узел

В Godot нет «God-объекта». Игра — это дерево из маленьких кирпичиков-узлов.

# Node (узел) — базовый кирпичик движка.
# У каждого узла есть имя, родитель, дети и (опционально) скрипт.
#
# Сцена (Scene) — это просто ДЕРЕВО узлов, сохранённое в файл .tscn.
# Пример дерева сцены игрока:
#
#   Player (CharacterBody2D)   # корневой узел сцены
#    ├── Sprite2D              # картинка
#    ├── CollisionShape2D      # форма столкновения
#    └── Camera2D              # камера следует за игроком
#
# Ключевая идея: СЦЕНА = переиспользуемый КОМПОНЕНТ.
# Сцену Player.tscn можно вставить как ребёнка в сцену уровня,
# а ту — в сцену главного меню. Сцены вкладываются друг в друга.
# Маленькие сцены собираются в большие — это и есть архитектура игры.
#
# Запущенная игра — это одно большое "дерево сцены" (SceneTree),
# в корне которого живёт активная сцена.

Типы узлов: краткий обзор

Тип узла определяет, что он умеет. Выбираешь подходящий базовый класс — и наследуешь его поведение.

# Node          — голый узел: логика, таймеры, менеджеры (без позиции на экране).
# Node2D        — добавляет 2D-трансформ: position, rotation, scale.
# Node3D        — то же самое, но для 3D-пространства.
# Control        — узлы интерфейса (UI): кнопки, метки, контейнеры; знают про anchors.
#
# Физические тела (наследники Node2D):
# CharacterBody2D — тело под РУЧНЫМ управлением (игрок, враг); двигаешь кодом.
# RigidBody2D     — тело под управлением ФИЗИКИ (ящики, мячи); движок сам считает.
# StaticBody2D    — неподвижная преграда (пол, стена).
# Area2D          — зона-детектор: не сталкивается, а РЕГИСТРИРУЕТ вход/выход
#                   (триггеры, подбор монет, урон по площади).
#
# У большинства 2D-узлов есть зеркальные 3D-версии: CharacterBody3D, Area3D и т.д.

Дерево сцены и доступ к узлам

extends Node2D

func _ready() -> void:
	# get_node("Имя") — найти узел-ребёнка по имени или пути.
	var sprite = get_node("Sprite2D")

	# $ — это короткий синоним get_node(). Самый частый способ.
	var sprite2 = $Sprite2D

	# Путь через слэши — спускаемся вглубь дерева.
	var shape = $Player/CollisionShape2D

	# get_parent() — подняться к родителю.
	var parent_node = get_parent()

	# get_children() — получить массив всех прямых детей.
	for child in get_children():
		print(child.name)

	# Уникальное имя через % (узел помечен в редакторе как "% Access as Unique Name").
	# Работает из любого места сцены, даже если узел переместили в дереве.
	var hp_bar = %HealthBar

	# get_tree() — доступ ко ВСЕМУ дереву сцены (SceneTree), а не только к своей ветке.
	get_tree().paused = false

Жизненный цикл узла

Движок сам вызывает эти методы в нужные моменты — тебе остаётся их переопределить.

extends Node2D

func _ready() -> void:
	# Вызывается ОДИН раз, когда узел и все его дети вошли в дерево.
	# Здесь безопасно обращаться к $Child — дети уже готовы. Точка инициализации.
	print("узел готов")

func _process(delta: float) -> void:
	# Каждый КАДР. delta — секунд прошло с прошлого кадра (для плавности).
	# Сюда: анимация, неблокирующая логика, обновление UI.
	rotation += 1.0 * delta

func _physics_process(delta: float) -> void:
	# Каждый шаг ФИЗИКИ (фиксированная частота, обычно 60 Гц).
	# Сюда: всё, что связано с движением и столкновениями. delta здесь стабильный.
	pass

func _input(event: InputEvent) -> void:
	# На КАЖДОЕ событие ввода (клавиша, мышь, тач) — до обработки игрой.
	if event is InputEventMouseButton:
		print("клик")

func _unhandled_input(event: InputEvent) -> void:
	# Только то, что НЕ перехватил UI. Лучшее место для геймплейного ввода:
	# нажатие на кнопку интерфейса сюда уже не дойдёт.
	pass

func _exit_tree() -> void:
	# Узел покидает дерево (удаляется) — место для очистки.
	pass

Скрипт на узле

# Скрипт ПРИВЯЗЫВАЕТСЯ к узлу и расширяет его поведение.
# extends говорит: "этот скрипт — это такой узел".
extends Area2D

# class_name делает скрипт НОВЫМ типом узла, доступным во всём проекте
# (появится в списке "Add Node" и в подсказках). Необязательно.
class_name Coin

# Внутри скрипта self — это сам узел. Можно звать его методы напрямую:
func _ready() -> void:
	# монета — это Area2D, значит у неё есть все методы Area2D:
	monitoring = true        # свойство Area2D
	position.x += 10         # свойство Node2D (Area2D его наследует)
	name = "GoldCoin"        # свойство Node

# Один узел = один скрипт. Скрипт "знает" только про свой узел и его детей.
# Так логика остаётся локальной и переиспользуемой вместе со сценой.

Сигналы — нервная система Godot

Сигналы позволяют узлам сообщать о событиях, НЕ зная, кто их слушает. Это главный способ связи в движке.

extends Area2D
class_name Coin

# Объявляем свой сигнал (можно с параметрами).
signal collected(value: int)

var value: int = 5

func _ready() -> void:
	# Подключаемся к ВСТРОЕННОМУ сигналу Area2D "body_entered":
	# движок сам выстрелит им, когда в зону войдёт физическое тело.
	# .connect(метод) — кого вызвать при срабатывании.
	body_entered.connect(_on_body_entered)

func _on_body_entered(body: Node2D) -> void:
	if body.name == "Player":
		# .emit(...) — ИСПУСКАЕМ свой сигнал. Все, кто подключён, получат вызов.
		collected.emit(value)
		queue_free()  # удалить монету

# Где-то в другом узле (например, в игре) слушаем сигнал монеты:
# coin.collected.connect(_on_coin_collected)
#
# func _on_coin_collected(amount: int) -> void:
# 	score += amount
#
# Монета НЕ знает про счёт. Игра НЕ знает про устройство монеты.
# Они связаны только сигналом — это слабая связанность (loose coupling).

Экспорт свойств в инспектор

@export выносит переменную в редактор — настраивать можно мышкой, без правки кода.

extends CharacterBody2D

# Обычная переменная — видна только в коде.
var internal_state := 0

# @export — переменная появляется в Инспекторе редактора.
# Дизайнер задаёт значение для каждого экземпляра отдельно.
@export var speed: float = 300.0
@export var player_name: String = "Герой"

# @export_range — слайдер с границами (min, max, шаг).
@export_range(0, 100, 1) var health: int = 100

# Можно экспортировать ссылки на ресурсы и сцены:
@export var bullet_scene: PackedScene
@export var icon: Texture2D

# Экспорт-категории группируют поля в инспекторе:
@export_group("Бой")
@export var damage: int = 10
@export var crit_chance: float = 0.2

# Значения из инспектора УЖE подставлены к моменту _ready().

Инстансирование сцен

Сцена-файл — это «чертёж». Чтобы появилась в игре, её нужно создать (instantiate) и добавить в дерево.

extends Node2D

# preload — загрузить сцену-чертёж при компиляции (быстро, путь известен заранее).
const EnemyScene := preload("res://enemies/enemy.tscn")

# load(...) — загрузить во время выполнения (если путь вычисляется).

func spawn_enemy(pos: Vector2) -> void:
	# instantiate() — создать ЖИВОЙ экземпляр узла из чертежа.
	var enemy = EnemyScene.instantiate()
	enemy.position = pos

	# add_child(...) — вставить узел в дерево сцены. Только теперь он "оживает":
	# у него вызовется _ready(), и пойдут _process()/_physics_process().
	add_child(enemy)

func remove_enemy(enemy: Node) -> void:
	# queue_free() — пометить узел на удаление в конце кадра (безопасно).
	# НЕ используй free() во время обработки сигналов/физики — может крашнуть.
	enemy.queue_free()

Ввод (Input)

extends CharacterBody2D

# Действия ("move_left", "jump") настраиваются в Project Settings -> Input Map.
# Там одному действию назначают РАЗНЫЕ клавиши/кнопки геймпада — код не меняется.

func _physics_process(delta: float) -> void:
	# is_action_pressed — кнопка УДЕРЖИВАЕТСЯ (каждый кадр true).
	if Input.is_action_pressed("ui_right"):
		position.x += 5

	# is_action_just_pressed — сработало РОВНО в кадр нажатия (для прыжка, выстрела).
	if Input.is_action_just_pressed("jump"):
		print("прыжок!")

	# get_vector — сразу нормализованное направление из 4 действий.
	# Удобно для движения: возвращает Vector2 в диапазоне круга.
	var dir := Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
	velocity = dir * 300.0

	# get_action_strength — сила нажатия 0..1 (аналоговый стик геймпада).
	var throttle := Input.get_action_strength("accelerate")

Движение и физика

CharacterBody2D создан для персонажей под ручным управлением: ты задаёшь скорость, движок разруливает столкновения.

extends CharacterBody2D

@export var speed: float = 300.0
@export var jump_force: float = -400.0
var gravity: float = 980.0

func _physics_process(delta: float) -> void:
	# velocity — встроенное свойство CharacterBody2D (пиксели в секунду).

	# Гравитёж: пока в воздухе — тянем вниз.
	if not is_on_floor():
		velocity.y += gravity * delta

	# Прыжок возможен только стоя на полу.
	if Input.is_action_just_pressed("jump") and is_on_floor():
		velocity.y = jump_force

	# Горизонтальное направление: -1, 0 или +1.
	var dir := Input.get_axis("ui_left", "ui_right")
	velocity.x = dir * speed

	# move_and_slide() — ДВИГАЕТ тело по velocity и САМ обрабатывает столкновения:
	# скользит вдоль стен, останавливает на препятствиях, обновляет is_on_floor().
	move_and_slide()

	# После move_and_slide можно опросить столкновения:
	if get_slide_collision_count() > 0:
		var col := get_slide_collision(0)
		print("врезались в: ", col.get_collider().name)

Таймеры и анимация

extends Node2D

func _ready() -> void:
	# --- Timer: узел-будильник ---
	var timer := Timer.new()
	timer.wait_time = 2.0
	timer.one_shot = true          # сработать один раз (а не циклично)
	add_child(timer)
	timer.timeout.connect(_on_timeout)  # сигнал по истечении времени
	timer.start()

	# Быстрый таймер без узла — ждём прямо в корутине:
	await get_tree().create_timer(1.5).timeout
	print("прошло 1.5 сек")

	# --- Tween: плавная анимация значений по коду ---
	var tween := create_tween()
	# за 1 сек плавно сдвинуть position в точку (100, 0):
	tween.tween_property(self, "position", Vector2(100, 0), 1.0)
	# затем (chained) плавно изменить прозрачность:
	tween.tween_property(self, "modulate:a", 0.0, 0.5)

func _on_timeout() -> void:
	# --- AnimationPlayer: проигрывание заранее нарисованных анимаций ---
	# Узел AnimationPlayer хранит дорожки (треки) изменений свойств во времени.
	$AnimationPlayer.play("run")   # запустить анимацию по имени
	# $AnimationPlayer.play("idle") # переключить на другую

Группы узлов

Группа — это тег. Удобно командовать пачкой узлов, не держа на них прямые ссылки.

extends Node2D

func _ready() -> void:
	# Добавить узел в группу (тег "enemies"). Можно прямо в редакторе во вкладке Node.
	add_to_group("enemies")

func check() -> void:
	# Принадлежность к группе:
	if is_in_group("enemies"):
		print("это враг")

func explode_all() -> void:
	# Получить ВСЕ узлы группы по всему дереву:
	for enemy in get_tree().get_nodes_in_group("enemies"):
		enemy.queue_free()

	# Вызвать метод у всех узлов группы разом (если метода нет — пропустит):
	get_tree().call_group("enemies", "take_damage", 10)

Смена сцен

extends Node

func go_to_level() -> void:
	# Выгрузить текущую сцену и загрузить новую из файла.
	# get_tree() управляет тем, какая сцена сейчас "корневая".
	get_tree().change_scene_to_file("res://levels/level_1.tscn")

func restart() -> void:
	# Перезагрузить текущую сцену с нуля (рестарт уровня).
	get_tree().reload_current_scene()

func to_menu() -> void:
	# Можно сменить сцену на уже загруженный PackedScene:
	var menu := preload("res://ui/menu.tscn")
	get_tree().change_scene_to_packed(menu)

func quit() -> void:
	get_tree().quit()  # выход из игры

Автозагрузка (синглтоны)

Autoload — узел, который живёт ВСЮ игру и не исчезает при смене сцен. Идеально для глобального состояния.

# Файл game_state.gd. Регистрируется в Project Settings -> Autoload
# под именем, например, "GameState". После этого доступен ОТКУДА УГОДНО
# по этому имени — как глобальный синглтон.
extends Node

var score: int = 0
var player_name: String = ""
var current_level: int = 1

signal score_changed(new_score: int)

func add_score(amount: int) -> void:
	score += amount
	score_changed.emit(score)  # оповестить подписчиков (например, HUD)

# --- Использование из ЛЮБОГО другого скрипта ---
# GameState.add_score(10)
# print(GameState.score)
# GameState.score_changed.connect(_on_score_changed)
#
# Поскольку autoload переживает смену сцен, счёт и прогресс
# сохраняются между уровнями автоматически.

Типичный скрипт игрока

Собираем всё вместе: узел + экспорт + ввод + физика + сигналы.

extends CharacterBody2D
class_name Player

# Настройки — в инспекторе.
@export var speed: float = 250.0
@export var jump_force: float = -420.0
@export_range(1, 10) var max_health: int = 3

var gravity: float = 980.0
var health: int = 3

# Свои сигналы — пусть мир узнаёт о событиях игрока.
signal health_changed(value: int)
signal died

func _ready() -> void:
	health = max_health
	# Подключаем встроенный сигнал зоны-хитбокса (Area2D-ребёнок).
	$Hurtbox.body_entered.connect(_on_hurtbox_entered)

func _physics_process(delta: float) -> void:
	# Гравитация.
	if not is_on_floor():
		velocity.y += gravity * delta

	# Прыжок.
	if Input.is_action_just_pressed("jump") and is_on_floor():
		velocity.y = jump_force

	# Горизонтальное движение.
	var dir := Input.get_axis("move_left", "move_right")
	velocity.x = dir * speed

	# Повернуть спрайт по направлению.
	if dir != 0:
		$Sprite2D.flip_h = dir < 0

	# Двигаемся + разруливаем столкновения.
	move_and_slide()

func take_damage(amount: int) -> void:
	health -= amount
	health_changed.emit(health)  # обновить HUD через сигнал
	if health <= 0:
		died.emit()
		queue_free()

func _on_hurtbox_entered(body: Node2D) -> void:
	if body.is_in_group("enemies"):
		take_damage(1)
Поддержать проект