Parte 2

Resumen

En esta parte le daremos a nuestro personaje armas para usar.

../../../_images/PartTwoFinished.png

Al final de esta parte, tu tendras un personaje que podra disparar una pistola, un rifle y atacar usando un cuchillo. El jugador tambien podra tener animaciones con transiciones, y las armas podran interactuar con objetos del entorno.

Nota

Se supone que has terminado Parte 1 antes de llegar a esta parte del tutorial. La parte final de Parte 1 sera el inicio del poryecto de la parte 2

¡Vamos a empezar!

Hacer un sistema que maneje las animaciones

Primero necesitamos una forma de manejar las animaciones cambiantes. Abre el Player.tscn y selecciona el nodo AnimationPlayer (Player -> Rotation_Helper -> Model -> Animation_Player).

Crea un nuevo script llamado AnimationPlayer_Manager.gd y adjuntalo a AnimationPlayer.

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

extends AnimationPlayer

# Structure -> Animation name :[Connecting Animation states]
var states = {
    "Idle_unarmed":["Knife_equip", "Pistol_equip", "Rifle_equip", "Idle_unarmed"],

    "Pistol_equip":["Pistol_idle"],
    "Pistol_fire":["Pistol_idle"],
    "Pistol_idle":["Pistol_fire", "Pistol_reload", "Pistol_unequip", "Pistol_idle"],
    "Pistol_reload":["Pistol_idle"],
    "Pistol_unequip":["Idle_unarmed"],

    "Rifle_equip":["Rifle_idle"],
    "Rifle_fire":["Rifle_idle"],
    "Rifle_idle":["Rifle_fire", "Rifle_reload", "Rifle_unequip", "Rifle_idle"],
    "Rifle_reload":["Rifle_idle"],
    "Rifle_unequip":["Idle_unarmed"],

    "Knife_equip":["Knife_idle"],
    "Knife_fire":["Knife_idle"],
    "Knife_idle":["Knife_fire", "Knife_unequip", "Knife_idle"],
    "Knife_unequip":["Idle_unarmed"],
}

var animation_speeds = {
    "Idle_unarmed":1,

    "Pistol_equip":1.4,
    "Pistol_fire":1.8,
    "Pistol_idle":1,
    "Pistol_reload":1,
    "Pistol_unequip":1.4,

    "Rifle_equip":2,
    "Rifle_fire":6,
    "Rifle_idle":1,
    "Rifle_reload":1.45,
    "Rifle_unequip":2,

    "Knife_equip":1,
    "Knife_fire":1.35,
    "Knife_idle":1,
    "Knife_unequip":1,
}

var current_state = null
var callback_function = null

func _ready():
    set_animation("Idle_unarmed")
    connect("animation_finished", self, "animation_ended")

func set_animation(animation_name):
    if animation_name == current_state:
        print ("AnimationPlayer_Manager.gd -- WARNING: animation is already ", animation_name)
        return true


    if has_animation(animation_name):
        if current_state != null:
            var possible_animations = states[current_state]
            if animation_name in possible_animations:
                current_state = animation_name
                play(animation_name, -1, animation_speeds[animation_name])
                return true
            else:
                print ("AnimationPlayer_Manager.gd -- WARNING: Cannot change to ", animation_name, " from ", current_state)
                return false
        else:
            current_state = animation_name
            play(animation_name, -1, animation_speeds[animation_name])
            return true
    return false


func animation_ended(anim_name):

    # UNARMED transitions
    if current_state == "Idle_unarmed":
        pass
    # KNIFE transitions
    elif current_state == "Knife_equip":
        set_animation("Knife_idle")
    elif current_state == "Knife_idle":
        pass
    elif current_state == "Knife_fire":
        set_animation("Knife_idle")
    elif current_state == "Knife_unequip":
        set_animation("Idle_unarmed")
    # PISTOL transitions
    elif current_state == "Pistol_equip":
        set_animation("Pistol_idle")
    elif current_state == "Pistol_idle":
        pass
    elif current_state == "Pistol_fire":
        set_animation("Pistol_idle")
    elif current_state == "Pistol_unequip":
        set_animation("Idle_unarmed")
    elif current_state == "Pistol_reload":
        set_animation("Pistol_idle")
    # RIFLE transitions
    elif current_state == "Rifle_equip":
        set_animation("Rifle_idle")
    elif current_state == "Rifle_idle":
        pass
    elif current_state == "Rifle_fire":
        set_animation("Rifle_idle")
    elif current_state == "Rifle_unequip":
        set_animation("Idle_unarmed")
    elif current_state == "Rifle_reload":
        set_animation("Rifle_idle")

func animation_callback():
    if callback_function == null:
        print ("AnimationPlayer_Manager.gd -- WARNING: No callback function for the animation to call!")
    else:
        callback_function.call_func()

Repasemos lo que hace este script:


Empecemos con las variables de clase de este script:

  • states: Un diccionario para contener nuestros estados de animación. (Explanación más extensa abajo)

  • animation_speeds: Un diccionario para contener todas las velocidades a las que ejecutaremos nuestras animaciones.

  • current_state: Un variable para contener el nombre del estado de animación en que estamos actualmente.

  • callback_function: Una variable para mantener una función (Explicación más abajo)

Si estás familiarizado con las máquinas de estados, entonces habrás notado que states está estructurado como una máquina de estados básica. Aquí está más o menos cómo se configura el states:

states es un diccionario cuya clave es el nombre del estado actual, y el valor es un array que contiene todas las animaciones (estados) a las que podemos pasar. Por ejemplo, si estamos actualmente en el estado Idle_unarmed, sólo podemos pasar a Knife_equip, Pistol_equip, Rifle_equip, y Idle_unarmed.

Si intentamos hacer la transición a un estado que no está incluido en los posibles estados de transición para el estado en que estamos, entonces recibimos un mensaje de advertencia y la animación no cambia. También podemos pasar automáticamente de unos estados a otros, como se explicará más adelante en animation_ended.

Nota

Para mantener este tutorial simple, no estamos usando una máquina de estado adecuada. Si está interesado en saber más sobre las máquinas de estado, vea los siguientes artículos:

animation_speeds es la velocidad a la que se reproducirá cada animación. Algunas de las animaciones son un poco lentas y en un esfuerzo por hacer que todo se vea suave, tenemos que jugarlos a velocidades más rápidas.

Truco

Nota que todas las animaciones de disparo son más rápidas que su velocidad normal. ¡Recuerda esto para después!

current_state tendrá el nombre del estado de animación en el que estamos.

Finalmente, callback_function será un FuncRef pasado por el jugador para generar balas en el marco apropiado de la animación. Un FuncRef nos permite pasar una función como un argumento, permitiéndonos efectivamente llamar a una función desde otro script, que es como la usaremos más tarde.


Ahora veamos el _ready.

Primero ponemos nuestra animación en Idle_unarmed usando la función set_animation, así que seguro que empezamos en esa animación.

