Work in progress

The content of this page was not yet updated for Godot 4.1 and may be outdated. If you know how to improve this page or you can confirm that it’s up to date, feel free to open a pull request.

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.

image0

In the Inspector, assign a cylinder shape to it.

image1

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.

image2

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.

image3

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

image4

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.

GDScriptC#

  1. # Emitted when the player was hit by a mob.
  2. # Put this at the top of the script.
  3. signal hit
  4. # And this function at the bottom.
  5. func die():
  6. hit.emit()
  7. queue_free()
  8. func _on_mob_detector_body_entered(body):
  9. die()
  1. // Don't forget to rebuild the project so the editor knows about the new signal.
  2. // Emitted when the player was hit by a mob.
  3. [Signal]
  4. public delegate void HitEventHandler();
  5. // ...
  6. private void Die()
  7. {
  8. EmitSignal(SignalName.Hit);
  9. QueueFree();
  10. }
  11. // We also specified this function name in PascalCase in the editor's connection window
  12. private void OnMobDetectorBodyEntered(Node3D body)
  13. {
  14. Die();
  15. }

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

GDScriptC#

  1. var player_position = $Player.position
  1. 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.

image5

Get the timer, and stop it, in the _on_player_hit() function.

GDScriptC#

  1. func _on_player_hit():
  2. $MobTimer.stop()
  1. // We also specified this function name in PascalCase in the editor's connection window
  2. private void OnPlayerHit()
  3. {
  4. GetNode<Timer>("MobTimer").Stop();
  5. }

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.

GDScriptC#

  1. extends Node
  2. @export var mob_scene: PackedScene
  3. func _on_mob_timer_timeout():
  4. # Create a new instance of the Mob scene.
  5. var mob = mob_scene.instantiate()
  6. # Choose a random location on the SpawnPath.
  7. # We store the reference to the SpawnLocation node.
  8. var mob_spawn_location = get_node("SpawnPath/SpawnLocation")
  9. # And give it a random offset.
  10. mob_spawn_location.progress_ratio = randf()
  11. var player_position = $Player.position
  12. mob.initialize(mob_spawn_location.position, player_position)
  13. # Spawn the mob by adding it to the Main scene.
  14. add_child(mob)
  15. func _on_player_hit():
  16. $MobTimer.stop()
  1. using Godot;
  2. public partial class Main : Node
  3. {
  4. [Export]
  5. public PackedScene MobScene { get; set; }
  6. private void OnMobTimerTimeout()
  7. {
  8. // Create a new instance of the Mob scene.
  9. Mob mob = MobScene.Instantiate<Mob>();
  10. // Choose a random location on the SpawnPath.
  11. // We store the reference to the SpawnLocation node.
  12. var mobSpawnLocation = GetNode<PathFollow3D>("SpawnPath/SpawnLocation");
  13. // And give it a random offset.
  14. mobSpawnLocation.ProgressRatio = GD.Randf();
  15. Vector3 playerPosition = GetNode<Player>("Player").Position;
  16. mob.Initialize(mobSpawnLocation.Position, playerPosition);
  17. // Spawn the mob by adding it to the Main scene.
  18. AddChild(mob);
  19. }
  20. private void OnPlayerHit()
  21. {
  22. GetNode<Timer>("MobTimer").Stop();
  23. }
  24. }

Next is Mob.gd.

