Создание карточной игры на Godot: от основ до механик

Курс по созданию карточной игры на Godot для программистов без опыта работы с движком. Вы изучите архитектуру сцен, механику Drag & Drop [youtube.com](https://www.youtube.com/watch?v=Pa0P1lUoC-M), работу с UI [habr.com](https://habr.com/ru/articles/747206/) и логику управления колодой [youtube.com](https://www.youtube.com/watch?v=HF8A0OuyHho). Пошаговые инструкции помогут создать рабочий прототип с системой ходов и подготовить базу для сложных режимов.

1. Основы Godot для карточных игр

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

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

Архитектура Godot: Узлы и Сцены

Фундамент любой игры на Godot строится на двух ключевых понятиях: узлах (Nodes) и сценах (Scenes).

Узел — это мельчайший строительный блок игры. Каждый узел выполняет одну конкретную функцию. Например, узел Sprite2D умеет только отображать картинку, узел Label — выводить текст, а узел AudioStreamPlayer — воспроизводить звук. Сами по себе они ничего не значат, но если объединить их вместе, получается сложный объект.

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

!Схема архитектуры сцен карточной игры в Godot

Представьте создание карты. Вы создаете новую сцену и добавляете в нее:

  • Корневой узел Area2D (чтобы отслеживать клики мыши).
  • Дочерний узел Sprite2D (для отображения рубашки или лицевой стороны).
  • Дочерние узлы Label (для отображения стоимости, атаки и здоровья).
  • Эту сцену можно сохранить как отдельный файл (например, Card.tscn). Теперь вы можете создавать сотни копий (экземпляров) этой сцены на игровом столе, просто меняя текст в узлах Label и картинку в Sprite2D. Это избавляет от необходимости рисовать каждую карту с нуля.

    Анатомия цифрового карточного стола

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

    | Зона | Описание | Техническая реализация в Godot | | :--- | :--- | :--- | | Колода (Deck) | Источник новых карт. Игроки не видят её содержимое. | Массив данных (список ID карт). Визуально — один статичный спрайт рубашки. | | Рука (Hand) | Карты, доступные игроку для розыгрыша. | Контейнер (например, HBoxContainer), который автоматически выстраивает дочерние сцены карт в ряд. | | Поле (Board) | Зона активных карт, которые участвуют в сражении. | Узел Node2D с заданными координатами, куда перемещаются карты после розыгрыша. | | Сброс (Discard Pile) | Отыгранные или уничтоженные карты. | Массив данных. Визуально — спрайт верхней сброшенной карты. |

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

    Частая ошибка новичков — создавать 30 физических объектов карт и складывать их друг на друга, имитируя колоду. Это перегружает систему. Правильный подход: хранить колоду как список идентификаторов.

    Например, колода в памяти выглядит так: [12, 45, 7, 12, 8]. Когда игрок берет карту, движок удаляет первое число из списка (12), создает экземпляр сцены Card.tscn, загружает в него данные карты под номером 12 и помещает эту сцену в зону «Рука».

    Математика карточных игр также опирается на эти массивы. Вероятность вытянуть нужную карту рассчитывается по классической формуле:

    Где — вероятность, — количество копий нужной карты в колоде, а — общее количество оставшихся карт. Если в колоде осталось 20 карт, и вам нужны 2 конкретные копии «Огненного шара», вероятность составит (или 10%). Движок может рассчитывать это в реальном времени, помогая создавать умный искусственный интеллект для противника.

    Интерактивность: Сигналы и Drag & Drop

    Чтобы игра ожила, игрок должен взаимодействовать с картами. В Godot для связи между объектами используется система сигналов (Signals).

    Сигнал — это сообщение, которое узел рассылает всем желающим, когда происходит определенное событие. Например, узел Button имеет встроенный сигнал pressed. Когда игрок кликает по кнопке, она кричит: «Меня нажали!». Другой узел (например, скрипт управления ходом) может «подписаться» на этот сигнал и завершить ход игрока.

    Для реализации механики Drag & Drop (перетаскивания) карт, нам потребуются сигналы ввода. Процесс делится на три этапа:

  • Захват: Игрок нажимает левую кнопку мыши над картой. Сцена карты ловит сигнал input_event, проверяет, что нажата нужная кнопка, и меняет свою внутреннюю переменную is_dragging на true.
  • Перемещение: Каждый кадр игра проверяет: если is_dragging == true, то координаты карты приравниваются к текущим координатам курсора мыши.
  • Сброс: Игрок отпускает кнопку мыши. Карта проверяет, над какой зоной она находится (например, над игровым полем). Если зона правильная, карта фиксируется там, списывается мана, и срабатывают эффекты. Если зона неверная (или не хватает маны), карта плавно возвращается обратно в «Руку».
  • Этот код пишется на GDScript — встроенном языке программирования Godot, который синтаксически очень похож на Python. Он лаконичен и специально оптимизирован для работы с узлами.

    Разделение данных и визуала (Лучшие практики)

    При планировании игры с несколькими режимами и сотнями карт критически важно использовать правильную архитектуру. Главное правило профессиональной разработки: визуальное представление должно быть отделено от данных.

    Не пишите характеристики карты (урон, здоровье) прямо в скрипте визуальной сцены. Используйте встроенный в Godot класс Resource (Ресурс).

    Ресурс — это контейнер для данных, который сохраняется на жестком диске. Вы можете создать скрипт CardData.gd, наследующийся от Resource, и прописать в нем переменные: name, cost, attack, artwork. Затем в редакторе Godot вы создаете сотни файлов ресурсов (по одному на каждую карту), просто заполняя поля в удобном интерфейсе, без написания кода.

    Когда сцена карты появляется на столе, она просто получает нужный ресурс и говорит: «Ага, в ресурсе написано, что картинка — дракон, а атака — 5. Обновляю свои узлы Sprite2D и Label».

    Такой подход позволяет:

  • Легко балансировать игру (дизайнер может менять цифры в ресурсах, не трогая код).
  • Быстро добавлять новые карты.
  • Создавать разные режимы игры, которые будут использовать одну и ту же базу карт, но применять к ним разные правила стола.
  • Освоив концепции узлов, сцен, сигналов и разделения данных, вы заложите прочный фундамент. На этой базе можно построить как простую карточную дуэль, так и сложный roguelike с запутанными механиками и тысячами взаимодействий. В следующих материалах мы перейдем к практической реализации и создадим нашу первую интерактивную карту в редакторе.

    2. Визуализация карт и игрового поля

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

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

    Выбор фундамента: Node2D против Control

    Когда новичок открывает Godot и хочет отобразить карту, его рука инстинктивно тянется к узлу Sprite2D. Это логично: спрайты созданы для вывода 2D-графики. Однако для карточных игр это часто становится ловушкой.

    В Godot существует два параллельных семейства узлов для работы с двумерным пространством:

  • Node2D (Синие узлы): Предназначены для игрового мира. Они существуют в мировых координатах, подчиняются игровой физике и камере.
  • Control (Зеленые узлы): Предназначены для пользовательского интерфейса (UI). Они привязываются к краям экрана (якорям), умеют автоматически выстраиваться в списки и таблицы, а также легко перехватывают клики мыши.
  • > Использование Node2D для карт похоже на разбрасывание фотографий по реальному столу — вы можете положить их где угодно, но выравнивать придется вручную. Использование Control — это как вставка фотографий в специальный альбом, где кармашки сами удерживают их ровными рядами.

    Поскольку карты чаще всего находятся в руке (которая привязана к низу экрана) или в строгих зонах на столе, профессионалы предпочитают собирать сцену карты на базе узлов Control.

    Анатомия визуальной сцены карты

    Давайте спроектируем идеальную сцену карты (Card.tscn), используя UI-узлы. Корневым узлом нашей сцены станет TextureRect — это аналог спрайта, но из семейства Control. Он будет отвечать за отображение рубашки или фона карты.

    Внутрь корневого узла мы добавляем дочерние элементы:

  • TextureRect (Artwork) — для иллюстрации персонажа или заклинания.
  • Label (Title) — для названия карты.
  • Label (Description) — для текста эффекта.
  • Label (Cost/Attack/Health) — для числовых характеристик.
  • !Схема иерархии узлов для сцены карты в Godot

    Чтобы карта выглядела естественно, необходимо соблюдать правильные пропорции. В индустрии настольных и цифровых игр стандартом считается соотношение сторон (Aspect Ratio), которое вычисляется по классической формуле:

    Где — соотношение сторон, — ширина карты, а — её высота.

    Стандартная покерная карта имеет физический размер 2,5 на 3,5 дюйма. Подставив значения в формулу, получаем . При создании цифровых ассетов (например, в разрешении 500x700 пикселей) старайтесь придерживаться этого коэффициента (). Это гарантирует, что игроки подсознательно воспримут ваш объект именно как игральную карту, а не как странный прямоугольник.

    Магия контейнеров: Создаем руку игрока

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

    Если писать эту математику вручную через Node2D, вам придется вычислять отступы каждый кадр. Но семейство Control предлагает элегантное решение — Контейнеры (Containers).

    Для руки игрока идеально подходит узел HBoxContainer (Horizontal Box Container). Его единственная задача — брать все свои дочерние узлы и выстраивать их в ровный горизонтальный ряд.

    !Интерактивная демонстрация работы HBoxContainer с картами

    Вам достаточно добавить сцены карт внутрь HBoxContainer, и движок сам рассчитает их позиции. Если вы хотите, чтобы карты перекрывали друг друга (как веер), в настройках контейнера можно задать отрицательное значение свойства Theme Overrides -> Constants -> Separation (например, -50 пикселей).

    Игровое поле: Зоны и Сетки

    Когда игрок разыгрывает карту из руки, она перемещается на игровое поле. Визуализация поля зависит от жанра вашей игры.

    | Тип поля | Описание | Техническое решение в Godot | Пример игры | | :--- | :--- | :--- | :--- | | Свободные зоны | Карты просто лежат в определенной области экрана. | Узлы Panel или ColorRect в качестве визуальных подложек. | Hearthstone, Legends of Runeterra | | Строгая сетка | Карты ставятся в конкретные ячейки (например, 2 ряда по 5 слотов). | Узел GridContainer (автоматически строит сетку из UI-элементов). | Gwent, Marvel Snap | | Тактическая карта | Карты становятся юнитами и перемещаются по ландшафту. | Узел TileMapLayer (создание тайловых уровней). | Duelyst, Faeria |

    Особого внимания заслуживает TileMapLayer. Если ваша игра подразумевает тактическое перемещение карт по полю со сложным ландшафтом (горы, леса, вода), использование отдельных узлов для каждой клетки поля убьет производительность. TileMapLayer позволяет «рисовать» поле по сетке, объединяя тысячи клеток в один оптимизированный объект. Вы можете легко получить координаты клика мыши и перевести их в координаты сетки тайлов, чтобы понять, в какую именно клетку игрок хочет поставить карту.

    Оживление карт: Визуальный отклик

    Статичные картинки на экране не вызывают эмоций. Игра должна реагировать на действия пользователя. Базовое правило хорошего интерфейса: объект должен меняться, когда курсор мыши оказывается над ним (эффект Hover).

    Поскольку наша карта построена на базе Control, она автоматически умеет отслеживать мышь. У корневого узла TextureRect есть встроенные сигналы mouse_entered (мышь наведена) и mouse_exited (мышь убрана).

    Мы можем написать простой скрипт, который будет немного увеличивать карту при наведении. Для плавности анимации профессионалы используют встроенный класс Tween (от слова in-between — промежуточный), который плавно меняет значения от точки А к точке Б.

    Разберем этот код. Когда срабатывает сигнал mouse_entered, функция create_tween() генерирует аниматор. Мы приказываем ему взять свойство scale (масштаб) и за 0.1 секунды изменить его до значения 1.1. Свойство z_index отвечает за порядок отрисовки: значение 1 гарантирует, что увеличенная карта не перекроется соседними картами в руке.

    Важный нюанс: чтобы карта увеличивалась из центра, а не из левого верхнего угла, мы в функции _ready() смещаем pivot_offset (точку опоры) ровно на половину размера карты.

    Грамотное комбинирование узлов Control, автоматических контейнеров и плавных анимаций через Tween позволяет создать интерфейс, который ощущается дорогим и отзывчивым. Игрок еще не начал партию, но ему уже приятно просто перебирать карты в руке.

    3. Интерактивность и перетаскивание карт

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

    Основа любой цифровой карточной игры — это механика Drag & Drop (бери и тащи). Игрок должен иметь возможность схватить карту, вытянуть её из руки и положить на игровое поле. Технически эта задача сложнее, чем кажется на первый взгляд, так как она требует синхронизации пользовательского ввода, физики интерфейса и игровой логики.

    Два пути: Встроенный и Пользовательский Drag & Drop

    Движок Godot предлагает разработчикам два принципиально разных подхода к реализации перетаскивания объектов.

    | Характеристика | Встроенный Drag & Drop (Native) | Пользовательский Drag & Drop (Custom) | | :--- | :--- | :--- | | Как работает | Использует встроенные функции _get_drag_data, _can_drop_data и _drop_data. | Отслеживает клики и координаты мыши вручную через _gui_input и _process. | | Визуализация | Создает временную копию узла (preview), которая следует за курсором. Оригинал остается на месте. | Перемещает сам оригинальный узел карты за курсором. | | Идеально для | Инвентарей в RPG, где нужно перетащить иконку меча из одного слота в другой. | Карточных игр, где важна физика самой карты, её плавный возврат в руку при отмене и сложные анимации. |

    > Использование встроенного Drag & Drop для карточной игры — частая ошибка новичков. Эта система слишком жесткая. Когда вы отпускаете карту мимо стола, встроенная система просто мгновенно уничтожает «превью», в то время как в хорошей карточной игре карта должна плавно улететь обратно в руку.

    Поэтому профессиональные разработчики карточных игр всегда пишут пользовательскую систему перетаскивания.

    Анатомия перетаскивания: Состояния и Смещения

    Чтобы научить карту перемещаться за курсором, нам нужно превратить её в конечный автомат (State Machine). Это концепция в программировании, при которой объект может находиться только в одном из строго заданных состояний в любой момент времени.

    У нашей карты будет три состояния:

  • Idle (Покой): Карта лежит в руке, подчиняется правилам контейнера.
  • Dragging (Перетаскивание): Игрок зажал левую кнопку мыши. Карта игнорирует контейнер и следует за курсором.
  • Released (Отпускание): Игрок отпустил кнопку. Карта проверяет, находится ли она над игровым полем. Если да — разыгрывается. Если нет — возвращается в состояние Idle.
  • !Схема состояний карты при перетаскивании

    Математика захвата: Вычисление смещения

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

    Если в коде просто приравнять позицию карты к позиции мыши, центр карты мгновенно «прыгнет» к курсору. Это выглядит неестественно. Чтобы этого избежать, в момент клика необходимо вычислить смещение (Offset).

    Формула вычисления смещения выглядит так:

    Где — вектор смещения, — глобальные координаты курсора мыши на экране, а — глобальные координаты левого верхнего угла карты.

    Например, если карта находится в координатах (100, 200), а игрок кликнул по ней в координатах (120, 250), то смещение составит (20, 50). Теперь, при движении мыши, мы будем вычислять новую позицию карты по обратной формуле: позиция мыши минус смещение. Карта будет двигаться плавно, оставаясь «приклеенной» к курсору ровно в точке клика.

    Реализация логики на GDScript

    Давайте напишем скрипт для нашей сцены карты, который реализует эту математику. Поскольку наша карта построена на базе узла Control, мы будем использовать встроенную функцию _gui_input(), которая перехватывает все события мыши и клавиатуры, происходящие внутри границ этого узла.

    Магия свойства top_level

    В коде выше есть одна критически важная строчка: top_level = true.

    В прошлой статье мы поместили карты в HBoxContainer, который автоматически выстраивает их в ряд. Контейнеры в Godot работают агрессивно: они каждый кадр принудительно перезаписывают координаты своих дочерних узлов. Если вы попытаетесь изменить global_position карты, находящейся в контейнере, контейнер мгновенно вернет её обратно.

    Свойство top_level (верхний уровень) приказывает узлу временно игнорировать трансформации своего родителя. Карта физически остается внутри контейнера руки, но визуально отрывается от него и позволяет нам свободно менять её координаты. Когда мы отпускаем мышь и пишем top_level = false, карта послушно возвращается на свое законное место в веере.

    Проблема перекрытия: Настройка Mouse Filter

    Когда карты лежат в руке плотным веером, они перекрывают друг друга. Godot обрабатывает клики мыши сверху вниз: узел, который нарисован поверх остальных (имеет больший z_index или находится ниже в дереве сцены), первым получает событие клика.

    У каждого узла Control есть свойство Mouse Filter (Фильтр мыши), которое определяет, как узел реагирует на курсор. У него три режима:

  • Stop (Остановка): Узел поглощает клик. Узлы, лежащие под ним, ничего не почувствуют.
  • Pass (Пропуск): Узел реагирует на клик, но также передает его узлам, лежащим под ним.
  • Ignore (Игнорирование): Узел вообще не реагирует на мышь, клик проходит сквозь него.
  • Для корневого узла карты необходимо установить Mouse Filter в режим Stop (обычно стоит по умолчанию). Если вы оставите Pass, то при попытке перетащить верхнюю карту, вы случайно схватите и ту карту, которая лежит под ней, так как клик пронзит обе.

    А вот для всех дочерних элементов карты (тексты, иконки, иллюстрации) Mouse Filter нужно строго перевести в режим Ignore. Иначе текстовая метка с описанием карты может «перехватить» клик на себя, и корневой узел карты не узнает, что игрок пытается её перетащить.

    Определение зоны сброса (Drop Zone)

    Мы научились таскать карту, но как понять, что игрок отпустил её над игровым полем, а не просто бросил обратно в руку?

    Для этого используется система групп (Groups) и проверка пересечений. Игровое поле (например, узел ColorRect или TextureRect, обозначающий стол) добавляется в группу "drop_zone".

    Когда игрок отпускает кнопку мыши (event.is_pressed() == false), мы должны проверить, какие узлы интерфейса сейчас находятся под курсором. В Godot для этого не нужно писать сложные математические проверки пересечения прямоугольников. Мы можем спросить у самого дерева сцены, кто находится под мышкой.

    Хотя встроенный метод _drop_data делает это автоматически, в нашем кастомном подходе мы можем использовать сигналы mouse_entered и mouse_exited на самом игровом поле.

    Создадим глобальную переменную (через паттерн Singleton/Autoload), например Global.is_mouse_on_board = false. Когда мышь заходит на поле, поле меняет эту переменную на true. Когда выходит — на false.

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

    Создание собственного механизма Drag & Drop требует чуть больше кода, чем использование встроенных функций движка. Однако этот подход дает полный контроль над поведением объекта. Вы можете добавить плавную анимацию возврата карты в руку через Tween, заставить карту изгибаться в зависимости от скорости перемещения курсора или добавить эффект частиц, тянущийся за картой во время полета. Именно такие детали превращают сухой программный код в приятный игровой опыт.

    4. Механики колоды, руки и сброса

    Механики колоды, руки и сброса

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

    Основа любой карточной игры — это круговорот карт между тремя главными зонами: Колодой (Deck), Рукой (Hand) и Сбросом (Discard Pile). Техническая реализация этого круговорота в движке Godot требует понимания фундаментального принципа разработки: разделения данных и их визуального представления.

    Золотое правило: Данные отдельно, картинки отдельно

    Главная ошибка начинающих разработчиков — попытка сделать колоду физической стопкой объектов на сцене. Они создают 30 узлов (Nodes) карт, складывают их друг на друга, скрывают через свойство visible = false и пытаются перемещать эти узлы в руку.

    Это крайне неоптимизированный подход. Узлы интерфейса (Control) с текстурами, текстами и скриптами потребляют оперативную память и ресурсы процессора. Хранить десятки скрытых узлов «про запас» — значит замедлять игру.

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

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

    !Схема жизненного цикла карты в игре

    В Godot для хранения данных мы используем Массивы (Arrays), а сами данные описываем с помощью пользовательских ресурсов (класс Resource), о которых мы говорили в первой статье.

    Архитектура игровых зон

    Давайте создадим скрипт CardManager.gd, который будет управлять логикой перемещения карт. Этот скрипт можно прикрепить к корневому узлу игрового поля.

    Нам понадобятся три массива:

    Обратите внимание на типизацию Array[Resource]. Это означает, что в этих массивах могут лежать только ресурсы карт (информация об их названии, уроне, стоимости), а не сами узлы сцены.

    Формирование и перемешивание колоды

    В начале игры колоду нужно заполнить данными и перемешать. В Godot массивы имеют встроенный метод shuffle(), который использует алгоритм Фишера-Йетса для случайной перестановки элементов.

    Вероятность вытянуть конкретную карту из колоды подчиняется базовой формуле теории вероятностей:

    Где — вероятность события, — количество нужных карт в колоде (например, 3 копии «Огненного шара»), а — общее количество карт в колоде (например, 30). В данном случае вероятность составит 0.1 или 10%. По мере того как уменьшается (карты берутся в руку), вероятность вытянуть оставшиеся нужные копии возрастает.

    Механика раздачи карт (Draw)

    Процесс взятия карты из колоды в руку состоит из четырех строгих шагов:

  • Проверить, есть ли в массиве deck элементы.
  • Извлечь последний элемент из массива deck.
  • Добавить этот элемент в массив hand.
  • Создать визуальный узел карты на основе этих данных и поместить его в контейнер руки.
  • Для извлечения элемента идеально подходит метод массива pop_back(). Он берет последний элемент, удаляет его из массива и возвращает нам.

    Если вы вызовете draw_card() пять раз в начале игры, массив deck уменьшится на 5 элементов, массив hand увеличится на 5, а на экране в вашем HBoxContainer плавно появятся 5 визуальных карт.

    Розыгрыш карты и Сброс (Discard)

    Когда игрок перетаскивает карту на стол и отпускает кнопку мыши (механика Drag & Drop из прошлой статьи), карта считается разыгранной. Теперь нам нужно провести обратный процесс: убрать данные из руки, поместить их в сброс и уничтожить визуальный узел.

    В Godot для безопасного удаления узлов используется метод queue_free().

    Почему не просто free()? Метод free() уничтожает объект мгновенно. Если в этот момент движок обрабатывал какой-то сигнал от этой карты или физическое столкновение, игра вылетит с ошибкой. Метод queue_free() ставит узел в очередь на удаление и безопасно стирает его из памяти в конце текущего кадра, когда все процессы завершены.

    Цикличность колоды (Reshuffle)

    Во многих карточных играх (особенно в жанре Deckbuilder, таких как Slay the Spire) игра не заканчивается, когда пустеет колода. Вместо этого стопка сброса перемешивается и становится новой колодой.

    Реализовать это с нашей архитектурой массивов невероятно просто. Нам не нужно перемещать десятки узлов по экрану. Мы просто копируем массив сброса в массив колоды, очищаем сброс и вызываем shuffle().

    Эту функцию можно вызывать внутри draw_card(). Если deck.is_empty() возвращает true, мы сначала вызываем reshuffle_discard_into_deck(), а затем снова пытаемся вытянуть карту.

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

    5. Игровой цикл и система ходов

    Игровой цикл и система ходов

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

    Чтобы набор механик превратился в игру, нам нужны правила. А чтобы движок понимал эти правила, нам необходимо выстроить систему ходов (Turn System) и управлять игровым циклом (Game Loop). В этой статье мы разберем, как правильно организовать пошаговую логику в Godot, избежав распространенных ошибок новичков.

    Особенности игрового цикла в карточных играх

    В экшен-играх (шутерах, платформерах) игровой цикл непрерывен. Движок 60 раз в секунду опрашивает клавиатуру, двигает персонажей и проверяет столкновения. В Godot за это отвечает встроенная функция _process(delta).

    Однако карточные игры имеют асинхронную, событийно-ориентированную природу. Игра может минутами находиться в состоянии покоя, пока игрок обдумывает свой ход. Если мы попытаемся написать логику карточной игры внутри функции _process, наш код быстро превратится в запутанный клубок из сотен проверок if/else.

    Для наглядности сравним два подхода:

    | Характеристика | Непрерывный цикл (Экшен) | Событийный цикл (Карточная игра) | | :--- | :--- | :--- | | Частота обновлений | Каждый кадр (60+ раз в секунду) | Только при наступлении события (клик, конец таймера) | | Главный инструмент | Функция _process(delta) | Сигналы (Signals) и пользовательские функции | | Ожидание | Недопустимо (игра зависнет) | Норма (ожидание решения игрока) |

    Для реализации событийного цикла в программировании используется специальный архитектурный паттерн — Конечный автомат (Finite State Machine, FSM).

    Конечный автомат: Светофор вашей игры

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

    Самый простой бытовой пример конечного автомата — обычный светофор. Он может быть красным, желтым или зеленым. Он не может быть красным и зеленым одновременно. Переход от одного цвета к другому происходит по строгим правилам (после зеленого всегда идет желтый, а не красный).

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

    !Схема конечного автомата для карточной игры

    Реализация состояний через Enum

    В Godot (и языке GDScript) идеальным инструментом для создания списка состояний является перечисление — enum. Давайте создадим новый скрипт TurnManager.gd, который будет управлять всей пошаговой логикой, и добавим его как отдельный узел на сцену.

    Использование enum делает код читаемым. Вместо того чтобы запоминать, что цифра 1 — это ход игрока, а 2 — ход врага, мы используем понятные текстовые названия.

    Управление фазами хода

    Теперь нам нужна функция, которая будет переключать эти состояния и запускать соответствующие процессы. В GDScript для обработки множества вариантов отлично подходит оператор match (аналог switch в других языках).

    Начало хода игрока

    Что происходит, когда начинается ход игрока? Обычно игра должна выдать новую карту из колоды и восстановить ресурсы (например, ману или очки действий).

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

    Где: * — мана, которую игрок получит в этом ходу. * — абсолютный максимум маны в игре (например, 10). * — стартовая мана на первом ходу (например, 1). * — номер текущего хода. * Функция выбирает наименьшее из двух значений, не давая мане превысить лимит.

    Реализуем базовую логику старта хода:

    Интеграция с механикой Drag & Drop

    Теперь самое важное: мы должны связать наш TurnManager с картами. В прошлой статье мы написали логику розыгрыша карты при её сбросе на стол. Теперь мы обязаны добавить проверку состояния.

    В скрипте карты (или менеджера карт) перед тем, как разыграть карту, мы спрашиваем у конечного автомата разрешение:

    Это простое условие if гарантирует, что игрок не сможет жульничать и кидать карты на стол, пока враг совершает свои действия.

    Ход противника и искусственные паузы

    Когда игрок нажимает кнопку «Завершить ход» (которая просто вызывает TurnManager.change_state(GameState.ENEMY_TURN)), управление переходит к противнику.

    Здесь кроется неочевидная проблема. Компьютер думает мгновенно. Если мы напишем код, где враг берет карту, наносит урон и передает ход обратно, всё это произойдет за 1 миллисекунду. Игрок даже не поймет, что произошло, он просто увидит, как у него внезапно уменьшилось здоровье.

    Чтобы игра ощущалась естественной, нам нужно добавить искусственные паузы. В Godot для этого идеально подходит связка ключевого слова await и узла Timer (или создания таймера прямо в коде).

    Ключевое слово await заставляет функцию приостановить свое выполнение до тех пор, пока не произойдет указанное событие, при этом не замораживая всю остальную игру (анимации продолжат работать).

    Благодаря await, мы создаем комфортный темп игры. Игрок видит, что ход перешел к противнику, видит небольшую паузу, затем видит действие врага, и только потом ход возвращается к нему.

    Связь логики и интерфейса

    Обратите внимание на сигнал turn_changed, который мы добавили в функцию change_state. Это пример хорошей архитектуры.

    Наш TurnManager не должен знать о том, как выглядит кнопка «Завершить ход» или где находится текст с надписью «Ход противника». Он просто кричит в пустоту: «Состояние изменилось!».

    Узлы пользовательского интерфейса (UI) подписываются на этот сигнал. Когда кнопка «Завершить ход» слышит, что наступил ENEMY_TURN, она сама себя блокирует (свойство disabled = true), чтобы игрок не мог нажать её дважды. Когда наступает PLAYER_TURN, она снова становится активной.

    Такой подход называется слабой связностью (Loose Coupling). Он позволяет вам удалять, изменять и перерисовывать интерфейс, не ломая при этом базовую логику игры.

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