A continuación conectamos la señal de animation_finished a este script y la asignamos a la llamada animation_ended. Esto significa que cuando una animación esté terminada, se llamará animation_ended.


Let's look at set_animation next.

set_animation cambia la animación a la animación llamada animation_name,*si* podemos hacer la transición a ella. En otras palabras, si el estado de animación en el que estamos actualmente tiene el nombre del estado de animación pasado en states, entonces cambiaremos a esa animación.

En primer lugar, comprobamos si el nombre de la animación pasada es el mismo que el de la animación que se está reproduciendo. Si son iguales, entonces escribimos una advertencia a la consola y devolvemos true.

Segundo, vemos si AnimationPlayer tiene la animación con el nombre animation_name usando has_animation. Si no es así, devolvemos false.

En tercer lugar, comprobamos si el current_state está establecido. Si tenemos un estado en current_state, entonces tenemos todos los estados posibles a los que podemos pasar.

Si el nombre de la animación está en la lista de transiciones posibles, ponemos current_state al pasado en la animación (animation_name), le decimos AnimationPlayer para reproducir la animación con un tiempo de mezcla de -1 a la velocidad establecida en animation_speeds y devolver true.

Nota

El tiempo de mezcla es el tiempo de mezcla de las dos animaciones.

Poniendo un valor de -1, la nueva animación se reproduce instantáneamente, reemplazando cualquier animación que ya se esté reproduciendo.

Si pones un valor de 1, durante un segundo la nueva animación se reproducirá con mayor fuerza, mezclando las dos animaciones durante un segundo antes de reproducir sólo la nueva animación. Esto lleva a una transición suave entre las animaciones, que se ve muy bien cuando estás cambiando de una animación caminando a una animación corriendo.

Fijamos el tiempo de mezcla en -1 porque queremos cambiar las animaciones al instante.


Now let's look at animation_ended.

animation_ended es la función que será llamada por AnimationPlayer cuando termine de reproducir una animación.

Para ciertos estados de animación, puede que necesitemos hacer la transición a otro estado cuando haya terminado. Para manejar esto, comprobamos todos los estados de animación posibles. Si es necesario, haremos la transición a otro estado.

Advertencia

Si estás usando tus propios modelos animados, asegúrate de que ninguna de las animaciones esté configurada para hacer un bucle. Las animaciones en bucle no envían la señal de animation_finished cuando llegan al final de la animación y están a punto de repetirse en bucle.

Nota

Las transiciones en animation_ended idealmente serían parte de los datos de los states, pero en un esfuerzo por hacer el tutorial más fácil de entender, codificaremos cada transición de estado en animation_ended.


Finalmente, está la animation_callback. Esta función será llamada por un método de llamada de pistas en nuestras animaciones. Si tenemos un FuncRef asignado a callback_function, entonces llamamos a esa función pasada. Si no tenemos un FuncRef asignado a callback_function, imprimimos una advertencia a la consola.

Truco

Intenta ejecutar Testing_Area.tscn para asegurarte de que no hay problemas de tiempo de ejecución. Si el juego funciona pero nada parece haber cambiado, entonces todo funciona correctamente.

Preparando las animaciones

Ahora que tenemos un gerente de animación trabajando, necesitamos llamarlo desde nuestro script de jugador. Antes de eso, sin embargo, tenemos que establecer algunas pistas de llamada de animación en nuestras animaciones de disparo.

Abre Player.tscn si no lo tienes abierto y navega al nodo AnimationPlayer (Player -> Rotation_Helper -> Model -> Animation_Player).

Necesitamos adjuntar un método de llamada de seguimiento a tres de nuestras animaciones: La animación de disparo de la pistola, el rifle y el cuchillo. Empecemos con la pistola. Haz clic en la lista desplegable de la animación y selecciona "Pistol_fire".

Ahora desplácese hacia abajo hasta la parte inferior de la lista de pistas de animación. El último ítem de la lista debe decir Armature/Skeleton:Left_UpperPointer. Ahora, arriba de la lista, haz clic en el botón "Add track", a la izquierda de la línea de tiempo

../../../_images/AnimationPlayerAddTrack.png

Esto abrirá una ventana con algunas opciones. Queremos añadir un método de llamada, así que haz clic en la opción que dice "Call Method Track". Esto abrirá una ventana que muestra el árbol de nodos completo. Navega al nodo AnimationPlayer, selecciónalo y pulsa OK.

../../../_images/AnimationPlayerCallFuncTrack.png

Ahora en la parte inferior de la lista de pistas de animación tendrás una pista verde que dice "AnimationPlayer". Ahora tenemos que añadir el punto donde queremos llamar a nuestra función de devolución de llamada. Limpia la línea de tiempo hasta que llegues al punto en que el pico empiece a parpadear.

Nota

La línea de tiempo es la ventana donde se almacenan todos los puntos de nuestra animación. Cada uno de los pequeños puntos representa un punto de datos de animación.

Para ver la animación "Pistol_fire", selecciona el nodo debajo del Ayudante de Rotación Camera y marca la casilla "Preview" debajo de "Perspective" en la esquina superior izquierda.

Dezplazando la línea de tiempo significa moverse a través de la animación. Así que cuando decimos "desplaza la línea de tiempo hasta llegar a un punto", lo que queremos decir es moverse a través de la ventana de animación hasta llegar al punto de la línea de tiempo.

Además, la boca del arma es el punto final por donde sale la bala. El destello de la boca es el destello de luz que escapa de la boca cuando se dispara una bala. La boca del cañón también se conoce como el cañón del arma.

Truco

Para tener un control más fino al desplazar la línea de tiempo, presione Ctrl y desplácese hacia adelante con la rueda del ratón para hacer zoom. Si se desplaza hacia atrás, se alejará el zoom.

También puedes cambiar la forma en que se ajusta el scrubbing de la línea de tiempo cambiando el valor en Paso(s) a un valor más bajo/alto.

Una vez que llegues a un punto que te guste, haz clic con el botón derecho del ratón en la fila de "Reproductor de animación" y pulsa Insert Key. En el campo de nombre vacío, ingresa animation_callback y presiona Enter.

../../../_images/AnimationPlayerInsertKey.png

Ahora, cuando estamos reproduciendo esta animación, la pista del método de llamada se disparará en ese punto específico de la animación.


¡Repitamos el proceso para las animaciones de disparo de rifles y cuchillos!

Nota

Debido a que el proceso es exactamente el mismo que el de la pistola, el proceso se va a explicar con un poco menos de profundidad. ¡Sigue los pasos desde arriba si te pierdes! Es exactamente lo mismo, sólo que en una animación diferente.

Ve a la animación "Rifle_fire" desde el desplegable de animación. Añade la pista del método de llamada una vez que llegues a la parte inferior de la lista de pistas de la animación haciendo clic en el botón "Add Track" que está encima de la lista. Encuentra el punto donde la boca del cañón comienza a parpadear y haz clic con el botón derecho del ratón y presiona Insert Key para agregar un punto de pista del método de llamada en esa posición de la pista.

