Parte 5

Resumen

¡En esta parte, vamos a añadir granadas al jugador, darle la posibilidad de agarrar y lanzar objetos, y añadir torretas!

../../../_images/PartFiveFinished.png

Nota

Se supone que has terminado Parte 4 antes de pasar a esta parte del tutorial. El proyecto terminado de Parte 4 será el proyecto inicial de la parte 5

¡Vamos a empezar!

Añadir granadas

En primer lugar, démosle al jugador algunas granadas para jugar. Abre Grenada.tscn.

Hay algunas cosas a tener en cuenta aquí, la primera y más importante es que las granadas van a usar nodos RigidBody. Vamos a usar nodos RigidBody para nuestras granadas para que reboten alrededor del mundo de una manera (algo) realista.

La segunda cosa que hay que tener en cuenta es Blast_Area. Este es un nodo Area que representará el radio de la explosión de la granada.

Finalmente, lo último que hay que notar es Explosion. Este es el nodo Particles que emitirá un efecto de explosión cuando la granada explote. Una cosa a tener en cuenta aquí es que tenemos One shot activado. Esto es para que emitamos todas las partículas a la vez. Las partículas también se emiten usando coordenadas mundiales en lugar de locales, así que también tenemos las Local Coords desmarcadas.

Nota

Si quieres, puedes ver como las partículas están organizadas mirando a través del Process Material y Draw Passes de las partículas.

Escribamos el código necesario para la granada. Selecciona Grenade y haz un nuevo script llamado Grenade.gd. Añade lo siguiente:

extends RigidBody

const GRENADE_DAMAGE = 60

const GRENADE_TIME = 2
var grenade_timer = 0

const EXPLOSION_WAIT_TIME = 0.48
var explosion_wait_timer = 0

var rigid_shape
var grenade_mesh
var blast_area
var explosion_particles

func _ready():
    rigid_shape = $Collision_Shape
    grenade_mesh = $Grenade
    blast_area = $Blast_Area
    explosion_particles = $Explosion

    explosion_particles.emitting = false
    explosion_particles.one_shot = true

func _process(delta):

    if grenade_timer < GRENADE_TIME:
        grenade_timer += delta
        return
    else:
        if explosion_wait_timer <= 0:
            explosion_particles.emitting = true

            grenade_mesh.visible = false
            rigid_shape.disabled = true

            mode = RigidBody.MODE_STATIC

            var bodies = blast_area.get_overlapping_bodies()
            for body in bodies:
                if body.has_method("bullet_hit"):
                    body.bullet_hit(GRENADE_DAMAGE, body.global_transform.looking_at(global_transform.origin, Vector3(0, 1, 0)))

            # This would be the perfect place to play a sound!


        if explosion_wait_timer < EXPLOSION_WAIT_TIME:
            explosion_wait_timer += delta

            if explosion_wait_timer >= EXPLOSION_WAIT_TIME:
                queue_free()

Repasemos lo que está pasando, comenzando con las variables de clase:

  • GRENADE_DAMAGE: La cantidad de daño que la granada causa cuando explota.

  • GRENADE_TIME: La cantidad de tiempo que la granada tarda (en segundos) en explotar una vez que es creada/tirada.

  • grenade_timer: Una variable para rastrear cuánto tiempo la granada ha sido creada/tirada.

  • EXPLOSION_WAIT_TIME: La cantidad de tiempo necesario (en segundos) para esperar antes de destruir la escena de la granada después de la explosión

  • explosion_wait_timer: Una variable para rastrear cuánto tiempo ha pasado desde que la granada explotó.

  • rigid_shape: La CollisionShape para la granada RigidBody.

  • grenade_mesh: La :ref:``MeshInstance <class_MeshInstance>` para la granada.

  • blast_area: La explosión :ref:``Area <class_Area>` usada para dañar cosas cuando la granada explota.

  • explosion_particles: Las :ref:``Particles <class_Particles>` que salen cuando la granada explota.

Fíjate en cómo EXPLOSION_WAIT_TIME es un número bastante extraño (0.48). Esto se debe a que queremos que EXPLOSION_WAIT_TIME sea igual al tiempo que las partículas de la explosión están emitiendo, así que cuando las partículas terminen, destruimos/liberamos la granada. Calculamos el EXPLOSION_WAIT_TIME tomando el tiempo de vida de la partícula y dividiéndolo por la escala de velocidad de la partícula. Esto nos da el tiempo exacto que durarán las partículas de la explosión.


Ahora pongamos nuestra atención en _ready.

Primero obtenemos todos los nodos que necesitaremos y los asignamos a las variables de clase apropiadas.

Necesitamos obtener la CollisionShape y MeshInstance porque de manera similar al objetivo en Parte 4, estaremos ocultando la malla de la granada y desactivando la forma de colisión cuando la granada explote.

La razón por la que necesitamos la explosión Area es para que podamos dañar todo lo que hay dentro cuando la granada explote. Usaremos un código similar al código de los cuchillos del jugador. Necesitamos el Particles para poder emitir partículas cuando la granada explote.

Después de obtener todos los nodos y asignarlos a sus variables de clase, nos aseguramos de que las partículas de la explosión no emitan, y que estén preparadas para emitir una sola vez. Esto es para estar más seguros de que las partículas se comportarán como esperamos que lo hagan.


Ahora veamos el _process.

Primero, comprobamos si el grenade_timer es menor que el GRENADE_TIME. Si lo es, añadimos delta y volvemos. Esto es para que la granada tenga que esperar GRENADE_TIME segundos antes de explotar, permitiendo que el RigidBody se mueva.

Si grenade_timer está at GRENADE_TIMER o más alto, tenemos que comprobar si la granada ha esperado lo suficiente y necesita explotar. Lo hacemos comprobando si explosion_wait_timer es igual a 0 o menos. Como añadiremos Delta a explosion_wait_timer justo después, cualquier código que se compruebe sólo se llamará una vez, justo cuando la granada haya esperado lo suficiente y necesite explotar.

Si la granada ha esperado lo suficiente para explotar, primero le decimos a las explosion_particles que emitan. Luego hacemos invisible la grenade_mesh, y desactivamos la rigid_shape, ocultando la granada.

Luego ponemos el modo de RigidBody en MODE_STATIC para que la granada no se mueva.

Entonces tenemos todos los cuerpos en el blast_area, comprobamos si tienen el método/función del bullet_hit, y si lo tienen, lo llamamos y pasamos a ``GRENADE_DAMAGE` y la transformación del cuerpo mirando la granada. Esto hace que los cuerpos explotados por la granada exploten hacia afuera desde la posición de la granada.

