Tutorial VR para principiantes parte 2

Introducción

../../../_images/starter_vr_tutorial_sword.png

En esta parte de la serie de tutoriales de inicio de la RV, añadiremos una serie de nodos especiales basados en RigidBody que pueden ser utilizados en la RV.

Esto continúa desde donde lo dejamos en la última parte del tutorial, donde acabamos de terminar de hacer funcionar los controladores de RV y definimos una clase personalizada llamada VR_Interactable_Rigidbody.

Truco

Podrás encontrar el proyecto completado en el repositorio de OpenVR en GitHub.

Añadir objetivos destruibles

Antes de hacer cualquiera de los nodos especiales RigidBody basados en el cuerpo, necesitamos algo para que hagan. Hagamos una simple esfera objetivo que se rompa en un montón de pedazos cuando sea destruida.

Abre "Sphere_Target.tscn", que está en la carpeta Scenes. La escena es bastante simple, con sólo un nodo StaticBody con forma de esfera CollisionShape, un nodo MeshInstance que muestra una malla de esfera, y un nodo AudioStreamPlayer3D.

Los nodos especiales RigidBody se encargarán de dañar la esfera, por lo que estamos usando un nodo StaticBody en lugar de algo como un nodo Area o RigidBody. Fuera de eso, no hay mucho de que hablar, así que vamos a pasar directamente a escribir el código.

Selecciona el nodo Sphere_Target_Root y haz un nuevo script llamado Sphere_Target.gd. Añade el siguiente código:

extends Spatial

var destroyed = false
var destroyed_timer = 0
const DESTROY_WAIT_TIME = 80

var health = 80

const RIGID_BODY_TARGET = preload("res://Assets/RigidBody_Sphere.scn")


func _ready():
    set_physics_process(false)


func _physics_process(delta):
    destroyed_timer += delta
    if destroyed_timer >= DESTROY_WAIT_TIME:
        queue_free()


func damage(damage):
    if destroyed == true:
        return

    health -= damage

    if health <= 0:

        get_node("CollisionShape").disabled = true
        get_node("Shpere_Target").visible = false

        var clone = RIGID_BODY_TARGET.instance()
        add_child(clone)
        clone.global_transform = global_transform

        destroyed = true
        set_physics_process(true)

        get_node("AudioStreamPlayer").play()
        get_tree().root.get_node("Game").remove_sphere()

Repasemos lo que hace este script.

Explicación del código del objetivo de la Esfera

Primero, veamos todas las variables de la clase en el script:

  • destroyed: Una variable para verificar si la esfera usada como blanco fue destruida.

  • destroyed_timer: Una variable usada para verificar cuánto tiempo a transcurrido desde que la esfera fue destruida.

  • DESTROY_WAIT_TIME: Una constante para definir el tiempo durante el cual el objetivo puede ser destruido antes de que se libere/elimine.

  • health: Una variable que guarda la cantidad de salud que tiene el blanco.

  • RIGID_BODY_TARGET: Una constante para mantener la escena del objetivo de la esfera destruida.

Nota

