Work in progress

The content of this page was not yet updated for Godot 4.2 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.

Tuer le joueur

Nous pouvons tuer les ennemis en leur sautant dessus, mais le joueur ne peut pas encore mourir. Réglons ça.

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 avec le nœud Area

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

Dans l'Inspecteur, assignez-lui une forme de cylindre.

image1

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.

image2

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.

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

Quand les zones détectent une collision, elles émettent des signaux. Nous en connecterons un au nœud Player. Sélectionnez MobDetector et allez dans l'onglet Nœud de l'Inspecteur, double-cliquez sur le signal body_entered et connectez-le au 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.

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():
    hit.emit()
    queue_free()


func _on_mob_detector_body_entered(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

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.

Arrêter le jeu

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.

func _on_player_hit():
    $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

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 = randf()

    var player_position = $Player.position
    mob.initialize(mob_spawn_location.position, player_position)

    # Spawn the mob by adding it to the Main scene.
    add_child(mob)

func _on_player_hit():
    $MobTimer.stop()

Le suivant est Mob.gd.

extends CharacterBody3D

# 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

# Emitted when the player jumped on the mob
signal squashed

func _physics_process(_delta):
    move_and_slide()

# This function will be called from the Main scene.
func initialize(start_position, player_position):
    # We position the mob by placing it at start_position
    # and rotate it towards player_position, so it looks at the player.
    look_at_from_position(start_position, player_position, Vector3.UP)
    # Rotate this mob randomly within range of -90 and +90 degrees,
    # so that it doesn't move directly towards the player.
    rotate_y(randf_range(-PI / 4, PI / 4))

    # We calculate a random speed (integer)
    var random_speed = randi_range(min_speed, max_speed)
    # We calculate a forward velocity that represents the speed.
    velocity = Vector3.FORWARD * random_speed
    # We then rotate the velocity vector based on the mob's Y rotation
    # in order to move in the direction the mob is looking.
    velocity = velocity.rotated(Vector3.UP, rotation.y)

func _on_visible_on_screen_notifier_3d_screen_exited():
    queue_free()

func squash():
    squashed.emit()
    queue_free() # Destroy this node

Finalement, le script le plus long, Player.gd :

extends CharacterBody3D

signal hit

# How fast the player moves in meters per second
@export var speed = 14
# The downward acceleration while 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 target_velocity = Vector3.ZERO


func _physics_process(delta):
    # We create a local variable to store the input direction
    var direction = Vector3.ZERO

    # We check for each move input and update the direction accordingly
    if Input.is_action_pressed("move_right"):
        direction.x = direction.x + 1
    if Input.is_action_pressed("move_left"):
        direction.x = direction.x - 1
    if Input.is_action_pressed("move_back"):
        # Notice how we are working with the vector's x and z axes.
        # In 3D, the XZ plane is the ground plane.
        direction.z = direction.z + 1
    if Input.is_action_pressed("move_forward"):
        direction.z = direction.z - 1

    # Prevent diagonal moving fast af
    if direction != Vector3.ZERO:
        direction = direction.normalized()
        $Pivot.look_at(position + direction, Vector3.UP)

    # Ground Velocity
    target_velocity.x = direction.x * speed
    target_velocity.z = direction.z * speed

    # Vertical Velocity
    if not is_on_floor(): # If in the air, fall towards the floor. Literally gravity
        target_velocity.y = target_velocity.y - (fall_acceleration * delta)

    # Jumping.
    if is_on_floor() and Input.is_action_just_pressed("jump"):
        target_velocity.y = jump_impulse

    # Iterate through all collisions that occurred this frame
    # in C this would be for(int i = 0; i < collisions.Count; i++)
    for index in range(get_slide_collision_count()):
        # We get one of the collisions with the player
        var collision = get_slide_collision(index)

        # If the collision is with ground
        if collision.get_collider() == null:
            continue

        # If the collider is with a mob
        if collision.get_collider().is_in_group("mob"):
            var mob = collision.get_collider()
            # we check that we are hitting it from above.
            if Vector3.UP.dot(collision.get_normal()) > 0.1:
                # If so, we squash it and bounce.
                mob.squash()
                target_velocity.y = bounce_impulse
                # Prevent further duplicate calls.
                break

    # Moving the Character
    velocity = target_velocity
    move_and_slide()

# And this function at the bottom.
func die():
    hit.emit()
    queue_free()

func _on_mob_detector_body_entered(body):
    die()

Rendez-vous dans la prochaine leçon pour ajouter le score et l'option pour rejouer.