Хабрахабр

Создание игры «Like coins» на Godot Engine. Часть 1

Пожалуй, это самый дружелюбный и легкий в освоении инструмент для создания игр, и чтобы в этом убедиться, попробуем сделать небольшую 2D-игру. "Godot Engine" очень быстро развивается и завоевывает сердца разработчиков игр со всего мира. Хотя сам по себе переход на 3D не столь сложная задача, как может показаться, ведь большинство функций в "Godot Engine" могут успешно использоваться как в 2D, так и 3D. Для хорошего понимания процесса разработки игр, следует начинать именно с 2D-игр — это позволит снизить порог вхождения в более серьезный игрострой.

Введение

Чтобы немного ее усложнить добавим препятствие и время, как ограничивающий фактор. Самое простое, что можно придумать — игра, в которой наш главный герой будет собирать монетки. В игре будет 3 сцены: Player, Coin и HUD (в этой статье не рассматривается), которые будут объединены в одну Main сцену.

Настройки проекта

В крупных проектах полезно создавать отдельные папки для хранения сценариев, сцен, изображений и звуков, и нам определенно стоит взять это на заметку, ведь кто знает к какому конечному результату в последствии мы придем. Перед тем, как погрузиться в написание сценариев (скриптов), а это примерно 80-90% от всего времени затрачиваемого на создание игры, первое, что необходимо сделать — настроить наш будущий проект.

Хочу сразу оговориться, что эта статья подразумевает, что вы немного знакомы с "Godot Engine" и у вас имеются некоторые познания и навыки пользования данным инструментом, хотя я буду ориентироваться на то, что вы сталкиваетесь с "Godot Engine" впервые, я все же советую для начала ознакомиться с базовой составляющей движка, изучить синтаксис GDScript и прийти к пониманию используемой терминологии (ноды, сцены, сигналы и т.п.), а уже затем вернуться сюда и продолжить знакомство.

В меню программы переходим к Project -> Project Settings.

Я всегда буду приводить примеры исходя из того, что конечный пользователь пользуется англоязычным интерфейсом движка, несмотря на то, что в "Godot Engine" есть поддержка русского языка. Еще небольшое отступление. Это сделано для того, чтобы избавиться от возможного недопонимания или конфузов, связанных с неправильным/неточным переводом тех или иных элементов интерфейса программы.

Также в этом разделе следует установить Stretch/Mode на 2D, а Aspect на Keep. Находим раздел Display/Window и устанавливаем ширину — 800, а высоту — 600. Я советую вам поиграть с этими параметрами. Это предотвратит растяжение и деформацию содержимого окна при изменении его размера, но, чтобы запретить изменение размера окна просто снимем галочку Resizable.

Для чего это нужно? Теперь переходим в раздел Rendering/Quality и в правой панели включаем Use Pixel Snap. Поскольку объекты не могут быть нарисованы лишь на половину пикселя, это несоответствие может вызвать визуальные дефекты для игр где используется pixelart. Координаты векторов в "Godot Engine" — это числа с плавающей запятой. Имейте это в виду. И стоит отметить, что в 3D данный параметр бесполезен.

Сцена "Игрок"

Приступим к созданию первой сцены — Player.

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

Это наша родительская нода, и чтобы расширить функциональность необходимо добавить дочерние ноды. Создание сцены тривиально простое действие — на вкладке Scene щелкаем + (Add/Create) и выбираем ноду Area2D и сразу изменяем его имя, чтобы не путаться. Далее следует сразу "залочить" корневую ноду: В нашем случае это AnimatedSprite и CollisionShape2D, но не будем торопиться, а начнем по порядку.

С этой включенной опцией, "родитель" и все его "дети" всегда будут перемещаться вместе. Если форма столкновения тела (CollisionShape2D) или спрайт (AnimatedSprite) будут смещены, растянуты относительно родительского узла, это точно приведет к непредвиденным ошибкам и в последствии будет трудно их исправить. Звучит смешно, но использовать данную возможность крайне полезно.

AnimatedSprite

Название ноды подсказывает, что иметь дело мы будем с анимацией и спрайтами. Area2D очень полезная нода в случае если нужно узнать о событии перекрытия с другими объектами или об их столкновении, но сама по себе она невидима глазу и чтобы сделать объект Player видимым добавим AnimatedSprite. Работа с панелью SpriteFrames заключается в том, чтобы создать нужные анимации и загрузить соответствующие спрайты к ним. В окне Inspector переходим к параметру Frames и создаем новый SpriteFrames. Не забудьте значение SPEED (FPS) должно равняться 8 (хотя вы можете выбрать другое значение — подходящее). Мы не будем подробно разбирать все этапы создания анимаций, оставив это на самостоятельное изучение, скажу лишь, что у нас должно быть три анимации: walk (анимация ходьбы), idle (состояния покоя) и die (анимация смерти или провала).

