Up to date

This page is up to date for Godot 4.2. If you still find outdated information, please open an issue.

Движение Игрока

В этом уроке мы добавим передвижение игрока, анимацию и настроим всё для определения столкновений.

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

../../_images/add_script_button.webp

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

Примечание

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

../../_images/attach_node_window.webp

Примечание

Если вы впервые столкнулись с 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" в Инспекторе. Помните, что если изменить значение здесь, то оно перезапишет значение, установленное в скрипте.

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

If you're using C#, you need to (re)build the project assemblies whenever you want to see new export variables or signals. This build can be manually triggered by clicking the Build button at the top right of the editor.

../../_images/build_dotnet.webp
../../_images/export_variable.webp

Ваш скрипт player.gd уже должен содержать функции _ready() и _process(). Если вы не выбрали шаблон по умолчанию, показанный выше, создайте эти функции, следуя уроку.

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

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

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

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

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

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

Во-первых, мы должны проверить ввод — нажимает ли игрок на клавишу? Для нашей игры нужно проверять 4 направления ввода. Функции ввода определяются в Настройках проекта во вкладке "Список действий". В ней можно определять отдельные события и назначать им различные клавиши, события мыши или другой ввод. В нашей игре мы присвоим клавиши стрелок для четырёх направлений.

Нажмите на Проект -> Настройки проекта, чтобы открыть окно настроек проекта, и нажмите вверху на вкладку Input Map. Введите "move_right" в верхней панели и нажмите на кнопку "Добавить", чтобы добавить действие move_right.

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

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

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

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

../../_images/input-mapping-event-configuration.webp

Нажмите кнопку "ок". Клавиша "вправо" теперь связана с действием move_right.

Повторите эти шаги, чтобы добавить ещё три маппинга:

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

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

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

Вкладка Список действий должна выглядеть так:

../../_images/input-mapping-completed.webp

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

Примечание

Мы присвоили только одну клавишу каждому действию, но Вы можете присвоить тем же самым действиям несколько клавиш, кнопки джойстика или мыши.

Вы можете определить, нажата ли клавиша с помощью функции 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
        $AnimatedSprite2D.play()
    else:
        $AnimatedSprite2D.stop()

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

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

Совет

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

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

Совет

$ является сокращением для get_node(). В приведенном выше коде $AnimatedSprite.play() то же самое, что и 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 AnimatedSprite2D is a child of the current node, we can use $AnimatedSprite2D.

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

position += velocity * delta
position = position.clamp(Vector2.ZERO, screen_size)

Совет

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

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

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

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

``Attempt to call function 'play' in base 'null instance' on a null instance``

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

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

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

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

Примечание

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

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

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

Совет

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

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

hide()

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

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

Добавьте следующее в верхней части скрипта. Если вы используете GDScript, добавьте его после extends Area2D. Если вы используете C#, добавьте его после public partial class Player : Area2D:

signal hit

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

../../_images/player_signals.webp

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

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

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

Если вы пользуетесь внешним текстовым редактором (например, Visual Studio Code), внутренняя ошибка Godot сейчас не позволяет это сделать. Вы будете перенаправлены во внешний редактор, но новая функция там не будет создана.

В этом случае вам надо будет написать функцию в файле скрипта для игрока (Player) самостоятельно.

../../_images/player_signal_connection.webp

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

Затем добавьте этот код в функцию:

func _on_body_entered(body):
    hide() # Player disappears after being hit.
    hit.emit()
    # 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

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