Eliminando al jugador¶
Podemos eliminar a los enemigos saltando sobre ellos, pero el jugador aún no puede morir. Arreglemos esto.
Queremos detectar cuando somos golpeados por un enemigo de manera diferente a cuando los aplastamos. Queremos que el jugador muera cuando se mueve sobre el suelo, pero no si está en el aire. Podríamos usar matemáticas de vectores para distinguir los dos tipos de colisiones. Sin embargo, en su lugar, utilizaremos un nodo Area, que funciona bien para las cajas de colisión.
Colisión con el nodo Área¶
Regresa a la escena del Player y agrega un nuevo nodo Área. Nómbralo MobDetector. Agrega un nodo CollisionShape como hijo de este.
En el Inspector, asígnale una forma de cilindro.
Aquí tienes un truco que puedes usar para hacer que las colisiones solo ocurran cuando el jugador está en el suelo o cerca de él. Puedes reducir la altura del cilindro y moverlo hacia arriba hasta la parte superior del personaje. De esta manera, cuando el jugador salte, la forma estará demasiado alta para que los enemigos colisionen con ella.
También quieres que el cilindro sea más ancho que la esfera. De esta manera, el jugador recibirá el golpe antes de colisionar y ser empujado encima de la caja de colisión del monstruo.
Cuanto más ancho sea el cilindro, más fácilmente el jugador será eliminado.
A continuación, selecciona nuevamente el nodo MobDetector y en el Inspector, desactiva su propiedad Monitorable. Esto hace que otros nodos de física no puedan detectar el área. La propiedad complementaria Monitoring le permite detectar colisiones. Luego, elimina la conexión Collision -> Layer y establece la máscara en la capa "enemies".
Cuando las áreas detectan una colisión, emiten señales. Vamos a conectar una al nodo Player. En la pestaña Node, haz doble clic en la señal body_entered
y conéctala al Player.
El MobDetector emitirá body_entered
cuando un nodo KinematicBody o RigidBody entre en él. Como solo tiene enmascaradas las capas de física "enemies", solo detectará los nodos Mob.
En cuanto al código, vamos a hacer dos cosas: emitir una señal que luego utilizaremos para finalizar el juego y destruir al jugador. Podemos envolver estas operaciones en una función llamada die()
que nos ayuda a poner una etiqueta descriptiva en el 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();
}
Intenta jugar de nuevo presionando F5. Si todo está configurado correctamente, el personaje debería morir cuando un enemigo colisione con él.
Sin embargo, ten en cuenta que esto depende completamente del tamaño y la posición de las formas de colisión del Player y del Mob. Es posible que necesites moverlos y redimensionarlos para lograr una sensación de juego ajustada.
Finalizando el juego¶
Podemos utilizar la señal hit
del Player para finalizar el juego. Todo lo que necesitamos hacer es conectarla al nodo Main y detener el MobTimer en reacción.
Abre Main.tscn
, selecciona el nodo Player y en el panel Node, conecta su señal hit
al nodo Main.
Obtén y detén el temporizador en la función _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();
}
Si pruebas el juego ahora, los monstruos dejarán de aparecer cuando mueras y los que queden desaparecerán de la pantalla.
Puedes darte palmaditas en la espalda: has creado un prototipo de un juego completo en 3D, aunque todavía esté un poco áspero.
A partir de ahí, agregaremos un puntaje, la opción de volver a intentar el juego y verás cómo puedes hacer que el juego se sienta mucho más vivo con animaciones minimalistas.
Codificar el checkpoint¶
Aquí están los scripts completos para los nodos Main, Mob y Player, como referencia. Puedes usarlos para comparar y verificar tu código.
Empezando con Main.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();
}
}
A continuación 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();
}
}
Por último, el script más largo, 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 en la próxima lección para agregar la puntuación y la opción de volver a intentarlo.