Escriba "animation_callback" en el campo del nombre del pop up que se abrió y presione Enter.

Ahora tenemos que aplicar el método de devolución de llamada a la animación del cuchillo. Selecciona la animación "Knife_fire" y desplázate hasta la parte inferior de las pistas de animación. Haz clic en el botón "Add Track" que está encima de la lista y añade una pista de método. A continuación, busca un punto alrededor del primer tercio de la animación para colocar el punto del método de devolución de llamada de la animación.

Nota

No dispararemos el cuchillo, y la animación es una animación de apuñalamiento más que de disparo. En este tutorial estamos reutilizando la lógica de disparo del arma para nuestro cuchillo, por lo que la animación ha sido nombrada en un estilo que es consistente con las otras animaciones.

A partir de ahí, haz clic con el botón derecho del ratón en la línea de tiempo y haz clic en "Insert Key". Ponga "animation_callback" en el campo de nombre y presione Enter.

Truco

¡Asegúrate de guardar tu trabajo!

Con esto hecho, estamos casi listos para empezar a añadir la habilidad de disparar a nuestro script de jugador! Necesitamos preparar una última escena: La escena para nuestro objeto bala.

Creando la escena de la bala

Hay varias maneras de manejar las balas de un arma en los videojuegos. En esta serie de tutoriales, exploraremos dos de las formas más comunes: Objetos, y raycasts.


Una de las dos formas es usando un objeto bala. Este será un objeto que viaja a través del mundo y maneja su propio código de colisión. En este método, creamos un objeto bala en la dirección en la que se dirige nuestro arma, y luego se desplaza hacia adelante.

Este método tiene varias ventajas. La primera es que no tenemos que almacenar las balas en nuestro jugador. Podemos simplemente crear la bala y luego seguir adelante, y la bala misma se encargará de comprobar si hay colisiones, enviando la(s) señal(es) adecuada(s) al objeto con el que choca, y destruyéndose a sí misma.

Otra ventaja es que podemos tener un movimiento más complejo de la bala. Si queremos que la bala caiga ligeramente a medida que pasa el tiempo, podemos hacer que el script de control de la bala empuje lentamente la bala hacia el suelo. El uso de un objeto también hace que la bala tarde en alcanzar su objetivo, no golpea instantáneamente a lo que sea que esté apuntando. Esto se siente más realista porque nada en la vida real se mueve instantáneamente de un punto a otro.

Una de las grandes desventajas es el rendimiento. Si bien el hecho de que cada bala calcule su propia trayectoria y maneje su propia colisión permite una gran flexibilidad, esto se produce a costa del rendimiento. Con este método estamos calculando el movimiento de cada bala en cada paso, y aunque esto puede no ser un problema para unas pocas docenas de balas, puede convertirse en un gran problema cuando potencialmente tienes varios cientos de balas.

A pesar del impacto en el rendimiento, muchos shooters en primera persona incluyen alguna forma de balas de objeto. Los lanzadores de cohetes son un buen ejemplo porque en muchos de los que disparan en primera persona, los cohetes no explotan instantáneamente en la posición del objetivo. También puedes encontrar balas como objetos muchas veces con granadas porque generalmente rebotan por el mundo antes de explotar.

Nota

Aunque no puedo decir con certeza que este sea el caso, estos juegos probablemente utilizan objetos bala de una forma u otra: (Estos son enteramente de mis observaciones. Pueden estar completamente equivocados. Nunca he trabajado en cualquier de los siguientes juegos)

  • Halo (Lanzacohetes, granadas de fragmentación, rifles de francotirador, tiro bruto y más)

  • Destino (Lanzacohetes, granadas, rifles de fusión, rifles de francotirador, súper movimientos y más)

  • Call of Duty (Lanzacohetes, granadas, cuchillos balísticos, ballestas y más)

  • Campo de batalla (Lanzacohetes, granadas, minas claymore, morteros y más)

Otra desventaja de los objetos bala es la interconexión. Los objetos bala tienen que sincronizar las posiciones (al menos) con todos los clientes que están conectados al servidor.

Aunque no estamos implementando ningún tipo de red (como sería en toda la serie de tutoriales), es una consideración que hay que tener en cuenta al crear tu shooter en primera persona, especialmente si planeas añadir alguna forma de red en el futuro.


La otra forma de manejar las colisiones de bala que veremos es el raycasting.

Este método es extremadamente común en armas que tienen balas de movimiento rápido que rara vez cambian de trayectoria en el tiempo.

En lugar de crear un objeto bala y enviarlo a través del espacio, en su lugar enviamos un rayo desde el cañón/boca del arma hacia adelante. Fijamos el origen del rayo en la posición inicial de la bala, y basándonos en la longitud podemos ajustar la distancia que la bala "viaja" a través del espacio.

Nota

Aunque no puedo decir con certeza que este sea el caso, estos juegos probablemente usan raycasts de una forma u otra: (Estos son enteramente de mis observaciones. Pueden estar completamente equivocados. Nunca he trabajado en cualquier de los siguientes juegos)

  • Halo (Rifles de asalto, DMRs, rifles de batalla, carabina de pacto, láser espartano, y más)

  • Destiny (Rifles automáticos, rifles de pulso, rifles de exploración, cañones de mano, ametralladoras y más)

  • Call of Duty (Rifles de asalto, ametralladoras ligeras, subametralladoras, pistolas y más)

  • Campo de batalla (Rifles de asalto, SMGs, carabinas, pistolas y más)

Una gran ventaja de este método es que es de bajo rendimiento. Enviar un par de cientos de rayos a través del espacio es mucho más fácil de calcular para la computadora que enviar un par de cientos de objetos bala.

Otra ventaja es que podemos saber instantáneamente si hemos golpeado algo o no exactamente cuando lo pedimos. Para la red esto es importante porque no necesitamos sincronizar los movimientos de la bala por Internet, sólo necesitamos enviar si el raycast golpeó o no.

Sin embargo, el Raycasting tiene algunas desventajas. Una gran desventaja es que no podemos lanzar fácilmente un rayo en otra cosa que no sea una línea lineal. Esto significa que sólo podemos disparar en línea recta por la longitud de nuestro rayo. Puedes crear la ilusión del movimiento de la bala lanzando múltiples rayos en diferentes posiciones, pero no sólo es difícil de implementar en el código, sino que también es más pesado en el rendimiento.

Otra desventaja es que no podemos ver la bala. Con los objetos de la bala podemos ver realmente que la bala viaja a través del espacio si le ponemos una malla, pero debido a que los rayos ocurren instantáneamente, no tenemos una forma decente de mostrar las balas. Podrías dibujar una línea desde el origen del raycast hasta el punto donde el raycast colisionó, y esa es una forma popular de mostrar los raycasts. Otra forma es simplemente no dibujar el raycast en absoluto, porque teóricamente las balas se mueven tan rápido que nuestros ojos no podrían verlo de todos modos.


