Score and replay

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

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

В основной сцене добавьте новый узел Control в качестве дочернего узла Main и назовите его UserInterface. Вы автоматически попадете на экран 2D, где можно редактировать пользовательский интерфейс (UI).

Добавьте узел Label и переименуйте его в ScoreLabel.

image0

В инспекторе установите для параметра метки Text значение "Score: 0".

image1

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

Прокрутите вниз до Theme Overrides, разверните Colors и щёлкните чёрное поле рядом с Font Color, чтобы оттенить текст.

image2

Выберите тёмный тон, чтобы он хорошо контрастировал с 3D сценой.

image3

Наконец, нажмите и перетащите текст в области просмотра, чтобы переместить его от левого верхнего угла.

image4

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

Creating a UI theme

Снова выберите узел UserInterface. В инспекторе создайте новый ресурс темы в Theme -> Theme.

image5

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

image6

По умолчанию тема имеет только одно свойство - Default Font (Шрифт по умолчанию).

См.также

Вы можете добавить больше свойств к ресурсу темы для разработки сложных пользовательских интерфейсов, но это выходит за рамки данной серии статей. Чтобы узнать больше о создании и редактировании тем, смотрите Introduction to GUI skinning.

Щёлкните свойство Default Font и создайте новый DynamicFont (Динамический шрифт).

image7

Разверните DynamicFont, щёлкнув по нему, и разверните его раздел Font. Там вы увидите пустое поле Font Data (Данные шрифта).

image8

This one expects a font file like the ones you have on your computer. DynamicFont supports the following formats:

  • TrueType (.ttf)

  • OpenType (.otf)

  • Web Open Font Format 1 (.woff)

  • Web Open Font Format 2 (.woff2, since Godot 3.5)

In the FileSystem dock, expand the fonts directory and click and drag the Montserrat-Medium.ttf file we included in the project onto the Font Data. The text will reappear in the theme preview.

Текст немного мелковат. Установите параметры Settings -> Size на 22 пикселя, чтобы увеличить размер текста.

image9

Keeping track of the score

Далее поработаем со счётом игры. Прикрепите новый скрипт к ScoreLabel и определите переменную score.

extends Label

var score = 0

Счёт должен увеличиваться на 1 каждый раз, когда мы раздавливаем монстра. Мы можем использовать их сигнал squashed, чтобы узнать, когда это произойдет. Однако, поскольку мы создаём экземпляры монстров из кода, мы не можем сделать соединение в редакторе.

Вместо этого нам придётся создавать связь из кода каждый раз, когда мы порождаем монстра.

Откройте скрипт Main.gd. Если он всё ещё открыт, вы можете щёлкнуть по его названию в левой колонке редактора скриптов.

image10

Также можно дважды щёлкнуть файл Main.gd в панели FileSystem.

В нижней части функции _on_MobTimer_timeout() добавьте следующую строку.

func _on_MobTimer_timeout():
    #...
    # We connect the mob to the score label to update the score upon squashing one.
    mob.connect("squashed", $UserInterface/ScoreLabel, "_on_Mob_squashed")

Эта строка означает, что когда моб испустит сигнал squashed, узел ScoreLabel примет его и вызовет функцию _on_Mob_squashed().

Вернитесь к скрипту ScoreLabel.gd, чтобы определить функцию обратного вызова _on_Mob_squashed().

Там мы увеличиваем счёт и обновляем отображаемый текст.

func _on_Mob_squashed():
    score += 1
    text = "Score: %s" % score

Вторая строка использует значение переменной score для замены заполнителя %s. При использовании этой функции Godot автоматически преобразует значения в текст, что удобно для вывода текста в метках или с помощью функции print().

См.также

You can learn more about string formatting here: Строки формата GDScript.

Теперь вы можете играть в игру и раздавить несколько врагов, чтобы увидеть, как увеличивается счёт.

image11

Примечание

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

Retrying the game

Теперь мы добавим возможность играть снова после смерти. Когда игрок умирает, мы выводим сообщение на экран и ждём ввода.

Вернитесь к сцене Main, выберите узел UserInterface, добавьте узел ColorRect в качестве его дочернего элемента и назовите его Retry. Этот узел заполняет прямоугольник однородным цветом и будет служить в качестве наложения для затемнения экрана.

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

image12

Откройте его и примените команду Full Rect.

image13

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

image14

