Создание 3D-хоррора на Godot: от основ GDScript до прототипа в стиле Lethal Company

Курс для начинающих разработчиков, которые хотят создать прототип 3D-хоррор-игры на движке Godot. Все концепции объясняются через жизненные аналогии: от базового синтаксиса GDScript до настройки физики, освещения и системы инвентаря. Идеально подходит для тех, кто знаком с интерфейсом Godot, но пока плохо владеет программированием.

1. Основы GDScript на простых примерах: переменные, условия, циклы и функции

Переменные, условия, циклы и функции: пишем первые скрипты на GDScript

Представь, что ты приходишь в ресторан и заказываешь бургер. Официант записывает твой заказ (это переменная), проверяет, есть ли нужные ингредиенты на кухне (это условие), готовит бургер, повторяя одинаковые действия для каждой котлеты (это цикл), и в конце подаёт готовое блюдо — результат чёткой последовательности шагов (это функция). Всё программирование, включая GDScript, работает точно так же. Разница лишь в том, что вместо бургеров мы будем управлять монстрами, фонариками и дверями в нашем хорроре.

Переменные — коробки с наклейками

Переменная — это именованная ячейка памяти, куда ты кладёшь какое-то значение. Представь шкаф с выдвижными ящиками. На каждом ящике наклейка: «Здоровье игрока», «Количество патронов», «Имя монстра». Внутри ящика лежит конкретное значение: 100, 12, «Тень».

В GDScript переменная объявляется через ключевое слово var:

Здесь мы создали четыре переменные разных типов:

  • health — целое число (int), здоровье игрока
  • player_name — строка (String), имя персонажа
  • is_alive — логическое значение (bool), жив ли персонаж
  • speed — дробное число (float), скорость движения
  • Можно явно указывать тип переменной — это считается хорошей практикой, потому что помогает движку работать быстрее и предотвращает ошибки:

    Представь разницу между явным и неявным типом так: если ты кладёшь в ящик с наклейкой «Только цифры» кусок пиццы — это ошибка. Явное указание типа работает как замок на ящике: ты не положишь туда ничего лишнего.

    Ещё важная идея — константа. Это переменная, которую нельзя изменить после создания. Объявляется через const:

    Константа — как табличка «Не трогать!» на двери серверной. Значение зафиксировано раз и навсегда.

    Условия — если... то...

    Условие — это развилка на дороге. Ты стоишь перед перекрёстком: если светофор зелёный — идёшь, если красный — стоишь. В коде это выглядит так:

    Обрати внимание на два важных момента:

  • Сравнение «равно» в GDScript записывается как == (два знака равно), а не = (один знак). Один = используется только для присваивания значения переменной.
  • Блоки кода внутри if, elif, else отделяются отступами (табуляцией или пробелами), а не фигурными скобками, как в C++ или JavaScript. Это работает точно так же, как в Python.
  • Условия можно вкладывать друг в друга, как матрёшка:

    Также в GDScript есть удобная конструкция для проверки нескольких условий одновременно через операторы and (и), or (или), not (не):

    Циклы — повторяй, пока не надоест

    Цикл — это конвейер на заводе. Один и тот же набор действий выполняется многократно, пока не закончатся детали или конвейер не остановят.

    В GDScript есть два основных типа циклов: for и while.

    Цикл for — перебирает элементы одного за другим. Представь, что ты перебираешь карточки в каталоге — берёшь одну, смотришь, кладёшь обратно, берёшь следующую:

    Функция range(5) создаёт последовательность чисел от 0 до 4. Если нужен другой диапазон:

    Цикл for удобен для работы с массивами (списками). Например, если у нас есть массив предметов в инвентаре:

    Цикл while — работает, пока условие истинно. Это как кипятильник: он греет воду, пока температура не достигнет 100°C:

    Будь осторожен с while — если условие никогда не станет ложным, цикл будет работать бесконечно и «повесит» игру. Это как забыть выключить газ: вода выкипит, кастрюля сгорит, а пламя продолжит гореть.

    Функции — рецепты из кулинарной книги

    Функция — это именованный набор инструкций, который можно вызвать в любой момент. Представь рецепт борща: в нём записана чёткая последовательность шагов. Тебе не нужно каждый раз заново придумывать, как варить борщ — ты просто открываешь рецепт и следуешь ему.

    В GDScript функция объявляется через func:

    Разберём по частям:

  • func — ключевое слово, говорящее движку: «начинается функция»
  • take_damage — имя функции (название рецепта)
  • (amount: int)параметр: входные данные, которые функция получает извне (сколько урона нанести)
  • -> void — тип возвращаемого значения. void означает, что функция ничего не возвращает (просто выполняет действие)
  • Если функция должна вернуть результат, указываем конкретный тип:

    Эта функция принимает базовый урон и множитель, вычисляет итоговый урон и возвращает его через return. Мы можем использовать результат так:

    Функции с параметрами по умолчанию позволяют вызывать их с разным количеством аргументов:

    Встроенные функции Godot — жизненный цикл объекта

    Godot автоматически вызывает определённые функции в конкретные моменты жизни объекта. Две самые важные:

  • _ready() — вызывается один раз, когда объект появляется на сцене. Это как первый день на новой работе: ты знакомишься с окружением, расставляешь вещи на столе.
  • _process(delta) — вызывается каждый кадр (примерно 60 раз в секунду). Это как сердцебиение игры — именно здесь происходит постоянное обновление: движение, анимация, проверка условий.
  • Параметр delta критически важен для плавного движения. Если ты просто прибавляешь speed к позиции каждый кадр, то на мощном ПК (120 FPS) объект будет двигаться вдвое быстрее, чем на слабом ноутбуке (60 FPS). Умножая скорость на delta, ты компенсируешь это: расстояние за кадр становится одинаковым независимо от частоты кадров.

    Переменные уровня узла — видимость и доступ

    В Godot переменные, объявленные в скрипте, доступны только внутри этого скрипта. Но иногда нужно, чтобы один объект знал о другом. Для этого используется ссылка на узел через символ Flashlight # Получаем ссылку на дочерний узел flashlight.visible = false # Выключаем фонарик `

    Путь Room/Door/Handle`.

    С этими четырьмя китами — переменными, условиями, циклами и функциями — ты готов писать реальные игровые механики. В следующей статье мы применим эти знания, чтобы заставить объекты сталкиваться, подниматься и летать по комнате.

    2. Физика объектов и взаимодействие с предметами: столкновения, поднятие и бросание

    Физика объектов и взаимодействие с предметами: столкновения, поднятие и бросание

    Когда ты идёшь по комнате в темноте и натыкаешься на стол — это столкновение. Когда ты поднимаешь ключ с пола — это захват объекта. Когда ты швыряешь банку в стену — это применение силы. Все эти действия в реальной жизни управляются физикой, и в Godot мы можем воспроизвести их с помощью встроенных физических узлов. Именно эти механики — основа любого хоррора вроде Lethal Company или Content Warning, где игрок хватает предметы, бросает их и взаимодействует с окружением.

    Коллизии — невидимые стены реальности

    Коллизия (от латинского collisio — столкновение) — это механизм, который не позволяет объектам проходить сквозь друг друга. В реальном мире ты не можешь пройти сквозь стену, потому что атомы твоего тела и стены отталкиваются. В Godot роль «атомов» выполняют формы коллизий (CollisionShape).

    Каждый физический объект в Godot требует двух компонентов:

  • Физическое тело — определяет, как объект ведёт себя в физическом мире
  • Форма коллизии — определяет границы объекта, его «силуэт» для физики
  • Три типа физических тел

    Godot предлагает три типа тел, и выбор зависит от того, что именно ты хочешь создать:

    | Тип тела | Поведение | Пример использования | |----------|-----------|---------------------| | RigidBody3D | Полностью управляется физикой: падает под действием гравитации, отскакивает от стен, катится | Бутылка, которую можно сбить; мяч; обломки | | StaticBody3D | Неподвижно, но другие объекты сталкиваются с ним | Стены, пол, стол, дверной проём | | CharacterBody3D | Управляется кодом, но сталкивается с окружением | Игрок, монстр, NPC |

    Для хоррора нам понадобятся все три типа. Стены и пол будут StaticBody3D, подбираемые предметы — RigidBody3D, а игрок и монстры — CharacterBody3D.

    Формы коллизий

    Форма коллизии — это невидимая геометрическая фигура, которая определяет, где заканчивается объект. Godot предлагает несколько вариантов:

  • BoxShape3D — прямоугольный параллелепипед. Подходит для книг, коробок, дверей
  • SphereShape3D — сфера. Подходит для мячей, голов
  • CapsuleShape3D — капсула. Идеальна для персонажей, потому что не цепляется за углы, как прямоугольник
  • ConcavePolygonShape3D — произвольная форма. Используется для сложных поверхностей (рельеф, неровный пол)
  • Представь, что ты обтягиваешь объект пищевой плёнкой. Плёнка повторяет примерный контур — вот это и есть форма коллизии. Прямоугольная коробка оборачивается в BoxShape3D, а человеческая фигура — в CapsuleShape3D.

    Создаём физический предмет

    Давай создадим бутылку, которую можно будет поднять и бросить — типичный объект для хоррора.

    Шаг 1: Структура узлов

    Создай новую сцену с такой иерархией узлов:

    Корневой узел RigidBody3D говорит движку: «этот объект управляется физикой — он падает, катится, отскакивает». MeshInstance3D отвечает за внешний вид, а CollisionShape3D — за физические границы.

    Шаг 2: Настраиваем физические свойства

    В инспекторе у RigidBody3D есть важные параметры:

  • Mass (масса) — влияет на то, насколько тяжело объект толкать. Бутылка — 0.5 кг, ящик — 10 кг
  • Physics Material — определяет трение и упругость. Создай новый PhysicsMaterial и настрой:
  • - Friction (трение): 0.5 — бутылка будет скользить, но не как на льду - Bounce (упругость): 0.2 — слегка отскакивает от пола
  • Gravity Scale — множитель гравитации. Значение 1.0 — нормальная гравитация, 0.0 — невесомость
  • Шаг 3: Скрипт физического предмета

    Разберём ключевые моменты. Свойство freeze — это «кнопка паузы» для физики объекта. Когда freeze = true, объект перестаёт реагировать на гравитацию и столкновения — он буквально зависает в воздухе. Мы используем это, когда игрок держит предмет в руках.

    Метод apply_central_impulse() — это «пинок» для объекта. Он мгновенно прикладывает силу в указанном направлении. Представь, что ты ударяешь по мячу ногой: именно так работает импульс. Параметр direction — это вектор направления (куда летит), а force — сила удара.

    Механизм подбора предметов

    Подбор предмета — это трёхэтапный процесс: обнаружение, захват, удержание.

    Обнаружение: «Что я вижу перед собой?»

    Для определения, находится ли предмет в зоне досягаемости, используется raycast (лучевой запрос). Представь, что из глаз игрока вылетает невидимый лазерный луч. Если луч попадает в объект — значит, его можно подобрать.

    Бросок: «Швыряю с силой»

    Бросок — это снятие заморозки и применение импульса:

    Столкновения и сигналы

    Godot умеет сообщать о столкновениях через сигналы. У RigidBody3D есть несколько полезных сигналов:

  • body_entered — срабатывает, когда другой физический объект входит в зону
  • body_exited — когда объект покидает зону
  • input_event — когда игрок кликает мышью по объекту
  • Для использования этих сигналов нужен дочерний узел Area3D с формой коллизии. Пример: бутылка разбивается при падении с большой высоты.

    Сигнал body_entered срабатывает при каждом столкновении. Мы проверяем скорость объекта через linear_velocity.length() — если она слишком высокая, значит, удар был сильным, и бутылка разбивается. Метод queue_free() удаляет объект из сцены.

    Маски и слои коллизий — кто с кем сталкивается

    В хорроре важно контролировать, какие объекты сталкиваются друг с другом. Игрок должен проходить сквозь триггер-зоны, но не сквозь стены. Предметы должны падать на пол, но не сталкиваться друг с другом, когда их несут.

    Godot использует систему слоёв и масок коллизий:

  • Collision Layer (слой) — «на каком слое находится этот объект»
  • Collision Mask (маска) — «с какими слоями этот объект сталкивается»
  • Представь вечеринку с цветными браслетами. Если у тебя красный браслет (слой), а маска настроена на синий — ты будешь сталкиваться только с теми, у кого синий браслет. Красные будут проходить сквозь тебя.

    | Объект | Слой | Маска | Сталкивается с | |--------|------|-------|----------------| | Игрок | 1 | 1, 2 | Пол, стены, предметы | | Предмет | 2 | 1, 2 | Пол, игрок, другие предметы | | Пол/стены | 1 | — | Всё | | Триггер-зона | 3 | 1 | Только игрок |

    Настройка осуществляется в инспекторе каждого CollisionShape3D или RigidBody3D — там есть чекбоксы для 32 слоёв.

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

    3. Создание игрового персонажа и физика тела: передвижение, камера и управление

    Создание игрового персонажа и физика тела: передвижение, камера и управление

    Представь, что ты надеваешь VR-шлем: ты видишь мир от первого лица, но не можешь пошевелиться — ноги не слушаются, голова не поворачивается. Именно так чувствует себя игрок, если персонаж настроен неправильно. В предыдущей статье мы научили объекты падать, сталкиваться и летать. Теперь соберём живого человека — того, кто будет бродить по тёмным коридорам нашего хоррора, дрожа от страха и крутя головой в поисках монстра.

    CharacterBody3D — тело, которое слушается кода

    Для физических предметов мы использовали RigidBody3D — он полностью подчиняется физике. Но игрок не должен катиться по лестнице как бутылка. Ему нужен CharacterBody3D — тип тела, которое управляется исключительно твоим кодом, но при этом сталкивается с миром. Это как инвалидное кресло с роботизированным управлением: оно не падает само, но не проходит сквозь стены.

    Создай новую сцену с такой иерархией:

    Капсула в качестве формы коллизии — стандарт для FPS-персонажей. Прямоугольник цепляется за ступеньки и пороги, а капсула скользит плавно, потому что у неё закруглённые края. В инспекторе у CollisionShape3D выбери CapsuleShape3D и выставь высоту около 1.8 метра (средний рост человека), радиус — 0.4.

    Передвижение — шаги в пространстве

    Открой скрипт корневого узла Player и начнём с базовой структуры:

    gdscript var current_speed: float = 0.0 var acceleration: float = 10.0

    func _physics_process(delta): # ... гравитация ... var input_dir = Input.get_vector("move_left", "move_right", "move_forward", "move_back") var direction = (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized() if direction: current_speed = lerp(current_speed, speed, acceleration * delta) velocity.x = direction.x * current_speed velocity.z = direction.z * current_speed else: current_speed = lerp(current_speed, 0.0, acceleration * delta) velocity.x = move_toward(velocity.x, 0, current_speed) velocity.z = move_toward(velocity.z, 0, current_speed) move_and_slide() ``

    lerp(current, target, weight) плавно перемещает значение от current к target на долю weight. При weight = 1 мгновенно перескочит к цели, при weight = 0.1 — будет двигаться медленно. Умножая acceleration на delta`, мы делаем скорость интерполяции независимой от FPS.

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

    4. Освещение и атмосфера хоррора: источники света, тени и настроение сцены

    Освещение и атмосфера хоррора: источники света, тени и настроение сцены

    Когда ты заходишь в тёмную комнату и включаешь фонарик — круг света выхватывает из темноты кусок стены, а за пределами луча что-то шевелится. Именно этот контраст между светом и тьмой создаёт страх. В Lethal Company игрок постоянно включает и выключает фонарик, потому что батарея садится, а в темноте слышны шаги. В Content Warning тьма буквально пожирает тех, кто не вернулся на базу до заката. Без правильного освещения хоррор превращается в блуждание по пустым комнатам — и в этой статье мы научимся делать так, чтобы темнота была настоящим врагом.

    Три типа источников света в Godot

    Godot предлагает три основных типа освещения для 3D-сцен, и каждый из них играет свою роль в хорроре:

    OmniLight3D — светит во все стороны, как лампочка. Это потолочное освещение в коридоре, мерцающая люстра в заброшенном зале, свет от костра. Радиус действия настраивается параметром omni_range: за его пределами свет обрезается, создавая резкую границу между светом и тьмой. Для хоррора это идеально — игрок видит безопасный круг и неизвестную темноту за его границей.

    SpotLight3D — конус света, как у прожектора или фонарика. У него есть два важных угла: spot_angle (ширина конуса) и spot_angle_attenuation (насколько быстро свет слабеет к краям). Фонарик персонажа — это именно SpotLight3D, прикреплённый к камере.

    DirectionalLight3D — имитирует солнце или луну: светит параллельными лучами на всю сцену одинаково. В хорроре его используют для лунного света, пробивающегося через окна. Параметр energy определяет яркость: значение 0.05–0.15 создаёт едва заметное голубоватое свечение, характерное для ночи.

    Настройка фонарика — свет на грани гибели

    Фонарик — главный инструмент выживания в хорроре. Он должен быть не идеальным: узкий луч, мигание, садящаяся батарея. Создай узел SpotLight3D как дочерний от Camera3D игрока:

    Плотность 0.01–0.03 — оптимальный диапазон: игрок видит на 10–30 метров вперёд, дальше — серая пелена. Цвет тумана лучше делать не белым, а тёмно-серым или синеватым — это создаёт «грязную» атмосферу.

    Цветокоррекция — настроение через цвет

    Параметр adjustment_enabled позволяет сместить цветовую гамму всей сцены. Для хоррора характерны:

  • Общее затемнение: adjustment_brightness = 0.7–0.9 — сцена становится мрачнее
  • Сдвиг в синие/зелёные тона: adjustment_color_correction — создаёт больничную или подвальную атмосферу
  • Насыщенность: adjustment_saturation = 0.5–0.7 — приглушённые цвета выглядят тревожнее
  • Мерцающие источники света — атмосфера распада

    Заброшенные здания в хоррорах полны мерцающих ламп. Это не просто красиво — мерцание создаёт непредсказуемость: в одну секунду ты видишь коридор, в следующую — абсолютную тьму.

    Здесь мы комбинируем две синусоиды с разными частотами — это даёт нерегулярное мерцание, похожее на настоящую неисправную лампу. Один sin() создаёт равномерное мигание, а два叠加 — хаотичное.

    Глобальное освещение — отражённый свет

    В реальном мире свет отражается от стен и пола, заполняя тени. В Godot есть два способа это эмулировать:

  • LightmapGI — предварительно рассчитанное освещение. Идеально для статичных сцен (коридоры, комнаты), потому что считается один раз при сборке, а не каждый кадр. Результат — мягкие, реалистичные тени без нагрузки на процессор.
  • VoxelGI — динамическое глобальное освещение в реальном времени. Подходит, если источники света двигаются (фонарик игрока), но требовательно к производительности.
  • Для хоррора с фонариком рекомендуется комбинация: LightmapGI для статичного освещения комнаты + динамический SpotLight3D для фонарика. Так тёмные углы остаются тёмными, а луч фонарика реалистично освещает стены.

    Постобработка — финальный штрих

    Помимо тумана и цветокоррекции, WorldEnvironment управляет эффектами:

  • Glow (свечение) — создаёт «ореол» вокруг ярких объектов. В хорроре используется дозированно: свечение выхода или монитора в тёмной комнате выглядит жутко.
  • SSAO (Screen Space Ambient Occlusion) — затенение в углах и щелях. Без него углы выглядят плоскими, с ним — глубокими и зловещими.
  • Volumetric Fog — объёмный туман, через который видны лучи света. Это эффект «свет в пыльном воздухе», который создаёт потрясающую атмосферу заброшенности.
  • Теперь наша сцена дышит: туман ограничивает видимость, тени прячут угрозы, мерцающие лампы создают нервозность, а фонарик с садящейся батареей заставляет торопиться. В следующей статье мы соберём всё вместе — инвентарь, предметы и механику взаимодействия — и получим работающий прототип хоррора.

    5. Система инвентаря и прототип механики хоррора: сбор предметов и взаимодействие с окружением

    Система инвентаря и прототип механики хоррора: сбор предметов и взаимодействие с окружением

    В Lethal Company ты забегаешь в заброшенное здание, хватаешь с пола сломанный компьютер, запихиваешь в рюкзак, а потом бежишь к выходу, потому что счётчик обратного отсчёта показывает 30 секунд. В Content Warning ты снимаешь монстра на камеру, а потом продаёшь запись, чтобы купить лучший фонарик. Все эти механики — инвентарь, сбор предметов, взаимодействие с окружением — и есть то, что превращает ходилку по комнатам в настоящую игру. Мы уже умеем создавать персонажа, физику и атмосферу. Теперь соберём всё в работающий прототип.

    Архитектура инвентаря — данные отдельно от картинки

    Главная ошибка начинающих — хранить данные о предметах прямо в UI-элементах. Это как записывать рецепты на салфетках и держать их в кошельке: работает, пока у тебя один рецепт, но разваливается при масштабировании.

    Правильный подход — разделить данные (что лежит в инвентаре) и отображение (как это выглядит на экране). Данные хранятся в отдельном скрипте-менеджере, а UI просто читает их и рисует.

    Ресурсы предметов — шаблоны для всего

    В Godot есть удобный инструмент — Resource (ресурс). Это контейнер данных, который можно сохранить как файл и переиспользовать. Создадим кастомный ресурс для описания предметов:

    class_name ItemData регистрирует тип глобально — теперь можно создавать ресурсы этого типа через меню Создать → Ресурс → ItemData. Каждый такой файл — это один предмет: фонарик, батарейка, ключ, аптечка. Заполняешь поля в инспекторе, сохраняешь — и предмет готов к использованию.

    Менеджер инвентаря — мозг системы

    Менеджер инвентаря — это Autoload (глобальный синглтон), к которому обращаются все скрипты. Добавь его в Проект → Настройки проекта → Автозагрузка:

    Массив slots — это и есть инвентарь. Каждый элемент содержит ссылку на ресурс предмета и количество. Сигнал inventory_changed оповещает UI об изменениях — это паттерн «наблюдатель»: менеджер не знает о существовании UI, он просто кричит «что-то изменилось!», а UI слушает и обновляется.

    UI инвентаря — картинка для данных

    Создай сцену инвентаря на CanvasLayer, чтобы она рисовалась поверх игры:

    Слот инвентаря — ячейка с иконкой

    Создай отдельную сцену для слота:

    Скрипт слота:

    Генерация слотов и обновление

    RayCast3D в Godot работает непрерывно — он автоматически обновляет данные каждый кадр. Метод is_colliding() возвращает true, если луч столкнулся с чем-то, а get_collider() — ссылку на объект.

    Собираем прототип — финальная сцена

    Теперь у нас есть все компоненты для прототипа:

  • Игрок с фонариком, садящейся батареей и камерой от первого лица
  • Физические предметы, которые подбираются в инвентарь
  • Интерактивные объекты: двери с замками, точки сдачи
  • Атмосфера: туман, мерцающие лампы, тени
  • Инвентарь с отображением на экране
  • Создай тестовую сцену: комната с несколькими предметами на полу, запертая дверь, ключ в дальнем углу и точка сдачи за дверью. Игрок должен найти ключ, открыть дверь, собрать предметы и сдать их. Добавь таймер обратного отсчёта — и у тебя есть ядро геймплея Lethal Company.

    Этот менеджер — Autoload, который тикает каждый кадр и уменьшает таймер. Когда время заканчивается — перезагрузка сцены. Сигнал time_updated позволяет UI отображать оставшееся время.

    Каждый элемент — фонарик с садящейся батареей, туман, мерцающие лампы, инвентарь, таймер — работает сам по себе. Но вместе они создают переживание: ты в темноте, фонарик мигает, в инвентаре один слот свободен, а до конца миссии 40 секунд. Именно это и есть хоррор — не монстры, а давление обстоятельств.