Tutorial VR para principiantes parte 2¶
Introducción¶

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
You can find the finished project on the OpenVR GitHub repository.
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 delGame
, hay10
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¶

¡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 explotefuse_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 propiedadlifetime
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 script!
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
: A constant to define the amount of damage the sword does. This damage is applied to every object in the sword on every_physics_process
callCOLLISION_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¶

¡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¶

¡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
You can download the finished project for this tutorial series on the OpenVR GitHub repository, under the releases tab!