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.
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ó Player
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".
Once you've created the animation, the timeline appears with numbers representing time in seconds.
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ó Character
.
Selecione o Character
e no Inspetor expanda a seção Transform. Clique no ícone de chave ao lado de Position e Rotation.
Para este tutorial, basta criar uma faixa RESET que é a escolha padrão
Duas faixas aparecem no editor com um ícone de diamante representando cada quadro-chave.
You can click and drag on the diamonds to move them in time. Move the
position key to 0.3
seconds and the rotation key to 0.1
seconds.
Mova o cursor de tempo para 0.5
segundos clicando e arrastando na linha do tempo cinza.
No Inspetor, defina o eixo Position Y para os metros 0.65
e o eixo Rotation X para 8
.
Crie um quadro-chave para ambas propriedades
Agora, mova o quadro-chave de posiçã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 posiçã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 posiçã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 Player
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.speed_scale = 4
else:
$AnimationPlayer.speed_scale = 1
public override void _PhysicsProcess(double delta)
{
// ...
if (direction != Vector3.Zero)
{
// ...
GetNode<AnimationPlayer>("AnimationPlayer").SpeedScale = 4;
}
else
{
GetNode<AnimationPlayer>("AnimationPlayer").SpeedScale = 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 Pivot``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(double delta)
{
// ...
var pivot = GetNode<Node3D>("Pivot");
pivot.Rotation = new Vector3(Mathf.Pi / 6.0f * 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 Mob
e Player
têm um nó Pivot
e Character
, para que possamos reutilizar animações entre elas.
Abra a cena Player, selecione o nó AnimationPlayer e abra a animação "float". Em seguida, clique em Animação > Entendido. Em seguida, abra ``mob.tscn``, crie um nó filho AnimationPlayer e selecione-o. Clique **Animation > Paste e certifique-se de que o botão com um ícone "A+" (Autoplay on Load) e as setas de looping (Animation looping) também estão ligados no editor de animação no painel inferior. É isso; todos os monstros vão agora executar a animação flutuante.
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.speed_scale = random_speed / min_speed
public void Initialize(Vector3 startPosition, Vector3 playerPosition)
{
// ...
GetNode<AnimationPlayer>("AnimationPlayer").SpeedScale = 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 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()
using Godot;
public partial class Player : CharacterBody3D
{
// Emitted when the player was hit by a mob.
[Signal]
public delegate void HitEventHandler();
// How fast the player moves in meters per second.
[Export]
public int Speed { get; set; } = 14;
// The downward acceleration when in the air, in meters per second squared.
[Export]
public int FallAcceleration { get; set; } = 75;
// Vertical impulse applied to the character upon jumping in meters per second.
[Export]
public int JumpImpulse { get; set; } = 20;
// Vertical impulse applied to the character upon bouncing over a mob in meters per second.
[Export]
public int BounceImpulse { get; set; } = 16;
private Vector3 _targetVelocity = Vector3.Zero;
public override void _PhysicsProcess(double 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.IsActionPressed("move_right"))
{
direction.X += 1.0f;
}
if (Input.IsActionPressed("move_left"))
{
direction.X -= 1.0f;
}
if (Input.IsActionPressed("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 += 1.0f;
}
if (Input.IsActionPressed("move_forward"))
{
direction.Z -= 1.0f;
}
// Prevent diagonal movement being very fast.
if (direction != Vector3.Zero)
{
direction = direction.Normalized();
GetNode<Node3D>("Pivot").LookAt(Position + direction, Vector3.Up);
GetNode<AnimationPlayer>("AnimationPlayer").PlaybackSpeed = 4;
}
else
{
GetNode<AnimationPlayer>("AnimationPlayer").PlaybackSpeed = 1;
}
// Ground velocity
_targetVelocity.X = direction.X * Speed;
_targetVelocity.Z = direction.Z * Speed;
// Vertical velocity
if (!IsOnFloor())
{
_targetVelocity.Y -= FallAcceleration * (float)delta;
}
// Jumping.
if (IsOnFloor() && Input.IsActionJustPressed("jump"))
{
_targetVelocity.Y += JumpImpulse;
}
// Iterate through all collisions that occurred this frame.
for (int index = 0; index < GetSlideCollisionCount(); index++)
{
// We get one of the collisions with the player.
KinematicCollision3D collision = GetSlideCollision(index);
// If the collision is with a mob.
if (collision.GetCollider() is Mob mob)
{
// We check that we are hitting it from above.
if (Vector3.Up.Dot(collision.GetNormal()) > 0.1f)
{
// If so, we squash it and bounce.
mob.Squash();
_targetVelocity.Y = BounceImpulse;
// Prevent further duplicate calls.
break;
}
}
}
// Moving the character
Velocity = _targetVelocity;
MoveAndSlide();
var pivot = GetNode<Node3D>("Pivot");
pivot.Rotation = new Vector3(Mathf.Pi / 6.0f * Velocity.Y / JumpImpulse, pivot.Rotation.Y, pivot.Rotation.Z);
}
private void Die()
{
EmitSignal(SignalName.Hit);
QueueFree();
}
private void OnMobDetectorBodyEntered(Node body)
{
Die();
}
}
E o script do Inimigo.
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
using Godot;
public partial class Mob : CharacterBody3D
{
// Emitted when the played jumped on the mob.
[Signal]
public delegate void SquashedEventHandler();
// Minimum speed of the mob in meters per second
[Export]
public int MinSpeed { get; set; } = 10;
// Maximum speed of the mob in meters per second
[Export]
public int MaxSpeed { get; set; } = 18;
public override void _PhysicsProcess(double delta)
{
MoveAndSlide();
}
// This function will be called from the Main scene.
public void Initialize(Vector3 startPosition, Vector3 playerPosition)
{
// We position the mob by placing it at startPosition
// and rotate it towards playerPosition, so it looks at the player.
LookAtFromPosition(startPosition, playerPosition, Vector3.Up);
// Rotate this mob randomly within range of -90 and +90 degrees,
// so that it doesn't move directly towards the player.
RotateY((float)GD.RandRange(-Mathf.Pi / 4.0, Mathf.Pi / 4.0));
// We calculate a random speed (integer).
int randomSpeed = GD.RandRange(MinSpeed, MaxSpeed);
// We calculate a forward velocity that represents the speed.
Velocity = Vector3.Forward * randomSpeed;
// 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);
GetNode<AnimationPlayer>("AnimationPlayer").SpeedScale = randomSpeed / MinSpeed;
}
public void Squash()
{
EmitSignal(SignalName.Squashed);
QueueFree(); // Destroy this node
}
private void OnVisibilityNotifierScreenExited()
{
QueueFree();
}
}