Создание игры "Like Coins" на Godot Engine. Часть 2

Я надеюсь вы заждались второй части статьи затрагивающей аспекты разработки игр при помощи “Godot Engine”, на примере игры “Like Coins”? На повестке дня приготовлено много всего “вкусного” и “полезного”. Сразу оговорюсь, что в этой статье мы завершим ранее начатую игру, начало которой вы можете прочитать тут - Создание игры «Like Coins» на Godot Engine. Часть 1, но цикл статей продолжится, т.к. материала оказалось на столько много, что заставило меня часть его отложить в сторону, но мы обязательно вернемся к нему позже. Да начнется “gamedev”!

Сцена “Main”

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

ColorRect (“Background”) — заливка цветом заднего фона; Player - объект “Игрок” (я надеюсь вы не путаетесь, из-за того, что я называю сцену Player объектом?); Node (“Container”) — “контейнер” для временного хранения монет; Position2D (“PlayerStart”) — при старте игры задает начальную позицию объекта “Игрок”; Timer (“GameTimer”) — счетчик лимита времени;

Выделяем ColorRect и на панели инструментов выбираем: Layout -> Full Rect, чтобы растянуть его на всю область экрана (в дальнейшем мы часто будем прибегать к этой функции, поэтому советую изучить самостоятельно и другие операции, указанные в списке Layout), у данной ноды, в свойстве “Color” укажем нужный цвет заливки. Тоже самое вы можете проделать с TextureRect, только вместо заливки нужно будет загрузить изображение через свойство “Texture”. Для Position2D, в свойстве “position”, укажем значения “х” и “y” - это послужит начальной позицией для Player. Конечно с помощью сценария можно задать значения позиционирования непосредственно в самом Player, но мы же не только учимся разрабатывать игры, но и изучаем “Godot”, поэтому рассмотрение разных вариантов решения одной задачи лишним не будет.

Скрипт для “Main”

Добавим скрипт для Node и напечатаем следующее:

extends Node
	#PackedScene обеспечит упрощенный доступ к сериализованному объекту сцены
	export (PackedScene) var Coin	
	export (int) var playtime
	
	var level	#текущий уровень
	var score	#очки
	var left_time	#время за которое длится игра
	var window_size	#размер игрового окна
	var playing = false	#игровая сессия не запущена

Свойства “Coin” и “playtime” будут отображаться в Inspector. Перетащите сцену “Coin.tscn” в свойство “Coin”, а в значении “playtime” установите “40” (длительность игры в секундах).

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

func _ready():
	randomize() #инициализируем генератор случайных чисел с некоторой случайной величиной
	window_size = get_viewport().get_visible_rect().size #Определяем видимую область приложения
	$Player.window_size = window_size #область ограничения для объекта "Игрок"
	$Player.hide() #делаем игрока невидимым

Заметьте, что при указании имени объекта Player используется символ “$” - это “синтаксический сахар” позволяющий напрямую обратиться к ноде в текущей сцене, хорошая альтернатива методу get_node("Node1") (хотя использование последнего не возбраняется). Если у “Node1” есть потомок “Node2”, вы также можете использовать данный способ - $Node1/Node2. Учтите, что в “Godot” прекрасно работает автозаполнение, поэтому не пренебрегайте его помощью. Использование пробела в именах нод нежелательно, но все же допустимо, в этом случае используйте кавычки - $"My best Node1".

Новая игра

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

func new_game():
	playing = true #игровая сессия запущена
	level = 1 
	score = 0
	time_left = playtime
	$Player.start($PlayerStart.position)
	$Player.show()
	$GameTimer.start() #запуск таймера обратного отсчета
	spawn_coins() #спавн монеток

Функция “start()” аргументом которой служит $PlayerStart.position, переместит игрока в начальное местоположение, а функция “spawn_coins()” отвечает, как не сложно догадаться, за спавн монеток на игровом поле.

func spawn_coins():
	for i in range(4 + level):
		var c = Coin.instance()
		$CoinContainer.add_child(c)
		c.window_size = window_size
		c.position = Vector2(rand_range(0, window_size.x),
			rand_range(0, window_size.y))