Luego comprobamos si explosion_wait_timer es menos que EXPLOSION_WAIT_TIME. Si lo es, añadimos delta a explosion_wait_timer.

A continuación, comprobamos si explosion_wait_timer es mayor o igual que EXPLOSION_WAIT_TIME. Como añadimos delta, esto sólo se llamará una vez. Si explosion_wait_timer es mayor o igual a EXPLOSION_WAIT_TIME, la granada ha esperado lo suficiente para que la Particles juegue y podamos liberar/destruir la granada, ya que no la necesitamos.


Preparemos rápidamente la granada adhesiva también. Abre Sticky_Grenade.tscn.

Sticky_Grenade.tscn es casi idéntico a Grenade.tscn, con una pequeña adición. Ahora tenemos un segundo Area, llamado Sticky_Area. Usaremos Stick_Area para detectar cuando la granada pegajosa ha chocado con el ambiente y necesita pegarse a algo.

Selecciona Sticky_Grenade y haz un nuevo script llamado Sticky_Grenade.gd. Añade lo siguiente:

extends RigidBody

const GRENADE_DAMAGE = 40

const GRENADE_TIME = 3
var grenade_timer = 0

const EXPLOSION_WAIT_TIME = 0.48
var explosion_wait_timer = 0

var attached = false
var attach_point = null

var rigid_shape
var grenade_mesh
var blast_area
var explosion_particles

var player_body

func _ready():
    rigid_shape = $Collision_Shape
    grenade_mesh = $Sticky_Grenade
    blast_area = $Blast_Area
    explosion_particles = $Explosion

    explosion_particles.emitting = false
    explosion_particles.one_shot = true

    $Sticky_Area.connect("body_entered", self, "collided_with_body")


func collided_with_body(body):

    if body == self:
        return

    if player_body != null:
        if body == player_body:
            return

    if attached == false:
        attached = true
        attach_point = Spatial.new()
        body.add_child(attach_point)
        attach_point.global_transform.origin = global_transform.origin

        rigid_shape.disabled = true

        mode = RigidBody.MODE_STATIC


func _process(delta):

    if attached == true:
        if attach_point != null:
            global_transform.origin = attach_point.global_transform.origin

    if grenade_timer < GRENADE_TIME:
        grenade_timer += delta
        return
    else:
        if explosion_wait_timer <= 0:
            explosion_particles.emitting = true

            grenade_mesh.visible = false
            rigid_shape.disabled = true

            mode = RigidBody.MODE_STATIC

            var bodies = blast_area.get_overlapping_bodies()
            for body in bodies:
                if body.has_method("bullet_hit"):
                    body.bullet_hit(GRENADE_DAMAGE, body.global_transform.looking_at(global_transform.origin, Vector3(0, 1, 0)))

            # This would be the perfect place to play a sound!


        if explosion_wait_timer < EXPLOSION_WAIT_TIME:
            explosion_wait_timer += delta

            if explosion_wait_timer >= EXPLOSION_WAIT_TIME:
                if attach_point != null:
                    attach_point.queue_free()
                queue_free()

El código de arriba es casi idéntico al código de Grenade.gd, así que repasemos lo que ha cambiado.

Primero, tenemos algunas variables de clase más:

  • attached: Una variable para rastrear si la granada pegajosa se ha unido o no a un :ref:``PhysicsBody <class_PhysicsBody>`.

  • attach_point: Una variable para mantener un Spatial que estará en la posición donde la granada pegajosa colisionó.

  • player_body: El jugador es :ref:``KinematicBody <class_KinematicBody>`.

Se han añadido para permitir que la granada pegajosa se adhiera a cualquier PhysicsBody que pueda impactar. También necesitamos el KinematicBody del jugador para que la granada no se pegue al jugador cuando éste la lance.


Ahora veamos el pequeño cambio en _ready. En _ready hemos añadido una línea de código para que cuando un cuerpo entre en Stick_Area, se llame a la función collided_with_body.


A continuación, echemos un vistazo a collided_with_body.

En primer lugar, nos aseguramos de que la granada pegajosa no colisione consigo misma. Debido a que la pegajosa Area no sabe que está pegada a la granada RigidBody, tenemos que asegurarnos de que no se va a pegar a sí misma comprobando que el cuerpo con el que ha chocado no es el mismo. Si hemos chocado con nosotros mismos, lo ignoramos regresando.

