Animação de personagem¶
Nesta lição final, usaremos as ferramentas de animação embutidas do Godot para fazer nossos personagens flutuarem e baterem asas. Você aprenderá a criar animações no editor e usar o código para dar vida ao seu jogo.
Começaremos com uma introdução para usar o editor de animação.
Usando o editor de animação¶
A engine vem com ferramentas para criar animações no editor. Você pode então usar o código para executá-las e controlá-los em tempo de execução.
Abra a cena do jogador, selecione o nó jogador e adicione um nó AnimationPlayer.
O painel Animação aparece no painel inferior.
Ele apresenta uma barra de ferramentas e o menu suspenso de animação na parte superior, um editor de pistas no faixa que está atualmente vazio, e opções de filtro, snap e zoom na parte inferior.
Vamos criar uma animação. Clique em Animação -> Novo.
Chame a animação de "float".
Depois de ter criado a animação, a linha do tempo aparece com números que representam o tempo em segundos.
Queremos que a animação inicie a reprodução automaticamente no início do jogo. Além disso, ele deve fazer um loop.
Para fazer isso, você pode clicar no botão com um ícone "A +" na barra de ferramentas de animação e nas setas em loop, respectivamente.
Você também pode fixar o editor de animação clicando no ícone de fixação no canto superior direito. Isso evita que ele se dobre quando você clica no visor e desmarca os nós.
Defina a duração da animação para 1.2
segundos na parte superior direita do painel.
Você deve ver a faixa cinza se alargar um pouco. Ela mostra o início e o fim da animação e a linha azul vertical é o cursor de tempo.
Você pode clicar e arrastar o controle deslizante no canto inferior direito para aumentar e diminuir o zoom da linha do tempo.
A animação fluida¶
Com o nó do reprodutor de animação, você pode animar a maioria das propriedades em quantos nós precisar. Observe o ícone de chave ao lado das propriedades no Inspetor. Você pode clicar em qualquer um deles para criar um quadro-chave, um par de tempo e valor para a propriedade correspondente. O quadro-chave é inserido onde seu cursor de tempo está na linha do tempo.
Vamos inserir nossas primeiras chaves. Aqui, animaremos a translação e a rotação do nó Personagem.
Selecione o Personagem e clique no ícone de chave ao lado de Translação no Inspetor. Faça o mesmo para Graus de rotação.
Duas faixas aparecem no editor com um ícone de diamante representando cada quadro-chave.
Você pode clicar e arrastar os diamantes para movê-los no tempo. Mova a chave de translação para 0.2
segundos e a chave de rotação para 0.1
segundos.
Mova o cursor de tempo para 0.5
segundos clicando e arrastando na linha do tempo cinza. No Inspector, defina o eixo Y de Translação para cerca de 0.65
metros e o eixo X de Graus de rotação' para 8
.
Crie um quadro-chave para ambas as propriedades e mude a chave de tradução para 0,7
segundos arrastando-o na linha do tempo.
Nota
Uma palestra sobre os princípios da animação está além do escopo deste tutorial. Apenas observe que você não deseja cronometrar e espaçar tudo uniformemente. Em vez disso, os animadores brincam com o tempo e o espaçamento, dois princípios básicos da animação. Você quer variar e contrastar o movimento do seu personagem para fazê-lo parecer vivo.
Mova o cursor de tempo para o final da animação, em 1.2
segundos. Defina a translação Y para cerca de 0.35
e a rotação X para -9
graus. Mais uma vez, crie uma chave para ambas as propriedades.
Você pode visualizar o resultado clicando no botão play ou pressionando Shift + D. Clique no botão parar ou pressione S para parar a reprodução.
Você pode ver que a engine interpola entre seus quadros-chave para produzir uma animação contínua. No momento, porém, o movimento parece muito robótico. Isso ocorre porque a interpolação padrão é linear, causando transições constantes, ao contrário de como os seres vivos se movem no mundo real.
Podemos controlar a transição entre quadros-chave usando curvas de suavisação.
Clique e arraste ao redor das duas primeiras chaves na linha do tempo para usar a seleção em retângulo.
Você pode editar as propriedades de ambas as chaves simultaneamente no Inspector, onde você pode ver uma propriedade de*Suavisação*.
Clique e arraste sobre a curva, puxando-a para a esquerda. Isso suavizará, ou seja, a transição será rápida inicialmente e desacelerada quando o cursor de tempo atingir o próximo quadro-chave.
Reproduza a animação novamente para ver a diferença. A primeira metade já deve parecer um pouco mais animada.
Aplique uma suavização ao segundo quadro-chave na faixa de rotação.
Faça o oposto para o segundo quadro-chave de translação, arrastando-o para a direita.
Sua animação deve ser algo parecido com isto.
Nota
As animações atualizam as propriedades dos nós animados a cada quadro, substituindo os valores iniciais. Se animarmos diretamente o nó Player, isso nos impediria de movê-lo no código. É aqui que o nó Pivô é útil: mesmo que tenhamos animado o Personagem, ainda podemos mover e girar o Pivô e camada muda sobre a animação em um script.
Se você jogar o jogo, a criatura do jogador vai flutuar agora!
Se a criatura estiver um pouco perto demais do chão, você pode mover o Pivô para cima para deslocá-lo.
Controlando a animação por código¶
Podemos usar o código para controlar a reprodução da animação com base na entrada do jogador. Vamos mudar a velocidade da animação quando o personagem estiver se movendo.
Abra o script do Jogador clicando no ícone de script próximo a ele.
Em _physics_process()
, após a linha onde verificamos o vetor direction
, adicione o seguinte código.
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;
}
}
Este código faz com que quando o jogador se mova, multipliquemos a velocidade de reprodução por 4
. Quando eles param, nós redefinimos para o normal.
Mencionamos que o pivô pode transformar camadas em cima da animação. Podemos fazer o arco do personagem ao pular usando a seguinte linha de código. Adicione-o no final de _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);
}
Animando os inimigos¶
Aqui está outro bom truque com animações no Godot: contanto que você use uma estrutura de nó semelhante, você pode copiá-los para cenas diferentes.
Por exemplo, as cenas Inimigo e Jogador têm um nó Pivô e Personagem, para que possamos reutilizar animações entre elas.
Abra a cena Jogador, selecione o nó do player de animação e abra a animação "float". Em seguida, clique em Animação > Copiar. Em seguida, abra Mob.tscn
e abra seu player de animação. Clique em Animação > Colar. É isso. todos os monstros agora vão reproduzir a animação de flutuar.
Podemos alterar a velocidade de reprodução com base na random_speed
da criatura. Abra o script Inimigo e no final da função initialize()
, adicione a seguinte linha.
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;
}
E com isso, você terminou de programar seu primeiro jogo 3D completo.
Parabéns!
Na próxima parte, vamos recapitular rapidamente o que você aprendeu e dar alguns links para continuar aprendendo mais. Mas por enquanto, aqui estão os Jogador.gd
e Mob.gd
completos para que você possa comparar com seu código.
Aqui está o script do Jogador.
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();
}
}
E o script do Inimigo.
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();
}
}