Siéntete libre de ver la escena de RIGID_BODY_TARGET`. Es sólo un montón de nodos de RigidBody y un modelo de esfera rota.

Instalaremos esta escena para que cuando el objetivo sea destruido, parezca que se rompió en un montón de pedazos.

``_ready``explicación paso a paso de la función

Todo lo que hace la función _ready es que evita que el _physics_process sea llamado llamando set_physics_process y pasando false. La razón por la que hacemos esto es porque todo el código en _physics_process es para destruir este nodo cuando ha pasado suficiente tiempo, lo que sólo queremos hacer cuando el objetivo ha sido destruido.

_physics_process explicación paso a paso de la función

Primero esta función añade tiempo, delta, a la variable destroyed_timer. Luego comprueba si el destroyed_timer es mayor o igual al DESTROY_WAIT_TIM. Si destroyed_timer es mayor o igual a DESTROY_WAIT_TIME, entonces el objetivo de la esfera se libera/borra a sí mismo llamando a la función queue_free.

explicación paso a paso de la función damage

La función damage será llamada por los nodos especiales RigidBody, que pasarán la cantidad de daño hecho al objetivo, que es una variable de argumento de la función llamada damage. La variable damage contendrá la cantidad de daño que el nodo especial RigidBody hizo al objetivo de la esfera.

Primero esta función comprueba que el objetivo no esté ya destruido comprobando si la variable destroyed es igual a true. Si destroyed es igual a true, entonces la función llama return para que no se llame a ningún otro código. Esto es sólo una comprobación de seguridad para que si dos cosas dañan el objetivo exactamente al mismo tiempo, el objetivo no pueda ser destruido dos veces.

A continuación, la función elimina la cantidad de daño tomado, damage, de la salud del objetivo, health. Entonces comprueba si la health es igual a cero o menos, lo que significa que el objetivo acaba de ser destruido.

Si el objetivo acaba de ser destruido, entonces deshabilitamos la CollisionShape estableciendo su propiedad disabled como true. Luego hacemos que el Sphere_Target MeshInstance sea invisible poniendo la propiedad visible a false. Hacemos esto para que el objetivo ya no pueda afectar al mundo de la física y para que la malla del objetivo no rota no sea visible.

Después de esto, la función entonces instancia la escena RIGID_BODY_TARGET y la agrega como un hijo del objetivo. Luego establece la global_transform de la escena recién creada, llamada clone, a la global_transform del objetivo no roto. Esto hace que el objetivo roto comience en la misma posición que el objetivo no roto con la misma rotación y escala.

Luego la función pone la variable destroyed" en true para que el objetivo sepa que ha sido destruido y llama a la función set_physics_process y pasa a true. Esto comenzará a ejecutar el código en physics_process para que después de DESTROY_WAIT_TIME segundos hayan pasado, el objetivo de la esfera se libere/destruya a sí mismo.

La función entonces obtiene el nodo AudioStreamPlayer3D y llama a la función play para que reproduzca su sonido.

Finalmente, la función remove_sphere es llamada en Game.gd. Para obtener Game.gd, el código usa el árbol de la escena y trabaja desde la raíz del árbol de la escena hasta la raíz de la escena Game.tscn.

Añadiendo la función remove_sphere a Game.gd

Habrás notado que estamos llamando a una función en Game.gd, llamada remove_sphere, que aún no hemos definido. Abre Game.gd y añade las siguientes variables de clase adicionales:

var spheres_left = 10
var sphere_ui = null
  • spheres_left: La cantidad de objetivos de la esfera que quedan en el mundo. En la escena del Game, hay 10 esferas, así que ese es el valor inicial.

  • sphere_ui: Una referencia a la esfera UI. Usaremos esto más adelante en el tutorial para mostrar la cantidad de esferas que quedan en el mundo.

Con estas variables definidas, ahora podemos agregar la función remove_sphere. Añade el siguiente código a Game.gd:

func remove_sphere():
    spheres_left -= 1

    if sphere_ui != null:
        sphere_ui.update_ui(spheres_left)

Repasemos rápidamente lo que hace esta función:

Primero, elimina uno de la variable spheres_left. Luego comprueba si la variable sphere_ui no es igual a null, y si no es igual a null llama a la función update_ui en sphere_ui, pasando el número de esferas como un argumento a la función.

Nota

¡Añadiremos el código de sphere_ui más adelante en este tutorial!

Ahora el Sphere_Target está listo para ser usado, pero no tenemos ninguna manera de destruirlo. Arreglémoslo añadiendo algunos nodos especiales RigidBody basados en nodos que puedan dañar los objetivos.

Añadir una pistola

Añadamos una pistola como el primer nodo interactivo RigidBody. Abre "Pistol.tscn", que puedes encontrar en la carpeta "Scenes".

Repasemos rápidamente algunas cosas importantes en Pistol.tscn antes de agregar el código.

Todos los nodos en Pistol.tscn esperan que el nodo raíz esté rotado. Esto es así para que la pistola esté en la rotación correcta en relación con el controlador VR cuando se recoge. El nodo raíz es un nodo RigidBody, que necesitamos porque vamos a usar la clase VR_Interactable_Rigidbody que creamos en la última parte de esta serie de tutoriales.

Hay un nodo MeshInstance llamado Pistol_Flash, que es una simple malla que usaremos para simular el destello de la boca del cañón de la pistola. Un nodo MeshInstance llamado LaserSight se usa como guía para apuntar la pistola, y sigue la dirección del nodo Raycast, llamado Raycast, que la pistola usa para detectar si su 'bala' golpea algo. Finalmente, hay un nodo AudioStreamPlayer3D al final de la pistola que usaremos para reproducir el sonido del disparo de la pistola.

Siéntase libre de mirar las otras partes de la escena si quieres. La mayor parte de la escena es bastante sencilla, con los cambios principales mencionados anteriormente. Selecciona el nodo RigidBody llamado Pistol y haz un nuevo script llamado Pistol.gd. Añade el siguiente código:

extends VR_Interactable_Rigidbody

var flash_mesh
const FLASH_TIME = 0.25
var flash_timer = 0

var laser_sight_mesh
var pistol_fire_sound

var raycast
const BULLET_DAMAGE = 20
const COLLISION_FORCE = 1.5


func _ready():
    flash_mesh = get_node("Pistol_Flash")
    flash_mesh.visible = false

    laser_sight_mesh = get_node("LaserSight")
    laser_sight_mesh.visible = false

    raycast = get_node("RayCast")
    pistol_fire_sound = get_node("AudioStreamPlayer3D")


func _physics_process(delta):
    if flash_timer > 0:
        flash_timer -= delta
        if flash_timer <= 0:
            flash_mesh.visible = false


func interact():
    if flash_timer <= 0:

        flash_timer = FLASH_TIME
        flash_mesh.visible = true

        raycast.force_raycast_update()
        if raycast.is_colliding():

            var body = raycast.get_collider()
            var direction_vector = raycast.global_transform.basis.z.normalized()
            var raycast_distance = raycast.global_transform.origin.distance_to(raycast.get_collision_point())

            if body.has_method("damage"):
                body.damage(BULLET_DAMAGE)
            elif body is RigidBody:
                var collision_force = (COLLISION_FORCE / raycast_distance) * body.mass
                body.apply_impulse((raycast.global_transform.origin - body.global_transform.origin).normalized(), direction_vector * collision_force)

        pistol_fire_sound.play()

        if controller != null:
            controller.rumble = 0.25


func picked_up():
    laser_sight_mesh.visible = true


func dropped():
    laser_sight_mesh.visible = false

Repasemos lo que hace este script.

Explicando el código de la pistola

Primero, noten cómo en lugar de extends RigidBody, tenemos extends VR_Interactable_Rigidbody. Esto hace que el script de la pistola extienda la clase VR_Interactable_Rigidbody para que los controladores de VR sepan que este objeto puede ser interactuado y que las funciones definidas en VR_Interactable_Rigidbody pueden ser llamadas cuando este objeto es sostenido por un controlador de VR.

A continuación, veamos las variables de la clase:

  • flash_mesh: Una variable para mantener el nodo MeshInstance usado para simular el resplandor del disparo en la pistola.

  • FLASH_TIME: Una constante para definir cuánto tiempo será visible el destello del cañón. Esto también definirá cuán rápido puede disparar la pistola.

  • flash_timer: Una variable para mantener el tiempo en que estuvo visible el flash de la boca del arma.

  • laser_sight_mesh: Una variable que contiene el nodo MeshInstance que actúa como 'mira láser' de la pistola.

  • pistol_fire_sound: Una variable para mantener el nodo AudioStreamPlayer3D usado para el sonido de disparo de la pistola.

  • raycast: Una variable que contiene el nodo Raycast que es usado para calcular la posición de la bala y su normal cuando la pistola es disparada.

  • BULLET_DAMAGE: Una constante para definir la cantidad de daño que hace una sola bala de la pistola.

  • COLLISION_FORCE: Una constante que define la cantidad de fuerza que se aplica a los nodos RigidBody cuando la bala de la pistola colisiona.

``_ready``explicación paso a paso de la función

Esta función obtiene los nodos y los asigna a sus propias variables. Para los nodos flash_mesh y laser_sight_mesh, ambos tienen su propiedad visible puesta en false por lo que no son visibles inicialmente.

_physics_process explicación paso a paso de la función

La función physics_process comprueba primero si el flash del cañón de la pistola es visible comprobando si el flash_timer es máyor de cero. Si flash_timer es máyor de cero, entonces quitamos el tiempo, delta de él. A continuación, comprobamos si la variable flash_timer es cero o menor ahora que hemos eliminado delta de ella. Si es así, entonces el temporizador de la pistola acaba de terminar y tenemos que hacer invisible la flash_mesh estableciendo su propiedad visible como false.

interact explicación paso a paso de la función

La función interact comprueba primero si el flash del cañón de la pistola es invisible comprobando si el flash_timer es menor o igual a cero. Hacemos esto para poder limitar la velocidad de disparo de la pistola al tiempo en que el flash de la boca es visible, lo cual es una solución simple para limitar la velocidad de disparo del jugador.

Si flash_timer es menor o igual a 0, entonces ponemos flash_timer a FLASH_TIME para que haya un retraso antes de que la pistola pueda disparar de nuevo. Después de eso ponemos flash_mesh.visible en true para que el flash del cañón del arma sea visible mientras que flash_timer sea mayor que cero.

A continuación llamamos a la función force_raycast_update en el nodo Raycast en raycast para que reciba la última información sobre colisiones del mundo de la física. Luego verificamos si el raycast golpea algo, verificando si la función is_colliding es igual a true.


Si el raycast golpea algo, entonces obtenemos el PhysicsBody con el que colisionó a través de la función get_collider. Asignamos el hit PhysicsBody a una variable llamada body.

Entonces obtenemos la dirección del Raycast obteniendo su eje direccional positivo Z de la Base en la global_transform del nodo raycast. Esto nos dará la dirección en la que el raycast apunta al eje Z, que es la misma dirección que la flecha azul del gizmo Spatial cuando el Local space mode está habilitado en el editor de Godot. Guardamos esta dirección en una variable llamada direction_vector.

A continuación obtenemos la distancia desde el origen Raycast hasta el punto de colisión Raycast obteniendo la distancia desde la posición global, global_transform. origin del nodo raycast al punto de colisión de Raycast, raycast.get_collision_point, usando la función distance_to. Esto nos dará la distancia que el Raycast recorrió antes de colisionar, que guardamos en una variable llamada raycast_distance.

Entonces el código comprueba si el PhysicsBody, body, tiene una función/método llamado damage usando la función has_method. Si PhysicsBody tiene una función/método llamado damage, entonces llamamos a la función damage y le pasamos BULLET_DAMAGE para que reciba el daño de la bala que choca contra él.

Independientemente de si el PhysicsBody tiene una función de damage, entonces comprobamos si body es un nodo basado en RigidBody. Si body es un nodo basado en RigidBody, entonces queremos empujarlo cuando la bala colisione.

Para calcular la cantidad de fuerza aplicada, simplemente tomamos COLLISION_FORCE y lo dividimos por``raycast_distance``, a continuación lo multiplicamos todo por body.mass. Guardamos este cálculo en una variable llamada collision_force. Esto hará que las colisiones a una distancia más corta apliquen una fuerza de movimiento que las que se producen a distancias más largas, dando una respuesta de colisión ligeramente más realista.

Luego presionamos la RigidBody usando la función apply_impulse, donde la posición es un Vector3 cero por lo que la fuerza se aplica desde el centro, y la fuerza de colisión es la variable collision_force que hemos calculado.


Independientemente de si la variable raycast golpeó algo o no, entonces reproducimos el sonido del disparo de la pistola llamando a la función play en la variable pistol_fire_sound.

Finalmente, comprobamos si la pistola está siendo sostenida por un controlador VR comprobando si la variable controller no es igual a null. Si no es igual a null, entonces ponemos la propiedad rumble del controlador VR en 0.25, de modo que haya un ligero temblor cuando la pistola dispare.

explicación paso a paso de la función picked_up

Esta función simplemente hace que la laser_sight_mesh s ref:MeshInstance <class_MeshInstance> sea visible estableciendo la propiedad visible a true.

explicación paso a paso de la función dropped

Esta función simplemente hace que la laser_sight_mesh MeshInstance sea invisible poniendo la propiedad visible a false.

Pistola terminada

../../../_images/starter_vr_tutorial_pistol.png

¡Eso es todo lo que necesitamos para tener pistolas que funcionen en el proyecto! Adelante, dirige el proyecto. Si subes las escaleras y coges las pistolas, puedes dispararlas a los objetivos de la esfera en la escena usando el botón de disparo del controlador VR! Si disparas a los objetivos el tiempo suficiente, se romperán en pedazos.

Añadir una escopeta

A continuación añadiremos una escopeta al proyecto de RV.

Añadir una escopeta especial RigidBody debería ser bastante sencillo, ya que casi todo con la escopeta es lo mismo que la pistola.

Abre el archivo Shotgun.tscn, que puedes encontrar en la carpeta Scenes y echa un vistazo a la escena. Casi todo es igual que en Pistola.tscn. Lo único que es diferente, más allá de los cambios de nombre, es que en lugar de un solo nodo Raycast, hay cinco nodos Raycast. Esto se debe a que una escopeta generalmente dispara en forma de cono, por lo que vamos a emular ese efecto teniendo varios nodos Raycast que girarán aleatoriamente en forma de cono cuando la escopeta dispare.

Fuera de eso, todo es más o menos lo mismo que Pistol.tscn.

Escribamos el código de la escopeta. Selecciona el nodo RigidBody llamado Shotgun y haz un nuevo script llamado Shotgun.gd. Añade el siguiente código:

extends VR_Interactable_Rigidbody

var flash_mesh
const FLASH_TIME = 0.25
var flash_timer = 0

var laser_sight_mesh
var shotgun_fire_sound

var raycasts
const BULLET_DAMAGE = 30
const COLLISION_FORCE = 4


func _ready():
    flash_mesh = get_node("Shotgun_Flash")
    flash_mesh.visible = false

    laser_sight_mesh = get_node("LaserSight")
    laser_sight_mesh.visible = false

    raycasts = get_node("Raycasts")
    shotgun_fire_sound = get_node("AudioStreamPlayer3D")


func _physics_process(delta):
    if flash_timer > 0:
        flash_timer -= delta
        if flash_timer <= 0:
            flash_mesh.visible = false


func interact():
    if flash_timer <= 0:

        flash_timer = FLASH_TIME
        flash_mesh.visible = true

        for raycast in raycasts.get_children():

            if not raycast is RayCast:
                continue

            raycast.rotation_degrees = Vector3(90 + rand_range(10, -10), 0, rand_range(10, -10))

            raycast.force_raycast_update()
            if raycast.is_colliding():

                var body = raycast.get_collider()
                var direction_vector = raycasts.global_transform.basis.z.normalized()
                var raycast_distance = raycasts.global_transform.origin.distance_to(raycast.get_collision_point())

                if body.has_method("damage"):
                    body.damage(BULLET_DAMAGE)

                if body is RigidBody:
                    var collision_force = (COLLISION_FORCE / raycast_distance) * body.mass
                    body.apply_impulse((raycast.global_transform.origin - body.global_transform.origin).normalized(), direction_vector * collision_force)

        shotgun_fire_sound.play()

        if controller != null:
            controller.rumble = 0.25


func picked_up():
    laser_sight_mesh.visible = true


func dropped():
    laser_sight_mesh.visible = false

La mayoría de este código es exactamente el mismo que el de la pistola con sólo unos pocos cambios menos que son principalmente sólo nombres diferentes. Debido a lo similares que son estos scripts, centrémonos en los cambios.

Explicando el código de la escopeta

Al igual que con la pistola, la escopeta se extiende VR_Interactable_Rigidbody para que los controladores de VR sepan que se puede interactuar con este objeto y qué funciones están disponibles.

Aquí hay solo una nueva variable en la clase:

  • raycasts: Una variable para mantener el nodo que tiene todos los nodos Raycast como hijos.

La nueva variable de clase reemplaza a la variable raycast de Pistol.gd, porque con la escopeta necesitamos procesar múltiples nodos Raycast en lugar de uno solo. Todas las demás variables de clase son iguales a Pistol.gd y funcionan de la misma manera, algunas sólo se renombran para que no sean específicas de la pistola.

interact explicación paso a paso de la función

La función de interacción primero comprueba si el flash de la escopeta es invisible comprobando si el flash_timer es menor o igual a cero. Hacemos esto para poder limitar la velocidad de disparo de la escopeta al tiempo en que el flash de la boca es visible, lo cual es una solución simple para limitar la velocidad de disparo del jugador.

Si flash_timer es menor o igual a cero, entonces ponemos flash_timer en FLASH_TIME para que haya un retraso antes de que la escopeta pueda disparar de nuevo. Después de eso, ponemos flash_mesh.visible en true para que el flash de la boca del cañón sea visible mientras que el flash_timer sea mayor que cero.

A continuación llamamos a la función force_raycast_update en el nodo Raycast en raycast para que reciba la última información sobre colisiones del mundo de la física. Luego verificamos si el raycast golpea algo, verificando si la función is_colliding es igual a true.

Luego pasamos por cada uno de los nodos hijos de la variable raycasts usando un bucle "for". De esta manera el código pasará por cada uno de los nodos Raycast que son hijos de la variable raycasts.


Para cada nodo, comprobamos si raycast es no un nodo Raycast. Si el nodo no es un nodo Raycast, simplemente usamos continue para saltarlo.

Luego rotamos el nodo raycast aleatoriamente alrededor de un pequeño cono de 10 grados ajustando la variable rotation_degrees del raycast a un Vector 3 donde los ejes X y Z son un número aleatorio de -10 a 10. Este número aleatorio se selecciona usando la función rand_range.

Luego llamamos a la función force_raycast_update en el nodo Raycast en raycast para que reciba la última información sobre colisiones del mundo de la física. Luego verificamos si el raycast golpea algo, verificando si la función is_colliding es igual a true.

El resto del código es exactamente el mismo, pero este proceso se repite para cada nodo Raycast que es hijo de la variable raycasts.


Si el raycast golpea algo, entonces obtenemos el PhysicsBody con el que colisionó a través de la función get_collider. Asignamos el hit PhysicsBody a una variable llamada body.

Luego obtenemos la dirección del raycast obteniendo su eje direccional positivo Z del Basis en el nodo raycast del global_transform. Esto nos dará la dirección en la que el raycast apunta al eje Z, que es la misma dirección que la flecha azul del gizmo Spatial cuando el Local space mode está habilitado en el editor de Godot. Guardamos esta dirección en una variable llamada direction_vector.

A continuación obtenemos la distancia desde el origen del raycast al punto de colisión del raycast, obteniendo la distancia desde la posición global, global_transform.origin del nodo raycast al punto de colisión del raycast, raycast.get_collision_point, usando la función distance_to. Esto nos dará la distancia que el Raycast viajó antes de colisionar, que guardamos en una variable llamada raycast_distance.

Entonces el código comprueba si el PhysicsBody, body, tiene una función/método llamado damage usando la función has_method. Si PhysicsBody tiene una función/método llamado damage, entonces llamamos a la función damage y le pasamos BULLET_DAMAGE para que reciba el daño de la bala que choca contra él.

Independientemente de si el PhysicsBody tiene una función de damage, entonces comprobamos si body es un nodo basado en RigidBody. Si body es un nodo basado en RigidBody, entonces queremos empujarlo cuando la bala colisione.

Para calcular la cantidad de fuerza aplicada, simplemente tomamos COLLISION_FORCE y lo dividimos por``raycast_distance``, a continuación lo multiplicamos todo por body.mass. Guardamos este cálculo en una variable llamada collision_force. Esto hará que las colisiones a una distancia más corta apliquen una fuerza de movimiento que las que se producen a distancias más largas, dando una respuesta de colisión ligeramente más realista.

Luego presionamos la RigidBody usando la función apply_impulse, donde la posición es un Vector3 cero por lo que la fuerza se aplica desde el centro, y la fuerza de colisión es la variable collision_force que hemos calculado.


Una vez que todos los Raycast de la variable raycast han sido iterados, entonces reproducimos el sonido del disparo de la escopeta llamando a la función play en la variable shotgun_fire_sound.

Por último, comprobamos si la escopeta está siendo sostenida por un controlador VR comprobando si la variable controller no es igual a null. Si no es igual a null, entonces ponemos la propiedad rumble del controlador VR en 0.25, de modo que haya un ligero temblor cuando la escopeta se dispare.

Escopeta terminada

Todo lo demás es exactamente igual que la pistola, con algunos cambios de nombre sencillos.

¡Ahora la escopeta está terminada! Puedes encontrar la escopeta en la escena de muestra mirando alrededor de la parte de atrás de una de las paredes (¡aunque no en el edificio!).

Añadir una bomba

Vale, añadamos un especial diferente RigidBody. En lugar de añadir algo que dispare, añadamos algo que podamos lanzar - ¡una bomba!

Abre Bomb.tscn, que está en la carpeta Scenes.

El nodo raíz es un nodo RigidBody que extenderemos para usar VR_Interactable_Rigidbody, que tiene un CollisionShape como los otros nodos RigidBody especiales que hemos hecho hasta ahora. De la misma manera, hay un MeshInstance llamado Bomb que se usa para mostrar la malla de la bomba.

Entonces tenemos un nodo Area simplemente llamado Area que tiene un gran ColisiónShape como tu hijo. Usaremos este nodo Area para afectar cualquier cosa dentro de él cuando la bomba explote. Esencialmente, este nodo Area será el radio de explosión de la bomba.

También hay un par de nodos Particles. Uno de los nodos Particles es para el humo que sale del fusible de la bomba, mientras que otro es para la explosión. Puedes echar un vistazo a los recursos ParticlesMaterial, que definen cómo funcionan las partículas, si quieres. No vamos a cubrir cómo funcionan las partículas en este tutorial debido a que está fuera del alcance de este tutorial.

Hay una cosa con los nodos Particles que debemos anotar. Si seleccionas el nodo Explosion_Particles, verás que su propiedad lifetime está establecida en 0.75 y que la casilla one shot está activada. Esto significa que las partículas sólo se reproducirán una vez, y las partículas durarán 0.75 segundos. Necesitamos saber esto para poder cronometrar la eliminación de la bomba con el fin de la explosión :ref:``Particles <class_Particles>`.

