Анімація персонажа¶
В цьому останньому уроці ми будемо використовувати вбудовані в Godot інструменти анімації, щоб наші персонажі плавали і підстрибували. Ви навчитеся розробляти анімацію в редакторі та використовувати код, щоб ваша гра виглядала живою.
Почнемо з вступу до використання редактора анімації.
Використання редактора анімації¶
Рушій поставляється з інструментами для створення анімації в редакторі. Потім ви можете використовувати код для відтворення та керування нею під час виконання.
Відкрийте сцену гравця, виберіть вузол гравця та додайте вузол AnimationPlayer.
Внизу з'явиться панель Анімація.
Вона містить панель інструментів і спливаюче меню анімації вгорі, редактор композицій посередині, який наразі порожній, а також параметри фільтрування, прив'язки та масштабування внизу.
Давайте створимо анімацію. Натисніть Анімація -> Новий.
Назвіть анімацію "float".
Після створення анімації з'явиться часова шкала з числами, що представляють час у секундах.
Ми хочемо, щоб анімація почала відтворюватися автоматично на початку гри. Крім того, вона має бути зациклена.
Для цього можна натиснути кнопку з іконкою "A+" на панелі інструментів анімації і кнопку зі стрілками.
Ви також можете закріпити редактор анімації, натиснувши значок шпильки у верхньому правому куті. Це запобігає його складанню, коли ви натискаєте на вікно перегляду і знімаєте виділення з вузлів.
Зображення
Установіть тривалість анімації на 1.2
секунди у верхньому правому куті панелі.
Ви повинні побачити, що сіра полоса трохи розширилася. Вона показує вам початок і кінець вашої анімації, а вертикальна синя позначка - ваш курсор часу.
Ви можете клацнути та перетягнути повзунок у нижньому правому куті, щоб збільшити та зменшити часову шкалу.
Анімація плавання¶
За допомогою вузла AnimationPlayer ви можете анімувати більшість властивостей на всіх вузлах, для яких це потрібно. Турайте на значок ключа поруч із властивостями в інспекторі. Ви можете клацнути будь-який з них, щоб створити ключовий кадр, пару з часу та значень для відповідної властивості. Ключовий кадр вставляється там, де курсор часу знаходиться на часовій шкалі.
Давайте вставимо наші перші ключі. Тут ми оживимо як переміщення, так і обертання вузла 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. Натисніть кнопку зупинки або S, щоб зупинити відтворення.
Ви можете бачити, що рушій інтерполює між вашими ключовими кадрами, щоб створити безперервну анімацію. На даний момент, однак, рух виглядає дуже роботизованим. Це пояснюється тим, що інтерполяція за замовчуванням є лінійною, викликаючи постійні переходи, на відміну від руху живих істот в реальному світі.
Ми можемо контролювати перехід між ключовими кадрами за допомогою кривих пом'якшення.
Клацніть і обведіть перші два ключа на часовій шкалі, щоб вибрати їх.
Властивості обох ключів можна редагувати одночасно в Інспекторі, де можна побачити властивість Easing.
Клацніть і потягніть криву вліво. Це зробить анімацію легкою, тобто переміщення буде спочатку прискорюватися, а потім сповільнюватися, по мірі наближення курсора часу до наступного ключового кадру.
Відтворіть анімацію ще раз, щоб побачити різницю. У першій половині стрибок вже повинен виглядати краще.
Застосуйте пом'якшення до другого ключового кадру на треку обертання.
Зробіть протилежне для другого ключового кадру переміщення, перетягнувши його вправо.
Анімація повинна виглядати приблизно так.
Примітка
Анімація оновлює властивості анімованих вузлів кожного кадру, перевизначаючи початкові значення. Якби ми анімували безпосередньо вузол Player, це завадило б нам переміщати його в коді. Ось де вузол Pivot стає в нагоді: незважаючи на те, що ми анімували Character, ми все ще можемо переміщати та обертати Pivot вверх з анімацією у скрипті.
Якщо ви запустите гру персонаж гравця тепер буде плавати!
Якщо він знаходиться трохи заблизько до підлоги, ви можете перемістити Pivot вгору, щоб компенсувати це.
Керування анімацією в коді¶
Ми можемо використовувати код для управління відтворенням анімації на основі введення гравця. Давайте змінимо швидкість анімації, коли персонаж рухається.
Відкрийте скрипт 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);
}
Анімація монстрів¶
Ось ще один приємний трюк з анімацією в Godot: поки ви використовуєте подібну структуру вузлів, ви можете скопіювати її на різні сцени.
Наприклад, сцени Mob і Player мають вузол Pivot і Character, тому ми можемо повторно використовувати анімацію між ними.
Відкрийте сцену Player, виберіть вузол анімації гравця та відкрийте анімацію плавання "float". Далі натисніть Анімація -> Копіювати. Потім відкрийте Mob.tscn
та відкрийте його АnimationРlayer. Натисніть кнопку Анімація -> Вставити. Ось і все; всі монстри тепер будуть відтворювати анімацію плавання.
Ми можемо змінити швидкість відтворення на основі швидкості монстра 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();
}
}