GDScriptC#

  1. extends CharacterBody3D
  2. # Minimum speed of the mob in meters per second.
  3. @export var min_speed = 10
  4. # Maximum speed of the mob in meters per second.
  5. @export var max_speed = 18
  6. # Emitted when the player jumped on the mob
  7. signal squashed
  8. func _physics_process(_delta):
  9. move_and_slide()
  10. # This function will be called from the Main scene.
  11. func initialize(start_position, player_position):
  12. # We position the mob by placing it at start_position
  13. # and rotate it towards player_position, so it looks at the player.
  14. look_at_from_position(start_position, player_position, Vector3.UP)
  15. # Rotate this mob randomly within range of -90 and +90 degrees,
  16. # so that it doesn't move directly towards the player.
  17. rotate_y(randf_range(-PI / 4, PI / 4))
  18. # We calculate a random speed (integer)
  19. var random_speed = randi_range(min_speed, max_speed)
  20. # We calculate a forward velocity that represents the speed.
  21. velocity = Vector3.FORWARD * random_speed
  22. # We then rotate the velocity vector based on the mob's Y rotation
  23. # in order to move in the direction the mob is looking.
  24. velocity = velocity.rotated(Vector3.UP, rotation.y)
  25. func _on_visible_on_screen_notifier_3d_screen_exited():
  26. queue_free()
  27. func squash():
  28. squashed.emit()
  29. queue_free() # Destroy this node
  1. using Godot;
  2. public partial class Mob : CharacterBody3D
  3. {
  4. // Emitted when the played jumped on the mob.
  5. [Signal]
  6. public delegate void SquashedEventHandler();
  7. // Minimum speed of the mob in meters per second
  8. [Export]
  9. public int MinSpeed { get; set; } = 10;
  10. // Maximum speed of the mob in meters per second
  11. [Export]
  12. public int MaxSpeed { get; set; } = 18;
  13. public override void _PhysicsProcess(double delta)
  14. {
  15. MoveAndSlide();
  16. }
  17. // This function will be called from the Main scene.
  18. public void Initialize(Vector3 startPosition, Vector3 playerPosition)
  19. {
  20. // We position the mob by placing it at startPosition
  21. // and rotate it towards playerPosition, so it looks at the player.
  22. LookAtFromPosition(startPosition, playerPosition, Vector3.Up);
  23. // Rotate this mob randomly within range of -90 and +90 degrees,
  24. // so that it doesn't move directly towards the player.
  25. RotateY((float)GD.RandRange(-Mathf.Pi / 4.0, Mathf.Pi / 4.0));
  26. // We calculate a random speed (integer)
  27. int randomSpeed = GD.RandRange(MinSpeed, MaxSpeed);
  28. // We calculate a forward velocity that represents the speed.
  29. Velocity = Vector3.Forward * randomSpeed;
  30. // We then rotate the velocity vector based on the mob's Y rotation
  31. // in order to move in the direction the mob is looking.
  32. Velocity = Velocity.Rotated(Vector3.Up, Rotation.Y);
  33. }
  34. public void Squash()
  35. {
  36. EmitSignal(SignalName.Squashed);
  37. QueueFree(); // Destroy this node
  38. }
  39. private void OnVisibilityNotifierScreenExited()
  40. {
  41. QueueFree();
  42. }
  43. }

Finally, the longest script, Player.gd:

GDScriptC#

  1. extends CharacterBody3D
  2. signal hit
  3. # How fast the player moves in meters per second
  4. @export var speed = 14
  5. # The downward acceleration while in the air, in meters per second squared.
  6. @export var fall_acceleration = 75
  7. # Vertical impulse applied to the character upon jumping in meters per second.
  8. @export var jump_impulse = 20
  9. # Vertical impulse applied to the character upon bouncing over a mob
  10. # in meters per second.
  11. @export var bounce_impulse = 16
  12. var target_velocity = Vector3.ZERO
  13. func _physics_process(delta):
  14. # We create a local variable to store the input direction
  15. var direction = Vector3.ZERO
  16. # We check for each move input and update the direction accordingly
  17. if Input.is_action_pressed("move_right"):
  18. direction.x = direction.x + 1
  19. if Input.is_action_pressed("move_left"):
  20. direction.x = direction.x - 1
  21. if Input.is_action_pressed("move_back"):
  22. # Notice how we are working with the vector's x and z axes.
  23. # In 3D, the XZ plane is the ground plane.
  24. direction.z = direction.z + 1
  25. if Input.is_action_pressed("move_forward"):
  26. direction.z = direction.z - 1
  27. # Prevent diagonal moving fast af
  28. if direction != Vector3.ZERO:
  29. direction = direction.normalized()
  30. $Pivot.look_at(position + direction, Vector3.UP)
  31. # Ground Velocity
  32. target_velocity.x = direction.x * speed
  33. target_velocity.z = direction.z * speed
  34. # Vertical Velocity
  35. if not is_on_floor(): # If in the air, fall towards the floor. Literally gravity
  36. target_velocity.y = target_velocity.y - (fall_acceleration * delta)
  37. # Jumping.
  38. if is_on_floor() and Input.is_action_just_pressed("jump"):
  39. target_velocity.y = jump_impulse
  40. # Iterate through all collisions that occurred this frame
  41. # in C this would be for(int i = 0; i < collisions.Count; i++)
  42. for index in range(get_slide_collision_count()):
  43. # We get one of the collisions with the player
  44. var collision = get_slide_collision(index)
  45. # If the collision is with ground
  46. if (collision.get_collider() == null):
  47. continue
  48. # If the collider is with a mob
  49. if collision.get_collider().is_in_group("mob"):
  50. var mob = collision.get_collider()
  51. # we check that we are hitting it from above.
  52. if Vector3.UP.dot(collision.get_normal()) > 0.1:
  53. # If so, we squash it and bounce.
  54. mob.squash()
  55. target_velocity.y = bounce_impulse
  56. # Moving the Character
  57. velocity = target_velocity
  58. move_and_slide()
  59. # And this function at the bottom.
  60. func die():
  61. hit.emit()
  62. queue_free()
  63. func _on_mob_detector_body_entered(body):
  64. die()
  1. using Godot;
  2. public partial class Player : CharacterBody3D
  3. {
  4. // Emitted when the player was hit by a mob.
  5. [Signal]
  6. public delegate void HitEventHandler();
  7. // How fast the player moves in meters per second.
  8. [Export]
  9. public int Speed { get; set; } = 14;
  10. // The downward acceleration when in the air, in meters per second squared.
  11. [Export]
  12. public int FallAcceleration { get; set; } = 75;
  13. // Vertical impulse applied to the character upon jumping in meters per second.
  14. [Export]
  15. public int JumpImpulse { get; set; } = 20;
  16. // Vertical impulse applied to the character upon bouncing over a mob in meters per second.
  17. [Export]
  18. public int BounceImpulse { get; set; } = 16;
  19. private Vector3 _targetVelocity = Vector3.Zero;
  20. public override void _PhysicsProcess(double delta)
  21. {
  22. // We create a local variable to store the input direction.
  23. var direction = Vector3.Zero;
  24. // We check for each move input and update the direction accordingly.
  25. if (Input.IsActionPressed("move_right"))
  26. {
  27. direction.X += 1.0f;
  28. }
  29. if (Input.IsActionPressed("move_left"))
  30. {
  31. direction.X -= 1.0f;
  32. }
  33. if (Input.IsActionPressed("move_back"))
  34. {
  35. // Notice how we are working with the vector's X and Z axes.
  36. // In 3D, the XZ plane is the ground plane.
  37. direction.Z += 1.0f;
  38. }
  39. if (Input.IsActionPressed("move_forward"))
  40. {
  41. direction.Z -= 1.0f;
  42. }
  43. // Prevent diagonal moving fast af
  44. if (direction != Vector3.Zero)
  45. {
  46. direction = direction.Normalized();
  47. GetNode<Node3D>("Pivot").LookAt(Position + direction, Vector3.Up);
  48. }
  49. // Ground Velocity
  50. _targetVelocity.X = direction.X * Speed;
  51. _targetVelocity.Z = direction.Z * Speed;
  52. // Vertical Velocity
  53. if (!IsOnFloor()) // If in the air, fall towards the floor. Literally gravity
  54. {
  55. _targetVelocity.Y -= FallAcceleration * (float)delta;
  56. }
  57. // Jumping.
  58. if (IsOnFloor() && Input.IsActionJustPressed("jump"))
  59. {
  60. _targetVelocity.Y = JumpImpulse;
  61. }
  62. // Iterate through all collisions that occurred this frame.
  63. for (int index = 0; index < GetSlideCollisionCount(); index++)
  64. {
  65. // We get one of the collisions with the player.
  66. KinematicCollision3D collision = GetSlideCollision(index);
  67. // If the collision is with a mob.
  68. if (collision.GetCollider() is Mob mob)
  69. {
  70. // We check that we are hitting it from above.
  71. if (Vector3.Up.Dot(collision.GetNormal()) > 0.1f)
  72. {
  73. // If so, we squash it and bounce.
  74. mob.Squash();
  75. _targetVelocity.Y = BounceImpulse;
  76. }
  77. }
  78. }
  79. // Moving the Character
  80. Velocity = _targetVelocity;
  81. MoveAndSlide();
  82. }
  83. private void Die()
  84. {
  85. EmitSignal(SignalName.Hit);
  86. QueueFree();
  87. }
  88. private void OnMobDetectorBodyEntered(Node3D body)
  89. {
  90. Die();
  91. }
  92. }

See you in the next lesson to add the score and the retry option.