Escribamos el código de la bomba. Selecciona el nodo Bomb RigidBody y haz un nuevo script llamado Bomb.gd. Añade el siguiente código:

extends VR_Interactable_Rigidbody

var bomb_mesh

const FUSE_TIME = 4
var fuse_timer = 0

var explosion_area
const EXPLOSION_DAMAGE = 100
const EXPLOSION_TIME = 0.75
var explosion_timer = 0
var exploded = false

const COLLISION_FORCE = 8

var fuse_particles
var explosion_particles
var explosion_sound


func _ready():

    bomb_mesh = get_node("Bomb")
    explosion_area = get_node("Area")
    fuse_particles = get_node("Fuse_Particles")
    explosion_particles = get_node("Explosion_Particles")
    explosion_sound = get_node("AudioStreamPlayer3D")

    set_physics_process(false)


func _physics_process(delta):

    if fuse_timer < FUSE_TIME:

        fuse_timer += delta

        if fuse_timer >= FUSE_TIME:

            fuse_particles.emitting = false

            explosion_particles.one_shot = true
            explosion_particles.emitting = true

            bomb_mesh.visible = false

            collision_layer = 0
            collision_mask = 0
            mode = RigidBody.MODE_STATIC

            for body in explosion_area.get_overlapping_bodies():
                if body == self:
                    pass
                else:
                    if body.has_method("damage"):
                        body.damage(EXPLOSION_DAMAGE)

                    if body is RigidBody:
                        var direction_vector = body.global_transform.origin - global_transform.origin
                        var bomb_distance = direction_vector.length()
                        var collision_force = (COLLISION_FORCE / bomb_distance) * body.mass
                        body.apply_impulse(Vector3.ZERO, direction_vector.normalized() * collision_force)

            exploded = true
            explosion_sound.play()


    if exploded:

        explosion_timer += delta

        if explosion_timer >= EXPLOSION_TIME:

            explosion_area.monitoring = false

            if controller != null:
                controller.held_object = null
                controller.hand_mesh.visible = true

                if controller.grab_mode == "RAYCAST":
                    controller.grab_raycast.visible = true

            queue_free()