Preparemos el objeto de la bala. Esto es lo que nuestra pistola creará cuando se llame a la función de llamada de la animación "Pistol_fire".

Abre Bullet_Scene.tscn. La escena contiene un nodo Spatial llamado bala, con una MeshInstance y una Area con una CollisionShape hijo a la misma.

Crea un nuevo script llamado Bullet_script.gd y adjúntalo al Bullet Spatial.

Vamos a mover todo el objeto bala en la raíz (Bullet). Usaremos el Area para comprobar si hemos chocado o no con algo

Nota

¿Por qué usamos un Area y no un RigidBody? La razón principal por la que no estamos usando un RigidBody es porque no queremos que la bala interactúe con otros nodos RigidBody. Usando un Area nos aseguramos de que ninguno de los otros nodos RigidBody, incluyendo las otras balas, se vean afectados.

!Otra razón es simplemente porque es más fácil detectar colisiones con un Area!

Aquí está el script que controlará nuestra bala:

extends Spatial

var BULLET_SPEED = 70
var BULLET_DAMAGE = 15

const KILL_TIMER = 4
var timer = 0

var hit_something = false

func _ready():
    $Area.connect("body_entered", self, "collided")


func _physics_process(delta):
    var forward_dir = global_transform.basis.z.normalized()
    global_translate(forward_dir * BULLET_SPEED * delta)

    timer += delta
    if timer >= KILL_TIMER:
        queue_free()


func collided(body):
    if hit_something == false:
        if body.has_method("bullet_hit"):
            body.bullet_hit(BULLET_DAMAGE, global_transform)

    hit_something = true
    queue_free()

Analicemos el script:


Primero, declaramos unas cuantas clases variables:

  • BULLET_SPEED: La velocidad a la que la bala viaja.

  • BULLET_DAMAGE: El daño que causa una bala cuando colisiona con algo.

  • KILL_TIMER: El tiempo que la bala puede viajar sin colisionar con algo.

  • Un float para saber cuanto tiempo la bala ha estado viva.

  • hit_something: Un boolean para saber si hemos colisionado con algo o no.

Salvo las excepciones de timer``y ``hit_something, todas estas variables cambian como la bala interactua con su ambiente.

Nota

La razon por la cual utilizamos un contador es para que no ocurra el caso en el que la bala viaja infinitamente. Al utilizar un contador, nos aseguramos que ninguna bala viajara infinitamente ni consumira recursos.

Truco

Tal cual es presentado en:ref:doc_fps_tutorial_part_one, hay dos variables con solo mayusculas. La razon por esto es la misma a la razon presentadada en Parte 1: queremos tratar estas variables como constantes, pero queremos poder cambiarlas. En este caso necesitaremos cambiar el daño y la velocidad de las balas mas tarde, por lo tanto, necesitaremos que sean variables y no constantes.


En _ready nos ponemos la señal de body_entered del área para que llame a la función de collided cuando un cuerpo entra en el área.


_physics_process obtiene el eje Z local de la bala. Si miras la escena en modo local, encontrarás que la bala se enfrenta al eje Z local positivo.

Luego trasladamos la bala en esa dirección de avance, multiplicando por nuestra velocidad y tiempo delta.

Después de eso añadimos tiempo delta a nuestro temporizador y comprobamos si el temporizador ha alcanzado un valor tan grande o mayor que nuestra constante``KILL_TIME``. Si lo ha hecho, usamos queue_free para liberar la bala.


En collided comprobamos si hemos golpeado algo todavía.

Recuerda que collided sólo se llama cuando un cuerpo ha entrado en el nodo Area. Si la bala no ha colisionado ya con algo, entonces procedemos a comprobar si el cuerpo con el que ha colisionado la bala tiene una función/método llamado bullet_hit. Si es así, la llamamos y pasamos el daño de la bala y la transformación global de la bala para que podamos obtener la rotación y la posición de la bala.

Nota

en collided, el cuerpo pasado puede ser un StaticBody, RigidBody, o KinematicBody

Ponemos la variable hit_something de la bala en true porque independientemente de si el cuerpo con el que chocó la bala tiene la función/método bullet_hit, ha golpeado algo y por lo tanto tenemos que asegurarnos de que la bala no golpee nada más.

Entonces liberamos la bala usando queue_free.

Truco

Tal vez te preguntes por qué tenemos una variable hit_something si liberamos la bala usando queue_free tan pronto como golpea algo.

La razón por la que necesitamos rastrear si hemos golpeado algo o no, es porque queue_free no libera inmediatamente el nodo, por lo que la bala podría chocar con otro cuerpo antes de que Godot tenga la oportunidad de liberarla. Al rastrear si la bala ha golpeado algo, podemos asegurarnos de que la bala sólo golpeará un objeto.


Antes de empezar a programar el jugador de nuevo, echemos un vistazo rápido a Player.tscn. Abre Player.tscn de nuevo.

Expande Rotation_Helper y observa como tiene dos nodos: Gun_Fire_Points y Gun_Aim_Point.

Gun_aim_point es el punto al que apuntarán las balas. Fíjate en cómo se alinea con el centro de la pantalla y se desplaza una distancia hacia adelante en el eje Z. Gun_aim_point servirá como el punto con el que las balas seguro chocarán a medida que avanza.

Nota

Hay una instancia de malla invisible para propósitos de depuración. La malla es una pequeña esfera que muestra visualmente a qué objetivo apuntarán las balas.

Abre los Gun_Fire_Points y encontrarás tres nodos más, :ref:`Spatial <class_Spatial>`uno para cada arma.

Abre el Rifle_Point y encontrarás un nodo Raycast. Aquí es donde enviaremos los "raycasts" para las balas de nuestro rifle. La longitud del raycast dictará cuán lejos viajarán nuestras balas.

Estamos usando un nodo Raycast para manejar la bala del rifle porque queremos disparar muchas balas rápidamente. Si usamos objetos con balas, es muy posible que nos encontremos con problemas de rendimiento en máquinas antiguas.

Nota

Si te preguntas de dónde vinieron las posiciones de los puntos, son las posiciones aproximadas de los extremos de cada arma. Puedes verlo yendo a AnimationPlayer, seleccionando una de las animaciones de disparo y recorriendo la línea de tiempo. El punto de cada arma debería alinearse con el extremo de cada arma.

Abre el Knife_Point y encontrarás un nodo Area. Estamos usando un Area para el cuchillo porque sólo nos preocupamos por todos los cuerpos cercanos a nosotros, y porque nuestro cuchillo no dispara al espacio. Si hiciéramos un cuchillo lanzador, probablemente generaríamos un objeto bala que se parece a un cuchillo.

Finalmente, tenemos Pistol_Point. Este es el punto donde crearemos/iniciaremos nuestros objetos bala. No necesitamos ningún nodo adicional aquí, ya que la bala maneja toda su propia detección de colisiones.

