Work in progress

The content of this page was not yet updated for Godot 4.2 and may be outdated. If you know how to improve this page or you can confirm that it's up to date, feel free to open a pull request.

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

В цьому останньому уроці ми будемо використовувати вбудовані в Godot інструменти анімації, щоб наші персонажі плавали і підстрибували. Ви навчитеся розробляти анімацію в редакторі та використовувати код, щоб ваша гра виглядала живою.

image0

Почнемо з вступу до використання редактора анімації.

Використання редактора анімації

Рушій поставляється з інструментами для створення анімації в редакторі. Потім ви можете використовувати код для відтворення та керування нею під час виконання.

Відкрийте сцену гравця, виберіть вузол Player і додайте вузол AnimationPlayer.

Внизу з'явиться панель Анімація.

image1

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

Давайте створимо анімацію. Натисніть Анімація -> Новий.

image2

Назвіть анімацію "float".

image3

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

image4

Ми хочемо, щоб анімація почала відтворюватися автоматично на початку гри. Крім того, вона має бути зациклена.

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

image5

Ви також можете закріпити редактор анімації, натиснувши значок шпильки у верхньому правому куті. Це запобігає його складанню, коли ви натискаєте на вікно перегляду і знімаєте виділення з вузлів.

Зображення

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

image7

Ви повинні побачити, що сіра полоса трохи розширилася. Вона показує вам початок і кінець вашої анімації, а вертикальна синя позначка - ваш курсор часу.

image8

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

image9

Анімація плавання

За допомогою вузла AnimationPlayer ви можете анімувати більшість властивостей на всіх вузлах, для яких це потрібно. Турайте на значок ключа поруч із властивостями в інспекторі. Ви можете клацнути будь-який з них, щоб створити ключовий кадр, пару з часу та значень для відповідної властивості. Ключовий кадр вставляється там, де курсор часу знаходиться на часовій шкалі.

Вставимо наші перші клавіші. Тут ми анімуватимемо як положення, так і обертання вузла Символ.

Виберіть Символ, а потім клацніть значок ключа поруч із Переклад в інспекторі. Зробіть те саме для ступенів обертання.

image10

../../_images/curves.webp

For this tutorial, just create RESET Track(s) which is the default choice

У редакторі з'являться дві доріжки зі значком ромбика, що представляє кожен ключовий кадр.

image11

Ви можете клацнути і перетягнути діаманти, щоб перемістити їх у часі. Перемістіть клавішу положення на 0.3 секунди, а клавішу обертання на 0.1 секунди.

image12

Move the time cursor to 0.5 seconds by clicking and dragging on the gray timeline.

../../_images/timeline_05_click.webp

В Інспекторі встановіть для осі Position (Позиція) значення 0.65 метрів, а для осі Rotation (Обертання) - 8.

image13

Створіть ключовий кадр для обох властивостей

../../_images/second_keys_both.webp

Тепер перемістіть позиційний ключовий кадр на 0,7 секунди, перетягнувши його на шкалі часу.

image14

Примітка

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

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

../../_images/animation_final_keyframes.webp

Ви можете переглянути результат, натиснувши кнопку відтворення або клавіші Shift + D. Натисніть кнопку зупинки або S, щоб зупинити відтворення.

image15

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

Ми можемо контролювати перехід між ключовими кадрами за допомогою кривих пом'якшення.

Клацніть і обведіть перші два ключа на часовій шкалі, щоб вибрати їх.

image16

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

image17

Клацніть і потягніть криву вліво. Це зробить анімацію легкою, тобто переміщення буде спочатку прискорюватися, а потім сповільнюватися, по мірі наближення курсора часу до наступного ключового кадру.

image18

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

Застосуйте пом'якшення до другого ключового кадру на треку обертання.

image19

Зробіть протилежне для другого позиційного ключового кадру, перетягнувши його праворуч.

image20

Анімація повинна виглядати приблизно так.

image21

Примітка

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

Якщо ви запустите гру персонаж гравця тепер буде плавати!

Якщо істота знаходиться занадто близько до підлоги, ви можете перемістити Pivot вгору, щоб змістити її.

Керування анімацією в коді

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

Відкрийте скрипт Плеєр, натиснувши на іконку скрипта поруч з ним.

image22

В _physics_process(), після рядка, де ми перевіряємо direction вектор, додайте наступний код.

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

Цей код працює таким чином, що коли гравець рухається, ми множимо швидкість відтворення на 4. Коли він зупиняється, оновлюємо її до нормального стану.

Ми згадували, що Pivot може накладати трансформації поверх анімації. Ми можемо зробити дугу символу при стрибку за допомогою наступного рядка коду. Додайте його в кінець _physics_process().

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