func interact():
    set_physics_process(true)

    fuse_particles.emitting = true

Repasemos lo que hace este script.

Ejemplo de código de bomba

Como con los otros nodos especiales RigidBody, la bomba extiende VR_Interactable_Rigidbody para que los controladores de VR sepan que este objeto puede interactuar con él y que las funciones definidas en VR_Interactable_Rigidbody pueden ser llamadas cuando este objeto es sostenido por un controlador de VR.

A continuación, veamos las variables de la clase:

  • bomb_mesh: Una variable que contiene el nodo MeshInstance que es utilizado para la bomba que no ha explotado.

  • FUSE_TIME: Una constante para definir cuánto tiempo se 'quemará' el fusible antes de que la bomba explote

  • fuse_timer: Una variable para guardar cuanto tiempo ha pasado desde que la mecha de la bomba se encendió.

  • explosion_area: Una variable que contiene el nodo Area utilizado para detectar los objetos que se encuentran dentro de la explosión.

  • EXPLOSION_DAMAGE: Una constante que define cuánto daño es aplicado cuando explota la bomba.

  • EXPLOSION_TIME: Una constante para definir cuánto tiempo durará la bomba en la escena después de que explote. Este valor debe ser el mismo que la propiedad lifetime del nodo de la explosión Particles.

  • explosion_timer: Una variable que guarda el tiempo que ha pasado desde que explotó la bomba.

  • exploded: Una variable que guarda el estado de la bomba, si ha explotado o no.

  • COLLISION_FORCE: Una constante que define la cantidad de fuerza que se aplica a los nodos RigidBody cuando la bomba explota.

  • fuse_particles: Una variable para mantener el nodo Particles usado para la mecha de la bomba.

  • explosion_particles: Una variable para mantener el nodo Particles usado para la explosión de la bomba.

  • explosion_sound: Una variable para mantener una referencia al nodo AudioStreamPlayer3D utilizado para el sonido de la explosión.

