Parte 4

Resumen

En esta parte, añadiremos las recogidas de salud, las recogidas de munición, los objetivos que el jugador puede destruir, el soporte para los joypads, y añadiremos la capacidad de cambiar las armas con la rueda de desplazamiento.

../../../_images/PartFourFinished.png

Nota

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

¡Vamos a empezar!

Añadiendo la entrada del joypad

Nota

En Godot, cualquier controlador de juego se conoce como un joypad. Esto incluye: ¡Controladores de consola, Joysticks (como los de los simuladores de vuelo), Ruedas (como los de los simuladores de conducción), Controladores VR, y más!

En primer lugar, necesitamos cambiar algunas cosas en el mapa de entrada de nuestro proyecto. Abre la configuración del proyecto y selecciona la pestaña Input Map.

Ahora tenemos que añadir algunos botones del joypad a nuestras diversas acciones. Haz clic en el icono más y selecciona Joy Button.

../../../_images/ProjectSettingsAddKey.png

Siéntete libre de usar la disposición de los botones que quieras. Asegúrate de que el dispositivo seleccionado esté puesto en 0. En el proyecto terminado, usaremos lo siguiente:

  • movement_sprint: Device 0, Button 4 (L, L1)

  • fire: Device 0, Button 0 (PS Cross, XBox A, Nintendo B)

  • reload: Device 0, Button 0 (PS Square, XBox X, Nintendo Y)

  • flashlight: Device 0, Button 12 (D-Pad Up)

  • shift_weapon_positive: Device 0, Button 15 (D-Pad Right)

  • shift_weapon_negative: Device 0, Button 14 (D-Pad Left)

  • fire_grenade: Device 0, Button 1 (PS Circle, XBox B, Nintendo A).

Nota

Estos ya están configurados para ti si descargaste los activos de recursos

Una vez que esté satisfecho con la entrada, cierre la configuración del proyecto y guárdela.


Ahora abramos Player.gd y agreguemos la entrada del joypad.

Primero, necesitamos definir algunas nuevas variables de clase. Añade las siguientes variables de clase a Player.gd:

# You may need to adjust depending on the sensitivity of your joypad
var JOYPAD_SENSITIVITY = 2
const JOYPAD_DEADZONE = 0.15

Veamos lo que hace cada uno de estos:

  • JOYPAD_SENSITIVITY: Esta es la rapidez con la que los joysticks del Joypad moverán la cámara.

  • JOYPAD_DEADZONE: La zona muerta para el joypad. Puede que tengas que ajustarlo dependiendo de tu joypad.

Nota

Muchos mandos se mueven alrededor de un cierto punto. Para contrarrestar esto, ignoramos cualquier movimiento dentro de un radio de JOYPAD_DEADZONE. Si no ignoráramos dicho movimiento, la cámara se podría temblar.

Además, estamos definiendo JOYPAD_SENSITIVITY como una variable en lugar de una constante porque más tarde la cambiaremos.

¡Ahora estamos listos para empezar a manejar la entrada del joypad!


En process_input, añade el siguiente código justo antes de input_movement_vector = input_movement_vector.normalized():

# Add joypad input if one is present
if Input.get_connected_joypads().size() > 0:

    var joypad_vec = Vector2(0, 0)

    if OS.get_name() == "Windows":
        joypad_vec = Vector2(Input.get_joy_axis(0, 0), -Input.get_joy_axis(0, 1))
    elif OS.get_name() == "X11":
        joypad_vec = Vector2(Input.get_joy_axis(0, 1), Input.get_joy_axis(0, 2))
    elif OS.get_name() == "OSX":
        joypad_vec = Vector2(Input.get_joy_axis(0, 1), Input.get_joy_axis(0, 2))

    if joypad_vec.length() < JOYPAD_DEADZONE:
        joypad_vec = Vector2(0, 0)
    else:
        joypad_vec = joypad_vec.normalized() * ((joypad_vec.length() - JOYPAD_DEADZONE) / (1 - JOYPAD_DEADZONE))

    input_movement_vector += joypad_vec
# Add joypad input if one is present
if Input.get_connected_joypads().size() > 0:

    var joypad_vec = Vector2(0, 0)

    if OS.get_name() == "Windows" or OS.get_name() == "X11":
        joypad_vec = Vector2(Input.get_joy_axis(0, 0), -Input.get_joy_axis(0, 1))
    elif OS.get_name() == "OSX":
        joypad_vec = Vector2(Input.get_joy_axis(0, 1), Input.get_joy_axis(0, 2))

    if joypad_vec.length() < JOYPAD_DEADZONE:
        joypad_vec = Vector2(0, 0)
    else:
        joypad_vec = joypad_vec.normalized() * ((joypad_vec.length() - JOYPAD_DEADZONE) / (1 - JOYPAD_DEADZONE))

    input_movement_vector += joypad_vec