Luego comprobamos si tenemos algo asignado al player_body, y si el cuerpo con el que ha chocado la granada pegajosa es el jugador que la ha lanzado. Si el cuerpo con el que la granada pegajosa ha chocado es en efecto el player_body, lo ignoramos retornando.

A continuación, comprobamos si la granada pegajosa ya se ha adherido a algo o no.

Si la granada pegajosa no está adherida, entonces ponemos attached a true para saber que la granada pegajosa se ha adherido a algo.

Luego hacemos un nuevo nodo Spatial, y lo convertimos en un hijo del cuerpo con el que chocó la granada pegajosa. Luego establecemos la posición de Spatial en la posición global actual de la granada pegajosa.

Nota

Porque hemos añadido la Spatial como hijo del cuerpo con el que la granada pegajosa ha chocado, seguirá junto con dicho cuerpo. Podemos usar este Space para establecer la posición de la granada pegajosa, de modo que siempre estará en la misma posición relativa al cuerpo con el que ha chocado.

Luego deshabilitamos la rigid_shape para que la granada pegajosa no se mueva constantemente con cualquier cuerpo con el que haya chocado. Por último, fijamos nuestro modo en MODE_STATIC para que la granada no se mueva.


Finalmente, repasemos los pocos cambios en el _process.

Ahora estamos comprobando si la granada pegajosa está pegada justo en la parte superior del _process.

Si la granada pegajosa está pegada, nos aseguramos de que el punto pegado no sea igual a null. Si el punto de unión no es igual a null, fijamos la posición global de la granada pegajosa (usando su global Transform's origin) a la posición global de la Spatial asignada al attach_point (usando su global Transform's origin).

El único otro cambio es que antes de liberar/destruir la granada adhesiva es comprobar si la granada adhesiva tiene un punto de enganche. Si lo tiene, también llamamos queue_free al punto de unión, así que también se libera/destruye.

Añadiendo granadas al jugador

Ahora necesitamos añadir algún código a Player.gd para poder usar las granadas.

En primer lugar, abre Player.tscn y expande el árbol de nodos hasta que llegues a Rotation_Helper. Fíjate en que en Rotation_Helper tenemos un nodo llamado Grenade_Toss_Pos. Aquí es donde generaremos las granadas.

También noten cómo está ligeramente girado en el eje X, así que no está apuntando derecho, sino ligeramente hacia arriba. Cambiando la rotación de Grenade_Toss_Poss, puedes cambiar el ángulo en el que se lanzan las granadas.

Bien, ahora empecemos a hacer que las granadas funcionen con el jugador. Añade las siguientes variables de clase a Player.gd:

var grenade_amounts = {"Grenade":2, "Sticky Grenade":2}
var current_grenade = "Grenade"
var grenade_scene = preload("res://Grenade.tscn")
var sticky_grenade_scene = preload("res://Sticky_Grenade.tscn")
const GRENADE_THROW_FORCE = 50
  • grenade_amounts: La cantidad de granadas que el jugador lleva actualmente (para cada tipo de granada).

  • current_grenade: El nombre de la granada que el jugador está usando actualmente.

  • grenade_scene: La escena de la granada en la que trabajamos antes.

  • sticky_grenade_scene: La escena de la granada pegajosa en la que trabajamos antes.

  • GRENADE_THROW_FORCE: La fuerza a la que el jugador lanzará las granadas.

La mayoría de estas variables son parecidas a la forma en que tenemos nuestras armas configuradas.

Truco

Aunque es posible hacer un sistema de granadas más modular, descubrí que no valía la pena la complejidad adicional por sólo dos granadas. Si fueras a hacer un FPS más complejo con más granadas, probablemente querrías hacer un sistema para granadas similar a como tenemos las armas puestas.


Ahora necesitamos añadir algún código en _process_input Añadimos lo siguiente a _process_input:

# ----------------------------------
# Changing and throwing grenades

if Input.is_action_just_pressed("change_grenade"):
    if current_grenade == "Grenade":
        current_grenade = "Sticky Grenade"
    elif current_grenade == "Sticky Grenade":
        current_grenade = "Grenade"

if Input.is_action_just_pressed("fire_grenade"):
    if grenade_amounts[current_grenade] > 0:
        grenade_amounts[current_grenade] -= 1

        var grenade_clone
        if current_grenade == "Grenade":
            grenade_clone = grenade_scene.instance()
        elif current_grenade == "Sticky Grenade":
            grenade_clone = sticky_grenade_scene.instance()
            # Sticky grenades will stick to the player if we do not pass ourselves
            grenade_clone.player_body = self

        get_tree().root.add_child(grenade_clone)
        grenade_clone.global_transform = $Rotation_Helper/Grenade_Toss_Pos.global_transform
        grenade_clone.apply_impulse(Vector3(0, 0, 0), grenade_clone.global_transform.basis.z * GRENADE_THROW_FORCE)
# ----------------------------------

Repasemos lo que está pasando aquí.

En primer lugar, comprobamos si la acción change_grenade acaba de ser presionada. Si lo ha hecho, entonces comprobamos qué granada está usando actualmente el jugador. Basándonos en el nombre de la granada que el jugador está usando actualmente, cambiamos current_grenade por el nombre de la granada opuesta.

A continuación comprobamos si la acción fire_grenade acaba de ser presionada. Si lo ha hecho, entonces comprobamos si el jugador tiene más de 0 granadas para el tipo de granada seleccionada.