Это связано с тем, что узлы пользовательского интерфейса (все узлы с зелёным значком) работают с якорями и полями относительно ограничивающей рамки их родителя. Здесь узел UserInterface имеет небольшой размер, а узел Retry ограничен им.

Выберите UserInterface и примените к нему Layout -> Full Rect. Узел Retry теперь должен занимать всю область просмотра.

Давайте изменим его цвет, чтобы он затемнял игровую область. Выберите Retry и в инспекторе установите его Color на что-то одновременно тёмное и прозрачное. Для этого в окне выбора цвета перетащите ползунок A влево. Он управляет альфа-каналом цвета, то есть его непрозрачностью.

image15

Затем добавьте метку Label в качестве дочернего элемента Retry и введите в поле Text надпись "Press Enter to retry."

image16

Чтобы переместить его и закрепить в центре экрана, примените к нему Layout -> Center.

image17

Coding the retry option

Теперь мы можем перейти к коду, чтобы показывать и скрывать узел Retry, когда игрок умирает и играет снова.

Откройте скрипт Main.gd. Во-первых, мы хотим скрыть наложение в начале игры. Добавьте эту строку в функцию _ready().

func _ready():
    #...
    $UserInterface/Retry.hide()

Затем, когда игрок получает удар, мы показываем затенение.

func _on_Player_hit():
    #...
    $UserInterface/Retry.show()

Наконец, когда узел Retry становится видимым, нам нужно прослушать ввод игрока и перезапустить игру, если он нажмёт клавишу Enter. Для этого мы используем встроенный обратный вызов _unhandled_input().

Если игрок нажал предопределённое входное действие ui_accept и Retry является видимым, мы перезагружаем текущую сцену.

func _unhandled_input(event):
    if event.is_action_pressed("ui_accept") and $UserInterface/Retry.visible:
        # This restarts the current scene.
        get_tree().reload_current_scene()

Функция get_tree() предоставляет нам доступ к глобальному объекту SceneTree, который позволяет нам перезагрузить и перезапустить текущую сцену.

Добавление музыки

Чтобы добавить музыку, которая непрерывно играет на заднем плане, мы воспользуемся другой функцией в Godot: autoloads.

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

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

Создайте новую сцену, перейдя в меню Scene и выбрав New Scene.

image18

Нажмите кнопку Other Node, чтобы создать AudioStreamPlayer, и переименуйте его в MusicPlayer.

image19

Мы включили музыкальный файл House In a Forest Loop.ogg в каталог art/. Щёлкните и перетащите его на свойство Stream в инспекторе. Также включите Autoplay, чтобы музыка автоматически проигрывалась в начале игры.

image20

Save the scene as MusicPlayer.tscn.

We have to register it as an autoload. Head to the Project -> Project Settings… menu and click on the Autoload tab.

В поле Path введите путь к вашей сцене. Щёлкните на значке папки, чтобы открыть браузер файлов, и дважды щёлкните на MusicPlayer.tscn. Затем нажмите кнопку Add справа, чтобы зарегистрировать узел.

image21

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

Прежде чем мы завершим этот урок, вот краткий обзор того, как это работает под капотом. Когда вы запускаете игру, ваша панель Scene изменяется, чтобы показать вам две вкладки: Remote и Local.

image22

Вкладка Remote позволяет визуализировать дерево узлов запущенной игры. Здесь вы увидите узел Main и всё, что содержит сцена, а также экземпляры мобов в нижней части.

image23

В верхней части находятся автозагружаемый MusicPlayer и корневой узел root, который является экраном вашей игры.

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

Это полный скрипт Main.gd для справки.

extends Node

export (PackedScene) var mob_scene


func _ready():
    randomize()
    $UserInterface/Retry.hide()


func _unhandled_input(event):
    if event.is_action_pressed("ui_accept") and $UserInterface/Retry.visible:
        get_tree().reload_current_scene()


func _on_MobTimer_timeout():
    var mob = mob_scene.instance()

    var mob_spawn_location = get_node("SpawnPath/SpawnLocation")
    mob_spawn_location.unit_offset = randf()

    var player_position = $Player.transform.origin
    mob.initialize(mob_spawn_location.translation, player_position)

    add_child(mob)
    mob.connect("squashed", $UserInterface/ScoreLabel, "_on_Mob_squashed")


func _on_Player_hit():
    $MobTimer.stop()
    $UserInterface/Retry.show()