Разработка гибридной игры: Tower Defense и Блэкджек на Godot

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

Введение в Godot: Настройка проекта и создание базового интерфейса стола

Введение в Godot: Настройка проекта и создание базового интерфейса стола

Добро пожаловать в курс по разработке гибридной игры! Наша цель — создать уникальный проект, объединяющий стратегическую глубину Tower Defense (защита башни) и азартную механику Блэкджека (21). В этой игре карты будут служить ресурсами для постройки и улучшения башен, а удача при раздаче будет влиять на вашу способность сдерживать волны врагов.

В этой первой статье мы познакомимся с игровым движком Godot, настроим проект и создадим визуальную основу нашей игры — игровой стол.

Почему Godot?

Godot Engine — это бесплатный движок с открытым исходным кодом, который идеально подходит для 2D-игр. Его главная особенность — узловая система (Node system). В Godot всё является узлом: персонаж, звук, кнопка интерфейса и даже сам игровой уровень. Это делает структуру проекта логичной и гибкой.

Основные концепции

Перед тем как открыть редактор, важно усвоить два термина:

  1. Узел (Node): Базовый строительный блок. У каждого узла есть имя, свойства и способность выполнять скрипты.
  2. Сцена (Scene): Группа узлов, организованная в дерево. Сцена может быть персонажем, меню или целым уровнем. Сцены можно вкладывать друг в друга.

Шаг 1: Создание проекта

Запустите Godot. В окне менеджера проектов нажмите кнопку New Project (Новый проект).

  1. Project Name: Назовите проект, например, BlackjackTD.
  2. Project Path: Выберите пустую папку на вашем диске.
  3. Renderer: Для 2D-игры выберите Compatibility (ранее OpenGL ES 3). Это обеспечит максимальную совместимость со старыми устройствами и веб-браузерами.
  4. Нажмите Create & Edit.

Шаг 2: Знакомство с интерфейсом

Когда редактор откроется, вы увидите рабочее пространство, разделенное на несколько зон:

  • Scene Tree (Дерево сцены): Слева (или справа, в зависимости от настроек). Здесь отображается иерархия узлов текущей сцены.
  • FileSystem (Файловая система): Внизу слева. Здесь хранятся все ресурсы: скрипты, картинки, звуки.
  • Viewport (Окно просмотра): Большая область в центре. Здесь вы визуально собираете игру.
  • Inspector (Инспектор): Справа. Здесь настраиваются свойства выбранного узла (позиция, цвет, текстура).

Шаг 3: Настройка базовых параметров

Наша игра будет сочетать карточное поле и карту для защиты башен. Нам нужно достаточно места на экране.

  1. В верхнем меню выберите Project -> Project Settings.
  2. Перейдите в раздел Display -> Window.
  3. Установите Viewport Width (Ширина) на 1920.
  4. Установите Viewport Height (Высота) на 1080.
  5. В разделе Stretch установите Mode на canvas_items. Это позволит игре корректно масштабироваться при изменении размера окна.

Шаг 4: Создание игрового стола

Теперь создадим нашу первую сцену. Это будет основной экран игры, где происходит всё действие.

Корневой узел

  1. В панели Scene нажмите кнопку 2D Scene. Это создаст корневой узел типа Node2D.
  2. Дважды кликните по имени узла и переименуйте его в MainTable.
  3. Нажмите Ctrl + S (или Cmd + S на Mac), чтобы сохранить сцену. Создайте папку Scenes и сохраните файл как MainTable.tscn.

Совет: Хорошая организация файлов критически важна. Сразу создайте папки Scripts, Assets и Scenes в панели FileSystem, чтобы не запутаться в будущем.

Добавление фона

