Matando o Jogador¶
Podemos matar inimigos pulando sobre eles, mas o jogador ainda não pode morrer. Vamos corrigir isso.
Queremos detectar ao sermos atingidos por um inimigo de uma forma diferente de esmagá-los. Queremos que o jogador morra quando estiver se movendo no chão, mas não se ele estiver no ar. Poderíamos usar matemática vetorial para distinguir os dois tipos de colisões. Ao invés disso, usaremos um nó Area, que funciona bem para as caixas de impacto.
Caixa de colisão com o nó Área¶
Volte para a cena Player e adicione um novo nó Area. Nomeie-o DetectorInimigo. Adicione um nó CollisionShape como filho dele.
No Inspetor, atribua uma forma de cilindro a ele.
Aqui está uma dica que você pode usar para fazer as colisões só acontecerem quando o jogador estiver no chão ou perto dele. Você pode reduzir a altura do cilindro e movê-lo até o topo do personagem. Desta forma, quando o jogador pula, a forma será muito alta para os inimigos colidirem com ela.
Você também quer que o cilindro seja mais largo do que a esfera. Desta forma, o jogador é atingido antes de colidir e ser empurrado para cima da caixa de colisão do monstro.
Quanto mais largo o cilindro, mais facilmente o jogador será morto.
Em seguida, selecione novamente o nó DetectorInimigo, e no Inspetor, desligue sua propriedade Monitorável. Isto faz com que outros nós de física não possam detectar a área. A propriedade complementar Monitoramento lhe permite detectar colisões. Em seguida, remover a Colisão -> Camada e colocar a máscara na camada "inimigos".
Quando as áreas detectam uma colisão, elas emitem sinais. Vamos conectar um ao nó * Player . Na aba *Nó, clique duas vezes no sinal body_entered
e conecte-o ao Player.
O DetectorInimigo irá emitir body_entered
quando um KinematicBody ou um RigidBody entrar nele. Como ele apenas mascara as camadas físicas "inimigas", ele só detectará os nós Inimigo.
Em termos de código, faremos duas coisas: emitir um sinal que usaremos posteriormente para encerrar o jogo e destruir o jogador. Podemos agrupar essas operações em uma função die()
que nos ajuda a colocar um rótulo descritivo no código.
# Emitted when the player was hit by a mob.
# Put this at the top of the script.
signal hit
# And this function at the bottom.
func die():
emit_signal("hit")
queue_free()
func _on_MobDetector_body_entered(_body):
die()
// Don't forget to rebuild the project so the editor knows about the new signal.
// Emitted when the player was hit by a mob.
[Signal]
public delegate void Hit();
// ...
private void Die()
{
EmitSignal(nameof(Hit));
QueueFree();
}
// We also specified this function name in PascalCase in the editor's connection window
public void OnMobDetectorBodyEntered(Node body)
{
Die();
}
Teste o jogo novamente pressionando :kbd:'F5'. Se tudo estiver configurado corretamente, o personagem deve morrer quando um inimigo bater nele.
Entretanto, observe que isso depende inteiramente do tamanho e da posição das formas de colisão do Player e Inimigo. Talvez seja necessário movê-las e redimensioná-las para conseguir uma jogabilidade mais interessante.
Terminando o jogo¶
Podemos utilizar o sinal hit
do Player para terminar o jogo. Tudo o que precisamos fazer é conectá-lo ao nó Principal e parar a reação do MobTimer.
Abra Principal.tscn
, selecione o nó Player, e no painel Nó, conecte seu sinal hit
ao nó Principal.
Obtenha e pare o temporizador na função _on_Player_hit()
.
func _on_Player_hit():
$MobTimer.stop()
// We also specified this function name in PascalCase in the editor's connection window
public void OnPlayerHit()
{
GetNode<Timer>("MobTimer").Stop();
}
Se você testar o jogo agora, os monstros deixarão de surgir quando você morrer, e os outros deixarão a tela.
Você pode bater palmas para você mesmo: você prototipou um jogo 3D completo, mesmo que ainda seja um pouco bruto.
A partir daí, acrescentaremos uma pontuação, a opção de tentar novamente o jogo, e você verá como pode fazer o jogo se parecer muito mais vivo com animações minimalistas.
Programar o Ponto de controle(checkpoint)¶
Aqui estão os scripts completos para os nós Principal, Inimigo e Player, para referência. Você pode usá-los para comparar e verificar seu código.
Começando com Principal.gd
.
extends Node
export(PackedScene) var mob_scene
func _ready():
randomize()
func _on_MobTimer_timeout():
# Create a new instance of the Mob scene.
var mob = mob_scene.instance()
# Choose a random location on the SpawnPath.
var mob_spawn_location = get_node("SpawnPath/SpawnLocation")
# And give it a random offset.
mob_spawn_location.unit_offset = randf()
# Communicate the spawn location and the player's location to the mob.
var player_position = $Player.transform.origin
mob.initialize(mob_spawn_location.translation, player_position)
# Spawn the mob by adding it to the Main scene.
add_child(mob)
func _on_Player_hit():
$MobTimer.stop()
public class Main : Node
{
#pragma warning disable 649
[Export]
public PackedScene MobScene;
#pragma warning restore 649
public override void _Ready()
{
GD.Randomize();
}
public void OnMobTimerTimeout()
{
// Create a new instance of the Mob scene.
var mob = (Mob)MobScene.Instance();
// Choose a random location on the SpawnPath.
// We store the reference to the SpawnLocation node.
var mobSpawnLocation = GetNode<PathFollow>("SpawnPath/SpawnLocation");
// And give it a random offset.
mobSpawnLocation.UnitOffset = GD.Randf();
// Communicate the spawn location and the player's location to the mob.
Vector3 playerPosition = GetNode<Player>("Player").Transform.origin;
mob.Initialize(mobSpawnLocation.Translation, playerPosition);
// Spawn the mob by adding it to the Main scene.
AddChild(mob);
}
public void OnPlayerHit()
{
GetNode<Timer>("MobTimer").Stop();
}
}
O próximo é Mob.gd
.
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)
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);
}
public void Squash()
{
EmitSignal(nameof(Squashed));
QueueFree();
}
public void OnVisibilityNotifierScreenExited()
{
QueueFree();
}
}
Finalmente, o script mais longo, Player.gd
.
extends KinematicBody
# Emitted when a mob hit the player.
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 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 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)
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
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);
}
_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;
}
}
}
}
private void Die()
{
EmitSignal(nameof(Hit));
QueueFree();
}
public void OnMobDetectorBodyEntered(Node body)
{
Die();
}
}
Nos vemos na próxima lição para adicionar a pontuação e a opção de nova tentativa.