Coding the player

In this lesson, we'll add player movement, animation, and set it up to detect collisions.

To do so, we need to add some functionality that we can't get from a built-in node, so we'll add a script. Click the Player node and click the "Attach Script" button:

../../_images/add_script_button.png

В окне настроек скрипта вы можете оставить все настройки по умолчанию. Просто нажмите "Создать":

Примечание

Если вы создаете скрипт на C# или другом языке, то перед созданием выберете этот язык в выпадающем меню Язык.

../../_images/attach_node_window.png

Примечание

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

Начните с объявления переменных - членов, которые понадобятся этому объекту:

extends Area2D

export var speed = 400 # How fast the player will move (pixels/sec).
var screen_size # Size of the game window.

Использование ключевого слова export у первой переменной speed позволяет устанавливать ее значение в Инспекторе. Это может быть полезно, если вы хотите изменять значения точно так же как и встроенные свойства узла. Щелкните на узел Player и вы увидите, что свойство появилось в разделе "Script Variables" в Инспекторе. Помните, что если изменить значение здесь, то оно перезапишет значение, установленное в скрипте.

Предупреждение

Если вы используете C#, вам нужно (пере)собирать сборки проекта всякий раз, когда вы хотите увидеть новые экспортируемые переменные или сигналы. Эта сборка может быть запущена вручную путем нажатия на слово "Mono" в нижней части окна редактора, чтобы открыть Mono Panel, а затем на кнопку "Build Project".

../../_images/export_variable.png

Функция _ready () вызывается, когда узел входит в дерево сцены, что является хорошим моментом для определения размера игрового окна:

func _ready():
    screen_size = get_viewport_rect().size

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

  • Проверка ввода.

  • Перемещение в заданном направлении.

  • Воспроизвести соответствующую анимацию.

First, we need to check for input - is the player pressing a key? For this game, we have 4 direction inputs to check. Input actions are defined in the Project Settings under "Input Map". Here, you can define custom events and assign different keys, mouse events, or other inputs to them. For this game, we will map the arrow keys to the four directions.

Click on Project -> Project Settings to open the project settings window and click on the Input Map tab at the top. Type "move_right" in the top bar and click the "Add" button to add the move_right action.

../../_images/input-mapping-add-action.png

We need to assign a key to this action. Click the "+" icon on the right, then click the "Key" option in the drop-down menu. A dialog asks you to type in the desired key. Press the right arrow on your keyboard and click "Ok".

../../_images/input-mapping-add-key.png

Repeat these steps to add three more mappings:

  1. move_left соответствует стрелке влево.

  2. move_up соответствует стрелке вверх.

  3. А move_down соответствует стрелке вниз.

Your input map tab should look like this:

../../_images/input-mapping-completed.png

Нажмите кнопку "Закрыть" чтобы закрыть настройки проекта.

Примечание

We only mapped one key to each input action, but you can map multiple keys, joystick buttons, or mouse buttons to the same input action.

Вы можете определить, нажата ли клавиша с помощью функции Input.is_action_pressed(), которая возвращает true, если клавиша нажата, или false, если нет.

func _process(delta):
    var velocity = Vector2.ZERO # The player's movement vector.
    if Input.is_action_pressed("move_right"):
        velocity.x += 1
    if Input.is_action_pressed("move_left"):
        velocity.x -= 1
    if Input.is_action_pressed("move_down"):
        velocity.y += 1
    if Input.is_action_pressed("move_up"):
        velocity.y -= 1

    if velocity.length() > 0:
        velocity = velocity.normalized() * speed
        $AnimatedSprite.play()
    else:
        $AnimatedSprite.stop()

Начнём с того, что установим значение velocity в (0, 0) - по умолчанию игрок двигаться не должен. Затем, мы проверяем каждый ввод и добавляем/вычитаем значение из velocity, чтобы получить общее направление. Например, если вы одновременно удерживаете right и down, полученный вектор velocity будет (1, 1). В этом случае, поскольку мы добавляем горизонтальное и вертикальное движение, игрок будет двигаться быстрее, чем если бы он перемещался только по горизонтали.

Можно избежать этого, если мы нормализуем скорость, что означает, что мы устанавливаем ее длину на 1 и умножаем на желаемую скорость. Это означает отсутствие более быстрого диагонального движения.

Совет

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

Мы также проверяем, движется ли игрок, чтобы мы могли вызвать play() или stop() на AnimatedSprite.

Совет

$ is shorthand for get_node(). So in the code above, $AnimatedSprite.play() is the same as get_node("AnimatedSprite").play().