Si el jugador tiene más de 0 granadas, entonces quitamos una de las cantidades de granadas para la granada actual. Entonces, basándonos en la granada que el jugador está usando actualmente, ponemos la escena de granada apropiada y la asignamos a grenade_clone.

A continuación añadimos grenade_clone como hijo del nodo de la raíz y ponemos su global Transform al global de Grenade_Toss_Pos Transform. Finalmente, aplicamos un impulso a la granada para que sea lanzada hacia adelante, relativo al vector direccional Z del grenade_clone.


Ahora el jugador puede usar ambos tipos de granadas, pero todavía hay algunas cosas que probablemente deberíamos añadir antes de pasar a añadir las otras cosas.

Aún necesitamos una forma de mostrarle al jugador cuántas granadas quedan, y probablemente deberíamos añadir una forma de conseguir más granadas cuando el jugador recoja munición.

En primer lugar, cambiemos algo del código de Player.gd para mostrar cuántas granadas quedan. Cambia process_UI por lo siguiente:

func process_UI(delta):
    if current_weapon_name == "UNARMED" or current_weapon_name == "KNIFE":
        # First line: Health, second line: Grenades
        UI_status_label.text = "HEALTH: " + str(health) + \
                "\n" + current_grenade + ": " + str(grenade_amounts[current_grenade])
    else:
        var current_weapon = weapons[current_weapon_name]
        # First line: Health, second line: weapon and ammo, third line: grenades
        UI_status_label.text = "HEALTH: " + str(health) + \
                "\nAMMO: " + str(current_weapon.ammo_in_weapon) + "/" + str(current_weapon.spare_ammo) + \
                "\n" + current_grenade + ": " + str(grenade_amounts[current_grenade])

Ahora mostraremos cuántas granadas le quedan al jugador en la UI.

Mientras estamos en Player.gd, agreguemos una función para agregar granadas al jugador. Añade la siguiente función a Player.gd:

func add_grenade(additional_grenade):
    grenade_amounts[current_grenade] += additional_grenade
    grenade_amounts[current_grenade] = clamp(grenade_amounts[current_grenade], 0, 4)

Ahora podemos añadir una granada usando add_grenade, y automáticamente será sujetada a un máximo de 4 granadas.

Truco

Puedes cambiar el 4 a una constante si quieres. Necesitarías hacer una nueva constante global, algo como MAX_GRENADES, y luego cambiar el la cantidad de de clamp(grenade_amounts[current_grenade], 0, 4) a clamp(grenade_amounts[current_grenade], 0, MAX_GRENADES)

Si no quieres limitar la cantidad de granadas que el jugador puede llevar, ¡quita la línea que sujeta las granadas por completo!

Ahora tenemos una función para añadir granadas, ¡abramos AmmoPickup.gd y usémosla!

Abre AmmoPickup.gd y ve a la función trigger_body_entered. Cámbialo a lo siguiente:

func trigger_body_entered(body):
    if body.has_method("add_ammo"):
        body.add_ammo(AMMO_AMOUNTS[kit_size])
        respawn_timer = RESPAWN_TIME
        kit_size_change_values(kit_size, false)

    if body.has_method("add_grenade"):
        body.add_grenade(GRENADE_AMOUNTS[kit_size])
        respawn_timer = RESPAWN_TIME
        kit_size_change_values(kit_size, false)

Ahora también estamos comprobando si el cuerpo tiene la función add_grenade. Si la tiene, la llamamos como si llamáramos a add_ammo.

Habrás notado que estamos usando una nueva constante que aún no hemos definido, GRENADE_AMOUNTS. Agreguémosla. Añade la siguiente variable de clase a AmmoPickup.gd con las otras variables de clase:

const GRENADE_AMOUNTS = [2, 0]
  • GRENADE_AMOUNTS: La cantidad de granadas que contiene cada recogida.

Fíjate en cómo el segundo elemento en GRENADE_AMOUNTS es 0. Esto es así para que la pequeña recogida de municiones no le dé al jugador ninguna granada adicional.


¡Ahora deberías ser capaz de lanzar granadas! ¡Inténtalo!

Añadiendo la capacidad de agarrar y lanzar los nodos de RigidBody al jugador

En el siguiente paso, demos al jugador la habilidad de agarrar y lanzar nodos RigidBody.

Abre Player.gd y añade las siguientes variables de clase:

var grabbed_object = null
const OBJECT_THROW_FORCE = 120
const OBJECT_GRAB_DISTANCE = 7
const OBJECT_GRAB_RAY_DISTANCE = 10
  • grabbed_object: Una variable para mantener el nodo agarrado RigidBody.

  • OBJECT_THROW_FORCE: La fuerza con la que el jugador lanza el objeto agarrado.

  • OBJECT_GRAB_DISTANCE: La distancia de la cámara a la que el jugador sostiene el objeto agarrado.

  • OBJECT_GRAB_RAY_DISTANCE: La distancia a la que va el Raycast. Esta es la distancia de agarre del jugador.

Una vez hecho esto, todo lo que tenemos que hacer es añadir algún código a process_input':

# ----------------------------------
# Grabbing and throwing objects

