Анимация персонажей

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

image0

Мы начнем с введения в использование редактора анимации.

Использование редактора анимаций

Движок поставляется с инструментами для создания анимаций в редакторе. Затем вы можете использовать код для воспроизведения и управления ими во время выполнения.

Откройте сцену игрока, выберите узел игрока и добавьте узел AnimationPlayer.

На нижней панели появится вкладка Animation.

image1

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

Давайте создадим анимацию. Щёлкните на Animation -> New.

image2

Назовите анимацию "float".

image3

После создания анимации появляется временная шкала с цифрами, обозначающими время в секундах.

image4

Мы хотим, чтобы анимация начинала воспроизводиться автоматически в начале игры. Кроме того, она должна быть зациклена.

Для этого можно нажать кнопку с пиктограммой "A+" на панели инструментов анимации и зацикленные стрелки соответственно.

image5

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

image6

Установите длительность анимации 1.2 секунды в правом верхнем углу панели.

image7

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

image8

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

image9

Плавающая анимация

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

Давайте вставим наши первые ключи. Здесь мы будем анимировать как перемещение, так и поворот узла Character.

Выберите Character и нажмите на значок ключа рядом с Translation в инспекторе . Сделайте то же самое для Rotation Degrees.

image10

В редакторе появятся две дорожки с ромбовидным значком, обозначающим каждый ключевой кадр.

image11

Вы можете щёлкнуть и перетащить ромбы, чтобы переместить их во времени. Переместите ключ перемещения на 0.2 секунды, а ключ вращения на 0.1 секунды.

image12

Переместите курсор времени на 0.5 секунды, щёлкнув и перетащив его на серой временной шкале. В инспекторе установите Translation по оси Y примерно на 0.65 метров, а Rotation Degrees по оси X на 8.

image13

Создайте ключевой кадр для обоих свойств и сдвиньте ключ перемещения на 0.7 секунд, перетащив его на временной шкале.

image14

Примечание

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

Переместите курсор времени в конец анимации, на 1.2 секунд. Установите перемещение по Y примерно на 0.35 и поворот по X на -9 градусов. Снова создайте ключ для обоих свойств.

Вы можете просмотреть результат, нажав на кнопку воспроизведения или нажав Shift + D. Нажмите кнопку "Stop" или нажмите S, чтобы остановить воспроизведение.

image15

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

Мы можем управлять переходом между ключевыми кадрами с помощью кривых смягчения.

Щёлкните и перетащите вокруг первых двух ключей на временной шкале, чтобы выделить их.

image16

Вы можете редактировать свойства обеих ключей одновременно в инспекторе, где вы можете увидеть свойство Easing.

image17

Щёлкните и перетащите кривую, потянув её влево. Это заставит её ослабнуть, то есть сначала переход будет быстрым, а затем замедлится, когда курсор времени достигнет следующего ключевого кадра.

image18

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

Примените ослабление ко второму ключевому кадру на дорожке вращения.

|изображение19|

Проделайте обратные действия для второго ключевого кадра перемещения, перетащив его вправо.

|изображение20|

Ваша анимация должна выглядеть примерно так.

|изображение21|

Примечание

Анимации обновляют свойства анимируемых узлов каждый кадр, переопределяя начальные значения. Если бы мы напрямую анимировали узел Player, это не позволило бы нам перемещать его в коде. Именно здесь пригодится узел Pivot: даже если мы анимировали Character, мы все равно можем перемещать и вращать Pivot и накладывать изменения поверх анимации в сценарии.

Если вы играете в игру, то существо игрока теперь будет плавать!

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

Управление анимацией в коде

Мы можем использовать код для управления воспроизведением анимации на основе ввода данных игроком. Давайте изменим скорость анимации, когда персонаж движется.

Откройте скрипт Player, нажав на значок скрипта рядом с ним.

|изображение22|

В _physics_process(), после строки, где мы проверяем вектор direction, добавьте следующий код.

