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)