if Input.is_action_just_pressed("fire_grenade") and current_weapon_name == "UNARMED":
    if grabbed_object == null:
        var state = get_world().direct_space_state

        var center_position = get_viewport().size / 2
        var ray_from = camera.project_ray_origin(center_position)
        var ray_to = ray_from + camera.project_ray_normal(center_position) * OBJECT_GRAB_RAY_DISTANCE

        var ray_result = state.intersect_ray(ray_from, ray_to, [self, $Rotation_Helper/Gun_Fire_Points/Knife_Point/Area])
        if !ray_result.empty():
            if ray_result["collider"] is RigidBody:
                grabbed_object = ray_result["collider"]
                grabbed_object.mode = RigidBody.MODE_STATIC

                grabbed_object.collision_layer = 0
                grabbed_object.collision_mask = 0

    else:
        grabbed_object.mode = RigidBody.MODE_RIGID

        grabbed_object.apply_impulse(Vector3(0, 0, 0), -camera.global_transform.basis.z.normalized() * OBJECT_THROW_FORCE)

        grabbed_object.collision_layer = 1
        grabbed_object.collision_mask = 1

        grabbed_object = null

if grabbed_object != null:
    grabbed_object.global_transform.origin = camera.global_transform.origin + (-camera.global_transform.basis.z.normalized() * OBJECT_GRAB_DISTANCE)
# ----------------------------------

Repasemos lo que está pasando.

En primer lugar, comprobamos si la acción presionada es la de fire, y que el jugador está usando el UNARMED arma. Esto se debe a que sólo queremos que el jugador sea capaz de recoger y lanzar objetos cuando el jugador no está usando ningún arma. Esta es una elección de diseño, pero creo que le da a UNARMED un uso.

A continuación comprobamos si el grabbed_object es null o no.


Si grabbed_object es null, queremos ver si podemos coger un RigidBody.

Primero obtenemos el estado espacial directo de la actual World. Esto es para que podamos lanzar un rayo completamente desde el código, en lugar de tener que usar un nodo Raycast.

Nota

Consulte Ray-casting para obtener más información sobre raycasting en Godot.

Entonces obtenemos el centro de la pantalla dividiendo el tamaño actual Viewport por la mitad. Luego obtenemos el punto de origen y el punto final del rayo usando project_ray_origin y project_ray_normal de la cámara. Si quieres saber más sobre cómo funcionan estas funciones, mira Ray-casting.

A continuación enviamos el rayo al estado espacial y vemos si obtiene un resultado. Añadimos el jugador y el cuchillo Area como dos excepciones para que el jugador no se pueda llevar a sí mismo o la colisión del cuchillo Area.

Luego comprobamos si tenemos un resultado del rayo. Si ningún objeto ha colisionado con el rayo, se devolverá un Diccionario vacío. Si el Diccionario no está vacío (es decir, si al menos un objeto ha colisionado), entonces veremos si el colisionador con el rayo es un RigidBody.

Si el rayo chocó con un RigidBody, ponemos grabbed_object al colisionador con el que chocó el rayo. Luego ponemos el modo en el RigidBody con el que chocamos a MODE_STATIC para que no se mueva en nuestras manos.

Por último, ponemos la capa de colisión y la máscara de colisión del RigidBody agarrador a 0. Esto hará que el agarrado RigidBody no tenga capa o máscara de colisión, lo que significa que no podrá colisionar con nada mientras lo mantengamos.

Nota