Repasemos lo que estamos haciendo.

En primer lugar, comprobamos si hay un Joypad conectado.

Si hay un joypad conectado, entonces tenemos sus ejes de palo izquierdo para derecha/izquierda y arriba/abajo. Debido a que un controlador Xbox 360 con cable tiene diferentes mapeos de ejes de joystick basados en el sistema operativo, usaremos diferentes ejes basados en el sistema operativo.

Advertencia

Este tutorial asume que estás usando un XBox 360 o un controlador con cable de PlayStation. Además, no tengo (actualmente) acceso a un ordenador Mac, así que puede que haya que cambiar los ejes del joystick. Si es así, por favor, abre un problema de GitHub en el repositorio de documentación de Godot! ¡Gracias!

A continuación, comprobamos si la longitud del vector del joypad está dentro del radio JOYPAD_DEADZONE. Si es así, ponemos joypad_vec en un Vector2 vacío. Si no lo está, usamos una zona Muerta Radial escalada para un cálculo preciso de la zona muerta.

Nota

Puedes encontrar un gran artículo explicando todo sobre cómo manejar las zonas muertas del joypad/controlador aquí.

Estamos usando una versión traducida del código de la zona muerta radial escalada que se proporciona en ese artículo. El artículo es una gran lectura, ¡y sugiero encarecidamente que le echen un vistazo!

Finalmente, añadimos joypad_vec a input_movement_vector.

Truco

¿Recuerdas cómo normalizamos el input_movement_vector? ¡Esto es el porqué! !Si no normalizamos el input_movement_vector, el jugador puede moverse más rápido si empuja en la misma dirección con el teclado y el joypad!


Haz una nueva función llamada process_view_input y añade lo siguiente:

func process_view_input(delta):

    if Input.get_mouse_mode() != Input.MOUSE_MODE_CAPTURED:
        return

    # NOTE: Until some bugs relating to captured mice are fixed, we cannot put the mouse view
    # rotation code here. Once the bug(s) are fixed, code for mouse view rotation code will go here!

    # ----------------------------------
    # Joypad rotation

    var joypad_vec = Vector2()
    if Input.get_connected_joypads().size() > 0:

        if OS.get_name() == "Windows":
            joypad_vec = Vector2(Input.get_joy_axis(0, 2), Input.get_joy_axis(0, 3))
        elif OS.get_name() == "X11":
            joypad_vec = Vector2(Input.get_joy_axis(0, 3), Input.get_joy_axis(0, 4))
        elif OS.get_name() == "OSX":
            joypad_vec = Vector2(Input.get_joy_axis(0, 3), Input.get_joy_axis(0, 4))

        if joypad_vec.length() < JOYPAD_DEADZONE:
            joypad_vec = Vector2(0, 0)
        else:
            joypad_vec = joypad_vec.normalized() * ((joypad_vec.length() - JOYPAD_DEADZONE) / (1 - JOYPAD_DEADZONE))

        rotation_helper.rotate_x(deg2rad(joypad_vec.y * JOYPAD_SENSITIVITY))

        rotate_y(deg2rad(joypad_vec.x * JOYPAD_SENSITIVITY * -1))

        var camera_rot = rotation_helper.rotation_degrees
        camera_rot.x = clamp(camera_rot.x, -70, 70)
        rotation_helper.rotation_degrees = camera_rot
    # ----------------------------------
func process_view_input(delta):

   if Input.get_mouse_mode() != Input.MOUSE_MODE_CAPTURED:
       return

   # NOTE: Until some bugs relating to captured mice are fixed, we cannot put the mouse view
   # rotation code here. Once the bug(s) are fixed, code for mouse view rotation code will go here!

   # ----------------------------------
   # Joypad rotation

   var joypad_vec = Vector2()
   if Input.get_connected_joypads().size() > 0:

       if OS.get_name() == "Windows" or OS.get_name() == "X11":
           joypad_vec = Vector2(Input.get_joy_axis(0, 2), Input.get_joy_axis(0, 3))
       elif OS.get_name() == "OSX":
           joypad_vec = Vector2(Input.get_joy_axis(0, 3), Input.get_joy_axis(0, 4))

       if joypad_vec.length() < JOYPAD_DEADZONE:
           joypad_vec = Vector2(0, 0)
       else:
           joypad_vec = joypad_vec.normalized() * ((joypad_vec.length() - JOYPAD_DEADZONE) / (1 - JOYPAD_DEADZONE))

       rotation_helper.rotate_x(deg2rad(joypad_vec.y * JOYPAD_SENSITIVITY))

       rotate_y(deg2rad(joypad_vec.x * JOYPAD_SENSITIVITY * -1))

       var camera_rot = rotation_helper.rotation_degrees
       camera_rot.x = clamp(camera_rot.x, -70, 70)
       rotation_helper.rotation_degrees = camera_rot
   # ----------------------------------