Так как у нас есть элементы казино (Блэкджек), сделаем классический зеленый суконный стол.

  1. Кликните правой кнопкой мыши на узел MainTable -> Add Child Node (Добавить дочерний узел).
  2. В поиске введите ColorRect и выберите его.
  3. Выделите новый узел ColorRect.
  4. В Инспекторе найдите свойство Color и выберите темно-зеленый цвет (например, Hex: #2d5a27).
  5. Чтобы фон покрывал весь экран, в верхней панели вьюпорта нажмите на иконку Anchors (Якоря) и выберите Full Rect (Полный прямоугольник).

Теперь у нас есть зеленый стол, готовый к размещению карт и башен.

Шаг 5: Создание зон интерфейса (UI)

В Godot для интерфейса используются узлы зеленого цвета, которые наследуются от класса Control. Они умеют автоматически выравниваться и менять размер.

Слой интерфейса

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

  1. Добавьте к MainTable дочерний узел CanvasLayer. Назовите его HUD (Heads-Up Display).
  2. Все элементы интерфейса мы будем добавлять внутрь HUD.

Зона руки игрока

Нам нужно место, где будут лежать карты игрока. Для этого идеально подойдет контейнер, который автоматически выстраивает элементы в ряд.

  1. Нажмите правой кнопкой на HUD -> Add Child Node.
  2. Выберите HBoxContainer (Horizontal Box Container). Назовите его PlayerHand.
  3. Настроим его положение:
    • Выберите PlayerHand.
    • В меню Anchors выберите Bottom Center (Внизу по центру).
    • В Инспекторе измените Custom Minimum Size (Минимальный размер), задав y (высоту) равной 200. Это зарезервирует место под карты.
    • Сместите его немного вверх, изменив Position Y или настроив Margin Bottom.

Зона колоды и дилера

Аналогично создадим место для колоды.

  1. Добавьте к HUD узел Control и назовите его DeckPosition.
  2. Разместите его в правом нижнем углу или справа по центру, используя Anchors.

Теперь у нас есть структура:

  • MainTable (Node2D) — мир игры.
    • ColorRect — фон.
    • HUD (CanvasLayer) — слой интерфейса.
      • PlayerHand (HBoxContainer) — место для карт.
      • DeckPosition (Control) — место для колоды.

Шаг 6: Первый скрипт

Чтобы оживить игру, нам нужен код. В Godot используется язык GDScript, который очень похож на Python.

  1. Выберите корневой узел MainTable.
  2. Нажмите иконку свитка с зеленым плюсом (Attach Script) над деревом сцен.
  3. Убедитесь, что путь сохранения ведет в папку res://Scripts/ и имя файла MainTable.gd.
  4. Нажмите Create.

Откроется редактор скриптов. Вы увидите базовый шаблон:

extends Node2D

# Called when the node enters the scene tree for the first time.
func _ready():
    pass # Replace with function body.

# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
    pass

Давайте разберем, что здесь написано:

  • extends Node2D: Означает, что наш скрипт наследует все возможности стандартного 2D-узла.
  • func _ready(): Эта функция вызывается один раз, когда сцена полностью загрузилась и появилась в игре. Это идеальное место для начальной настройки (например, перетасовать колоду).
  • func _process(delta): Эта функция вызывается каждый кадр (обычно 60 раз в секунду). delta — это время, прошедшее с прошлого кадра. Здесь пишут логику движения и обновлений.

Для проверки работоспособности изменим функцию _ready:

func _ready():
    print("Добро пожаловать в Blackjack Tower Defense!")

Запустите игру, нажав кнопку Play (треугольник вверху справа) или клавишу F5. При первом запуске Godot спросит, какую сцену считать главной — выберите Select Current. В нижней панели Output вы увидите наше приветственное сообщение.

Заключение

Поздравляю! Вы сделали первый шаг к созданию гибридной игры. Мы настроили проект, разобрались с системой узлов Godot, создали визуальную основу стола и написали первый скрипт.

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

Реализация механики Блэкджека: скрипты колоды, логика дилера и подсчет очков

Реализация механики Блэкджека: скрипты колоды, логика дилера и подсчет очков

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

В этой статье мы займемся «мозгами» нашего Блэкджека. Мы создадим объект карты, научимся генерировать колоду из 52 карт, перемешивать её и раздавать игроку, а также напишем алгоритм подсчета очков с учетом хитрой логики Тузов.

Шаг 1: Создание сцены Карты

В Godot, чтобы отобразить множество одинаковых объектов (карт), мы создаем одну Сцену (шаблон) и затем создаем её копии (экземпляры) через код.

Визуальная часть

  1. Создайте новую сцену (нажмите + рядом с вкладкой MainTable).
  2. В качестве корневого узла выберите Panel. Это узел интерфейса, который имеет фон и рамку. Назовите его Card.
  3. В Инспекторе настройте размер:
    • Layout -> Transform -> Size: x: 100, y: 140 (стандартные пропорции карты).
    • Custom Minimum Size: также установите 100 на 140. Это важно для того, чтобы контейнеры (HBoxContainer) не сжимали карту до нуля.
  4. Добавьте внутрь узла Card дочерний узел Label (для отображения цифры/буквы).
  5. Настройте Label:
    • Напишите в поле Text что-то для теста, например, "A".
    • В Layout -> Anchors Preset выберите Center (по центру).
    • В Theme Overrides -> Font Sizes увеличьте размер шрифта до 40.

Скрипт Карты

Теперь карте нужны данные. Она должна знать, какая она: «Туз Пик» или «Десятка Червей».

  1. Прикрепите скрипт к корневому узлу Card. Сохраните его как res://Scripts/Card.gd.
  2. Напишите следующий код:
extends Panel

# Свойства карты
var suit = ""       # Масть (Hearts, Diamonds, etc.)
var rank = ""       # Ранг (2, 3, ..., 10, J, Q, K, A)
var value = 0       # Числовое значение для очков

# Ссылки на узлы интерфейса
@onready var label = $Label

func _ready():
	# Обновляем вид при появлении, если данные уже заданы
	update_visuals()

# Функция для настройки карты извне
func set_card_data(new_suit, new_rank, new_value):
	suit = new_suit
	rank = str(new_rank)
	value = new_value
	update_visuals()

func update_visuals():
	# Если узел Label еще не готов, выходим
	if label == null:
		return

	# Отображаем ранг на карте
	label.text = str(rank)

	# Красим красные масти
	if suit in ["Hearts", "Diamonds"]:
		label.modulate = Color(1, 0, 0) # Красный
	else:
		label.modulate = Color(1, 1, 1) # Белый

Сохраните сцену как res://Scenes/Card.tscn.

Шаг 2: Логика колоды в MainTable

Вернемся к нашему главному скрипту MainTable.gd. Здесь будет происходить управление игрой.

Нам нужно:

  1. Создать список всех возможных карт.
  2. Перемешать их.
  3. Хранить текущую колоду.

Откройте MainTable.gd и добавьте переменные:

extends Node2D

# Предзагружаем сцену карты, чтобы создавать её копии
var card_scene = preload("res://Scenes/Card.tscn")

# Данные для генерации колоды
var suits = ["Hearts", "Diamonds", "Clubs", "Spades"]
var ranks = [2, 3, 4, 5, 6, 7, 8, 9, 10, "J", "Q", "K", "A"]

# Сама колода (массив словарей)
var deck = []

# Ссылки на контейнеры (пути могут отличаться, проверьте свое дерево сцен!)
@onready var player_hand_container = $HUD/PlayerHand

Генерация колоды

Добавим функцию, которая наполняет массив deck. В Блэкджеке:

  • Карты 2-10 имеют номинал, равный рангу.
  • Картинки (J, Q, K) стоят 10 очков.
  • Туз (A) стоит 11 очков (логику превращения в 1 добавим позже).
func build_deck():
	deck.clear()
	for s in suits:
		for r in ranks:
			var val = 0

			# Определяем стоимость карты
			if typeof(r) == TYPE_INT:
				val = r
			elif r in ["J", "Q", "K"]:
				val = 10
			elif r == "A":
				val = 11

			# Добавляем описание карты в колоду
			deck.append({
				"suit": s,
				"rank": r,
				"value": val
			})

	# Перемешиваем колоду
	deck.shuffle()

Важно: Функция shuffle() использует генератор случайных чисел. Чтобы при каждом запуске игры порядок был разным, в функции _ready() нужно вызвать randomize() (в Godot 3) или ничего не делать в Godot 4 (там это происходит автоматически), но для надежности можно использовать RandomNumberGenerator.

Шаг 3: Раздача карт

Теперь самое интересное — вытащить карту из виртуального массива и положить её на стол.

Добавим функцию deal_card:

func deal_card(target_container):
	# Проверяем, остались ли карты
	if deck.size() == 0:
		build_deck() # Если кончились, создаем новую колоду

	# Берем последнюю карту из массива (и удаляем её оттуда)
	var card_data = deck.pop_back()

	# Создаем визуальный экземпляр карты
	var new_card = card_scene.instantiate()

	# Передаем данные в скрипт карты
	new_card.set_card_data(card_data["suit"], card_data["rank"], card_data["value"])

	# Добавляем карту в контейнер на экране
	target_container.add_child(new_card)

Обновим _ready(), чтобы проверить работу:

func _ready():
	build_deck()
	# Раздадим игроку две карты
	deal_card(player_hand_container)
	deal_card(player_hand_container)

	calculate_score(player_hand_container)

Запустите игру. Вы должны увидеть две карты внизу экрана. Если вы перезапустите игру, карты должны быть другими.

Шаг 4: Подсчет очков и логика Туза

Главная сложность Блэкджека — это Туз. Он может давать 11 очков или 1 очко, если сумма перевалила за 21. Это называется «мягкая» и «жесткая» рука.

Алгоритм подсчета следующий:

  1. Считаем сумму всех карт, считая Туз всегда за 11.
  2. Считаем количество Тузов в руке.
  3. Если сумма больше 21 и у нас есть Тузы, вычитаем 10 из суммы и «сжигаем» один Туз (считаем его за единицу).
  4. Повторяем пункт 3, пока сумма > 21 или пока есть Тузы.

Реализуем это в MainTable.gd:

func calculate_score(hand_container):
	var total_score = 0
	var ace_count = 0

	# Проходимся по всем детям контейнера (картам)
	for card in hand_container.get_children():
		# Убедимся, что это действительно карта и у нее есть свойство value
		if "value" in card:
			total_score += card.value
			if card.rank == "A":
				ace_count += 1

	# Корректировка для Тузов
	while total_score > 21 and ace_count > 0:
		total_score -= 10
		ace_count -= 1

	print("Текущий счет: ", total_score)
	return total_score

Разбор алгоритма

Представьте, что у вас рука: Туз, Туз, 9.

  1. Изначальный подсчет: 11+11+9=3111 + 11 + 9 = 31.
  2. Количество тузов: 2.
  3. 31>2131 > 21, вычитаем 10. Счет стал 2121. Тузов осталось 1.
  4. 2121 не больше 2121. Цикл останавливается.

Результат: 21 очко. Это идеальный Блэкджек (или просто 21), где один туз сыграл как 11, а второй как 1.

Шаг 5: Добавление кнопок управления

Чтобы играть, нам нужны кнопки «Взять» (Hit) и «Хватит» (Stand).

  1. В сцене MainTable, внутри HUD, добавьте VBoxContainer и назовите его Actions.
  2. Разместите его справа внизу (над колодой).
  3. Внутри Actions добавьте две кнопки Button:
    • Назовите первую HitButton, текст: "Взять карту".
    • Назовите вторую StandButton, текст: "Хватит".

Подключение сигналов

Теперь нужно связать нажатия кнопок с кодом.

  1. Выберите HitButton.
  2. В панели Node (справа, вкладка рядом с Inspector) дважды кликните на сигнал pressed().
  3. Подтвердите подключение к скрипту MainTable.
  4. Повторите то же самое для StandButton.

В скрипте появятся две функции. Допишем логику:

func _on_hit_button_pressed():
	deal_card(player_hand_container)
	var score = calculate_score(player_hand_container)
	if score > 21:
		print("Перебор! Вы проиграли.")
		# Здесь позже добавим логику завершения раунда

func _on_stand_button_pressed():
	var score = calculate_score(player_hand_container)
	print("Вы остановились на: ", score)
	# Здесь будет ход дилера (в следующей статье)

Заключение

Мы проделали огромную работу! Теперь у нас есть:

  • Умная колода, которая знает все 52 карты.
  • Система раздачи, создающая карты на лету.
  • Математика Блэкджека, корректно обрабатывающая сложные ситуации с Тузами.

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

Готовьтесь, скоро начнется настоящая битва!

Основы Tower Defense: TileMap, навигация врагов и система волн

Основы Tower Defense: TileMap, навигация врагов и система волн

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

Сегодня мы переходим ко второй половине игры — Tower Defense (защита башни). Мы создадим игровое поле, проложим путь для монстров и научим их наступать волнами.

Шаг 1: Создание карты с помощью TileMap

В стратегических играх редко используют отдельные картинки для каждого кустика или участка дороги. Это неэффективно для памяти компьютера. Вместо этого используется технология TileMap (Карта плиток).

Представьте мозаику. У вас есть набор маленьких квадратных картинок (тайлов): трава, дорога, поворот дороги, вода. Из этих кусочков вы собираете огромный уровень. В Godot узел TileMap (или TileMapLayer в новейших версиях) обрабатывает это автоматически, оптимизируя отрисовку.

Настройка TileSet

  1. Откройте сцену MainTable. Мы будем строить карту прямо на столе, но под интерфейсом.
  2. Добавьте узел TileMap как дочерний к MainTable. Убедитесь, что он находится в дереве сцен выше, чем HUD, чтобы интерфейс перекрывал карту, а не наоборот.
  3. В Инспекторе найдите свойство Tile Set и выберите New TileSet.
  4. Внизу откроется панель TileSet. Сюда нужно перетащить изображение с вашими тайлами (атлас).

Совет: Для прототипа можно использовать простые цветные квадраты 64x64 пикселя, нарисованные в Paint. Один зеленый (трава), один серый (дорога).

  1. Настройте Tile Size (размер тайла) в свойствах TileSet, например, 64x64 пикселя.
  2. Перейдите на вкладку TileMap (рядом с TileSet в нижней панели), выберите кисть и нарисуйте извилистую дорогу от левого края экрана к правому.

Шаг 2: Прокладывание пути (Path2D)

Враги должны знать, куда идти. В жанре Tower Defense враги обычно не ищут путь динамически (как в RTS), а следуют по заранее заданной траектории.

Для этого в Godot существует идеальная связка узлов: Path2D (Путь) и PathFollow2D (Следование по пути).

  1. Добавьте узел Path2D к MainTable. Назовите его EnemyPath.
  2. В верхней панели инструментов появятся кнопки управления кривой (зеленая линия с точками).
  3. Выберите инструмент Add Point (Добавить точку) и прокликайте центр вашей нарисованной дороги от начала до конца.

Теперь у нас есть невидимая рельса, по которой поедут наши враги.

Шаг 3: Создание врага

Враг в нашей игре — это не самостоятельный объект, который «ходит» ногами. Технически, это вагончик, который едет по рельсам Path2D.

Сцена врага

  1. Создайте новую сцену.
  2. Корневым узлом выберите PathFollow2D. Это критически важно! Именно этот узел умеет прилипать к Path2D.
  3. Переименуйте его в Enemy.
  4. Добавьте дочерний узел Sprite2D (или AnimatedSprite2D) для визуализации врага. Загрузите текстуру (например, красного круга или монстра).
  5. Добавьте Area2D с CollisionShape2D внутри. Это нужно, чтобы в будущем пули башен могли попадать во врага. Настройте форму коллизии под размер спрайта.

Скрипт движения

Прикрепите скрипт к корневому узлу Enemy (PathFollow2D).

Чтобы враг двигался, нам нужно менять его свойство progress (прогресс). Это свойство определяет, сколько пикселей прошел объект от начала пути.

Математика движения описывается простой формулой:

S=v⋅tS = v \cdot t

Где SS — пройденное расстояние, vv — скорость движения, а tt — время.

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

extends PathFollow2D

# Скорость в пикселях в секунду
@export var speed = 150

# Здоровье врага
var hp = 10

func _process(delta):
	# delta - это время, прошедшее с прошлого кадра (часть секунды)
	# Увеличиваем прогресс вдоль пути
	progress += speed * delta

	# Если дошли до конца пути (ratio - это прогресс от 0.0 до 1.0)
	if progress_ratio >= 1.0:
		reach_end()

func reach_end():
	print("Враг прорвался!")
	# Здесь мы будем отнимать жизни у игрока
	queue_free() # Удаляем врага

func take_damage(amount):
	hp -= amount
	if hp <= 0:
		die()

func die():
	# Здесь можно начислить деньги/очки
	queue_free()

Сохраните сцену как res://Scenes/Enemy.tscn.

Шаг 4: Система волн (Spawner)

Теперь нам нужно создавать этих врагов на уровне. Этим займется скрипт в MainTable.

Подготовка

Вернитесь в скрипт MainTable.gd. Нам нужно:

  1. Ссылка на сцену врага.
  2. Ссылка на путь EnemyPath.
  3. Таймер для запуска волн.

Добавьте в начало скрипта:

@onready var enemy_path = $EnemyPath
var enemy_scene = preload("res://Scenes/Enemy.tscn")

# Настройки волны
var enemies_to_spawn = 0
var spawn_timer = 0.0
var spawn_interval = 1.0 # Секунд между врагами

Логика спавна

В функции _process мы будем отсчитывать время и создавать врагов.

func _process(delta):
	# Если нужно кого-то заспавнить
	if enemies_to_spawn > 0:
		spawn_timer -= delta
		if spawn_timer <= 0:
			spawn_enemy()
			spawn_timer = spawn_interval

func spawn_enemy():
	# Создаем экземпляр врага
	var new_enemy = enemy_scene.instantiate()

	# ВАЖНО: Добавляем врага как ребенка EnemyPath
	# PathFollow2D работает только внутри Path2D
	enemy_path.add_child(new_enemy)

	# Уменьшаем счетчик
	enemies_to_spawn -= 1

Запуск волны

Теперь создадим функцию, которая начинает волну. Мы можем вызывать её, например, когда игрок нажимает кнопку "Start Wave" (которую вы можете добавить в интерфейс по аналогии с кнопками Блэкджека).

func start_wave(count):
	print("Волна пошла! Врагов: ", count)
	enemies_to_spawn = count
	spawn_timer = 0 # Спавним первого сразу

Для теста добавьте вызов start_wave(5) в конце функции _ready().

Как это работает вместе?

  1. TileMap создает визуальный коридор.
  2. Path2D задает математическую кривую поверх этого коридора.
  3. Spawner создает экземпляры сцены Enemy.
  4. Так как Enemy является узлом PathFollow2D и добавляется внутрь Path2D, движок Godot автоматически привязывает его координаты к кривой.
  5. В скрипте врага мы меняем progress, и враг скользит по рельсам к финишу.

Заключение

Мы создали классическую механику движения для Tower Defense. Теперь по вашему столу могут бежать враги, а карты лежат в руке. Чего не хватает? Взаимодействия!

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

Синтез жанров: покупка и улучшение башен за счет побед в картах

Синтез жанров: покупка и улучшение башен за счет побед в картах

Мы подошли к самому захватывающему этапу разработки. У нас есть две независимые системы: карточный стол, где можно собрать 21 очко, и поле с дорогой, по которой бегут враги. Но пока они существуют в параллельных вселенных. Игрок может выиграть миллион в карты, но монстры все равно пройдут к финишу.

В этой статье мы объединим эти миры. Мы превратим очки Блэкджека в Золото, реализуем логику Дилера, создадим боевые Башни и научимся строить их на поле, используя заработанные ресурсы.

Шаг 1: Завершение цикла Блэкджека

В прошлой статье мы остановились на кнопке «Хватит» (Stand). Чтобы получить награду, нам нужно определить победителя. Для этого необходимо реализовать ход Дилера.

Логика Дилера

В классическом Блэкджеке правила дилера строги: он обязан брать карты, пока сумма его очков меньше 17. Как только сумма достигает 17 или больше, он обязан остановиться.

Добавим эту логику в MainTable.gd. Нам понадобится контейнер для карт дилера (создайте HBoxContainer с именем DealerHand в HUD, аналогично руке игрока).

# MainTable.gd

@onready var dealer_hand_container = $HUD/DealerHand
var gold = 100 # Стартовый капитал

func _on_stand_button_pressed():
    # Блокируем кнопки игрока, чтобы он не мог нажимать их во время хода дилера
    $HUD/Actions/HitButton.disabled = true
    $HUD/Actions/StandButton.disabled = true

    play_dealer_turn()

func play_dealer_turn():
    var dealer_score = calculate_score(dealer_hand_container)

    # Дилер берет карты, пока у него меньше 17
    while dealer_score < 17:
        deal_card(dealer_hand_container)
        dealer_score = calculate_score(dealer_hand_container)
        # В реальной игре здесь нужна пауза (await), чтобы видеть процесс
        await get_tree().create_timer(1.0).timeout

    finalize_round()

Определение победителя и награда

Теперь сравним очки и выдадим золото. Золото — это наш мост между картами и защитой башен.

func finalize_round():
    var player_score = calculate_score(player_hand_container)
    var dealer_score = calculate_score(dealer_hand_container)

    var winnings = 0

    if player_score > 21:
        print("Перебор игрока. Вы проиграли.")
    elif dealer_score > 21:
        print("Перебор у дилера! Вы выиграли!")
        winnings = 50
    elif player_score > dealer_score:
        print("Победа игрока!")
        winnings = 50
        # Бонус за Блэкджек (ровно 21)
        if player_score == 21:
            winnings += 25
    elif player_score == dealer_score:
        print("Ничья. Ставки возвращаются.")
    else:
        print("Победа дилера.")

    add_gold(winnings)
    reset_game() # Функция очистки стола для нового раунда

func add_gold(amount):
    gold += amount
    # Не забудьте обновить Label с золотом в интерфейсе
    $HUD/GoldLabel.text = "Gold: " + str(gold)

Теперь каждая победа в карты дает ресурсы для войны.

Шаг 2: Создание Башни

Башня — это стационарный объект, который обнаруживает врагов и стреляет в них. В Godot для обнаружения идеально подходит узел Area2D.

Сцена Башни

  1. Создайте новую сцену с корневым узлом Node2D (или StaticBody2D). Назовите её Tower.
  2. Добавьте Sprite2D с текстурой башни (вид сверху).
  3. Добавьте Area2D и назовите его DetectionZone.
  4. Внутри DetectionZone добавьте CollisionShape2D. Выберите форму CircleShape2D (круг). Радиус круга — это дальность стрельбы башни (например, 150 пикселей).
  5. Добавьте узел Timer (назовите ReloadTimer). Установите Wait Time на 1.0 (один выстрел в секунду) и поставьте галочку Autostart.

Скрипт Башни

Башня должна выбирать цель. Самая простая логика: стрелять в того, кто ближе всего к финишу (имеет наибольший progress на пути).

extends Node2D

var bullet_scene = preload("res://Scenes/Bullet.tscn") # Сделаем пулю позже
var current_target = null

@onready var detection_zone = $DetectionZone

func _physics_process(delta):
    update_target()
    if current_target != null:
        # Поворачиваем башню в сторону врага
        look_at(current_target.global_position)

func update_target():
    # Получаем список всех тел, вошедших в радиус Area2D
    var enemies_in_range = detection_zone.get_overlapping_areas()

    if enemies_in_range.size() > 0:
        # Выбираем первого попавшегося или реализуем логику выбора
        # Враг - это Area2D внутри PathFollow2D, нам нужен родитель
        current_target = enemies_in_range[0].get_parent()
    else:
        current_target = null

func _on_reload_timer_timeout():
    if current_target != null:
        shoot()

func shoot():
    var bullet = bullet_scene.instantiate()
    bullet.global_position = global_position
    bullet.target = current_target
    get_tree().root.add_child(bullet)

Важно: Чтобы Area2D башни видела врага, у врага тоже должен быть Area2D. Убедитесь, что слои коллизий (Collision Layers) настроены так, чтобы башня «видела» слой врагов.

Шаг 3: Создание Пули

Пуля — это простой объект, который летит к цели и наносит урон при контакте.

  1. Создайте сцену Bullet (Area2D).
  2. Добавьте спрайт и коллизию.
  3. Скрипт пули:

Для расчета движения мы используем векторную математику. Вектор направления рассчитывается как разность позиций цели и пули.

Формула вектора направления:

V⃗=Ptarget−Pbullet\vec{V} = P_{target} - P_{bullet}

Где V⃗\vec{V} — искомый вектор, PtargetP_{target} — позиция цели, а PbulletP_{bullet} — позиция пули.

extends Area2D

var target = null
var speed = 400
var damage = 2

func _process(delta):
    if target == null or not is_instance_valid(target):
        queue_free() # Если цель исчезла, удаляем пулю
        return

    # Движение к цели
    var direction = (target.global_position - global_position).normalized()
    position += direction * speed * delta

    # Проверка попадания (можно также использовать сигнал body_entered)
    if global_position.distance_to(target.global_position) < 10:
        hit_target()

func hit_target():
    if target.has_method("take_damage"):
        target.take_damage(damage)
    queue_free()

Шаг 4: Механика строительства

Теперь самое интересное. Как поставить башню на карту? Мы будем использовать сетку TileMap для выравнивания.

Режим строительства

В MainTable.gd добавим переменную состояния.

var is_building_mode = false
var tower_cost = 50
var tower_scene = preload("res://Scenes/Tower.tscn")

# Привяжите эту функцию к кнопке "Купить башню" в интерфейсе
func _on_buy_tower_pressed():
    if gold >= tower_cost:
        is_building_mode = true
        # Можно изменить курсор или показать "призрак" башни

Размещение по клику

Godot позволяет обрабатывать ввод мыши через функцию _unhandled_input. Мы будем проверять, куда кликнул игрок, переводить координаты экрана в координаты сетки (тайлов) и ставить башню по центру клетки.

Координаты мыши (x,y)(x, y) — это пиксели на экране. Нам нужно получить индексы клетки (i,j)(i, j) в сетке TileMap.

@onready var tile_map = $TileMap # Ссылка на ваш TileMap

func _unhandled_input(event):
    if is_building_mode and event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
        place_tower(event.position)

func place_tower(mouse_pos):
    # 1. Переводим координаты мыши в координаты тайловой сетки
    var local_pos = tile_map.to_local(mouse_pos)
    var map_pos = tile_map.local_to_map(local_pos)

    # 2. Проверяем, можно ли здесь строить
    # Например, проверяем ID тайла (чтобы это была трава, а не дорога)
    # В Godot 4 используется get_cell_source_id
    var tile_id = tile_map.get_cell_source_id(0, map_pos)

    # Предположим, что ID 0 - это трава, а 1 - дорога. Строим только на траве.
    if tile_id == 0:
        # 3. Создаем башню
        var new_tower = tower_scene.instantiate()

        # 4. Ставим башню в центр клетки
        # map_to_local возвращает центр клетки в пикселях
        new_tower.position = tile_map.map_to_local(map_pos)

        add_child(new_tower)

        # 5. Списываем деньги и выходим из режима строительства
        add_gold(-tower_cost)
        is_building_mode = false
    else:
        print("Здесь строить нельзя!")

Шаг 5: Баланс и игровой цикл

Теперь у нас замкнутый игровой цикл (Core Loop):

  1. Фаза карт: Игрок играет в Блэкджек против дилера.
  2. Награда: Победа приносит золото.
  3. Фаза строительства: Игрок тратит золото на покупку башен.
  4. Фаза защиты: Игрок запускает волну врагов (start_wave).
  5. Результат: Башни уничтожают врагов. Если враги проходят — игрок теряет жизни.

Математика урона

Чтобы игра была интересной, нужно настроить баланс. Урон в секунду (DPS) башни рассчитывается так:

DPS=DTreloadDPS = \frac{D}{T_{reload}}

Где DPSDPS — урон в секунду, DD — урон одного выстрела, а TreloadT_{reload} — время перезарядки.

Например, если урон пули 22, а перезарядка 1.01.0 сек, то DPS=2DPS = 2. Если у врага 1010 жизней, башне нужно 5 секунд непрерывной стрельбы, чтобы его убить. Зная скорость врага и длину пути в зоне действия башни, вы можете рассчитать, успеет ли одна башня убить врага или нужно ставить две.

Заключение

Поздравляю! Вы создали прототип гибридной игры. Теперь ваши навыки игры в карты напрямую влияют на вашу обороноспособность. Плохая раздача может оставить вас без денег перед сложной волной, а удачный Блэкджек позволит построить непробиваемую оборону.

В следующей, заключительной статье курса, мы займемся «полировкой»: добавим визуальные эффекты, звуки, главное меню и экран Game Over, чтобы превратить этот прототип в законченный продукт.

Полировка геймплея: визуальные эффекты, баланс и экспорт готовой игры

Полировка геймплея: визуальные эффекты, баланс и экспорт готовой игры

Поздравляю! Если вы читаете эту статью, значит, у вас уже есть работающий прототип уникальной игры, объединяющей Блэкджек и Tower Defense. У вас есть карты, башни, враги и экономика. Но будем честны: пока игра выглядит немного «сухой». Враги исчезают без следа, выстрелы беззвучны, а сложность не меняется.

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

Шаг 1: Визуальные эффекты (Particles)

Когда враг погибает, он не должен просто исчезать. Игрок должен чувствовать удовлетворение от уничтожения цели. Для этого в Godot используется система частиц.

Создание эффекта взрыва

Для 2D-игр, особенно если мы хотим максимальной совместимости (включая веб-браузеры), лучше использовать узел CPUParticles2D. Он обрабатывается процессором и работает везде.

  1. Создайте новую сцену. Корневой узел: CPUParticles2D. Назовите её Explosion.
  2. В Инспекторе настройте следующие параметры:
    • Amount: 15 (количество частиц).
    • One Shot: On (важно! Эффект должен проиграться один раз и остановиться).
    • Explosiveness: 1.0 (все частицы вылетают одновременно).
    • Spread: 180 (разлет во все стороны).
    • Gravity: (0, 0) (чтобы частицы не падали вниз, если это вид сверху).
    • Initial Velocity: Min: 50, Max: 100 (скорость разлета).
    • Scale Amount: Min: 3, Max: 5 (размер частиц, если это пиксели).
    • Color Ramp: Настройте градиент от желтого к красному и в конце к прозрачному (Alpha = 0).

Скрипт самоуничтожения

Частицы проиграются один раз, но сам узел останется в памяти. Нам нужно удалить его после завершения анимации.

Прикрепите скрипт к Explosion:

extends CPUParticles2D

func _ready():
    emitting = true
    # Ждем окончания времени жизни частиц + небольшой запас
    await get_tree().create_timer(lifetime).timeout
    queue_free()

Подключение к врагу

Теперь вернемся в скрипт Enemy.gd (из предыдущих уроков) и добавим спавн эффекта при смерти.

var explosion_scene = preload("res://Scenes/Explosion.tscn")

func die():
    var effect = explosion_scene.instantiate()
    effect.global_position = global_position
    get_tree().root.add_child(effect)
    queue_free()

Теперь уничтожение врагов выглядит эффектно!

Шаг 2: Добавление звука (Audio)

Звук — это 50% атмосферы. В Godot есть два основных узла для звука:

  • AudioStreamPlayer: Для звуков, которые слышны везде одинаково (музыка, интерфейс).
  • AudioStreamPlayer2D: Для звуков, имеющих позицию в пространстве (выстрелы, шаги врагов).

Фоновая музыка

  1. В сцене MainTable добавьте узел AudioStreamPlayer. Назовите его MusicPlayer.
  2. Перетащите ваш музыкальный файл (формат .ogg предпочтителен для музыки) в поле Stream.
  3. Поставьте галочку Autoplay.

Звуки выстрелов

  1. Откройте сцену Tower.
  2. Добавьте AudioStreamPlayer2D.
  3. Загрузите короткий звук выстрела (формат .wav лучше для эффектов).
  4. В скрипте башни, в момент выстрела, добавьте:
$AudioStreamPlayer2D.play()

Совет: Чтобы звуки не сливались в кашу, немного варьируйте их высоту тона (Pitch). audio_player.pitch_scale = randf_range(0.9, 1.1) сделает стрельбу более живой.

Шаг 3: Жизненный цикл и Game Over

До сих пор враги просто исчезали в конце пути. Пора ввести условие поражения.

Здоровье игрока

В MainTable.gd добавьте переменную жизней:

var player_hp = 20

func on_enemy_reached_end():
    player_hp -= 1
    $HUD/HPLabel.text = "HP: " + str(player_hp)

    if player_hp <= 0:
        game_over()

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

Экран поражения

  1. Создайте в HUD новый Control узел GameOverScreen.
  2. Сделайте его на весь экран, добавьте полупрозрачный черный фон.
  3. Добавьте кнопку RestartButton.
  4. Изначально скройте этот экран (глазик в дереве сцен).

В скрипте MainTable.gd:

func game_over():
    $HUD/GameOverScreen.visible = true
    get_tree().paused = true # Останавливаем всю игру

func _on_restart_button_pressed():
    get_tree().paused = false
    get_tree().reload_current_scene() # Перезагружаем сцену заново

Шаг 4: Математика баланса

Игра должна становиться сложнее с каждой волной. Если у врагов всегда 10 HP, а вы строите все больше башен, игра станет скучной. Нам нужна прогрессия.

Обычно используется экспоненциальный рост. Формула расчета здоровья врага для волны nn выглядит так:

HPn=HPbase⋅kn−1HP_n = HP_{base} \cdot k^{n-1}

Где:

  • HPnHP_n — здоровье врага в текущей волне.
  • HPbaseHP_{base} — базовое здоровье (например, 10).
  • kk — коэффициент роста (например, 1.2, то есть +20% каждую волну).
  • nn — номер текущей волны.

Применим это в коде спавнера (MainTable.gd):

var current_wave = 1
var difficulty_multiplier = 1.2

func start_wave(count):
    var enemy_hp = 10 * pow(difficulty_multiplier, current_wave - 1)
    # Передаем это значение спавнеру, а спавнер — врагу
    spawn_enemy(enemy_hp)
    current_wave += 1

Также можно увеличивать количество врагов:

Countn=Countbase+n⋅2Count_n = Count_{base} + n \cdot 2

Где CountnCount_n — количество врагов в волне nn, CountbaseCount_{base} — начальное количество, а n⋅2n \cdot 2 — добавка двух врагов за каждую новую волну.

Таблица роста сложности при k=1.2k=1.2:

Волна HP Врага Прирост
1 10 0%
2 12 +20%
3 14.4 +44%
5 20.7 +107%
10 51.6 +416%

Как видите, к 10-й волне враги станут в 5 раз сильнее. Это заставит игрока активнее играть в Блэкджек ради улучшений.

Шаг 5: Экспорт проекта

Ваша игра готова. Теперь нужно сделать из неё файл .exe (для Windows) или HTML5 (для браузера).

Установка шаблонов экспорта

Godot не включает в себя код для экспорта по умолчанию, чтобы быть легким. Вам нужно скачать шаблоны.

  1. В меню выберите Editor -> Manage Export Templates.
  2. Нажмите Download and Install (выберите версию, соответствующую вашей версии Godot).

Настройка экспорта

  1. Перейдите в Project -> Export.
  2. Нажмите Add... и выберите платформу (например, Windows Desktop).
  3. В настройках справа обратите внимание на опции, но для начала можно оставить всё по умолчанию.
  4. Нажмите Export Project.
  5. Снимите галочку Export With Debug, если хотите финальную версию (она работает быстрее и без консоли отладки).
  6. Выберите папку и имя файла (например, BlackjackTD.exe).
  7. Нажмите Save.

Особенности PCK

По умолчанию Godot создает два файла: .exe (исполняемый файл) и .pck (пакет с ресурсами: картинками, скриптами). Оба файла должны лежать в одной папке, чтобы игра запустилась. Если вы хотите один файл, в настройках экспорта включите опцию Embed PCK.

Заключение курса

Мы прошли долгий путь от пустого проекта до полноценной гибридной игры. Давайте вспомним, что мы сделали:

  1. Настроили Godot и разобрались с узлами.
  2. Реализовали Блэкджек: колода, тасовка, логика дилера.
  3. Создали Tower Defense: TileMap, навигация врагов, стрельба.
  4. Связали жанры: деньги из карт идут на постройку обороны.
  5. Отполировали игру: добавили эффекты, звук и баланс.

Что дальше?

Этот проект — отличная база. Вот идеи для развития:

  • Типы башен: Замедлюящая башня (лед), башня с уроном по площади (ракетница).
  • Карточные бонусы: Например, масть "Пики" увеличивает урон башен на 10 секунд.
  • Система улучшений: Тратить золото не только на новые башни, но и на апгрейд существующих.

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