Функция range(4 + level) вернет массив с заданным диапазоном, значение которого равно сумме количества монеток и значению текущего уровня. Диапазон может содержать один аргумент, как в нашем случае, либо два аргумента или три аргумента (третьим аргументом будет являться шаг массива). В этой функции мы создаем несколько экземпляров объекта “Coin” и добавляем в качестве дочерних элементов для ноды CoinContainer (я надеюсь, вы не забыли, что доступ к объекту мы уже имеем, благодаря PackedScene). Помните, что всякий раз при создании экземпляра новой ноды (метод instance()), он обязательно должен быть добавлен в дерево при помощи add_child(). Далее мы устанавливаем область для возможного спавна монеток, чтобы они случайно не появились за экраном, а затем случайным образом назначаем позицию. Последняя строка выглядит немного не эстетично, поэтому я предлагаю упростить ее, прибегнув к помощи “Синглтонов”.

Синглтоны

Второе имя “Синглтонов” - “Автозагрузка”. Уже наводит на некоторые мысли, да? Рассказываю, синглтон работает следующим образом: скрипт, в который мы можем записать все, что угодно (начиная от объявления переменных и заканчивая “переключателями” сцен, включая их загрузку и выгрузку) загружается первым, с запуском приложения, и все его содержимое доступно из любой точки проекта. В своем роде, это некое пользовательское глобальное хранилище “всего что угодно” доступное в любой момент времени.

На заметку, у проекта есть свое глобальное хранилище, содержимое которого мы также можем использовать, а получить доступ к нему можно используя ProjectSettings.get_setting(name), где name - это имя требуемого параметра.

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

extends Node

func rand():
	var rrand = Vector2(rand_range(40, 760),
		rand_range(40, 540))
	return rrand #возвращаем полученное значение

Далее сохраняем его и идем в настройки проекта: Project -> Project Settings -> AutoLoad. Выбираем наш недавно созданный скрипт, задаем для него имя, например, “_G”, и возвращаемся к функции “spawn_coins()” чтобы немного откорректировать последнюю сроку, заменив ее на следующий код:

	...
	c.position = _G.rand()

Теперь стоит проверить, что получилось, поместив “spawn_coins()” в блок “_ready()” и запустив приложение на F5. И не забудьте выбрать в качестве главной сцены Main.tscn, если по каким-то причинам вы ошиблись в выборе, то можете изменить главную сцену вручную, для этого в настройках проекта нужно перейти: General -> Run -> MainScene. Работает? Тогда идем дальше.

Сколько монет осталось?

Давайте продолжим. Далее необходимо проверять сколько монет осталось, чтобы перевести игрока на следующий уровень, дать ему небольшой “бонус” в виде увеличения времени на 5 секунд, а затем снова заспавнить монетки.

func _process(delta):
	#мы все еще играем? и количество монеток равно нулю?
	if playing and $CoinContainer.get_child_count() == 0:
		#тогда нужно увеличить уровень
		level += 1
		#немного "подсластим" игровой процесс
		time_left += 5
		#спавним монетки
		spawn_coins()

Пользовательский интерфейс

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

В “Godot” есть так называемые “управляющие ноды” позволяющие автоматически форматировать дочерние элементы относительно заданных параметров родителя. Каждый тип “управляющих нод” имеет специальные свойства, которые контролируют то, как они будут управлять расположением своих потомков. Яркий представитель этого типа MarginContainer, который необходимо добавить в сцену. При помощи Layout -> Top Wide растянем его в верхней части окна, а в свойствах этого объекта, в разделе Margin укажем отступы от краев: слева, сверху и справа. У MarginContainer должны быть три дочерних Label со следующими именами: ScoreLabel, LevelLabel и TimeLabel. Добавьте их на сцену. При помощи свойства Align сделайте так, чтобы они располагались слева, по центру и справа. Осталось добавить еще один Label (Messagelabel), разместив его по центру, все также при помощи Layout, а чуть ниже разместите кнопку (StartButton).

Теперь сделаем интерфейс отзывчивым, нам нужно чтобы обновлялось время, количество собранных монет и высвечивался текущий уровень. Добавим скрипт для ноды HUD.

extends CanvasLayer

signal start_game

func update_score(value):
	$MarginContainer/ScoreLabel.text = str(value)