CollisionShape2D

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

Сценарии

Во вкладке Scene создаем скрипт, оставляем настройки "по-умолчанию", стираем все комментарии (строки, начинающиеся со знака '#') и приступаем к объявлению переменных: Теперь, чтобы "оживить" наш игровой объект, нужно задать ему сценарий, по которому будут выполняться заданные нами действия, прописанные в этом сценарии.

export (int) var speed
var velocity = Vector2()
var window_size = Vector2(800, 600)

Это очень полезный метод если мы хотим получить настраиваемые значения, которые удобно редактировать в окне Inspector. Использование ключевого слова export позволяет задавать значение переменной speed в окне панели Inspector. Значение velocity будет определять направление движения, а window_size — область ограничивающая передвижение игрока. Укажите Speed (скорость передвижения) значение 350.

Затем осуществим перемещение объекта, согласно нажатым клавишам и далее проиграем анимацию. Дальнейший порядок наших действий таков: используем функцию 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.

В этом нам поможет Функция _process(), которая вызывается каждый раз когда происходит смена кадра, поэтому целесообразно использовать ее для тех объектов, которые будут часто меняться. Итак, события нажатия клавиш отследили, теперь нужно осуществить перемещение объекта Player.

func _process(delta): get_input() position += velocity * delta position.x = clamp(position.x, 0, screensize.x) position.y = clamp(position.y, 0, screensize.y)

Необходимо дать пояснение, что же это такое. Я надеюсь, вы заметили параметр delta, который в свою очередь умножается на скорость. Тем не менее, могут возникнуть ситуации, когда работа компьютера, либо самого "Godot Engine" замедляется. Игровой движок изначально настроен на работу со скоростью 60 кадров в секунду. "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

Сейчас нас интересует area_entered (), она предполагает, что объекты с которыми будет происходить столкновение тоже имеют тип Area2D. Просмотрите список сигналов в окне Inspector (вкладка Node), и обратите внимание на наши сигналы, и сигналы, что уже есть. Подключаем сигнал 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() определяет поведение монетки (например, в ней может быть воспроизведение анимации или звука, удаление объекта и т.п.).

Сцена "Монетка"

Значение Speed (FPS) можно увеличить до 14. При создании сцены с одной монеткой, необходимо сделать все, то, что мы проделали со сценой "Игрок", за исключением того, что в AnimatedSprite будет лишь одна анимация (блик). И размеры CollisionShape2D должны соответствовать изображению монетки, главное не масштабируйте ее, а именно изменяйте размер, используя соответствующие маркеры на форме в 2D редакторе, которая
регулирует радиус круга. Еще изменим масштаб AnimatedSprite0,5, 0,5.

Чтобы объект Player реагировал на касание с монетками, монетки должны принадлежать группе, назовем ее coins. Группы — это своеобразная маркировка нод, позволяющая идентифицировать аналогичные ноды. Выделяем ноду Area2D (с именем "Coin") и во вкладке Node -> Groups присваиваем ей тег, создав соответствующую группу.

Функция pickup() будет вызвана скриптом объекта Player и сообщит монете, что делать когда она сработала. Для ноды Coin создаем скрипт. Это гораздо безопаснее, чем сразу удалить ноду, потому что другие "участники" (ноды или сцены) в игре, могут все еще нуждаться в существовании этой ноды. Метод queue_free() безопасно удалит ноду из дерева со всеми ее дочерними нодами и отчистит память, но удаление сработает не сразу, сперва она будет перемещена в очередь, подлежащих удалению в конце текущего кадра.

func pickup (): queue_free ()

Заключение

Ну а я спешу сказать, что первая часть создания игры "Like Coins" закончена, разрешите откланяться и поблагодарить всех за внимание. Сейчас мы можем создать сцену Main, мышкой перетащить в 2D редактор обе сцены: Player и Coin, и проверить как работает перемещение игрока и его соприкосновение с монеткой запустив сцену (F5). Не бойтесь быть жестким и критичным. Если вам есть, что сказать, дополнить материал или вы увидели ошибки в статье — обязательно сообщите об этом, написав комментарий ниже. Ваш "обратный отклик" подскажет в правильном ли направлении я двигаюсь и что можно исправить, чтобы материал был еще интереснее и полезнее.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть