Создание игры «Like coins» на Godot Engine. Часть 1
“Godot Engine” очень быстро развивается и завоевывает сердца разработчиков игр со всего мира. Пожалуй, это самый дружелюбный и легкий в освоении инструмент для создания игр, и чтобы в этом убедиться, попробуем сделать небольшую 2D-игру. Для хорошего понимания процесса разработки игр, следует начинать именно с 2D-игр - это позволит снизить порог вхождения в более серьезный игрострой. Хотя сам по себе переход на 3D не столь сложная задача, как может показаться, ведь большинство функций в “Godot Engine” могут успешно использоваться как в 2D, так и 3D.
Введение
Самое простое, что можно придумать - игра, в которой наш главный герой будет собирать монетки. Чтобы немного ее усложнить добавим препятствие и время, как ограничивающий фактор. В игре будет 3 сцены: Player
, Coin
и HUD
(в этой статье не рассматривается), которые будут объединены в одну Main
сцену.
Настройки проекта
Перед тем, как погрузиться в написание сценариев (скриптов), а это примерно 80-90% от всего времени затрачиваемого на создание игры, первое, что необходимо сделать - настроить наш будущий проект. В крупных проектах полезно создавать отдельные папки для хранения сценариев, сцен, изображений и звуков, и нам определенно стоит взять это на заметку, ведь кто знает к какому конечному результату в последствии мы придем.
Хочу сразу оговориться, что эта статья подразумевает, что вы немного знакомы с “Godot Engine” и у вас имеются некоторые познания и навыки пользования данным инструментом, хотя я буду ориентироваться на то, что вы сталкиваетесь с “Godot Engine” впервые, я все же советую для начала ознакомиться с базовой составляющей движка, изучить синтаксис GDScript и прийти к пониманию используемой терминологии (ноды, сцены, сигналы и т.п.), а уже затем вернуться сюда и продолжить знакомство.
В меню программы переходим к Project -> Project Settings
.
Еще небольшое отступление. Я всегда буду приводить примеры исходя из того, что конечный пользователь пользуется англоязычным интерфейсом движка, несмотря на то, что в “Godot Engine” есть поддержка русского языка. Это сделано для того, чтобы избавиться от возможного недопонимания или конфузов, связанных с неправильным/неточным переводом тех или иных элементов интерфейса программы.
Находим раздел Display/Window
и устанавливаем ширину - 800
, а высоту - 600
. Также в этом разделе следует установить Stretch/Mode
на 2D
, а Aspect
на Keep
. Это предотвратит растяжение и деформацию содержимого окна при изменении его размера, но, чтобы запретить изменение размера окна просто снимем галочку Resizable
. Я советую вам поиграть с этими параметрами.
Теперь переходим в раздел Rendering/Quality
и в правой панели включаем Use Pixel Snap
. Для чего это нужно? Координаты векторов в “Godot Engine” - это числа с плавающей запятой. Поскольку объекты не могут быть нарисованы лишь на половину пикселя, это несоответствие может вызвать визуальные дефекты для игр где используется pixelart
. И стоит отметить, что в 3D данный параметр бесполезен. Имейте это в виду.
Сцена “Игрок”
Приступим к созданию первой сцены - Player
.
Преимущество любой сцены в том, что изначально они независимы от других частей игры и это дает возможность беспрепятственно тестировать их и получать тот результат, который изначально в них был заложен. В целом, разделение игровых объектов на сцены является полезным инструментом в создании сложных игр - так легче отлавливать ошибки, производить изменения самой сцены, при этом не будут затронуты другие части игры, также они могут служить шаблонами для других игр и определенно они будут работать ровно также, как и работали до переноса.
Создание сцены тривиально простое действие - на вкладке Scene
щелкаем +
(Add/Create) и выбираем ноду Area2D
и сразу изменяем его имя, чтобы не путаться. Это наша родительская нода, и чтобы расширить функциональность необходимо добавить дочерние ноды. В нашем случае это AnimatedSprite
и CollisionShape2D
, но не будем торопиться, а начнем по порядку. Далее следует сразу “залочить” корневую ноду:
Если форма столкновения тела (CollisionShape2D) или спрайт (AnimatedSprite) будут смещены, растянуты относительно родительского узла, это точно приведет к непредвиденным ошибкам и в последствии будет трудно их исправить. С этой включенной опцией, “родитель” и все его “дети” всегда будут перемещаться вместе. Звучит смешно, но использовать данную возможность крайне полезно.
AnimatedSprite
Area2D
очень полезная нода в случае если нужно узнать о событии перекрытия с другими объектами или об их столкновении, но сама по себе она невидима глазу и чтобы сделать объект Player
видимым добавим AnimatedSprite
. Название ноды подсказывает, что иметь дело мы будем с анимацией и спрайтами. В окне Inspector
переходим к параметру Frames
и создаем новый SpriteFrames
. Работа с панелью SpriteFrames
заключается в том, чтобы создать нужные анимации и загрузить соответствующие спрайты к ним. Мы не будем подробно разбирать все этапы создания анимаций, оставив это на самостоятельное изучение, скажу лишь, что у нас должно быть три анимации: walk
(анимация ходьбы), idle
(состояния покоя) и die
(анимация смерти или провала). Не забудьте значение SPEED (FPS)
должно равняться 8
(хотя вы можете выбрать другое значение - подходящее).
CollisionShape2D
Чтобы Area2D
смог обнаруживать столкновения необходимо предоставить ему форму объекта. Формы определяются параметром Shape2D
и включают в себя прямоугольники, круги, многоугольники и другие более сложные типы форм, а размеры уже редактируются в самом редакторе, но вы всегда можете использовать Inspector
для более точной настройки.
Сценарии
Теперь, чтобы “оживить” наш игровой объект, нужно задать ему сценарий, по которому будут выполняться заданные нами действия, прописанные в этом сценарии. Во вкладке Scene
создаем скрипт, оставляем настройки “по-умолчанию”, стираем все комментарии (строки, начинающиеся со знака ‘#’) и приступаем к объявлению переменных:
export (int) var speed
var velocity = Vector2()
var window_size = Vector2(800, 600)
Использование ключевого слова export
позволяет задавать значение переменной speed
в окне панели Inspector
. Это очень полезный метод если мы хотим получить настраиваемые значения, которые удобно редактировать в окне Inspector
. Укажите Speed
(скорость передвижения) значение 350
. Значение velocity
будет определять направление движения, а window_size
- область ограничивающая передвижение игрока.
Дальнейший порядок наших действий таков: используем функцию get_input()
, чтобы проверять производится ли ввод с клавиатуры. Затем осуществим перемещение объекта, согласно нажатым клавишам и далее проиграем анимацию. Игрок будет двигаться в четырех направлениях. По умолчанию, в “Godot Engine” есть события, назначенные клавишам стрелок (Project -> Project Settings -> Input Map
), поэтому мы можем применить их для своего проекта. Чтобы узнать, нажата ли, та или иная клавиша, следует использовать Input.is_action_pressed()
, подсунув ей имя события, которое хотим отследить.
func get_input():
velocity = Vector2()
if Input.is_action_pressed("ui_left"):
velocity.x -= 1
if Input.is_action_pressed("ui_right"):
velocity.x += 1
if Input.is_action_pressed("ui_up"):
velocity.y -= 1
if Input.is_action_pressed("ui_down"):
velocity.y += 1
if velocity.length() > 0:
velocity = velocity.normalized() * speed
На первый взгляд все выглядит хорошо, но есть один маленький нюанс. Сочетание нескольких нажатых клавиш (например вниз и влево) вызовет сложение векторов и в этом случае игрок будет двигаться быстрее, чем если бы он просто двигался вниз. Чтобы избежать этого, будем использовать метод normalized()
- он вернет длину вектора до 1
.
Итак, события нажатия клавиш отследили, теперь нужно осуществить перемещение объекта Player
. В этом нам поможет Функция _process()
, которая вызывается каждый раз когда происходит смена кадра, поэтому целесообразно использовать ее для тех объектов, которые будут часто меняться.
func _process(delta):
get_input()
position += velocity * delta
position.x = clamp(position.x, 0, window_size.x)
position.y = clamp(position.y, 0, window_size.y)
Я надеюсь, вы заметили параметр delta
, который в свою очередь умножается на скорость. Необходимо дать пояснение, что же это такое. Игровой движок изначально настроен на работу со скоростью 60 кадров в секунду. Тем не менее, могут возникнуть ситуации, когда работа компьютера, либо самого “Godot Engine” замедляется. Если частота кадров не согласована (время за которое сменяются кадры), это повлияет на “плавность” перемещения игровых объектов (как результат - движение “рывками”). “Godot Engine” решает эту проблему (как и большинство подобных движков) введя переменную delta
- она выдает значение, за которое сменились кадры. Благодаря этим значениям можно “выравнивать” движение. Обратите внимание еще на одну примечательную функцию - clamp
(возвращает значение в пределах двух заданных показателей), благодаря ей мы имеем возможность ограничить область, по которой может двигаться Player
, просто задав минимальное и максимальное значение области.
Не забываем анимировать наш объект. Обратите внимание, что когда объект движется вправо нужно зеркально отразить AnimatedSprite
(используя flip_h
) и наш герой при движении будет смотреть в ту сторону куда непосредственно двигается. Убедитесь, что в AnimatedSprite
параметр Playing
включен, чтобы началось воспроизведение анимации.
if velocity.length() > 0:
$AnimatedSprite.animation = "walk"
$AnimatedSprite.flip_h = velocity.x < 0
else:
$AnimatedSprite.animation = "idle"
Рождение и смерть
Запуская игру, главной сцене необходимо сообщить ключевым сценам о готовности начать новую игру, в нашем случае следует сообщить объекту Player
о начале игры и установить для него начальные параметры: позицию появления, анимацию по-умолчанию, запустить set_process
.
func start(pos):
set_process(true)
#глобальная позиция объекта в формате Vector2(x, y)
position = pos
$AnimatedSprite.animation = "idle"
Также предусмотрим событие смерти игрока, когда заканчивается время или игрок натыкается на препятствие, а установив set_process (false)
заставит функцию _process ()
больше не выполняться для этой сцены.
func die():
$AnimatedSprite.animation = "die"
set_process(false)
Добавление коллизий
Настал черед заставить игрока обнаруживать столкновения с монетами и препятствиями. Проще всего это реализуется с помощью сигналов. Сигналы - это отличный способ для отправки сообщения, чтобы другие ноды могли обнаруживать их и реагировать. Большинство нод уже имеют встроенные сигналы, но есть возможность определить “пользовательские” сигналы для собственных целей. Сигналы добавляются если объявить их, в начале скрипта:
signal pickup
signal die
Просмотрите список сигналов в окне Inspector
(вкладка Node
), и обратите внимание на наши сигналы, и сигналы, что уже есть. Сейчас нас интересует area_entered ()
, она предполагает, что объекты с которыми будет происходить столкновение тоже имеют тип Area2D
. Подключаем сигнал area_entered ()
при помощи кнопки Connect
, и в окне Connect Signal
выбираем подсвеченную ноду (Player), все остальное оставляем по-умолчанию.
func _on_Player_area_entered( area ):
if area.is_in_group("coins"):
#посылаем сигнал
emit_signal("pickup")
area.pickup()
if area.is_in_group("obstacles"):
emit_signal("die")
die()
Чтобы объекты можно было легко обнаружить и взаимодействовать с ними их нужно определять в соответствующие группы. Создание же самих групп сейчас опустим, но обязательно вернемся к ним позже. Функция pickup()
определяет поведение монетки (например, в ней может быть воспроизведение анимации или звука, удаление объекта и т.п.).
Сцена “Монетка”
При создании сцены с одной монеткой, необходимо сделать все, то, что мы проделали со сценой “Игрок”, за исключением того, что в AnimatedSprite
будет лишь одна анимация (блик). Значение Speed (FPS)
можно увеличить до 14
. Еще изменим масштаб AnimatedSprite
- 0,5, 0,5
. И размеры CollisionShape2D
должны соответствовать изображению монетки, главное не масштабируйте ее, а именно изменяйте размер, используя соответствующие маркеры на форме в 2D редакторе, которая
регулирует радиус круга.
Группы - это своеобразная маркировка нод, позволяющая идентифицировать аналогичные ноды. Чтобы объект Player
реагировал на касание с монетками, монетки должны принадлежать группе, назовем ее coins
. Выделяем ноду Area2D
(с именем “Coin”) и во вкладке Node -> Groups
присваиваем ей тег, создав соответствующую группу.
Для ноды Coin
создаем скрипт. Функция pickup()
будет вызвана скриптом объекта Player
и сообщит монете, что делать когда она сработала. Метод queue_free()
безопасно удалит ноду из дерева со всеми ее дочерними нодами и отчистит память, но удаление сработает не сразу, сперва она будет перемещена в очередь, подлежащих удалению в конце текущего кадра. Это гораздо безопаснее, чем сразу удалить ноду, потому что другие “участники” (ноды или сцены) в игре, могут все еще нуждаться в существовании этой ноды.
func pickup ():
queue_free ()
Заключение
Сейчас мы можем создать сцену Main
, мышкой перетащить в 2D редактор обе сцены: Player
и Coin
, и проверить как работает перемещение игрока и его соприкосновение с монеткой запустив сцену (F5). Ну а я спешу сказать, что первая часть создания игры “Like Coins” закончена, разрешите откланяться и поблагодарить всех за внимание. Если вам есть, что сказать, дополнить материал или вы увидели ошибки в статье - обязательно сообщите об этом, написав комментарий ниже. Не бойтесь быть жестким и критичным. Ваш “обратный отклик” подскажет в правильном ли направлении я двигаюсь и что можно исправить, чтобы материал был еще интереснее и полезнее.