Vea :ref:`Physics introduction <doc_physics_introduction_collision_layer_code_example> para más información sobre las máscaras de colisión de Godot.


Si grabbed_object" no es ``null, entonces tenemos que tirar el RigidBody que el jugador está sosteniendo.

Primero establecemos el mode del RigidBody que estamos agarrando en MODE_RIGID.

Nota

Esto es hacer una suposición bastante grande que todos los cuerpos rígidos estarán usando MODE_RIGID. Mientras que ese es el caso de esta serie de tutoriales, puede que no sea el caso en otros proyectos.

Si tienes cuerpos rígidos con diferentes modos, puede que necesites almacenar el modo del RigidBody que has recogido en una variable de clase para que puedas cambiarlo de nuevo al modo en que estaba antes de recogerlo.

Luego aplicamos un impulso para que vuele hacia delante. Lo mandamos en la dirección en la cual la cámara esta mirando, usando la fuerza que introducimos en la variable OBJECT_THROW_FORCE.

Luego ponemos la capa y la máscara de colisión de RigidBody en 1, para que pueda colisionar con cualquier cosa en la capa 1 de nuevo.

Nota

Esto, una vez más, supone una suposición bastante grande de que todos los cuerpos rígidos estarán solo en la capa de colisión 1, y todas las máscaras de colisión estarán en la capa 1. Si está utilizando este script en otros proyectos, es posible que deba almacenar la capa / máscara de colisión de RigidBody en una variable antes de cambiarlos a 0, por lo que tendría la capa / máscara de colisión original para configurarlos cuando está invirtiendo el proceso.

Finalmente, cambiamos grabbed_object a ``null``ya que el jugador ha lanzado satisfactoriamente el objeto que estaba sujetando.


Lo último que hacemos es comprobar si grabbed_object es igual a null, fuera de todo el código relacionado con agarrar/tirar.

Nota

Aunque técnicamente no está relacionado con la entrada, es bastante fácil mover el objeto agarrado aquí porque son sólo dos líneas, y entonces todo el código de agarre/lanzamiento está en un lugar

Si el jugador está sujetando un objeto, cambiamos su posición global a la de la cámara más OBJECT_GRAB_DISTANCE en la dirección en la que mira la cámara.


Antes de comprobar esto, tenemos que cambiar algo en _physics_process. Mientras que el jugador está sujetando un objeto, no queremos que el jugador pueda cambiar de armas o recargar, entonces cambiamos _physics_process a lo siguiente:

func _physics_process(delta):
    process_input(delta)
    process_view_input(delta)
    process_movement(delta)

    if grabbed_object == null:
        process_changing_weapons(delta)
        process_reloading(delta)

    # Process the UI
    process_UI(delta)

Ahora el jugador no puede cambiar de arma o recargar mientras sujeta un objeto.

Ahora puedes agarrar y lanzar nodos de RigidBody mientas que estas en el estado UNARMED. ¡Inténtalo tú!

Añadir una torreta

Ahora,¡ hagamos que la torreta dispare al jugador!

Abre Turret.tscn. Expande Turret si no estaba ya expandido.

Date cuenta de que la torreta esta dividida en varias partes: Base, Head, Vision_Area, y un nodo Smoke Particles.

Abre Base y encontrarás que es un StaticBody y una maya. Abre Head y encontrarás que son varias mayas, un StaticBody y un nodo Raycast.

Una cosa a tener en cuenta con el "Head" es que el raycast será desde donde las balas de la torreta dispararán si usamos el raycasting. También tenemos dos mallas llamadas Flash y Flash_2. Estos serán los destellos del cañón que muestra brevemente cuando la torreta dispara.

Vision_Area es un Area que usaremos como la habilidad de la torreta para ver. Cuando algo entra en Vision_Area, asumiremos que la torreta lo puede ver.

Smoke es un nodo Particles que se activará cuando la torreta esté destruida o reparándose.


Ahora que hemos visto como se compone una escena, podemos empezar a escribir el código para la torreta. Selecciona Turret y crea un fichero llamado Turret.gd. Añade lo siguiente a Turret.gd:

extends Spatial

export (bool) var use_raycast = false

const TURRET_DAMAGE_BULLET = 20
const TURRET_DAMAGE_RAYCAST = 5

const FLASH_TIME = 0.1
var flash_timer = 0

const FIRE_TIME = 0.8
var fire_timer = 0

var node_turret_head = null
var node_raycast = null
var node_flash_one = null
var node_flash_two = null

var ammo_in_turret = 20
const AMMO_IN_FULL_TURRET = 20
const AMMO_RELOAD_TIME = 4
var ammo_reload_timer = 0

var current_target = null

var is_active = false

const PLAYER_HEIGHT = 3

var smoke_particles

var turret_health = 60
const MAX_TURRET_HEALTH = 60

const DESTROYED_TIME = 20
var destroyed_timer = 0

var bullet_scene = preload("Bullet_Scene.tscn")

func _ready():

    $Vision_Area.connect("body_entered", self, "body_entered_vision")
    $Vision_Area.connect("body_exited", self, "body_exited_vision")

    node_turret_head = $Head
    node_raycast = $Head/Ray_Cast
    node_flash_one = $Head/Flash
    node_flash_two = $Head/Flash_2

    node_raycast.add_exception(self)
    node_raycast.add_exception($Base/Static_Body)
    node_raycast.add_exception($Head/Static_Body)
    node_raycast.add_exception($Vision_Area)

    node_flash_one.visible = false
    node_flash_two.visible = false

    smoke_particles = $Smoke
    smoke_particles.emitting = false

    turret_health = MAX_TURRET_HEALTH


func _physics_process(delta):

    if is_active == true:

        if flash_timer > 0:
            flash_timer -= delta

            if flash_timer <= 0:
                node_flash_one.visible = false
                node_flash_two.visible = false

        if current_target != null:

            node_turret_head.look_at(current_target.global_transform.origin + Vector3(0, PLAYER_HEIGHT, 0), Vector3(0, 1, 0))

            if turret_health > 0:

                if ammo_in_turret > 0:
                    if fire_timer > 0:
                        fire_timer -= delta
                    else:
                        fire_bullet()
                else:
                    if ammo_reload_timer > 0:
                        ammo_reload_timer -= delta
                    else:
                        ammo_in_turret = AMMO_IN_FULL_TURRET

    if turret_health <= 0:
        if destroyed_timer > 0:
            destroyed_timer -= delta
        else:
            turret_health = MAX_TURRET_HEALTH
            smoke_particles.emitting = false


func fire_bullet():

    if use_raycast == true:
        node_raycast.look_at(current_target.global_transform.origin + Vector3(0, PLAYER_HEIGHT, 0), Vector3(0, 1, 0))

        node_raycast.force_raycast_update()

        if node_raycast.is_colliding():
            var body = node_raycast.get_collider()
            if body.has_method("bullet_hit"):
                body.bullet_hit(TURRET_DAMAGE_RAYCAST, node_raycast.get_collision_point())

        ammo_in_turret -= 1

    else:
        var clone = bullet_scene.instance()
        var scene_root = get_tree().root.get_children()[0]
        scene_root.add_child(clone)

        clone.global_transform = $Head/Barrel_End.global_transform
        clone.scale = Vector3(8, 8, 8)
        clone.BULLET_DAMAGE = TURRET_DAMAGE_BULLET
        clone.BULLET_SPEED = 60

        ammo_in_turret -= 1

    node_flash_one.visible = true
    node_flash_two.visible = true

    flash_timer = FLASH_TIME
    fire_timer = FIRE_TIME

    if ammo_in_turret <= 0:
        ammo_reload_timer = AMMO_RELOAD_TIME


func body_entered_vision(body):
    if current_target == null:
        if body is KinematicBody:
            current_target = body
            is_active = true


func body_exited_vision(body):
    if current_target != null:
        if body == current_target:
            current_target = null
            is_active = false

            flash_timer = 0
            fire_timer = 0
            node_flash_one.visible = false
            node_flash_two.visible = false


func bullet_hit(damage, bullet_hit_pos):
    turret_health -= damage

    if turret_health <= 0:
        smoke_particles.emitting = true
        destroyed_timer = DESTROYED_TIME

Esto es un código un poco largo, así que vamos a dividirlo por funciones. Primero echemos un vistazo a la variables de la clase:

  • use_raycast: Un booleano exportado para que podamos cambiar si la torreta usa objetos o raycasting para las balas.

  • TURRET_DAMAGE_BULLET: La cantidad de daño que hace una sola escena de una bala.

  • TURRET_DAMAGE_RAYCAST:La cantidad de daño que hace una sola bala Raycast.

  • FLASH_TIME: La cantidad de tiempo (en segundos) que las mallas de flash del cañón son visibles.

  • flash_timer: Una variable para rastrear cuánto tiempo han sido visibles las mallas de flash del cañón.

  • FIRE_TIME: El tiempo(en segundos) necesitados para disparar una bala.

  • fire_timer: Una variable para traquear cuanto tiempo ha pasado desde el ultimo disparo de la torreta.

  • node_turret_head: Una variable para mantener el nodo``Head``.

  • node_raycast: Una variable para mantener el nodo Raycast junto a la cabeza de la torreta.

  • node_flash_one: Una variable para mantener el primer flash de disparo:ref:MeshInstance <class_MeshInstance>.

  • node_flash_two: Una variable para mentener el segundo flash de disparo MeshInstance.

  • ammo_in_turret: Cuanta municion hay en la torreta en el momento.

  • AMMO_IN_FULL_TURRET: La cantidad de municion en una torreta llena.

  • AMMO_RELOAD_TIME: La cantidad de tiempo necesaria para recargar la torreta.

  • ammo_reload_timer: Una variable para traquear cuanto tiempo la torreta ha estado recargando.

  • current_target: El objetivo de la torreta en el momento.

  • is_active: Una variable para traquear si la torreta tiene la posibilidad de disparar a su objetivo.

  • PLAYER_HEIGHT: La altura que le añadimos al objetivo para que no estemos disparandole a sus pies.

  • smoke_particles: Una variable para mantener el nodo de particulas de humo.

  • turret_health: La cantidad de vida que tiene la torreta en el momento.

  • MAX_TURRET_HEALTH: La cantidad de vida que una torreta al maximo de vida tiene.

  • DESTROYED_TIME: La cantidad de tiempo (en segundos) que son necesarios para que una torreta destrozada se repare.

  • destroyed_timer: Una variable para monitorear la cantidad de tiempo en el que una torreta ha sido destruida.

  • bullet_scene: La escena de la bala que la torreta dispara (misma escena que la de la pistola del jugador)

Esas son unas cuantas variables!


Pasemos al siguiente _ready.

En primer lugar, obtenemos el área de visión y conectamos las señales de body_entered y body_exited a body_entered_vision y body_exited_vision, respectivamente.

Luego obtenemos todos los nodos y los asignamos a las respectivas variables.

A continuación, añadimos algunas excepciones a la Raycast para que la torreta no pueda hacerse daño.

Entonces hacemos que ambas mallas de flash sean invisibles al principio, ya que no vamos a disparar durante el _ready.

Luego obtenemos el nodo de partículas de humo y lo asignamos a la variable smoke_particles. También ponemos emitting en false para asegurarnos de que las partículas no emitan hasta que la torreta se rompa.

Por último, ponemos la salud de la torreta en``MAX_TURRET_HEALTH`` para que empiece con plena salud.


Ahora vamos a pasar por el _physics_process.

En primer lugar, comprobamos si la torreta está activa. Si la torreta está activa, queremos procesar el código de disparo.

A continuación, si flash_timer es mayor que cero, que significa que las mallas de flash son visibles, queremos eliminar el delta de flash_timer. Si flash_timer llega a cero o menos después de restar delta, queremos ocultar ambas mallas de flash.

A continuación, comprobamos si la torreta tiene un objetivo. Si la torreta tiene un objetivo, hacemos que la cabeza de la torreta lo mire, añadiendo PLAYER_HEIGHT para que no apunte a los pies del jugador.

Entonces comprobamos si la salud de la torreta es mayor que cero. Si lo es, entonces comprobamos si hay munición en la torreta.

Si lo hay, entonces comprobamos si el fire_timer es mayor que cero. Si lo es, la torreta no puede disparar y tenemos que quitar delta del fire_timer. Si el fire_timer es menor o igual a cero, la torreta puede disparar una bala, así que llamamos a la función fire_bullet.

Si no hay munición en la torreta, comprobamos si el ammo_reload_timer es mayor que cero. Si lo es, restamos delta del ammo_relaod_timer. Si ammo_reload_timer es menor o igual a cero, ponemos ammo_in_turret en AMMO_IN_FULL_TURRET porque la torreta ha esperado lo suficiente para recargar su munición.

A continuación, comprobamos si la salud de la torreta es menor o igual que 0 fuera de si está activa o no. Si la salud de la torreta es cero o menos, entonces comprobamos si el destroyed_timer es mayor que cero. Si lo es, restamos delta de destroyed_timer.

A continuación, comprobamos si la salud de la torreta es menor o igual que 0 fuera de si está activa o no. Si la salud de la torreta es cero o menos, entonces comprobamos si el destroyed_timer es mayor que cero. Si lo es, restamos delta de destroyed_timer.


A continuación vamos a pasar a través de fire_bullet.

En primer lugar, comprobamos si la torreta está usando un raycast.

El código para usar un raycast es casi idéntico al código del rifle de Parte 2, así que sólo voy a repasarlo brevemente.

Primero hacemos que el raycast mire al objetivo, asegurándonos de que el raycast dará en el blanco si no hay nada en el camino. Luego forzamos el raycast a actualizarse para obtener un fotograma perfecto de verificación de colisión. Luego comprobamos si el raycast ha colisionado con algo. Si lo ha hecho, entonces comprobamos si el cuerpo colisionado tiene el método "bullet_hit". Si lo tiene, lo llamamos y pasamos el daño que hace una bala de un solo raycast junto con la transformación del raycast. Luego restamos 1 de ammo_in_turret.

Si la torreta no usa un raycast, en su lugar producimos un objeto de bala. Este código es casi completamente igual al código de la pistola de Parte 2, así que al igual que con el código de raycast, sólo voy a repasarlo brevemente.

Primero hacemos un clon de bala y lo asignamos a clone. Luego lo añadimos como un hijo del nodo raíz. Establecemos la transformación global de la bala en el extremo del cañón, la escalamos ya que es demasiado pequeña, y establecemos su daño y velocidad usando las variables de clase constante de la torreta. Luego restamos 1 de ammo_in_turret.

Entonces, independientemente del método de bala que utilizamos, hacemos visibles las dos mallas de flash del cañón. Ponemos flash_timer y fire_timer en FLASH_TIME y FIRE_TIME, respectivamente. Entonces comprobamos si la torreta ha usado la última bala de su munición. Si lo ha hecho, ponemos el ammo_reload_timer a AMMO_RELOAD_TIME para que la torreta se recargue.


Veamos a continuación body_entered_vision, y por suerte es bastante corta.

Primero comprobamos si la torreta tiene actualmente un objetivo comprobando si current_target es igual a null. Si la torreta no tiene un objetivo, entonces comprobamos si el cuerpo que acaba de entrar en la visión Area es un KinematicBody.

Nota

Asumimos que la torreta sólo debería disparar a los nodos KinematicBody ya que eso lo que está usando el jugador.

Si el cuerpo que acaba de entrar en la visión Area es un KinematicBody, ponemos current_target al cuerpo, y ponemos is_active a true.


Ahora veamos la body_exited_vision.

En primer lugar, comprobamos si la torreta tiene un objetivo. Si lo tiene, entonces comprobamos si el cuerpo que acaba de salir de la visión de la torreta Area es el objetivo de la torreta.

Si el cuerpo que acaba de salir de la visión Area es el objetivo actual de la torreta, ponemos current_target en null, ponemos is_active a false, y reajustamos todas las variables relacionadas con el disparo de la torreta ya que ésta ya no tiene un objetivo al que disparar.


Por último, veamos el bullet_hit.

Primero restamos el daño que la bala causa a la salud de la torreta.

Entonces, comprobamos si la torreta ha sido destruida (la salud es cero o menos). Si la torreta está destruida, empezamos a emitir las partículas de humo y ponemos destroyed_timer en DESTROYED_TIME para que la torreta tenga que esperar antes de ser reparada.


Uf, con todo eso hecho y codificado, sólo tenemos una última cosa que hacer antes de que la torreta esté lista para su uso. Abre Turret.tscn si no está ya abierta y selecciona uno de los nodos StaticBody de cualquiera de los nodos Base o Head. Crea un nuevo script llamado TurretBodies.gd y adjúntalo a cualquiera de los StaticBody que hayas seleccionado.

Agregue el siguiente código a `` TurretBodies.gd``:

extends StaticBody

export (NodePath) var path_to_turret_root

func _ready():
    pass

func bullet_hit(damage, bullet_hit_pos):
    if path_to_turret_root != null:
        get_node(path_to_turret_root).bullet_hit(damage, bullet_hit_pos)

Todo lo que este código hace es llamar bullet_hit en cualquier nodo al que path_to_turret_root lleva. Vuelve al editor y asigna el NodePath al nodo Turret.

Ahora selecciona el otro nodo StaticBody (ya sea en Body o Head) y asígnale el script TurretBodies.gd. Una vez que el script se adjunta, asigna de nuevo el nodo NodePath al nodo Turret.


Lo último que debemos hacer es agregar una forma para que el jugador resulte herido. Como todas las balas usan la función `` bullet_hit``, necesitamos agregar esa función al jugador.

Abre Player.gd y añade lo siguiente:

func bullet_hit(damage, bullet_hit_pos):
    health -= damage

Con todo eso hecho, ¡deberíais tener torretas totalmente operativas! ¡Ve a colocar algunas en una/ambas/todas las escenas y pruébalas!

Notas finales

../../../_images/PartFiveFinished.png

Ahora puedes coger RigidBody nodos y lanzar granadas. Ahora también tenemos torretas para disparar al jugador.

En: ref: doc_fps_tutorial_part_six, vamos a agregar un menú principal y un menú de pausa, agregar un sistema de reaparición (respawn) para el jugador y cambiar/mover el sistema de sonido para que podamos usarlo desde cualquier script.

Advertencia

¡Si alguna vez te pierdes, asegúrate de leer el código de nuevo!

Puede descargar el proyecto terminado para esta parte aquí: Godot_FPS_Part_5.zip