Анімація монстрів

Ось ще один приємний трюк з анімацією в Godot: поки ви використовуєте подібну структуру вузлів, ви можете скопіювати її на різні сцени.

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

Відкрийте сцену Player, виберіть вузол AnimationPlayer і відкрийте анімацію "float". Далі натисніть Анімація > Копіювати. Потім відкрийте mob.tscn, створіть дочірній вузол AnimationPlayer і виберіть його. Натисніть Animation > Paste і переконайтеся, що в редакторі анімації на нижній панелі також увімкнені кнопка зі значком "A+" (Autoplay on Load) і стрілки, що зациклюються (Animation looping). Все, тепер усі монстри відтворюватимуть анімацію плавання.

Ми можемо змінити швидкість відтворення на основі швидкості монстра random_speed. Відкрийте скрипт Mob і в кінці функції initialize() додайте наступний рядок.

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

І на цьому ви закінчили кодування своєї першої повної 3D-гри.

Вітаю!

У наступній частині ми швидко підсумуємо те, що ви дізналися, і дамо деякі посилання для подальшого навчання. А на даний момент ви можете перевірити свій код звірившись з повними зразками Player.gd і Mob.gd.

Ось скрипт Player.

extends CharacterBody3D

signal hit

# How fast the player moves in meters per second.
@export var speed = 14
# The downward acceleration while in the air, in meters per second squared.
@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 target_velocity = Vector3.ZERO


func _physics_process(delta):
    # We create a local variable to store the input direction
    var direction = Vector3.ZERO

    # We check for each move input and update the direction accordingly
    if Input.is_action_pressed("move_right"):
        direction.x = direction.x + 1
    if Input.is_action_pressed("move_left"):
        direction.x = direction.x - 1
    if Input.is_action_pressed("move_back"):
        # Notice how we are working with the vector's x and z axes.
        # In 3D, the XZ plane is the ground plane.
        direction.z = direction.z + 1
    if Input.is_action_pressed("move_forward"):
        direction.z = direction.z - 1

    # Prevent diagonal movement being very fast
    if direction != Vector3.ZERO:
        direction = direction.normalized()
        $Pivot.look_at(position + direction,Vector3.UP)
        $AnimationPlayer.speed_scale = 4
    else:
        $AnimationPlayer.speed_scale = 1

    # Ground Velocity
    target_velocity.x = direction.x * speed
    target_velocity.z = direction.z * speed

    # Vertical Velocity
    if not is_on_floor(): # If in the air, fall towards the floor
        target_velocity.y = target_velocity.y - (fall_acceleration * delta)

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

    # Iterate through all collisions that occurred this frame
    # in C this would be for(int i = 0; i < collisions.Count; i++)
    for index in range(get_slide_collision_count()):
        # We get one of the collisions with the player
        var collision = get_slide_collision(index)

        # If the collision is with ground
        if collision.get_collider() == null:
            continue

        # If the collider is with a mob
        if collision.get_collider().is_in_group("mob"):
            var mob = collision.get_collider()
            # we check that we are hitting it from above.
            if Vector3.UP.dot(collision.get_normal()) > 0.1:
                # If so, we squash it and bounce.
                mob.squash()
                target_velocity.y = bounce_impulse
                # Prevent further duplicate calls.
                break

    # Moving the Character
    velocity = target_velocity
    move_and_slide()

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

# And this function at the bottom.
func die():
    hit.emit()
    queue_free()

func _on_mob_detector_body_entered(body):
    die()

І скрипт Mob.

extends CharacterBody3D

# 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

# Emitted when the player jumped on the mob
signal squashed

func _physics_process(_delta):
    move_and_slide()

# This function will be called from the Main scene.
func initialize(start_position, player_position):
    # We position the mob by placing it at start_position
    # and rotate it towards player_position, so it looks at the player.
    look_at_from_position(start_position, player_position, Vector3.UP)
    # Rotate this mob randomly within range of -90 and +90 degrees,
    # so that it doesn't move directly towards the player.
    rotate_y(randf_range(-PI / 4, PI / 4))

    # We calculate a random speed (integer)
    var random_speed = randi_range(min_speed, max_speed)
    # We calculate a forward velocity that represents the speed.
    velocity = Vector3.FORWARD * random_speed
    # We then rotate the velocity vector based on the mob's Y rotation
    # in order to move in the direction the mob is looking.
    velocity = velocity.rotated(Vector3.UP, rotation.y)

    $AnimationPlayer.speed_scale = random_speed / min_speed

func _on_visible_on_screen_notifier_3d_screen_exited():
    queue_free()

func squash():
    squashed.emit()
    queue_free() # Destroy this node