``_ready``explicación paso a paso de la función

La función _ready obtiene primero todos los nodos de la escena bomba y los asigna a las respectivas variables para uso posterior.

Entonces llamamos a set_physics_process y pasamos false para que _physics_process no se ejecute. Hacemos esto porque el código en _physics_process comenzará a quemar el fusible y a explotará la bomba, lo que sólo queremos hacer cuando el usuario interactúe con la bomba. Si no desactivamos _physics_process, el fusible de la bomba se encenderá antes de que el usuario tenga la oportunidad de llegar a la bomba.

_physics_process explicación paso a paso de la función

La función _physics_process primero comprueba si el tiempo de fusión es menor que el tiempo de fusión. Si lo es, entonces el fusible de la bomba aún está ardiendo.

Si el fusible de la bomba aún está encendido, añadimos el tiempo, delta, a la variable fuse_timer. Luego comprobamos si el fuse_timer es mayor o igual que el FUSE_TIME ahora que le hemos añadido delta. Si fuse_timer es más o igual a FUSE_TIME, entonces el fusible acaba de terminar y necesitamos explotar la bomba.

Para explotar la bomba, primero dejamos de emitir partículas para el fusible poniendo emitting a false en fuse_particles. Luego le decimos al nodo de explosión Particles, explosion_particles, que emita todas sus partículas en un solo disparo, poniendo one_shot a true. Después de eso, ponemos emitting a true en explosion_particles para que parezca que la bomba ha explotado. Para ayudar a que parezca que la bomba ha explotado, ocultamos la bomba MeshInstance en el nodo bomb_mesh.visible a false.

