Character animation

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

image0

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

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

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

Open the player scene, select the player node, and add an AnimationPlayer node.

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

image1

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

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

image2

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

image3

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

image4

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

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

image5

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

image6

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

image7

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

image8

You can click and drag the slider in the bottom-right to zoom in and out of the timeline.

image9

The float animation

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

Давайте вставим наши первые ключи. Здесь мы будем анимировать как перемещение, так и поворот узла 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

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

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

image19

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

image20

Your animation should look something like this.

image21

Примечание

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

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

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

Controlling the animation in code

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

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

image22

В _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

Animating the mobs

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

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

Open the Player scene, select the animation player node and open the "float" animation. Next, click on Animation > Copy. Then open Mob.tscn and open its animation player. Click Animation > Paste. That's it; all monsters will now play the float animation.

Мы можем изменить скорость воспроизведения в зависимости от 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()