Attention: Here be dragons

This is the latest (unstable) version of this documentation, which may document features not available in or compatible with released stable versions of Godot.

Matando o Jogador

Podemos matar inimigos pulando sobre eles, mas o jogador ainda não pode morrer. Vamos corrigir isso.

Queremos detectar ao sermos atingidos por um inimigo de uma forma diferente de esmagá-los. Queremos que o jogador morra quando estiver se movendo no chão, mas não se ele estiver no ar. Poderíamos usar matemática vetorial para distinguir os dois tipos de colisões. Ao invés disso, usaremos um nó Area3D, que funciona bem para as caixas de impacto (hitboxes).

Caixa de colisão com o nó Área

Volte para a cena player.tscn e adicione um novo nó filho Area3D. Nomeie-o MobDetector. Adicione um nó CollisionShape3D como filho dele.

imagem0

No Inspetor, atribua uma forma de cilindro a ele.

imagem1

Aqui está uma dica que você pode usar para fazer as colisões só acontecerem quando o jogador estiver no chão ou perto dele. Você pode reduzir a altura do cilindro e movê-lo até o topo do personagem. Desta forma, quando o jogador pula, a forma será muito alta para os inimigos colidirem com ela.

imagem2

Você também quer que o cilindro seja mais largo do que a esfera. Desta forma, o jogador é atingido antes de colidir e ser empurrado para cima da caixa de colisão do monstro.

Quanto mais largo o cilindro, mais facilmente o jogador será morto.

Em seguida, selecione novamente o nó MobDetector, e no Inspetor, desligue sua propriedade Monitorável. Isto faz com que outros nós de física não possam detectar a área. A propriedade complementar Monitoramento lhe permite detectar colisões. Em seguida, remover a Colisão -> Camada e colocar a máscara na camada "inimigos".

imagem3

When areas detect a collision, they emit signals. We're going to connect one to the Player node. Select MobDetector and go to the Signals dock, double-click the body_entered signal and connect it to the Player

imagem4

O MobDetector irá emitir body_entered quando um CharacterBody3D ou um RigidBody3D entrar nele. Como ele apenas observa as camadas físicas "inimigas", ele só detectará os nós Inimigo.

Em termos de código, faremos duas coisas: emitir um sinal que usaremos posteriormente para encerrar o jogo e destruir o jogador. Podemos agrupar essas operações em uma função die() que nos ajuda a colocar um rótulo descritivo no 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():
    hit.emit()
    queue_free()


func _on_mob_detector_body_entered(body):
    die()

Terminando o jogo

Podemos utilizar o sinal hit do Player para terminar o jogo. Tudo o que precisamos fazer é conectá-lo ao nó Main e parar a reação do Main.

Open main.tscn, select the Player node, and in the Signals dock, connect its hit signal to the Main node.

imagem5

Obtenha o temporizador, e pare-o, na função _on_player_hit().

func _on_player_hit():
    $MobTimer.stop()

Se você testar o jogo agora, os monstros deixarão de surgir quando você morrer, e os outros deixarão a tela.

Observe também que o jogo não trava mais, e nem exibe um erro quando o jogador morre. Como estamos parando o MobTimer, ele não aciona mais a função _on_mob_timer_timeout().

Observe também que o inimigo colidindo com o jogador e morrendo depende do tamanho e da posição das formas de colisão do Player e Mob. Talvez seja necessário movê-las e redimensioná-las para conseguir uma jogabilidade mais desafiante.

You can pat yourself on the back: you prototyped a complete 3D game, even if it's still a bit rough.

A partir daí, acrescentaremos uma pontuação, a opção de tentar novamente o jogo, e você verá como pode fazer o jogo se parecer muito mais vivo com animações minimalistas.

Programar o Ponto de controle(checkpoint)

Aqui estão os scripts completos para os nós Main, Mob e Player, para referência. Você pode usá-los para comparar e verificar seu código.

Começando com 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()

Next is 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 -45 and +45 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

Finally, the longest script, 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()
        # Setting the basis property will affect the rotation of the node.
        $Pivot.basis = Basis.looking_at(direction)

    # 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()

Nos vemos na próxima lição para adicionar a pontuação e a opção de nova tentativa.