Pulando e esmagando monstros
In this part, we'll add the ability to jump and squash the monsters. In the next lesson, we'll make the player die when a monster hits them on the ground.
First, we have to change a few settings related to physics interactions. Enter the world of physics layers.
Controlando as interações físicas
Os corpos da física têm acesso a duas propriedades complementares: camadas e máscaras. As camadas definem em que camada(s) de física um objeto está.
As máscaras controlam as camadas que um corpo irá escutar e detectar. Isto afeta a detecção de colisões. Quando se deseja que dois corpos interajam, é necessário que pelo menos um tenha uma máscara correspondente ao outro.
Se isso for confuso, não se preocupe, veremos três exemplos em um segundo.
O ponto importante é que você pode usar camadas e máscaras para filtrar as interações físicas, controlar o desempenho e remover a necessidade de condições extras em seu código.
By default, all physics bodies and areas are set to both layer and mask 1.
This means they all collide with each other.
As camadas físicas são representadas por números, mas podemos dar-lhes nomes para acompanhar o que é o quê.
Definindo os nomes de camadas
Vamos dar um nome às nossas camadas físicas. Vá para Projeto -> Configurações do Projeto.

No menu à esquerda, navegue até Nomes de Camada -> Física 3D. Você pode ver uma lista de camadas com um campo ao lado de cada uma delas à direita. Você pode definir seus nomes lá. Nomeie as três primeiras camadas jogador, inimigos e mundo, respectivamente.

Agora, podemos atribuí-los aos nossos nós de física.
Atribuindo camadas e máscaras
In the Main scene, select the Ground node. In the Inspector, expand the
Collision section. There, you can see the node's layers and masks as a grid of
buttons.

The ground is part of the world, so we want it to be part of the third layer. Click the lit button to toggle off the first Layer and toggle on the third one. Then, toggle off the Mask by clicking on it.

As mentioned before, the Mask property allows a node to listen to interaction
with other physics objects, but we don't need it to have collisions. Ground doesn't need to listen to anything; it's just there to prevent
creatures from falling.
Observe que você pode clicar no botão "..." no lado direito das propriedades para ver uma lista de caixas de seleção nomeadas.

Next up are the Player and the Mob. Open player.tscn by double-clicking
the file in the FileSystem dock.
Select the Player node and set its Collision -> Mask to both "enemies" and "world". You can leave the default Layer property as it is, because the first layer is the "player" layer.

Then, open the Mob scene by double-clicking on mob.tscn and select the
Mob node.
Configurar sua Colisão -> Camada para "inimigos" e desestabilizar sua Colisão -> Máscara, deixando a máscara vazia.
Estas configurações significam que os monstros se moverão uns através dos outros. Se você quiser que os monstros colidam e deslizem uns contra os outros, ligue a máscara de "inimigos".
Nota
Os inimigos não precisam mascarar a camada "mundo" porque só se movem no plano XZ. Nós não aplicamos nenhuma gravidade a eles no desenvolvimento.
Pulando
O próprio mecanismo de salto requer apenas duas linhas de código. Abra o script Player. Precisamos de um valor para controlar a força do salto e atualizar _physics_process() para codificar o salto.
Após a linha que define fall_acceleration, no topo do script, adicione o jump_impulse.
#...
# Vertical impulse applied to the character upon jumping in meters per second.
@export var jump_impulse = 20
// Don't forget to rebuild the project so the editor knows about the new export variable.
// ...
// Vertical impulse applied to the character upon jumping in meters per second.
[Export]
public int JumpImpulse { get; set; } = 20;
Dentro de _physics_process(), adicione o seguinte código antes do bloco de código move_and_slide().
func _physics_process(delta):
#...
# Jumping.
if is_on_floor() and Input.is_action_just_pressed("jump"):
target_velocity.y = jump_impulse
#...
public override void _PhysicsProcess(double delta)
{
// ...
// Jumping.
if (IsOnFloor() && Input.IsActionJustPressed("jump"))
{
_targetVelocity.Y = JumpImpulse;
}
// ...
}
Isso é tudo que você precisa para pular!
O método is_on_floor() é uma ferramenta da classe CharacterBody3D. Ele retorna true se o corpo colidiu com o chão neste quadro. É por isso que aplicamos gravidade ao Player: para colidimos com o chão em vez de flutuar sobre ele como os monstros.
Se o personagem está no chão e o jogador pressiona "saltar", nós instantaneamente damos a eles muita velocidade vertical. Nos jogos, você realmente quer que os controles sejam responsivos e que dão aumentos instantâneos de velocidade como esses, embora irrealistas, são ótimos.
Observe que o eixo Y é positivo para cima. Isso é diferente do 2D, onde o eixo Y é positivo para baixo.
Esmagando monstros
Vamos adicionar a seguir a mecânica de esmagar. Vamos fazer o personagem saltar sobre os monstros e matá-los ao mesmo tempo.
Precisamos detectar colisões com um monstro e diferenciá-las das colisões com o chão. Para fazer isso, podemos usar a recurso de coleção group do Godot.
Open the scene mob.tscn again and select the Mob node. Go to the Node
dock on the right to see a list of signals. The Node dock has two tabs:
Signals, which you've already used, and Groups, which allows you to assign
tags to nodes. Click on the + button to open the Create new Group dialog.