Ahora que hemos visto como manejaremos nuestras otras armas, y donde engendraremos las balas, empecemos a trabajar en hacerlas funcionar.

Nota

También puedes mirar los nodos del HUD si quieres. No hay nada elegante allí y aparte de usar un simple Label, no tocaremos ninguno de esos nodos. Revisa doc_design_interfaces_with_the_control_nodes` para ver un tutorial sobre el uso de los nodos GUI.

Creando la primera arma

Escribamos el código de cada una de nuestras armas, empezando por la pistola.

Selecciona Pistol_Point (Player -> Rotation_Helper -> Gun_Fire_Points -> Pistol_Point) y crea un nuevo script llamado Weapon_Pistol.gd.

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

extends Spatial

const DAMAGE = 15

const IDLE_ANIM_NAME = "Pistol_idle"
const FIRE_ANIM_NAME = "Pistol_fire"

var is_weapon_enabled = false

var bullet_scene = preload("Bullet_Scene.tscn")

var player_node = null

func _ready():
    pass

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

    clone.global_transform = self.global_transform
    clone.scale = Vector3(4, 4, 4)
    clone.BULLET_DAMAGE = DAMAGE

func equip_weapon():
    if player_node.animation_manager.current_state == IDLE_ANIM_NAME:
        is_weapon_enabled = true
        return true

    if player_node.animation_manager.current_state == "Idle_unarmed":
        player_node.animation_manager.set_animation("Pistol_equip")

    return false

func unequip_weapon():
    if player_node.animation_manager.current_state == IDLE_ANIM_NAME:
        if player_node.animation_manager.current_state != "Pistol_unequip":
            player_node.animation_manager.set_animation("Pistol_unequip")

    if player_node.animation_manager.current_state == "Idle_unarmed":
        is_weapon_enabled = false
        return true
    else:
        return false

Repasemos cómo funciona el script.


Primero definimos algunas variables de clase que necesitaremos en el script:

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

  • IDLE_ANIM_NAME: El nombre de la animación en reposo de la pistola.

  • FIRE_ANIM_NAME: El nombre de la animación de disparo de la pistola.

  • is_weapon_enabled: Una variable para comprobar si esta arma está en uso o habilitada.

  • bullet_scene: La escena de la bala en la que trabajamos antes.

  • player_node: Una variable para mantener Player.gd.

La razón por la que definimos la mayoría de estas variables es para poder usarlas en Player.gd.

Cada una de las armas que haremos tendrá todas estas variables (menos bullet_scene) así que tenemos una interfaz uniforme para interactuar en Player.gd. Usando las mismas variables/funciones en cada arma, podemos interactuar con ellas sin tener que saber qué arma estamos usando, lo que hace que nuestro código sea mucho más modular porque podemos añadir armas sin tener que cambiar mucho del código en Player.gd y sencillamente funcionará.

Podríamos escribir todo el código en Player.gd, pero entonces Player.gd será cada vez más difícil de manejar a medida que agreguemos armas. Usando un diseño modular con una interfaz consistente, podemos mantener Player.gd limpio y agradable, mientras que también es más fácil añadir/quitar/modificar armas.


En _ready simplemente pasamos por encima.

Sin embargo, hay una cosa importante, la suposición de que en algún momento rellenaremos Player.gd.

Vamos a asumir que Player.gd se pasará a sí mismo antes de llamar a cualquiera de las funciones en Weapon_Pistol.gd.

Aunque esto puede llevar a situaciones en las que el jugador no se pasa (porque nos olvidamos), tendríamos que tener una larga string de get_parent para atravesar el árbol de la escena para recuperar al jugador. Esto no se ve bien (get_parent().get_parent().get_parent() y así sucesivamente) y es relativamente seguro asumir que nos acordaremos de pasarnos a cada arma en Player.gd.


A continuación, veamos el fire_weapon:

Lo primero que hacemos es instanciar la escena de la bala que hicimos antes.

Truco

Al instanciar la escena, estamos creando un nuevo nodo que contiene todos los nodos de la escena que instanciamos, clonando efectivamente esa escena.

Luego añadimos un clone al primer nodo hijo de la raíz de la escena en la que estamos. Haciendo esto, lo convertimos en un hijo del nodo raíz de la escena actualmente cargada.

En otras palabras, estamos añadiendo un clone como hijo del primer nodo (lo que esté en la parte superior del árbol de la escena) en la escena actualmente cargada/abierta. Si la escena actualmente cargada/abierta es Testing_Area.tscn, añadiremos nuestro clon como hijo de Testing_Area, el nodo raíz de esa escena.

Advertencia

Como se menciona más adelante en la sección sobre la adición de sonidos, este método hace una suposición. Esto se explicará más adelante en la sección sobre la adición de sonidos en Parte 3

A continuación establecemos la transformación global del clon en la transformación global de Pistol_Point. La razón por la que hacemos esto es para que la bala salga al final de la pistola.

Puedes ver que el Pistol_Point está colocado justo al final de la pistola haciendo clic en el AnimationPlayer y desplazándote a través de Pistol_fire. Verás que la posición está más o menos al final de la pistola cuando ésta dispara.

Luego los escalamos por un factor de 4 porque la escena de la bala es un poco demasiado pequeña por defecto.

Entonces fijamos el daño de la bala (BULLET_DAMAGE) a la cantidad de daño que hace una sola bala de pistola (DAMAGE).


Ahora veamos el equip_weapon:

Lo primero que hacemos es comprobar si el encargado de la animación está en la animación de la pistola en reposo. Si estamos en la animación de la pistola, ponemos is_weapon_enabled en true y devolvemos true porque la pistola ha sido equipada exitosamente.

Porque sabemos que la animación equip de nuestra pistola pasa automáticamente a la animación de la pistola en reposo, si estamos en la animación de la pistola en reposo, la pistola debe haber terminado de reproducir la animación del equipo.

Nota

Sabemos que estas animaciones harán la transición porque escribimos el código para hacer la transición en Animation_Manager.gd

A continuación comprobamos si el jugador está en el estado de animación Idle_unarmed. Debido a que todas las animaciones no equipadas van a este estado, y debido a que cualquier arma puede ser equipada desde este estado, cambiamos las animaciones de Pistol_equip si el jugador está en el estado "Idle_unarmed".

Como sabemos que Pistol_equip pasará a Pistol_idle, no necesitamos hacer más procesamiento adicional para equipar las armas, pero como no pudimos equipar la pistola todavía, volvemos false.


Finalmente, veamos el unequip_weapon:

unequip_weapon es similar a equip_weapon, pero en cambio estamos revisando las cosas al revés.

Primero comprobamos si el jugador está en el estado de animación inactivo. Luego nos aseguramos de que el jugador no esté en la animación Pistol_unequip. Si el jugador no está en la animación Pistol_unequip, queremos reproducir la animación Pistol_unequip.

Nota

Primero comprobamos si el jugador está en el estado de animación inactivo. Luego nos aseguramos de que el jugador no esté en la animación Pistol_unequip. Si el jugador no está en la animación Pistol_unequip, queremos reproducir la animación Pistol_unequip.

A continuación comprobamos si el jugador está en "Idle_unarmed", que es el estado de animación al que pasaremos de "Pistol_unequip". Si el jugador está en "Idle_unarmed", entonces ponemos "is_weapon_enabled" en false ya que ya no estamos usando esta arma, y devolvemos true porque hemos desarmado exitosamente la pistola.

Si el jugador no está en Idle_unarmed, devolvemos false porque aún no hemos desarmado la pistola con éxito.

Creando las otras dos armas

Ahora que tenemos todo el código que necesitamos para la pistola, añadamos el código del rifle y el cuchillo.

Selecciona Rifle_Point (Player -> Rotation_Helper -> Gun_Fire_Points -> Rifle_Point) y crea un nuevo script llamado Weapon_Rifle.gd, y añade lo siguiente:

extends Spatial

const DAMAGE = 4

const IDLE_ANIM_NAME = "Rifle_idle"
const FIRE_ANIM_NAME = "Rifle_fire"

var is_weapon_enabled = false

var player_node = null

func _ready():
    pass

func fire_weapon():
    var ray = $Ray_Cast
    ray.force_raycast_update()

    if ray.is_colliding():
        var body = ray.get_collider()

        if body == player_node:
            pass
        elif body.has_method("bullet_hit"):
            body.bullet_hit(DAMAGE, ray.global_transform)

func equip_weapon():
    if player_node.animation_manager.current_state == IDLE_ANIM_NAME:
        is_weapon_enabled = true
        return true

    if player_node.animation_manager.current_state == "Idle_unarmed":
        player_node.animation_manager.set_animation("Rifle_equip")

    return false

func unequip_weapon():

    if player_node.animation_manager.current_state == IDLE_ANIM_NAME:
        if player_node.animation_manager.current_state != "Rifle_unequip":
            player_node.animation_manager.set_animation("Rifle_unequip")

    if player_node.animation_manager.current_state == "Idle_unarmed":
        is_weapon_enabled = false
        return true

    return false

La mayor parte de esto es exactamente lo mismo que Weapon_Pistol.gd, así que sólo vamos a ver lo que ha cambiado: fire_weapon.

Lo primero que hacemos es obtener el nodo Raycast, que es un hijo de Rifle_Point.

A continuación forzamos el Raycast a actualizarse usando force_raycast_update. Esto forzará a Raycast a detectar colisiones cuando lo llamemos, lo que significa que obtenemos una comprobación de colisión perfecta con el mundo de la física 3D.

Entonces comprobamos si el Raycast colisionó con algo.

Si el Raycast ha colisionado con algo, primero obtenemos el cuerpo de la colisión con el que ha colisionado. Este puede ser un StaticBody, RigidBody, o un KinematicBody.

A continuación queremos asegurarnos de que el cuerpo con el que hemos chocado no es el jugador, ya que (probablemente) no queremos darle al jugador la posibilidad de dispararse en el pie.

Si el cuerpo no es el jugador, entonces comprobamos si tiene una función/método llamado bullet_hit. Si la tiene, la llamamos y le pasamos la cantidad de daño que hace esta bala (DAMAGE), y la transformación global del Raycast para que podamos saber de qué dirección vino la bala.


Ahora todo lo que tenemos que hacer es escribir el código del cuchillo.

Selecciona Knife_Point (Player -> Rotation_Helper -> Gun_Fire_Points -> Knife_Point) y crea un nuevo script llamado Weapon_Knife.gd, y añade lo siguiente:

extends Spatial

const DAMAGE = 40

const IDLE_ANIM_NAME = "Knife_idle"
const FIRE_ANIM_NAME = "Knife_fire"

var is_weapon_enabled = false

var player_node = null

func _ready():
    pass

func fire_weapon():
    var area = $Area
    var bodies = area.get_overlapping_bodies()

    for body in bodies:
        if body == player_node:
            continue

        if body.has_method("bullet_hit"):
            body.bullet_hit(DAMAGE, area.global_transform)

func equip_weapon():
    if player_node.animation_manager.current_state == IDLE_ANIM_NAME:
        is_weapon_enabled = true
        return true

    if player_node.animation_manager.current_state == "Idle_unarmed":
        player_node.animation_manager.set_animation("Knife_equip")

    return false

func unequip_weapon():

    if player_node.animation_manager.current_state == IDLE_ANIM_NAME:
        player_node.animation_manager.set_animation("Knife_unequip")

    if player_node.animation_manager.current_state == "Idle_unarmed":
        is_weapon_enabled = false
        return true

    return false

Como con Weapon_Rifle.gd, las únicas diferencias están en fire_weapon, así que veamos eso:

Lo primero que hacemos es obtener el nodo :ref:Area <class_Area>` hijo de ``Knife_Point.