Para evitar que la bomba colisione con otros objetos en el mundo de la física, fijamos las propiedades de collision_layer y collision_mask de la bomba en 0. También cambiamos el modo RigidBody a MODE_STATIC para que la bomba RigidBody no se mueva.

Entonces necesitamos conseguir todos los nodos PhysicsBody dentro del nodo explosion_area. Para hacer esto, usamos el get_overlapping_bodies en un bucle for. La función get_overlapping_bodies devolverá un array de nodos PhysicsBody dentro del nodo Area, que es exactamente lo que estamos buscando.


Para cada nodo PhysicsBody, que almacenamos en una variable llamada body, comprobamos si es igual a self. Lo hacemos para que la bomba no explote accidentalmente, ya que el explosion_area podría potencialmente detectar la Bomb RigidBody como un PhysicsBody dentro del área de explosión.

Si el nodo PhysicsBody, body, no es la bomba, entonces primero comprobamos si el nodo PhysicsBody tiene una función llamada damage. Si el nodo PhysicsBody tiene una función llamada damage, la llamamos y le pasamos EXPLOSION_DAMAGE para que reciba el daño de la explosión.

A continuación comprobamos si el nodo PhysicsBody es un RigidBody. Si body es un RigidBody, queremos moverlo cuando la bomba explote.

Para mover el nodo RigidBody cuando la bomba explote, primero tenemos que calcular la dirección de la bomba al nodo RigidBody. Para ello, restamos la posición global de la bomba, global_transform.origin de la posición global del nodo RigidBody. Esto nos dará un Vector3 que apunta desde la bomba al nodo RigidBody. Guardamos este Vector3 en una variable llamada direction_vector.

Entonces calculamos la distancia que el RigidBody está de la bomba usando la función length en direction_vector. Guardamos la distancia en una variable llamada bomb_distance.

Entonces calculamos la cantidad de fuerza que la bomba será aplicada al nodo RigidBody cuando la bomba explote dividiendo COLLISION_FORCE por bomb_distance, y multiplicando eso por collision_force. Esto hará que si el nodo RigidBody está más cerca de la bomba, será empujado más lejos.

Finalmente, empujamos el nodo RigidBody usando la función apply_impulse, con una posición Vector3 de cero y collision_force multiplicada por direction_vector.normalized como fuerza. Esto enviará el nodo RigidBody volando cuando la bomba explote.


Después de haber pasado por todos los nodos PhysicsBody dentro del explosion_area, ponemos la variable exploded a true para que el código sepa que la bomba ha explotado y llamamos a play en explosion_sound para que se reproduzca el sonido de una explosión.


Bien, la siguiente sección del código comienza por comprobar si exploded es igual a true.

Si exploded es igual a true, significa que la bomba está esperando que las partículas de la explosión terminen antes de liberarse/destruirse. Añadimos tiempo, delta, al explosion_timer para que podamos rastrear cuánto tiempo ha pasado desde que la bomba ha explotado.

Si el explosion_timer es mayor o igual que el EXPLOSION_TIME después de añadir delta, entonces el temporizador de explosión acaba de terminar.