Repasemos lo que está pasando:

En primer lugar, comprobamos el modo del ratón. Si el modo del ratón no es MOUSE_MODE_CAPTURED, queremos volver, lo que hará que se salte el código de abajo.

A continuación, definimos un nuevo Vector2 llamado joypad_vec. Esto mantendrá la posición correcta del joystick. Basándonos en el sistema operativo, fijamos sus valores para que se mapee a los ejes adecuados para el joystick derecho.

Advertencia

Como ya se ha dicho, no tengo (actualmente) acceso a un ordenador Mac, así que puede que haya que cambiar los ejes de los joysticks. ¡Si lo hacen, por favor, abran un número de GitHub en el repositorio de documentación de Godot! ¡Gracias!

Entonces tenemos en cuenta la zona muerta del Joypad, exactamente como en process_input.

Luego, rotamos rotation_helper y el jugador KinematicBody usando joypad_vec.

Fíjate en cómo el código que maneja la rotación del jugador y la rotation_helper es exactamente el mismo que el código en _input. Todo lo que hemos hecho es cambiar los valores para usar joypad_vec y JOYPAD_SENSITIVITY.

Nota

Debido a algunos errores relacionados con el ratón en Windows, no podemos poner la rotación del ratón en process_view también. Una vez que estos errores sean corregidos, esto probablemente se actualizará para colocar la rotación del ratón aquí en process_view_input también.

Finalmente, fijamos la rotación de la cámara para que el jugador no pueda mirar al revés.


La última cosa que necesitamos hacer es añadir process_view_input a _physics_process.

Una vez que process_view_input sea añadido a physics_process, deberías ser capaz de jugar usando un joypad!

Nota

Decidí no usar los disparadores del joypad para disparar porque entonces tendríamos que hacer un poco más de manejo del eje, y porque prefiero usar los botones del hombro para disparar.

Si quieres usar los disparadores para disparar, tendrás que cambiar cómo funciona el disparo en process_input. Necesitas obtener los valores de los ejes para los disparadores, y comprobar si está por encima de un cierto valor, digamos 0.8 por ejemplo. Si es así, agregas el mismo código que cuando se presionó la acción fire.

Añadir la entrada de la rueda de desplazamiento del ratón

Añadamos otra característica relacionada con la entrada antes de empezar a trabajar en las recogidas y el objetivo. Añadamos la posibilidad de cambiar de arma con la rueda de desplazamiento del ratón.

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

var mouse_scroll_value = 0
const MOUSE_SENSITIVITY_SCROLL_WHEEL = 0.08

Repasemos lo que hará cada una de estas nuevas variables:

  • mouse_scroll_value: El valor de la rueda de desplazamiento del ratón.

  • MOUSE_SENSITIVITY_SCROLL_WHEEL: Cuánto aumenta el valor de mouse_scroll_value con una sola acción de scroll


Ahora agreguemos lo siguiente a _input:

if event is InputEventMouseButton and Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED:
    if event.button_index == BUTTON_WHEEL_UP or event.button_index == BUTTON_WHEEL_DOWN:
        if event.button_index == BUTTON_WHEEL_UP:
            mouse_scroll_value += MOUSE_SENSITIVITY_SCROLL_WHEEL
        elif event.button_index == BUTTON_WHEEL_DOWN:
            mouse_scroll_value -= MOUSE_SENSITIVITY_SCROLL_WHEEL

        mouse_scroll_value = clamp(mouse_scroll_value, 0, WEAPON_NUMBER_TO_NAME.size() - 1)

        if changing_weapon == false:
            if reloading_weapon == false:
                var round_mouse_scroll_value = int(round(mouse_scroll_value))
                if WEAPON_NUMBER_TO_NAME[round_mouse_scroll_value] != current_weapon_name:
                    changing_weapon_name = WEAPON_NUMBER_TO_NAME[round_mouse_scroll_value]
                    changing_weapon = true
                    mouse_scroll_value = round_mouse_scroll_value

Repasemos lo que está pasando aquí:

En primer lugar, comprobamos si el evento es un evento InputEventMouseButton y que el modo del ratón es MOUSE_MODE_CAPTURED. Luego, comprobamos si el índice del botón es un índice BUTTON_WHEEL_UP o BUTTON_WHEEL_DOWN.