A continuación, queremos tener todos los cuerpos de colisión dentro de la :ref:Area <class_Area>` usando ``get_overlapping_bodies. Esto devolverá una lista de cada cuerpo que toque el Area.

Lo siguiente que queremos hacer es revisar cada uno de esos cuerpos.

Primero revisamos para asegurarnos que el cuerpo no es el jugador, porque no queremos que el jugador sea capaz de apuñalarse a sí mismo. Si el cuerpo es el jugador, usamos continue, así que saltamos y miramos el siguiente cuerpo en bodies.

Si no hemos saltado al siguiente cuerpo, entonces comprobamos si el cuerpo tiene la función/método "bullet_hit". Si la tiene, la llamamos, pasando la cantidad de daño que hace un solo golpe de cuchillo (DAMAGE) y la transformación global de la Area.

Nota

Aunque podríamos intentar calcular una posición aproximada para el lugar exacto donde el cuchillo golpeó, no lo haremos porque usar la posición de Area funciona bastante bien y el tiempo extra necesario para calcular una posición aproximada para cada cuerpo no vale la pena el esfuerzo.

Hacer que las armas funcionen

Empecemos a hacer que las armas funcionen en Player.gd.

Primero empecemos por añadir algunas variables de clase que necesitaremos para las armas:

# Place before _ready
var animation_manager

var current_weapon_name = "UNARMED"
var weapons = {"UNARMED":null, "KNIFE":null, "PISTOL":null, "RIFLE":null}
const WEAPON_NUMBER_TO_NAME = {0:"UNARMED", 1:"KNIFE", 2:"PISTOL", 3:"RIFLE"}
const WEAPON_NAME_TO_NUMBER = {"UNARMED":0, "KNIFE":1, "PISTOL":2, "RIFLE":3}
var changing_weapon = false
var changing_weapon_name = "UNARMED"

var health = 100

var UI_status_label

Veamos lo que harán las nuevas variables:

  • animation_manager: Esto contendrá el nodo AnimationPlayer y su script, que hemos escrito previamente.

  • current_weapon_name: El nombre del arma que estamos usando actualmente. Tiene cuatro valores posibles: UNARMED, KNIFE, PISTOL, and RIFLE.

  • weapons: Un diccionario que contiene todos los nodos de las armas.

  • WEAPON_NUMBER_TO_NAME: Un diccionario que nos permite convertir el número de un arma en su nombre. Usaremos esto para cambiar las armas.

  • WEAPON_NAME_TO_NUMBER: Un diccionario que nos permite convertir el nombre de un arma en su número. Usaremos esto para cambiar las armas.

  • changing_weapon: Un booleano para saber si estamos cambiando o no las armas.

  • changing_weapon_name: El nombre del arma a la que queremos cambiar.

  • health: Cuánta salud tiene nuestro jugador. En esta parte del tutorial no la usaremos.

  • UI_status_label: Una etiqueta para mostrar cuánta salud tenemos, y cuánta munición tenemos tanto en nuestra arma como en la reserva.


A continuación necesitamos añadir algunas cosas en _ready. Aquí está la nueva función _ready:

func _ready():
    camera = $Rotation_Helper/Camera
    rotation_helper = $Rotation_Helper

    animation_manager = $Rotation_Helper/Model/Animation_Player
    animation_manager.callback_function = funcref(self, "fire_bullet")

    Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)

    weapons["KNIFE"] = $Rotation_Helper/Gun_Fire_Points/Knife_Point
    weapons["PISTOL"] = $Rotation_Helper/Gun_Fire_Points/Pistol_Point
    weapons["RIFLE"] = $Rotation_Helper/Gun_Fire_Points/Rifle_Point

    var gun_aim_point_pos = $Rotation_Helper/Gun_Aim_Point.global_transform.origin

    for weapon in weapons:
        var weapon_node = weapons[weapon]
        if weapon_node != null:
            weapon_node.player_node = self
            weapon_node.look_at(gun_aim_point_pos, Vector3(0, 1, 0))
            weapon_node.rotate_object_local(Vector3(0, 1, 0), deg2rad(180))

    current_weapon_name = "UNARMED"
    changing_weapon_name = "UNARMED"

    UI_status_label = $HUD/Panel/Gun_label
    flashlight = $Rotation_Helper/Flashlight

Repasemos lo que ha cambiado.

Primero obtenemos el nodo AnimationPlayer y lo asignamos a la variable animation_manager. Luego ponemos la función de devolución de llamada en un FuncRef que llamará a la función fire_bullet del jugador. Por ahora no hemos escrito la función fire_bullet, pero llegaremos pronto.

A continuación, obtenemos todos los nodos de armas y los asignamos a armas. Esto nos permitirá acceder a los nodos de armas sólo con su nombre (KNIFE, PISTOL, or RIFLE).

Luego obtenemos la posición global de Gun_Aim_Point para poder rotar las armas del jugador para apuntar.

Luego revisamos cada arma en weapons.

Primero tenemos el nodo de armas. Si el nodo del arma no es null, entonces ponemos su variable Player_node en este script (Player.gd). Luego hacemos que mire a gun_aim_point_pos usando la función look_at, y luego lo rotamos por 180 grados en el eje Y.

Nota

Giramos todos los puntos de las armas en 180 grados sobre su eje Y porque nuestra cámara está apuntando hacia atrás. Si no rotamos todos estos puntos de armas en 180 grados, todas las armas dispararían al revés.

Luego ponemos current_weapon_name y changing_weapon_name en UNARMED.

Finalmente, conseguimos la UI Label de nuestro HUD.


Añadamos una nueva llamada a la función _physics_process para poder cambiar de arma. Aquí está el nuevo código:

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

Ahora llamaremos process_changing_weapons.


Ahora agreguemos todo el código de entrada del jugador para las armas en process input. Añade el siguiente código:

# ----------------------------------
# Changing weapons.
var weapon_change_number = WEAPON_NAME_TO_NUMBER[current_weapon_name]

if Input.is_key_pressed(KEY_1):
    weapon_change_number = 0
if Input.is_key_pressed(KEY_2):
    weapon_change_number = 1
if Input.is_key_pressed(KEY_3):
    weapon_change_number = 2
if Input.is_key_pressed(KEY_4):
    weapon_change_number = 3

if Input.is_action_just_pressed("shift_weapon_positive"):
    weapon_change_number += 1
if Input.is_action_just_pressed("shift_weapon_negative"):
    weapon_change_number -= 1

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

if changing_weapon == false:
    if WEAPON_NUMBER_TO_NAME[weapon_change_number] != current_weapon_name:
        changing_weapon_name = WEAPON_NUMBER_TO_NAME[weapon_change_number]
        changing_weapon = true
# ----------------------------------

# ----------------------------------
# Firing the weapons
if Input.is_action_pressed("fire"):
    if changing_weapon == false:
        var current_weapon = weapons[current_weapon_name]
        if current_weapon != null:
            if animation_manager.current_state == current_weapon.IDLE_ANIM_NAME:
                animation_manager.set_animation(current_weapon.FIRE_ANIM_NAME)
# ----------------------------------

Repasemos lo agregado, comenzando sobre cómo cambiamos las armas.

Primero obtenemos el número del arma actual y lo asignamos al weapon_change_number.

Luego comprobamos si alguna de las teclas numéricas (teclas 1-4) está pulsada. Si lo están, ponemos "weapon_change_number" al valor asignado a esa tecla.

Nota

La razón por la que la clave 1 está asignada a 0 es porque el primer elemento de la lista está asignado a cero, no a uno. La mayoría de los accesos a las listas/arrays en la mayoría de los lenguajes de programación empiezan en 0 en lugar de 1. Ver https://en.wikipedia.org/wiki/Zero-based_numbering para más información.

A continuación comprobamos si shift_weapon_positive o shift_weapon_negative está presionado. Si uno de ellos lo está, añadimos/restamos 1 de weapon_change_number.

Debido a que el jugador puede haber cambiado el weapon_change_number fuera del número de armas que tiene, lo fijamos para que no pueda exceder el número máximo de armas que tiene y se asegura de que el weapon_change_number sea 0 o más.

Luego revisamos para asegurarnos de que el jugador no esté ya cambiando de armas. Si el jugador no lo está, entonces comprobamos si el arma a la que el jugador quiere cambiar es una nueva arma y no el arma que el jugador está usando actualmente. Si el arma a la que el jugador quiere cambiarse es una nueva arma, entonces ponemos changing_weapon_name al arma en weapon_change_number y ponemos changing_weapon en true.

Para disparar el arma, primero revisamos si la acción fire está presionada. Luego comprobamos que el jugador no esté cambiando de arma. Luego obtenemos el nodo del arma para el arma actual.

Si el nodo del arma actual no es igual a null, y el jugador está en su estado de IDLE_ANIM_NAME, ponemos la animación del jugador en el estado de FIRE_ANIM_NAME del arma actual.


Añadamos process_changing_weapons a continuación.

Añade el siguiente código:

func process_changing_weapons(delta):
    if changing_weapon == true:

        var weapon_unequipped = false
        var current_weapon = weapons[current_weapon_name]

        if current_weapon == null:
            weapon_unequipped = true
        else:
            if current_weapon.is_weapon_enabled == true:
                weapon_unequipped = current_weapon.unequip_weapon()
            else:
                weapon_unequipped = true

        if weapon_unequipped == true:

            var weapon_equipped = false
            var weapon_to_equip = weapons[changing_weapon_name]

            if weapon_to_equip == null:
                weapon_equipped = true
            else:
                if weapon_to_equip.is_weapon_enabled == false:
                    weapon_equipped = weapon_to_equip.equip_weapon()
                else:
                    weapon_equipped = true

            if weapon_equipped == true:
                changing_weapon = false
                current_weapon_name = changing_weapon_name
                changing_weapon_name = ""

Repasemos lo que está pasando aquí:

The first thing we do is make sure we've received input to change weapons. We do this by making sure changing_weapon is true.

A continuación definimos una variable (weapon_unequipped) para que podamos comprobar si el arma actual ha sido desequipada con éxito o no.

Entonces obtenemos el arma actual de weapons.

Si el arma actual no es null, entonces tenemos que comprobar si el arma está habilitada. Si el arma está habilitada, llamamos a su función unequip_weapon para que inicie la animación del desequipar. Si el arma no está habilitada, ponemos weapon_unequipped en true porque el arma ha sido exitosamente desequipada.

Si el arma actual es null, entonces podemos simplemente establecer weapon_unequipped como true. La razón por la que hacemos esta comprobación es porque no hay ningún script/nodo de arma para UNARMED, pero tampoco hay animaciones para UNARMED, así que podemos empezar a equipar el arma a la que el jugador quiere cambiar.

Si el jugador ha logrado desarmar el arma actual (weapon_unequipped == true), necesitamos equipar la nueva arma.

Primero definimos una nueva variable (weapon_equipped) para rastrear si el jugador ha equipado exitosamente la nueva arma o no.

Entonces conseguimos el arma que el jugador quiere cambiar. Si el arma a la que el jugador quiere cambiar no es null, entonces comprobamos si está activada o no. Si no está activada, llamamos a su función equip_weapon para que empiece a equipar el arma. Si el arma está habilitada, ponemos weapon_equipped``a ``true.

Si el arma que el jugador quiere cambiar es null, simplemente ponemos weapon_equipped en true porque no tenemos ningún nodo/script para UNARMED, ni tenemos ninguna animación.

Finalmente, comprobamos si el jugador ha equipado con éxito la nueva arma. Si lo ha hecho, ponemos changing_weapon a false porque el jugador ya no está cambiando de arma. También ponemos current_weapon_name en changing_weapon_name ya que el arma actual ha cambiado, y luego ponemos changing_weapon_name en una string vacía.


Ahora, necesitamos añadir una función más al jugador, ¡y entonces estará listo para empezar a disparar sus armas!

Necesitamos añadir fire_bullet, que será llamado por la pista de la función AnimationPlayer en los puntos que establecimos antes en la pista de la función AnimationPlayer:

func fire_bullet():
    if changing_weapon == true:
        return

    weapons[current_weapon_name].fire_weapon()

Veamos lo que hace esta función:

Primero comprobamos si el jugador está cambiando de armas. Si el jugador está cambiando de arma, no queremos disparar, así que return.

Truco

Llamar return impide que se llame al resto de la función. En este caso, no estamos devolviendo una variable porque sólo nos interesa no ejecutar el resto del código, y porque tampoco buscamos una variable devuelta cuando llamamos a esta función.

Luego le decimos al arma actual que el jugador está usando para disparar su función de fire_weapon.

Truco

¿Recuerdas que mencionamos que la velocidad de las animaciones para disparos era más rápida que las otras animaciones? ¡Cambiando las velocidades de la animación de disparo, puedes cambiar la velocidad del arma para disparar balas!


Antes de que estemos listos para probar nuestras nuevas armas, todavía tenemos un poco de trabajo que hacer.

Creando algunos sujetos de prueba

Crea un nuevo script yendo a la ventana del guión, haciendo clic en "archivo" y seleccionando nuevo. Nombra este script RigidBody_hit_test y asegúrate de que extienda RigidBody.

Ahora tenemos que añadir este código:

extends RigidBody

const BASE_BULLET_BOOST = 9

func _ready():
    pass

func bullet_hit(damage, bullet_global_trans):
    var direction_vect = bullet_global_trans.basis.z.normalized() * BASE_BULLET_BOOST

    apply_impulse((bullet_global_trans.origin - global_transform.origin).normalized(), direction_vect * damage)

Repasemos cómo funciona el bullet_hit:

Primero tenemos el vector direccional de la bala. Esto es para que podamos decir desde cuál dirección la bala golpeará el RigidBody. Usaremos esto para empujar el RigidBody en la misma dirección que la bala.

Nota

Necesitamos aumentar el vector direccional por BASE_BULLET_BOOST para que las balas den un poco más de golpe y mueven los nodos RigidBody de forma visible. Puedes poner BASE_BULLET_BOOST a valores más bajos o más altos si quieres que haya más o menos reacción cuando las balas choquen con el RigidBody.

Luego aplicamos un impulso usando apply_impulse.

Primero, tenemos que calcular la posición del impulso. Debido a que apply_impulse toma un vector relativo a la RigidBody, necesitamos calcular la distancia desde la RigidBody a la bala. Lo hacemos restando el origen/posición global de RigidBody del origen/posición global de la bala. Esto nos da la distancia de la RigidBody a la bala. Normalizamos este vector para que el tamaño del colisionador no afecte a cuánto se mueven las balas el RigidBody.

Finalmente, necesitamos calcular la fuerza del impulso. Para ello, utilizamos la dirección a la que está orientada la bala y la multiplicamos por el daño de la bala. Esto da un buen resultado y para balas más fuertes, obtenemos un resultado más fuerte.


Ahora necesitamos anexar este script a todos los RigidBody que queremos afectar.

Abre Testing_Area.tscn y selecciona todos los cubos que están en el nodo Cubes.

Truco

Si seleccionas el cubo superior, y luego mantienes pulsado Shift y seleccionas el último cubo, Godot seleccionará todos los cubos intermedios!

Una vez que tengas todos los cubos seleccionados, baja en el inspector hasta que llegues a la sección de "scripts". Haga clic en el menú desplegable y seleccione "Cargar". Abre tu recién creado script RigidBody_hit_test.gd.

Notas finales

../../../_images/PartTwoFinished.png

¡Eso fue un montón de código! Pero ahora, con todo eso hecho, ¡puedes ir y probar sus armas!

Ahora deberías poder disparar tantas balas como quieras a los cubos y se moverán en respuesta a las balas que choquen con ellos.

En Parte 3, añadiremos munición a las armas, así como algunos sonidos!

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