Si el temporizador de explosión acaba de terminar, ponemos el explosion_area.monitoring en false. La razón por la que hacemos esto es porque había un error que imprimía un error cuando liberabas/borraste un nodo :ref:Area <class_Area>` cuando la propiedad ``monitoring era verdadera. Para asegurarnos de que esto no suceda, simplemente ponemos monitoring en falso en explosion_area.

A continuación comprobamos si la bomba está siendo sujetada por un controlador VR comprobando si la variable controller no es igual a null. Si la bomba está siendo sujetada por un controlador VR, ponemos la propiedad held_object del controlador VR, controller, a null. Debido a que el controlador de VR ya no sostiene nada, hacemos visible la malla de mano del controlador de VR estableciendo controller.hand_mesh.visible como true. Luego comprobamos si el modo de agarre del controlador de VR es RAYCAST, y si es así, ponemos controller.grab_raycast.visible en true para que la 'laser sight' para el raycast de agarre sea visible.

Finalmente, independientemente de si la bomba está siendo sujetada por un controlador VR o no, llamamos a queue_free para que la escena de la bomba sea liberada/eliminada de la escena.

interact explicación paso a paso de la función

Primero la función interactúa llama a set_physics_process y pasa true para que el código en physics_process empiece a ejecutarse. Esto iniciará el fusible de la bomba y eventualmente llevará a la explosión de la bomba.

Finalmente, empezamos las partículas del fusible poniendo fuse_particles.visible en true.

Bomba terminada

¡Ahora la bomba está lista para funcionar! Puedes encontrar las bombas en el edificio naranja.

Debido a la forma en que calculamos la velocidad del controlador de RV, es más fácil lanzar las bombas utilizando un movimiento de empuje en lugar de un movimiento de lanzamiento más natural. La curva suave de un movimiento de lanzamiento es más difícil de seguir con el código que estamos usando para calcular la velocidad de los controladores de RV, por lo que no siempre funciona correctamente y puede llevar a velocidades calculadas de forma inexacta.

Añadir una espada

Añadamos un último nodo especial RigidBody basado en el nodo que puede destruir objetivos. ¡Añadamos una espada para poder cortar los objetivos!

Abre el archivo Sword.tscn, que puedes encontrar en la carpeta Scenes.

No hay mucho que hacer aquí. Todos los nodos hijos del nodo raíz Sword RigidBody se giran hasta que se posicionan correctamente cuando el controlador VR los recoge, hay un :ref: MeshInstance <class_MeshInstance> nodo para mostrar la espada, y hay un nodo AudioStreamPlayer3D que contiene un sonido para la espada que colisionando con algo.

Sin embargo, hay una cosa que es ligeramente diferente. Hay un nodo KinematicBody llamado Damage_Body. Si le echas un vistazo, verás que no está en ninguna capa de colisión, y en su lugar sólo está en una única máscara de colisión. Esto es así porque el KinematicBody no afectará a otros nodos PhysicsBody de la escena, pero aún así se verá afectado por los nodos PhysicsBody.

Vamos a usar el nodo Damage_Body KinematicBody para detectar el punto de colisión y normal cuando la espada colisiona con algo en la escena.

Truco

Aunque quizás no sea la mejor manera de obtener la información de la colisión desde el punto de vista del rendimiento, ¡nos da mucha información que podemos usar para el post-procesamiento! Usando un KinematicBody de esta manera podemos detectar exactamente donde la espada colisionó con otros nodos PhysicsBody.

Esa es realmente la única nota digna de mención en la escena de la espada. Selecciona el nodo Sword RigidBody <class_RigidBody>`y haz un nuevo script llamado ``Sword.gd`. Añade el siguiente código:

extends VR_Interactable_Rigidbody

const SWORD_DAMAGE = 2

const COLLISION_FORCE = 0.15

var damage_body = null


func _ready():
    damage_body = get_node("Damage_Body")
    damage_body.add_collision_exception_with(self)
    sword_noise = get_node("AudioStreamPlayer3D")


func _physics_process(_delta):

    var collision_results = damage_body.move_and_collide(Vector3.ZERO, true, true, true);

    if (collision_results != null):
        if collision_results.collider.has_method("damage"):
            collision_results.collider.damage(SWORD_DAMAGE)

        if collision_results.collider is RigidBody:
            if controller == null:
                collision_results.collider.apply_impulse(
                    collision_results.position,
                    collision_results.normal * linear_velocity * COLLISION_FORCE)
            else:
                collision_results.collider.apply_impulse(
                    collision_results.position,
                    collision_results.normal * controller.controller_velocity * COLLISION_FORCE)

        sword_noise.play()

Repasemos cómo funciona este sctipt:

Explicando el código de la espada

Como con los otros nodos especiales RigidBody, la espada extiende VR_Interactable_Rigidbody para que los controladores VR sepan que se puede interactuar con este objeto y que las funciones definidas en VR_Interactable_Rigidbody pueden ser llamadas cuando este objeto es sujetado por un controlador VR.

A continuación, veamos las variables de la clase:

  • SWORD_DAMAGE: Una constante para definir la cantidad de daño que hace la espada. Este daño se aplica a cada objeto de la espada en cada llamada a _physics_process

  • COLLISION_FORCE: Una constante que define la cantidad de fuerza aplicada a los nodos RigidBody cuando la espada colisiona con un PhysicsBody.

  • damage_body: Una variable para mantener el nodo KinematicBody usado para detectar si la espada está apuñalando un nodo PhysicsBody o no.

  • sword_noise: Una variable para mantener el nodo AudioStreamPlayer3D usado para reproducir un sonido cuando la espada colisiona con algo.

``_ready``explicación paso a paso de la función

Todo lo que hacemos en la función _ready es obtener el nodo Damage_Body KinematicBody y asignarlo a damage_body. Como no queremos que la espada detecte una colisión con el nodo raíz RigidBody de la espada, llamamos a add_collision_exception_with en damage_body y se pasa self para que la espada no sea detectada.

Finalmente, obtenemos el nodo AudioStreamPlayer3D para el sonido de la colisión de la espada y lo aplicamos a la variable sword_noise.

_physics_process explicación paso a paso de la función

Primero tenemos que determinar si la espada está colisionando con algo o no. Para ello, usamos la función move_and_collide del nodo damage_body. A diferencia de cómo se usa normalmente move_and_collide, no pasamos una velocidad y en su lugar pasamos un Vector3 vacío. Como no queremos que el nodo damage_body se mueva, establecemos el argumento test_only (el cuarto argumento) como true de modo que el KinematicBody genera la información de la colisión sin causar ninguna colisión dentro del mundo de la colisión.

La función move_and_collide retorna un objeto de la clase KinematicCollision que contiene la información que necesitamos para detectar colisiones con la espada. Asignaremos el valor de retorno de move_and_collide a una variable llamada collision_results.

A continuación comprobamos si los collision_results no son iguales a null. Si collision_results no es igual a null, entonces sabemos que la espada ha colisionado con algo.

Entonces comprobamos si el :ref:PhysicsBody <class_PhysicsBody>` con el que colisionó la espada tiene una función/método llamado ``damage usando la función has_method. Si PhysicsBody tiene una función llamada damage_body, la llamamos y le pasamos la cantidad de daño que hace la espada, SWORD_DAMAGE.

A continuación comprobamos si el PhysicsBody con el que chocó la espada es un RigidBody. Si con lo que la espada colisionó es un nodo RigidBody, entonces comprobamos si la espada está siendo sostenida por un controlador VR o no comprobando si el controller es igual a null.

Si la espada no está siendo sujetada por un controlador VR, controller es igual a null, entonces movemos el nodo RigidBody <class_RigidBody>`con el que colisionó la espada usando la función ``apply_impulse`. Para la position de la función apply_impulse, usamos la variable collision_position almacenada dentro de la clase KinematicCollision en collision_results. Para la velocidad de la función apply_impulse, usamos la colisión normal multiplicada por la velocidad lineal de la espada RigidBody multiplicado por el nodo COLLISION_FORCE.