Si el índice del evento es de hecho un índice de rueda de botones, entonces comprobamos si es un índice de BUTTON_WHEEL_UP o BUTTON_WHEEL_DOWN. Basándonos en si está arriba o abajo, sumamos o restamos MOUSE_SENSITIVITY_SCROLL_WHEEL a/desde mouse_scroll_value.

A continuación, fijamos el valor de desplazamiento del ratón para asegurarnos de que está dentro del rango de armas seleccionables.

Luego comprobamos si el jugador está cambiando de armas o recargando. Si el jugador no hace ninguna de las dos cosas, redondeamos el mouse_scroll_value y lo ponemos en int.

Nota

Estamos asignando mouse_scroll_value a un int para poder usarla como clave en nuestro diccionario. Si lo dejamos como un real, obtendríamos un error al intentar ejecutar el proyecto.

A continuación, comprobamos si el nombre del arma en round_mouse_scroll_value no es igual al nombre actual del arma usando WEAPON_NUMBER_TO_NAME. Si el arma es diferente al arma actual del jugador, asignamos changing_weapon_name, ponemos changing_weapon en true para que el jugador cambie las armas en process_changing_weapon, y ponemos mouse_scroll_value a round_mouse_scroll_value.

Truco

La razón por la que estamos ajustando mouse_scroll_value al valor de desplazamiento redondeado es porque no queremos que el jugador mantenga su rueda de desplazamiento del ratón justo entre los valores, dándole la posibilidad de cambiar casi extremadamente rápido. Al asignar mouse_scroll_value al round_mouse_scroll_value, nos aseguramos de que cada arma necesite exactamente la misma cantidad de desplazamiento para cambiar.


Una cosa más que necesitamos cambiar es en process_input. En el código para cambiar las armas, añade lo siguiente justo después de la línea changing_weapon = true:

mouse_scroll_value = weapon_change_number

Ahora el valor de desplazamiento se cambiará con la entrada del teclado. Si no cambiamos esto, el valor de desplazamiento estaría fuera de sincronía. Si la rueda de desplazamiento estuviera desincronizada, el desplazamiento hacia adelante o hacia atrás no pasaría a la siguiente/última arma, sino a la siguiente/última arma a la que cambió la rueda de desplazamiento.


¡Ahora puedes cambiar de armas usando la rueda de desplazamiento! ¡Ve a probarlo!

Agregando los items de salud

Ahora que el jugador tiene salud y munición, idealmente necesitamos una forma de reponer esos recursos.

Abre Health_Pickup.tscn.

Expande el Holder si no está ya expandido. Fíjate en que tenemos dos nodos espaciales, uno llamado Health_Kit y otro llamado Health_Kit_Small.

Esto es porque en realidad vamos a hacer dos tamaños de recipientes para la salud, uno pequeño y otro grande/normal. Health_Kit y Health_Kit_Small sólo tienen un único MeshInstance como sus hijos.

A continuación, amplia Health_Pickup_Trigger. Este es un nodo Area que vamos a usar para comprobar si el jugador ha caminado lo suficientemente cerca para recoger el kit de salud. Si lo expandes, encontrarás dos formas de colisión, una para cada tamaño. Usaremos un tamaño diferente de forma de colisión basado en el tamaño del recipiente de salud, así que el recipiente de salud más pequeña tiene una forma de colisión de disparo más cercana a su tamaño.

Lo primero que notamos es que tenemos un nodo AnimationPlayer para que el kit de salud se mueva y rote lentamente.

Selecciona Health_Pickup y añade un nuevo script llamado Health_Pickup.gd. Añade lo siguiente:

extends Spatial

export (int, "full size", "small") var kit_size = 0 setget kit_size_change

# 0 = full size pickup, 1 = small pickup
const HEALTH_AMOUNTS = [70, 30]

const RESPAWN_TIME = 20
var respawn_timer = 0

var is_ready = false

func _ready():

    $Holder/Health_Pickup_Trigger.connect("body_entered", self, "trigger_body_entered")

    is_ready = true

    kit_size_change_values(0, false)
    kit_size_change_values(1, false)
    kit_size_change_values(kit_size, true)


func _physics_process(delta):
    if respawn_timer > 0:
        respawn_timer -= delta

        if respawn_timer <= 0:
            kit_size_change_values(kit_size, true)


func kit_size_change(value):
    if is_ready:
        kit_size_change_values(kit_size, false)
        kit_size = value
        kit_size_change_values(kit_size, true)
    else:
        kit_size = value