func _physics_process(delta):
    #...
    #if direction != Vector3.ZERO:
        #...
        $AnimationPlayer.playback_speed = 4
    else:
        $AnimationPlayer.playback_speed = 1

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

Мы упоминали, что pivot может накладывать трансформации поверх анимации. Мы можем сделать дугу персонажа при прыжке с помощью следующей строки кода. Добавьте её в конец _physics_process().

func _physics_process(delta):
    #...
    $Pivot.rotation.x = PI / 6 * velocity.y / jump_impulse

Анимация мобов

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

Например, обе сцены Mob и Player имеют узел Pivot и узел Character, поэтому мы можем повторно использовать анимацию между ними.

Откройте сцену Player, выберите узел animation player и откройте анимацию "float". Затем нажмите на Animation > Copy. Затем откройте Mob.tscn и откройте его проигрыватель анимации. Нажмите Animation > Paste. Вот и все; теперь все монстры будут воспроизводить анимацию "float".

Мы можем изменить скорость воспроизведения в зависимости от random_speed существа. Откройте скрипт Mob и в конце функции initialize() добавьте следующую строку.

func initialize(start_position, player_position):
    #...
    $AnimationPlayer.playback_speed = random_speed / min_speed

И на этом вы закончили написание кода для своей первой полноценной 3D-игры.

Поздравляем!

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

Вот скрипт Player.

extends KinematicBody

# Emitted when the player was hit by a mob.
signal hit

# How fast the player moves in meters per second.
export var speed = 14
# The downward acceleration when in the air, in meters per second per second.
export var fall_acceleration = 75
# Vertical impulse applied to the character upon jumping in meters per second.
export var jump_impulse = 20
# Vertical impulse applied to the character upon bouncing over a mob in meters per second.
export var bounce_impulse = 16

var velocity = Vector3.ZERO


func _physics_process(delta):
    var direction = Vector3.ZERO

    if Input.is_action_pressed("move_right"):
        direction.x += 1
    if Input.is_action_pressed("move_left"):
        direction.x -= 1
    if Input.is_action_pressed("move_back"):
        direction.z += 1
    if Input.is_action_pressed("move_forward"):
        direction.z -= 1

    if direction != Vector3.ZERO:
        direction = direction.normalized()
        $Pivot.look_at(translation + direction, Vector3.UP)
        $AnimationPlayer.playback_speed = 4
    else:
        $AnimationPlayer.playback_speed = 1

    velocity.x = direction.x * speed
    velocity.z = direction.z * speed

    # Jumping
    if is_on_floor() and Input.is_action_just_pressed("jump"):
        velocity.y += jump_impulse

    velocity.y -= fall_acceleration * delta
    velocity = move_and_slide(velocity, Vector3.UP)

    for index in range(get_slide_count()):
        var collision = get_slide_collision(index)
        if collision.collider.is_in_group("mob"):
            var mob = collision.collider
            if Vector3.UP.dot(collision.normal) > 0.1:
                mob.squash()
                velocity.y = bounce_impulse

    $Pivot.rotation.x = PI / 6 * velocity.y / jump_impulse


func die():
    emit_signal("hit")
    queue_free()


func _on_MobDetector_body_entered(_body):
    die()

И скрипт Mob.

extends KinematicBody

# Emitted when the player jumped on the mob.
signal squashed

# Minimum speed of the mob in meters per second.
export var min_speed = 10
# Maximum speed of the mob in meters per second.
export var max_speed = 18

var velocity = Vector3.ZERO


func _physics_process(_delta):
    move_and_slide(velocity)


func initialize(start_position, player_position):
    look_at_from_position(start_position, player_position, Vector3.UP)
    rotate_y(rand_range(-PI / 4, PI / 4))

    var random_speed = rand_range(min_speed, max_speed)
    velocity = Vector3.FORWARD * random_speed
    velocity = velocity.rotated(Vector3.UP, rotation.y)

    $AnimationPlayer.playback_speed = random_speed / min_speed


 func squash():
    emit_signal("squashed")
    queue_free()


func _on_VisibilityNotifier_screen_exited():
    queue_free()