In GDScript, $ returns the node at the relative path from the current node, or returns null if the node is not found. Since AnimatedSprite is a child of the current node, we can use $AnimatedSprite.

Теперь, когда у нас есть направление движения, мы можем обновить позицию игрока. Мы также можем использовать clamp(), чтобы он не покинул экран. Clamping означает ограничение движения диапазоном. Добавьте следующее в конец функции _process (убедитесь, что он не имеет отступа под else):

position += velocity * delta
position.x = clamp(position.x, 0, screen_size.x)
position.y = clamp(position.y, 0, screen_size.y)

Совет

Параметр "delta" в функции "_process()" означает длительность кадра, то есть время, потраченное на завершение обработки предыдущего кадра. Благодаря использованию этого значения скорость движения будет постоянной даже при изменении частоты кадров.

Нажмите "Запустить сцену" (F6, Cmd + R on macOS) и удостоверьтесь, что вы можете перемещать игрока по экрану во всех направлениях.

Предупреждение

Если вы получаете ошибку в панели "Отладчик", которая говорит

Попытка вызова функции 'play' в основании 'null instance' на нулевом экземпляре

это, скорее всего, означает, что вы ввели название узла AnimatedSprite неверно. Имена узлов чувствительны к регистру, а $NodeName должен совпадать с именем, которое вы видите в дереве сцены.

Выбор анимации

Теперь, когда игрок может двигаться, нам нужно изменять анимацию AnimatedSprite в зависимости от направления движения. У нас есть анимация "walk", которая показывает игрока, идущего направо. Эту анимацию следует перевернуть горизонтально, используя свойство flip_h для движения влево. У нас также есть анимация "up", которую нужно перевернуть вертикально с помощью flip_v для движения вниз. Поместим этот код в конец функции _process ():

if velocity.x != 0:
    $AnimatedSprite.animation = "walk"
    $AnimatedSprite.flip_v = false
    # See the note below about boolean assignment.
    $AnimatedSprite.flip_h = velocity.x < 0
elif velocity.y != 0:
    $AnimatedSprite.animation = "up"
    $AnimatedSprite.flip_v = velocity.y > 0

Примечание

Логические присваивания в коде выше являются общим сокращением для программистов. Поскольку мы проводим проверку сравнения (логическую, булеву), а также присваиваем булево значение, мы можем делать и то, и другое одновременно. Рассмотрим этот код в сравнении с однострочным логическим присваиванием выше:

if velocity.x < 0:
    $AnimatedSprite.flip_h = true
else:
    $AnimatedSprite.flip_h = false

Воспроизведите сцену еще раз и проверьте правильность анимации в каждом из направлений.

Совет

Общей ошибкой является неправильное именование анимаций. Имена анимаций в панели SpriteFrames должны совпадать с именами анимаций в вашем коде. Если вы назвали анимацию "Walk", вы должны также использовать заглавную букву "W" в коде.

Если вы уверены, что движение работает правильно, добавьте эту строку в _ready(), чтобы игрок был скрыт при запуске игры:

hide()

Подготовка к столкновениям

Мы хотим, чтобы Player обнаруживал столкновение с врагом, но мы еще не сделали никаких врагов! Это нормально, потому что мы будем использовать такой функционал Godot, как сигнал.

Добавьте следующее в верх скрипта после extends Area2d:

signal hit

Это определяет пользовательский сигнал под названием "hit" ("удар"), который наш игрок будет излучать (отправлять), когда он сталкивается с противником. Мы будем использовать Area2D для обнаружения столкновения. Выберите узел Player ("Игрок") и щелкните по вкладке "Узел" (Node) рядом с вкладкой "Инспектор" (Inspector), чтобы просмотреть список сигналов, которые игрок может посылать:

../../_images/player_signals.png

Обратите внимание, что наш пользовательский сигнал "hit" там также есть! Поскольку наши противники будут узлами RigidBody2D, нам нужен сигнал body_entered(body: Node). Он будет отправляться при контакте тела (body) с игроком. Нажмите "Присоединить..." - появится окно "Подключить сигнал к методу". Нам не нужно изменять какие-либо из этих настроек, поэтому еще раз нажмите "Присоединить". Godot автоматически создаст функцию в скрипте вашего игрока.

../../_images/player_signal_connection.png

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

func _on_Player_body_entered(body):
    hide() # Player disappears after being hit.
    emit_signal("hit")
    # Must be deferred as we can't change physics properties on a physics callback.
    $CollisionShape2D.set_deferred("disabled", true)

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

Примечание

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

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

func start(pos):
    position = pos
    show()
    $CollisionShape2D.disabled = false

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