func update_level(value):
	if len(str(value)) == 1:
		$MarginContainer/TimeLabel.text = "0: 0" + str(value)
	else:
		$MarginContainer/TimeLabel.text = "0: " + str(value)
func update_timer(value):
	$MarginContainer/TimeLabel.txt = str(value)

Для MessageLabel нам понадобится таймер, для того чтобы менять текст сообщения на короткий период. Добавим ноду Timer и заменим его имя на MessageTimer. В инспекторе следует установить время ожидания 2 секунды и установить флажок в поле One Shot. Это гарантирует, что при запуске таймер сработает только один раз.

func show_message(text):
	$MessageLabel.text = text
	$MessageLabel.show()
	$MessageTimer.start()

Соединим сигнал timeout() c “MessageTimer” и добавим следующее:

func _on_MessageTimer_timeout():
	$MessageLabel.hide()

На вкладке “Node” для StartButton, подключим сигнал pressed(). При нажатии на кнопку StartButton она должна скрыться вместе с MessageLabel, затем отправить сигнал в главную сцену, где мы его в последующем успешно перехватим заодно подсунув функцию на исполнение - “new_game()”. Реализуем это при помощи кода, указанного ниже. Не забудьте для кнопки, в свойстве Text установить любой текст призывающий начать игру.

func _on_StartButton_pressed():
	$StartButton.hide()
	$MessageLabel.hide()
	emit_signal("start_game")

Чтобы наконец уже закончить с интерфейсом напишем последнюю, финальную функцию - функцию вывода сообщения о конце игры. В этой функции нам нужно, чтобы надпись “Game Over” отображалось не более двух секунд, а затем исчезло, что возможно благодаря функции “show_message()”. Однако необходимо снова показать кнопку запуска новой игры, как только сообщение оповещающее, что игра закончена исчезнет. yield() приостановит выполнение функции до тех пор, пока не будет получен сигнал от MessageTimer, и получив сигнал от MessageTimer о его исполнении, функция продолжит выполнение, возвращая нас в исходное состояние, чтобы мы снова могли начать новую игру.

func show_game_over():
	show_message("Game Over")
	yield($MessageTimer, "timeout")
	$StartButton.show()
	$MessageLabel.text = "LIKE COINS!"
	$MessageLabel.show()

Ending?

Давайте уже настроим обратную связь между HUD и Main. Добавим сцену HUD в основную сцену и на главной сцене подключим сигнал GameTimer через timeout() добавив следующее:

func _on_GameTimer_timeout():
	time_left -= 1	#отсчет счетчика
	$HUD.update_timer(time_left)	#обновляем интерфейс счетчика
	if time_left <= 0:
		game_over()	#конец игры по окончанию счетчика

Затем подключим сигналы pickup() и die() игрока.

func _on_Player_pickup():
	score += 1
	$HUD.update_score(score)

func _on_Player_die():
	game_over()

При завершении игры должно произойти еще несколько вещей, которые нельзя упускать из виду. Запишите следующий код, а я объясню.

func game_over():
	playing = false
	$GameTimer.stop()
	for coin in $CoinContainer.get_children():
		coin.queue_free()
	$HUD.show_game_over()
	$Player.die()

Эта функция остановит игру, а затем произойдет пересчет оставшихся монет и удаление оставшиеся монеты, затем произойдет вызов show_game_over() для HUD. Следующим действием мы запустим анимацию и остановим процесс выполнения ноды Player.

Наконец, следует активировать StartButton, которая должна быть подключена к функции new_game(). Нажмите на ноду HUD и в диалоговом окне подключения нужно кликнуть на Make Function to Off (это запретит создание новой функции) и в поле Method In Node укажите имя подключаемой функции - new_game. Это подключит сигнал к существующей функции, а не создаст новую.

Последние штрихи - удалим new_game() из функции _ready() и добавим следующие две строки в функцию new_game():

...
$HUD.update_score(score)
$HUD.update_timer(time_left)

Теперь можно с уверенностью сказать, что игра готова, сейчас она вполне себе “играбельна”, но без эффектов. Последнее мы рассмотрим в следующей статье уделив огромное внимание различного рода “украшательствам”, чтобы разнообразить игровой процесс и еще больше изучить возможности “Godot”. Поэтому не забывайте следить за выпуском статей. Успехов!

Written on November 30, 2018