Character animation¶
В этом заключительном уроке мы используем встроенные в Godot инструменты анимации, чтобы заставить наших персонажей плавать и махать руками. Вы научитесь создавать анимацию в редакторе и использовать код, чтобы ваша игра стала живой.
Мы начнем с введения в использование редактора анимации.
Использование редактора анимаций¶
Движок поставляется с инструментами для создания анимаций в редакторе. Затем вы можете использовать код для воспроизведения и управления ими во время выполнения.
Open the player scene, select the player node, and add an AnimationPlayer node.
На нижней панели появится вкладка Animation.
В ней есть панель инструментов и раскрывающееся меню анимации в верхней части, редактор дорожек в середине, который в настоящее время пуст, и опции фильтрации, привязки и масштабирования в нижней части.
Давайте создадим анимацию. Щёлкните на Animation -> New.
Назовите анимацию "float".
После создания анимации появляется временная шкала с цифрами, обозначающими время в секундах.
Мы хотим, чтобы анимация начинала воспроизводиться автоматически в начале игры. Кроме того, она должна быть зациклена.
Для этого можно нажать кнопку с пиктограммой "A+" на панели инструментов анимации и зацикленные стрелки соответственно.
Вы также можете закрепить редактор анимации, нажав на значок булавки в правом верхнем углу. Это предотвратит его сворачивание при нажатии на область просмотра и отмене выбора узлов.
Установите длительность анимации 1.2
секунды в правом верхнем углу панели.
Вы должны увидеть, как серая лента немного расширится. Она показывает начало и конец вашей анимации, а вертикальная синяя линия - это ваш временной курсор.
You can click and drag the slider in the bottom-right to zoom in and out of the timeline.
The float animation¶
С помощью узла анимации игрока вы можете анимировать большинство свойств на стольких узлах, сколько вам нужно. Обратите внимание на значок ключа рядом со свойствами в инспекторе. Вы можете щёлкнуть любой из них, чтобы создать ключевой кадр - пару времени и значения для соответствующего свойства. Ключевой кадр будет вставлен туда, где находится курсор времени на временной шкале.
Давайте вставим наши первые ключи. Здесь мы будем анимировать как перемещение, так и поворот узла Character.
Выберите Character и нажмите на значок ключа рядом с Translation в инспекторе . Сделайте то же самое для Rotation Degrees.
В редакторе появятся две дорожки с ромбовидным значком, обозначающим каждый ключевой кадр.
Вы можете щёлкнуть и перетащить ромбы, чтобы переместить их во времени. Переместите ключ перемещения на 0.2
секунды, а ключ вращения на 0.1
секунды.
Переместите курсор времени на 0.5
секунды, щёлкнув и перетащив его на серой временной шкале. В инспекторе установите Translation по оси Y примерно на 0.65
метров, а Rotation Degrees по оси X на 8
.
Создайте ключевой кадр для обоих свойств и сдвиньте ключ перемещения на 0.7
секунд, перетащив его на временной шкале.
Примечание
Лекция о принципах анимации выходит за рамки данного руководства. Просто отметим, что вы не хотите равномерно распределять время и пространство. Вместо этого аниматоры играют с таймингом и интервалом - двумя основными принципами анимации. Вы хотите смещать и контрастировать движения персонажа, чтобы сделать их живыми.
Переместите курсор времени в конец анимации, на 1.2
секунд. Установите перемещение по Y примерно на 0.35
и поворот по X на -9
градусов. Снова создайте ключ для обоих свойств.
Вы можете просмотреть результат, нажав на кнопку воспроизведения или нажав Shift + D. Нажмите кнопку "Stop" или нажмите S, чтобы остановить воспроизведение.
Вы можете видеть, что движок интерполируется между вашими ключевыми кадрами для создания непрерывной анимации. Однако на данный момент движение кажется очень роботизированным. Это происходит потому, что интерполяция по умолчанию линейная, что приводит к постоянным переходам, в отличие от того, как живые существа двигаются в реальном мире.
Мы можем управлять переходом между ключевыми кадрами с помощью кривых смягчения.
Щёлкните и перетащите вокруг первых двух ключей на временной шкале, чтобы выделить их.
Вы можете редактировать свойства обеих ключей одновременно в инспекторе, где вы можете увидеть свойство Easing.
Щёлкните и перетащите кривую, потянув её влево. Это заставит её ослабнуть, то есть сначала переход будет быстрым, а затем замедлится, когда курсор времени достигнет следующего ключевого кадра.
Воспроизведите анимацию ещё раз, чтобы увидеть разницу. Первая половина уже должна казаться немного более подпрыгивающей.
Примените ослабление ко второму ключевому кадру на дорожке вращения.
Проделайте обратные действия для второго ключевого кадра перемещения, перетащив его вправо.
Your animation should look something like this.
Примечание
Анимации обновляют свойства анимируемых узлов каждый кадр, переопределяя начальные значения. Если бы мы напрямую анимировали узел Player, это не позволило бы нам перемещать его в коде. Именно здесь пригодится узел Pivot: даже если мы анимировали Character, мы все равно можем перемещать и вращать Pivot и накладывать изменения поверх анимации в сценарии.
Если вы играете в игру, то существо игрока теперь будет плавать!
Если существо находится слишком близко к полу, вы можете переместить Pivot вверх, чтобы сместить его.
Controlling the animation in code¶
Мы можем использовать код для управления воспроизведением анимации на основе ввода данных игроком. Давайте изменим скорость анимации, когда персонаж движется.
Откройте скрипт Player, нажав на значок скрипта рядом с ним.
В _physics_process()
, после строки, где мы проверяем вектор direction
, добавьте следующий код.
func _physics_process(delta):
#...
#if direction != Vector3.ZERO:
#...
$AnimationPlayer.playback_speed = 4
else:
$AnimationPlayer.playback_speed = 1
public override void _PhysicsProcess(float delta)
{
// ...
if (direction != Vector3.Zero)
{
// ...
GetNode<AnimationPlayer>("AnimationPlayer").PlaybackSpeed = 4;
}
else
{
GetNode<AnimationPlayer>("AnimationPlayer").PlaybackSpeed = 1;
}
}
Этот код делает так, что когда игрок движется, мы умножаем скорость воспроизведения на 4
. Когда они останавливаются, мы восстанавливаем нормальное значение.
Мы упоминали, что pivot может накладывать трансформации поверх анимации. Мы можем сделать дугу персонажа при прыжке с помощью следующей строки кода. Добавьте её в конец _physics_process()
.
func _physics_process(delta):
#...
$Pivot.rotation.x = PI / 6 * velocity.y / jump_impulse
public override void _PhysicsProcess(float delta)
{
// ...
var pivot = GetNode<Spatial>("Pivot");
pivot.Rotation = new Vector3(Mathf.Pi / 6f * _velocity.y / JumpImpulse, pivot.Rotation.y, pivot.Rotation.z);
}
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
public void Initialize(Vector3 startPosition, Vector3 playerPosition)
{
// ...
GetNode<AnimationPlayer>("AnimationPlayer").PlaybackSpeed = randomSpeed / MinSpeed;
}
И на этом вы закончили написание кода для своей первой полноценной 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()
public class Player : KinematicBody
{
// Emitted when the player was hit by a mob.
[Signal]
public delegate void Hit();
// How fast the player moves in meters per second.
[Export]
public int Speed = 14;
// The downward acceleration when in the air, in meters per second squared.
[Export]
public int FallAcceleration = 75;
// Vertical impulse applied to the character upon jumping in meters per second.
[Export]
public int JumpImpulse = 20;
// Vertical impulse applied to the character upon bouncing over a mob in meters per second.
[Export]
public int BounceImpulse = 16;
private Vector3 _velocity = Vector3.Zero;
public override void _PhysicsProcess(float delta)
{
var direction = Vector3.Zero;
if (Input.IsActionPressed("move_right"))
{
direction.x += 1f;
}
if (Input.IsActionPressed("move_left"))
{
direction.x -= 1f;
}
if (Input.IsActionPressed("move_back"))
{
direction.z += 1f;
}
if (Input.IsActionPressed("move_forward"))
{
direction.z -= 1f;
}
if (direction != Vector3.Zero)
{
direction = direction.Normalized();
GetNode<Spatial>("Pivot").LookAt(Translation + direction, Vector3.Up);
GetNode<AnimationPlayer>("AnimationPlayer").PlaybackSpeed = 4;
}
else
{
GetNode<AnimationPlayer>("AnimationPlayer").PlaybackSpeed = 1;
}
_velocity.x = direction.x * Speed;
_velocity.z = direction.z * Speed;
// Jumping.
if (IsOnFloor() && Input.IsActionJustPressed("jump"))
{
_velocity.y += JumpImpulse;
}
_velocity.y -= FallAcceleration * delta;
_velocity = MoveAndSlide(_velocity, Vector3.Up);
for (int index = 0; index < GetSlideCount(); index++)
{
KinematicCollision collision = GetSlideCollision(index);
if (collision.Collider is Mob mob && mob.IsInGroup("mob"))
{
if (Vector3.Up.Dot(collision.Normal) > 0.1f)
{
mob.Squash();
_velocity.y = BounceImpulse;
}
}
}
var pivot = GetNode<Spatial>("Pivot");
pivot.Rotation = new Vector3(Mathf.Pi / 6f * _velocity.y / JumpImpulse, pivot.Rotation.y, pivot.Rotation.z);
}
private void Die()
{
EmitSignal(nameof(Hit));
QueueFree();
}
public void OnMobDetectorBodyEntered(Node 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()
public class Mob : KinematicBody
{
// Emitted when the played jumped on the mob.
[Signal]
public delegate void Squashed();
// Minimum speed of the mob in meters per second
[Export]
public int MinSpeed = 10;
// Maximum speed of the mob in meters per second
[Export]
public int MaxSpeed = 18;
private Vector3 _velocity = Vector3.Zero;
public override void _PhysicsProcess(float delta)
{
MoveAndSlide(_velocity);
}
public void Initialize(Vector3 startPosition, Vector3 playerPosition)
{
LookAtFromPosition(startPosition, playerPosition, Vector3.Up);
RotateY((float)GD.RandRange(-Mathf.Pi / 4.0, Mathf.Pi / 4.0));
float randomSpeed = (float)GD.RandRange(MinSpeed, MaxSpeed);
_velocity = Vector3.Forward * randomSpeed;
_velocity = _velocity.Rotated(Vector3.Up, Rotation.y);
GetNode<AnimationPlayer>("AnimationPlayer").PlaybackSpeed = randomSpeed / MinSpeed;
}
public void Squash()
{
EmitSignal(nameof(Squashed));
QueueFree();
}
public void OnVisibilityNotifierScreenExited()
{
QueueFree();
}
}