Killing the player¶
We can kill enemies by jumping on them, but the player still can't die. Let's fix this.
We want to detect being hit by an enemy differently from squashing them. We want the player to die when they're moving on the floor, but not if they're in the air. We could use vector math to distinguish the two kinds of collisions. Instead, though, we will use an Area3D node, which works well for hitboxes.
Hitbox with the Area node¶
Head back to the player.tscn
scene and add a new child node Area3D. Name it
MobDetector
Add a CollisionShape3D node as a child of it.
In the Inspector, assign a cylinder shape to it.
Here is a trick you can use to make the collisions only happen when the player is on the ground or close to it. You can reduce the cylinder's height and move it up to the top of the character. This way, when the player jumps, the shape will be too high up for the enemies to collide with it.
You also want the cylinder to be wider than the sphere. This way, the player gets hit before colliding and being pushed on top of the monster's collision box.
The wider the cylinder, the more easily the player will get killed.
Next, select the MobDetector
node again, and in the Inspector, turn
off its Monitorable property. This makes it so other physics nodes
cannot detect the area. The complementary Monitoring property allows
it to detect collisions. Then, remove the Collision -> Layer and set
the mask to the "enemies" layer.
When areas detect a collision, they emit signals. We're going to connect
one to the Player
node. Select MobDetector
and go to Inspector's Node tab, double-click the
body_entered
signal and connect it to the Player
The MobDetector will emit body_entered
when a CharacterBody3D or a
RigidBody3D node enters it. As it only masks the "enemies" physics
layers, it will only detect the Mob
nodes.
Code-wise, we're going to do two things: emit a signal we'll later use
to end the game and destroy the player. We can wrap these operations in
a die()
function that helps us put a descriptive label on the 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():
hit.emit()
queue_free()
func _on_mob_detector_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 HitEventHandler();
// ...
private void Die()
{
EmitSignal(SignalName.Hit);
QueueFree();
}
// We also specified this function name in PascalCase in the editor's connection window
private void OnMobDetectorBodyEntered(Node3D body)
{
Die();
}
Try the game again by pressing F5. If everything is set up correctly,
the character should die when an enemy runs into the collider. Note that without a Player
, the following line
var player_position = $Player.position
Vector3 playerPosition = GetNode<Player>("Player").Position;
gives error because there is no $Player!
Also note that the enemy colliding with the player and dying depends on the size and position of the
Player
and the Mob
's collision shapes. You may need to move them
and resize them to achieve a tight game feel.
Ending the game¶
We can use the Player
's hit
signal to end the game. All we need
to do is connect it to the Main
node and stop the MobTimer
in
reaction.
Open main.tscn
, select the Player
node, and in the Node dock,
connect its hit
signal to the Main
node.
Get the timer, and stop it, in the _on_player_hit()
function.
func _on_player_hit():
$MobTimer.stop()
// We also specified this function name in PascalCase in the editor's connection window
private void OnPlayerHit()
{
GetNode<Timer>("MobTimer").Stop();
}
If you try the game now, the monsters will stop spawning when you die, and the remaining ones will leave the screen.
You can pat yourself in the back: you prototyped a complete 3D game, even if it's still a bit rough.
From there, we'll add a score, the option to retry the game, and you'll see how you can make the game feel much more alive with minimalistic animations.
Code checkpoint¶
Here are the complete scripts for the Main
, Mob
, and Player
nodes,
for reference. You can use them to compare and check your code.
Starting with main.gd
.
extends Node
@export var mob_scene: PackedScene
func _on_mob_timer_timeout():
# Create a new instance of the Mob scene.
var mob = mob_scene.instantiate()
# Choose a random location on the SpawnPath.
# We store the reference to the SpawnLocation node.
var mob_spawn_location = get_node("SpawnPath/SpawnLocation")
# And give it a random offset.
mob_spawn_location.progress_ratio =