func kit_size_change_values(size, enable):
    if size == 0:
        $Holder/Health_Pickup_Trigger/Shape_Kit.disabled = !enable
        $Holder/Health_Kit.visible = enable
    elif size == 1:
        $Holder/Health_Pickup_Trigger/Shape_Kit_Small.disabled = !enable
        $Holder/Health_Kit_Small.visible = enable


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

Vamos a repasar lo que está haciendo este script, empezando por sus variables de clase:

  • kit_size: El tamaño del recipiente de salud. Fíjate en cómo usamos la función setget para saber si ha cambiado.

  • HEALTH_AMMOUNTS: La cantidad de salud que contiene cada recipiente de cada tamaño.

  • RESPAWN_TIME: La cantidad de tiempo, en segundos, que le toma al recipiente de salud reaparecer

  • respawn_timer: Una variable usada para rastrear cuánto tiempo ha estado el recipiente de salud esperando para reaparecer.

  • is_ready: Una variable para rastrear si la función _ready ha sido llamada o no.

Estamos usando is_ready porque las funciones setget son llamadas antes de _ready; necesitamos ignorar la primera llamada de cambio de tamaño de kit, porque no podemos acceder a los nodos hijos hasta que _ready sea llamado. Si no ignoramos la primera llamada a setget, obtendríamos varios errores en el depurador.

Además, fíjate en cómo estamos usando una variable exportada. Esto es para que podamos cambiar el tamaño de las capturas de salud en el editor. Esto es para que no tengamos que hacer dos escenas para los dos tamaños, ya que podemos cambiar fácilmente los tamaños en el editor usando la variable exportada.

Truco

Vea Bases de GDScript y desplácese hacia abajo hasta la sección de Exportaciones para ver una lista de sugerencias de exportación que puede usar.


Veamos el _ready:

En primer lugar, conectamos la señal de body_entered de la Health_Pickup_Trigger con la función de trigger_body_entered. Esto hace que cualquier cuerpo que entre en la función Area<class_Area>` de trigger_body_entered.

Luego, ponemos is_ready en true para poder usar la función setget.

Luego escondemos todos los posibles kits y sus formas de colisión usando kit_size_change_values. El primer argumento es el tamaño del kit, mientras que el segundo argumento es si se activa o desactiva la forma de colisión y la malla de ese tamaño.

Entonces hacemos visible sólo el tamaño del kit que seleccionamos, llamando a kit_size_change_values y pasando en kit_size y true, para que el tamaño en kit_size esté habilitado.


A continuación, veamos el kit_size_change.

Lo primero que hacemos es comprobar si is_ready es true.

Si is_ready es true, entonces hacemos que cualquier kit ya asignado a kit_size sea desactivado usando kit_size_change_values, pasando kit_size y false.

Luego asignamos kit_size al nuevo valor pasado, value. Luego llamamos a kit_size_change_values pasando en kit_size otra vez, pero esta vez con el segundo argumento como true así que lo activamos. Como cambiamos kit_size al valor pasado, esto hará visible cualquier tamaño de kit que se haya pasado.

Si "is_ready" no es true, simplemente asignamos kit_size al valor pasado.


Ahora veamos los kit_size_change_values.

Lo primero que hacemos es comprobar qué tamaño se pasó. Basándonos en el tamaño que queremos activar/desactivar, queremos obtener diferentes nodos.

Obtenemos la forma de colisión del nodo correspondiente al size y lo deshabilitamos basándonos en el enabled pasado en el argumento/variable.

Nota

¿Por qué usamos !enable en lugar de enable? Esto es así, cuando decimos que queremos habilitar el nodo, podemos pasar en true, pero como CollisionShape usa disabled en lugar de enabled, necesitamos darle la vuelta. Al voltearlo, podemos habilitar la forma de colisión y hacer la malla visible cuando se pasa true.

Entonces obtenemos el nodo correcto Spatial que sostiene la malla y fijamos su visibilidad en enable.

Esta función puede ser un poco confusa; intente pensar en ella de esta manera: Estamos habilitando/deshabilitando los nodos apropiados para size usando enabled. Esto es para que no podamos recoger la salud para un tamaño que no es visible, y así sólo la malla para el tamaño adecuado será visible.


Finalmente, veamos trigger_body_entered.

Lo primero que hacemos es comprobar si el cuerpo que acaba de entrar tiene un método/función llamado add_health. Si lo tiene, entonces llamamos add_health y pasamos la salud proporcionada por el tamaño del kit actual.

Luego ponemos respawn_timer a RESPAWN_TIME para que el jugador tenga que esperar antes de que pueda recuperar la salud. Finalmente, llama a kit_size_change_value, pasando kit_size y false para que el kit en kit_size sea invisible hasta que haya esperado lo suficiente para reaparecer.


Lo último que tenemos que hacer antes de que el jugador pueda usar esta recogida de salud es añadir algunas cosas a Player.gd.

Abre Player.gd y añade la siguiente variable de clase:

const MAX_HEALTH = 150
  • MAX_HEALTH: La máxima cantidad de salud que un jugador puede tener.

Ahora necesitamos agregar la función add_health al jugador. Añade lo siguiente a Player.gd:

func add_health(additional_health):
    health += additional_health
    health = clamp(health, 0, MAX_HEALTH)

Repasemos rápidamente lo que esto hace.

Primero añadimos additional_health a la salud actual del jugador. Luego fijamos la salud de manera que no pueda tomar un valor más alto que MAX_HEALTH, ni un valor más bajo que 0.


Con eso hecho, el jugador puede ahora recoger la salud! Coloca algunas escenas de Health_Pickup alrededor y pruébalo. Puedes cambiar el tamaño de la recogida de salud en el editor cuando se selecciona una escena instanciada de Health_Pickup, desde un conveniente menú desplegable.

Añadiendo las recogidas de munición

Aunque añadir salud es bueno y todo eso, no podemos recoger las recompensas ya que nada puede (actualmente) dañarnos. ¡Ahora vamos a añadir algunas municiones!

Abre Ammo_Pickup.tscn. Fíjate en cómo está estructurado exactamente igual que Health_Pickup.tscn, pero con las mallas y las formas de colisión disparadoras cambiaran ligeramente para explicar la diferencia en los tamaños de las mallas.

Selecciona Ammo_Pickup y añade un nuevo script llamado Ammo_Pickup.gd. Añade lo siguiente:

extends Spatial

export (int, "full size", "small") var kit_size = 0 setget kit_size_change

# 0 = full size pickup, 1 = small pickup
const AMMO_AMOUNTS = [4, 1]

const RESPAWN_TIME = 20
var respawn_timer = 0

var is_ready = false

func _ready():

    $Holder/Ammo_Pickup_Trigger.connect("body_entered", self, "trigger_body_entered")

    is_ready = true

    kit_size_change_values(0, false)
    kit_size_change_values(1, false)

    kit_size_change_values(kit_size, true)


func _physics_process(delta):
    if respawn_timer > 0:
        respawn_timer -= delta

        if respawn_timer <= 0:
            kit_size_change_values(kit_size, true)


func kit_size_change(value):
    if is_ready:
        kit_size_change_values(kit_size, false)
        kit_size = value

        kit_size_change_values(kit_size, true)
    else:
        kit_size = value


func kit_size_change_values(size, enable):
    if size == 0:
        $Holder/Ammo_Pickup_Trigger/Shape_Kit.disabled = !enable
        $Holder/Ammo_Kit.visible = enable
    elif size == 1:
        $Holder/Ammo_Pickup_Trigger/Shape_Kit_Small.disabled = !enable
        $Holder/Ammo_Kit_Small.visible = enable


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)

Habrás notado que este código se ve casi exactamente igual que el de la salud. ¡Eso es porque en gran medida es el mismo! Sólo unas pocas cosas han sido cambiadas, y eso es lo que vamos a repasar.

Primero, nota el cambio a AMMO_AMOUNTS de HEALTH_AMMOUNTS. AMMO_AMMOUNTS será la cantidad de municiones/cargadores que se añade al arma actual. (A diferencia del caso de HEALTH_AMMOUNTS, que el cual nos dice cuantos puntos de salud se conceden, añadiremos un clip entero al arma actual en lugar de la cantidad de munición bruta)

La única otra cosa que hay que notar está en trigger_body_entered. Estamos comprobando la existencia y llamando a una función llamada add_ammo en lugar de add_health.

Aparte de esos dos pequeños cambios, ¡todo lo demás es lo mismo que la salud!


Todo lo que tenemos que hacer para que funcionen las recogidas de munición es añadir una nueva función al jugador. Abre Player.gd y añade la siguiente función:

func add_ammo(additional_ammo):
    if (current_weapon_name != "UNARMED"):
        if (weapons[current_weapon_name].CAN_REFILL == true):
            weapons[current_weapon_name].spare_ammo += weapons[current_weapon_name].AMMO_IN_MAG * additional_ammo

Repasemos lo que hace esta función.

Lo primero que comprobamos es si el jugador está UNARMED. Como UNARMED no tiene un nodo/script, queremos asegurarnos de que el jugador no está UNARMED antes de intentar que el nodo/script se adjunte al current_weapon_name.

A continuación, comprobamos si el arma actual puede ser recargada. Si el arma actual puede, añadimos un cargador completo de munición al arma multiplicando el valor de AMMO_IN_MAG del arma actual por todos los cargadores de munición que añadamos(additional_ammo).


¡Con eso hecho, ahora deberías ser capaz de conseguir munición adicional! ¡Ve a colocar algunas recogidas de munición en una/ambas/todas las escenas y pruébalas!

Nota

Fíjate en cómo no limitamos la cantidad de munición que puedes llevar. Para limitar la cantidad de munición que cada arma puede llevar, necesitas añadir una variable adicional al script de cada arma, y luego fijar la variable spare_ammo del arma después de añadir la munición en add_ammo.

Añadiendo objetivos destruibles

Antes de terminar esta parte, añadamos algunos objetivos.

Abre Target.tscn y echa un vistazo a las escenas en el árbol de escenas.

En primer lugar, fíjate en que no estamos usando un nodo RigidBody, sino un StaticBody uno. La razón de esto es que nuestros objetivos no rotos no se moverán a ninguna parte; usar un RigidBody sería más problemático de lo que vale ya que todo lo que tiene que hacer es quedarse quieto.

Truco

También ahorramos un poco de rendimiento usando un StaticBody sobre un RigidBody.

La otra cosa a tener en cuenta es que tenemos un nodo llamado Broken_Target_Holder. Este nodo va a albergar una escena creada/iniciada llamada Broken_Target.tscn. Abre Broken_Target.tscn.

Fíjate en cómo el objetivo se divide en cinco piezas, cada una de ellas un nodo RigidBody. Vamos a generar/instanciar esta escena cuando el objetivo reciba demasiado daño y necesite ser destruido. Entonces, vamos a esconder el objetivo no roto, para que parezca que el objetivo destrozado en lugar de un objetivo destrozado fue generado/instanciado.

Mientras aún tengas abierto Broken_Target.tscn, adjunta RigidBody_hit_test.gd a todos los nodos RigdBody. Esto hará que el jugador pueda disparar a las piezas rotas y que reaccionen a las balas.

Bien, ahora vuelve a Target.tscn, selecciona el nodo Target :ref: "StaticBody" <class_StaticBody> y crea un nuevo script llamado Target.gd.

Añade el siguiente código a Target.gd:

extends StaticBody

const TARGET_HEALTH = 40
var current_health = 40

var broken_target_holder

# The collision shape for the target.
# NOTE: this is for the whole target, not the pieces of the target.
var target_collision_shape

const TARGET_RESPAWN_TIME = 14
var target_respawn_timer = 0

export (PackedScene) var destroyed_target

func _ready():
    broken_target_holder = get_parent().get_node("Broken_Target_Holder")
    target_collision_shape = $Collision_Shape


func _physics_process(delta):
    if target_respawn_timer > 0:
        target_respawn_timer -= delta

        if target_respawn_timer <= 0:

            for child in broken_target_holder.get_children():
                child.queue_free()

            target_collision_shape.disabled = false
            visible = true
            current_health = TARGET_HEALTH


func bullet_hit(damage, bullet_transform):
    current_health -= damage

    if current_health <= 0:
        var clone = destroyed_target.instance()
        broken_target_holder.add_child(clone)

        for rigid in clone.get_children():
            if rigid is RigidBody:
                var center_in_rigid_space = broken_target_holder.global_transform.origin - rigid.global_transform.origin
                var direction = (rigid.transform.origin - center_in_rigid_space).normalized()
                # Apply the impulse with some additional force (I find 12 works nicely).
                rigid.apply_impulse(center_in_rigid_space, direction * 12 * damage)

        target_respawn_timer = TARGET_RESPAWN_TIME

        target_collision_shape.disabled = true
        visible = false

Repasemos lo que hace este script, empezando por las variables de clase:

  • TARGET_HEALTH: La cantidad de daño necesaria para romper un objetivo con su salud completa.

  • current_health: La cantidad de salud que este objetivo tiene actualmente.

  • broken_target_holder: Una variable para mantener el nodo Broken_Target_Holder para que podamos usarla fácilmente.

  • target_collision_shape: Una variable para mantener la CollisionShape para el objetivo no roto.

  • TARGET_RESPAWN_TIME: El tiempo, en segundos, que tarda un objetivo en reaparecer.

  • target_respawn_timer: Una variable para rastrear cuánto tiempo un objetivo ha sido roto.

  • destroyed_target: A PackedScene para mantener la escena del objetivo roto.

Fíjate en cómo usamos una variable exportada (a PackedScene) para obtener la escena objetivo rota en lugar de usar la preload. Usando una variable exportada, podemos elegir la escena desde el editor, y si necesitamos usar una escena diferente, es tan fácil como seleccionar una escena diferente en el editor; no necesitamos ir al código para cambiar la escena que estamos usando.


Veamos el _ready.

Lo primero que hacemos es conseguir el broken target holder y asignarlo a broken_target_holder. Fíjate en cómo usamos get_parent().get_node() aquí, en lugar de $. Si quisieras usar $, entonces necesitarías cambiar get_parent().get_node() a $"../Broken_Target_Holder".

Nota

Cuando esto fue escrito, no me di cuenta de que se puede usar $"../NodeName" para obtener los nodos padres usando $, por lo que se usa get_parent().get_node() en su lugar.

A continuación, obtenemos la forma de la colisión y la asignamos a "target_collision_shape". La razón por la que necesitamos la forma de colisión es porque incluso cuando la malla es invisible, la forma de colisión seguirá existiendo en el mundo de la física. Esto hace que el jugador pueda interactuar con un objetivo no roto aunque sea invisible, que no es lo que queremos. Para evitar esto, desactivaremos/habilitaremos la forma de colisión mientras hacemos la malla visible/invisible.


A continuación, veamos el _physics_process.

Sólo vamos a usar el _physics_process para la reaparición, y lo primero que hacemos es comprobar si el target_respawn_timer es mayor que 0.

Si lo es, entonces le restamos delta.

Entonces comprobamos si target_respawn_timer es 0 o menos. La razón detrás de esto es que acabamos de eliminar delta de target_respawn_timer, si es 0 o menos, entonces el objetivo acaba de llegar aquí, permitiéndonos hacer lo que necesitemos hacer cuando el temporizador esté terminado.

En este caso, queremos volver a generar nuestro objetivo.

Lo primero que hacemos es sacar a todos los hijos del contenedor de objetivo roto. Lo hacemos iterando sobre todos los hijos en broken_target_holder y los liberamos usando queue_free.

Luego, habilitamos la forma de colisión poniendo su disabled booleano a false.

Entonces hacemos que el objetivo, y todos sus nodos hijos, sean visibles de nuevo.

Finalmente, reajustamos la salud del objetivo (current_health) a TARGET_HEALTH.


Por último, veamos el bullet_hit.

Lo primero que hacemos es restar todo el daño que la bala hace a la salud del objetivo.

A continuación, comprobamos si el objetivo está en 0 de salud o más bajo. Si lo está, el objetivo acaba de morir y necesitamos crear un objetivo roto.

Primero, damos un ejemplo de una nueva escena destruida, y la asignamos a una nueva variable, un clone.

A continuación añadimos el clone como un hijo del portador del objetivo roto.

Para un efecto adicional, queremos hacer que todas las piezas del objetivo exploten hacia afuera. Para hacer esto, iteramos sobre todos los niños en clone.

Para cada niño, primero comprobamos si es un nodo RigidBody. Si lo es, entonces calculamos la posición central del objetivo relativa al nodo hijo. Luego averiguamos en qué dirección está el nodo hijo con respecto al centro. Usando esas variables calculadas, empujamos al niño desde el centro calculado, en la dirección opuesta al centro, usando el daño de la bala como fuerza.

Nota

Multiplicamos el daño por 12 para que tenga un efecto más dramático. Puedes cambiar esto a un valor más alto o más bajo dependiendo de la explosividad con la que quieras que se rompan tus objetivos.

A continuación, fijamos el temporizador de reposición del objetivo. Ponemos el temporizador en TARGET_RESPAWN_TIME, así que toma ``TARGET_RESPAWN_TIME``en segundos hasta que se regenera.

Luego deshabilitamos la forma de colisión del objetivo no roto, y ponemos la visibilidad del objetivo en false.


Advertencia

¡Asegúrate de poner el valor exportado de destroyed_target para Target.tscn en el editor! ¡De lo contrario los objetivos no serán destruidos y obtendrás un error!

Con eso hecho, ve a colocar algunas instancias de Target.tscn alrededor en uno/ambos/todos los niveles. Deberías encontrar que explotan en cinco pedazos después de haber recibido suficiente daño. Después de un rato, volverán a convertirse en un objetivo completo de nuevo.

Notas finales

../../../_images/PartFourFinished.png

Ahora puedes usar un joypad, cambiar de arma con la rueda de desplazamiento del ratón, reponer tu salud y munición, y romper objetivos con tus armas.

En la siguiente parte, Parte 5, ¡vamos a añadir granadas a nuestro jugador, dándole la posibilidad de agarrar y lanzar objetos, y añadir torretas!

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_4.zip