Digite "mob" no campo Name e clique no botão Ok.

O grupo "mob" agora é exibido na seção Scene Groups.

Um ícone aparece no painel Cena para indicar que o nó faz parte de pelo menos um grupo.
![]()
Agora podemos usar o grupo do código para distinguir colisões com monstros de colisões com o chão.
Programando a mecânica de esmagar
Volte para o script Player para programar as ações de esmagar e saltar.
Na parte superior do script, precisamos de outra propriedade, bounce_impulse. Ao esmagar um inimigo, não queremos necessariamente que o personagem vá tão alto quanto ao pular.
# Vertical impulse applied to the character upon bouncing over a mob in
# meters per second.
@export var bounce_impulse = 16
// Don't forget to rebuild the project so the editor knows about the new export variable.
// Vertical impulse applied to the character upon bouncing over a mob in meters per second.
[Export]
public int BounceImpulse { get; set; } = 16;
Em seguida, após o bloco de código Jumping que adicionamos acima em _physics_process(), adicione o seguinte loop. Com move_and_slide(), o Godot faz o corpo se mover às vezes várias vezes seguidas para suavizar o movimento do personagem. Então, temos que fazer uma repetição sobre todas as colisões que possam ter acontecido.
Em cada iteração da repetição, verificamos se caímos em um inimigo. Se assim for, nós o matamos e saltamos.
Com este código, se nenhuma colisão ocorrer em um determinado quadro, a repetição não será executada.
func _physics_process(delta):
#...
# Iterate through all collisions that occurred this frame
for index in range(get_slide_collision_count()):
# We get one of the collisions with the player
var collision = get_slide_collision(index)
# If there are duplicate collisions with a mob in a single frame
# the mob will be deleted after the first collision, and a second call to
# get_collider will return null, leading to a null pointer when calling
# collision.get_collider().is_in_group("mob").
# This block of code prevents processing duplicate collisions.
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
public override void _PhysicsProcess(double delta)
{
// ...
// 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.
// With C# we leverage typing and pattern-matching
// instead of checking for the group we created.
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;
}
}
}
}
São muitas funções novas. Aqui estão mais algumas informações sobre eles.
As funções get_slide_count() e get_slide_collision() ambas vêm da classe CharacterBody3D e estão relacionadas com move_and_slide().
get_slide_collision() retorna um objeto KinematicCollision3D que contém informações sobre onde e como ocorreu a colisão. Por exemplo, utilizamos sua propriedade collider para verificar se colidimos com um "inimigo", chamando is_in_group() no: collision.get_collider().is_in_group("mob").
Nota
O método is_in_group() está disponível em cada Node.
Para verificar se estamos pousando no monstro, utilizamos o produto vetorial pontual: Vector3.UP.dot(collision.normal()) > 0.1. A colisão normal é um vetor 3D que é perpendicular ao plano onde ocorreu a colisão. O produto pontal nos permite compará-lo com a direção ascendente.
Com os produtos pontuais, quando o resultado é maior que 0, os dois vetores estão a um ângulo inferior a 90 graus. Um valor maior que 0.1 nos diz que estamos aproximadamente acima do monstro.
Após lidar com a lógica de esmagamento e quique, encerramos a repetição antecipadamente com a instrução break para evitar chamadas duplicadas de inimigo.squash(), o que poderia resultar em bugs indesejados, como contar o placar várias vezes para uma única eliminação.
We are calling one undefined function, mob.squash(), so we have to add it to
the Mob class.
Open the script mob.gd by double-clicking on it in the FileSystem dock. At
the top of the script, we want to define a new signal named squashed. And at
the bottom, you can add the squash function, where we emit the signal and
destroy the mob.
# Emitted when the player jumped on the mob.
signal squashed
# ...
func squash():
squashed.emit()
queue_free()
// Don't forget to rebuild the project so the editor knows about the new signal.
// Emitted when the player jumped on the mob.
[Signal]
public delegate void SquashedEventHandler();
// ...
public void Squash()
{
EmitSignal(SignalName.Squashed);
QueueFree();
}
Nota
Ao usar C#, o Godot criará automaticamente os eventos apropriados para todos os sinais que terminam com EventHandler, veja Sinais em C#.
Usaremos o sinal para adicionar pontos à pontuação na próxima lição.
Com isso, você deve ser capaz de matar monstros pulando sobre eles. Você pode pressionar F5 para experimentar o jogo e definir main.tscn como cena principal do seu projeto.
No entanto, o jogador ainda não morre. Vamos trabalhar nisso na próxima parte.