Tuer le joueur¶
Nous pouvons tuer les ennemis en leur sautant dessus, mais le joueur ne peut pas encore mourir. Réglons ça.
Nous voulons que le fait d'être touché par un ennemi soit détecté différemment de l'écrasement. Nous voulons que le joueur meure quand il se déplace sur le sol, mais pas quand il est dans les airs. Nous pourrions utiliser des maths vectorielles pour distinguer les deux types de collisions. Mais à la place, nous utiliserons un nœud Area, qui fonctionne bien pour les hitboxes.
Hitbox avec le nœud Area¶
Retournez dans la scène Player et ajoutez un nouveau nœud Area. Nommez-le MobDetector. Ajoutez-lui un nœud CollisionShape comme enfant.
Dans l'Inspecteur, assignez-lui une forme de cylindre.
Voici une astuce que vous pouvez utiliser pour que les collisions ne se produisent que lorsque le joueur est au sol ou à proximité. Vous pouvez réduire la hauteur du cylindre et le déplacer en haut du personnage. De cette façon, quand le joueur saute, la forme sera trop haute pour que les ennemis puissent entrer en collision avec elle.
Vous voulez également que le cylindre soit plus large que la sphère. De cette façon, le joueur se fera toucher avant d'entrer en collision et d'être poussé au-dessus de la boîte de collision du monstre.
Plus le cylindre est large, plus le joueur se fera tuer facilement.
Ensuite, sélectionnez à nouveau le nœud MobDetector, et dans l'Inspecteur, désactivez sa propriété Monitorable. Ainsi, les autres nœuds physiques ne pourront pas détecter la zone. La propriété complémentaire Monitoring lui permet de détecter les collisions. Ensuite, enlevez le Collision -> Layer et définissez le masque sur le calque "enemies".
Quand les zones détectent une collision, elles émettent des signaux. Nous en connecterons un au nœud Player. Dans l'onglet Nœud, double-cliquez sur le signal body_entered
et connectez-le au Player.
Le MobDetector émettera body_entered
quand un nœud KinematicBody ou RigidBody entrera dans la zone. Comme le masque ne contient que les calques physiques des "enemies", il détectera seulement les nœuds Mob.
Au niveau du code, nous allons faire deux choses : émettre un signal que nous utiliserons plus tard pour mettre fin au jeu et détruire le joueur. Nous pouvons envelopper ces opérations dans une fonction die()
qui nous aide à mettre un terme descriptif sur le code.
# 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();
}
Essayez le jeu à nouveau en appuyant sur F5. Si tout est configuré correctement, le personnage devrait mourir quand un ennemi lui fonce dessus.
Cependant, notez que cela dépend entièrement de la taille et de la position des formes de collision du Player et du Mob. Vous devrez peut-être les déplacer et les redimensionner pour obtenir une meilleure sensation de jeu.
Arrêter le jeu¶
Nous pouvons utiliser le signal hit
du Player pour mettre fin à la partie. Tout ce que nous avons à faire est de le connecter au nœud Main et d'arrêter le MobTimer en réaction.
Ouvrez Main.tscn
, sélectionnez le nœud Player, et dans le dock Nœud, connectez son signal hit
au nœud Main.
Récupérez et arrêtez le timer dans la fonction _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 vous essayez le jeu maintenant, les monstres cesseront d'apparaître lorsque vous mourrez, et ceux qui restent quitteront l'écran.
Vous pouvez vous féliciter : vous avez réalisé le prototype d'un jeu 3D complet, même si il est encore un peu brouillon.
À partir de là, nous ajouterons un score, l'option de réessayer la partie, et vous verrez comment rendre le jeu beaucoup plus vivant grâce à des animations minimalistes.
Point de contrôle du code¶
Voici les scripts complets pour les nœuds Main, Mob, et Player, pour référence. Vous pouvez les utiliser pour comparer et vérifier votre code.
En commençant par 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();
}
}
Le suivant est 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();
}
}
Finalement, le script le plus long, 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();
}
}
Rendez-vous dans la prochaine leçon pour ajouter le score et l'option pour rejouer.