Si la espada no está siendo sujetada por un controlador VR, controller es igual a null, entonces movemos el nodo RigidBody <class_RigidBody>`con el que colisionó la espada usando la función ``apply_impulse`. Para la position de la función apply_impulse, usamos la variable collision_position almacenada dentro de la clase KinematicCollision en collision_results. Para la velocidad de la función apply_impulse, usamos la colisión normal multiplicada por la velocidad lineal de la espada RigidBody multiplicado por el nodo COLLISION_FORCE.

Finalmente, sin importar si el PhysicsBody es un RigidBody o no, tocamos el sonido de la espada colisionando con algo llamando a play en sword_noise.

Espada terminada

../../../_images/starter_vr_tutorial_sword.png

¡Con eso hecho, ahora puedes cortar los objetivos! Puedes encontrar la espada en la esquina entre la escopeta y la pistola.

Actualización de la UI de destino

Actualicemos la UI cuando los objetivos de la esfera sean destruidos.

Abre el Main_VR_GUI.tscn, que puedes encontrar en la carpeta Scenes. Si quieres, puedes ver cómo se configura la escena, pero en un esfuerzo por evitar que este tutorial se alargue demasiado, no vamos a cubrir la configuración de la escena en este tutorial.

Expande el nodo GUI Viewport y luego selecciona el nodo Base_Control. Añade un nuevo script llamado Base_Control.gd, y añade lo siguiente:

extends Control

var sphere_count_label

func _ready():
    sphere_count_label = get_node("Label_Sphere_Count")

    get_tree().root.get_node("Game").sphere_ui = self


func update_ui(sphere_count):
    if sphere_count > 0:
        sphere_count_label.text = str(sphere_count) + " Spheres remaining"
    else:
        sphere_count_label.text = "No spheres remaining! Good job!"

Veamos rápidamente cómo funciona este script.

Primero, en _ready, obtenemos la Label que muestra cuántas esferas quedan y las asignamos a la variable de clase sphere_count_label. A continuación, obtenemos Game.gd usando get_tree().root y asignamos sphere_ui a este script.

En update_ui, cambiamos el texto de la esfera Label. Si queda al menos una esfera, cambiamos el texto para mostrar cuántas esferas quedan en el mundo. Si no quedan más esferas, cambiamos el texto y felicitamos al jugador.

Añadiendo el final especial de RigidBody

Por último, antes de terminar este tutorial, añadamos una forma de reiniciar el juego mientras está en VR.

Abre Reset_Box.tscn, que encontrarás en Scenes. Selecciona el nodo Reset_Box RigidBody y haz un nuevo script llamado Reset_Box.gd. Añade el siguiente código:

extends VR_Interactable_Rigidbody

var start_transform

var reset_timer = 0
const RESET_TIME = 10
const RESET_MIN_DISTANCE = 1


func _ready():
    start_transform = global_transform


func _physics_process(delta):
    if start_transform.origin.distance_to(global_transform.origin) >= RESET_MIN_DISTANCE:
        reset_timer += delta
        if reset_timer >= RESET_TIME:
            global_transform = start_transform
            reset_timer = 0


func interact():
    # (Ignore the unused variable warning)
    # warning-ignore:return_value_discarded
    get_tree().change_scene("res://Game.tscn")


func dropped():
    global_transform = start_transform
    reset_timer = 0

Veamos rápidamente cómo funciona este script.

Explicando el código de la caja de reinicio

Como con los otros objetos especiales RigidBody basados que hemos creado, el reset box se extiende VR_Interactable_Rigidbody.

La variable de clase start_transform almacenará la transformación global del reset box cuando comience el juego, la variable de clase reset_timer mantendrá el tiempo que ha pasado desde que se ha movido la posición del reset box, la constante RESET_TIME define el tiempo que el reset box tiene que esperar antes de ser reiniciado, y la constante RESET_MIN_DISTANCE define cuán lejos tiene que estar el reset box de su posición inicial antes de que comience el temporizador de reinicio.

En la función _ready todo lo que hacemos es almacenar la global_transform de la posición de reinicio cuando comienza la escena. Esto es así para que podamos reajustar la posición, rotación y escala del objeto del cuadro de restablecimiento a esta transformación inicial cuando haya pasado suficiente tiempo.

En la función _physics_process, el código comprueba si la posición inicial del reset box a la posición actual del cuadro de restablecimiento está más lejos que RESET_MIN_DISTANCE. Si está más lejos, entonces comienza a agregar tiempo, delta, a reset_timer. Una vez que reset_timer es más o igual a RESET_TIME, reajustamos la global_transform a la start_transform para que la reset box vuelva a su posición inicial. Luego ponemos reset_timer a 0.

La función interact simplemente recarga la escena Game.tscn usando get_tree().change_scene. Esto recargará la escena del juego, reiniciando todo.

Finalmente, la función dropped restablece la global_transform a la transformación inicial en start_transform para que la reset box tenga su posición/rotación inicial. Entonces reset_timer se pone en 0 para que el temporizador se reinicie.

Reset box terminada

Una vez hecho esto, cuando agarres e interactúes con el cuadro de reinicio, toda la escena se restablecerá/reiniciará y podrás destruir todos los objetivos de nuevo!

Nota

Reajustar la escena de forma abrupta sin ningún tipo de transición puede llevar a la incomodidad en la RV.

Notas finales

../../../_images/starter_vr_tutorial_pistol.png

¡Wow! Eso fue un montón de trabajo.

Ahora tienes un proyecto de RV completamente funcional con múltiples tipos diferentes de nodos especiales RigidBody-basados que pueden ser usados y extendidos. Esperemos que esto ayude a servir como una introducción a la creación de juegos de RV con todas las características en Godot! El código y los conceptos detallados en este tutorial pueden ser ampliados para hacer juegos de puzzle, juegos de acción, juegos basados en historia y ¡más!

Advertencia

¡Puedes descargar el proyecto terminado para esta serie de tutoriales en el repositorio OpenVR GitHub, bajo la pestaña de releases!