Tutorial VR para principiantes parte 1

Introducción

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

Este tutorial te mostrará cómo crear un proyecto de juego VR básico en Godot.

Tenga en cuenta, una de las cosas más importantes al hacer el contenido de la RV es conseguir la escala de tus recursos correctamente! Puede requerir mucha práctica e iteraciones para hacerlo bien, pero hay algunas cosas que puedes hacer para hacerlo más fácil:

  • En la RV, una unidad se considera típicamente 1 metro. Si diseñas tus activos en torno a ese estándar, puedes ahorrarte muchos dolores de cabeza.

  • En tu programa de modelado 3D, vea si hay una forma de medir y usar las distancias del mundo real. En Blender, puedes usar el complemento MeasureIt; en Maya, puedes usar la Herramienta de Medición.

  • Puedes hacer modelos aproximados usando una herramienta como "Bloques de Google" <https://vr.google.com/blocks/>`_, y luego refinarlos en otro programa de modelado 3D.

  • ¡Pruebe a menudo, ya que los recursos pueden verse dramáticamente diferentes en la RV que en una pantalla plana!

A lo largo del curso de este tutorial, cubriremos:

  • Cómo decirle a Godot que corra en RV.

  • Cómo hacer un sistema de locomoción de teletransportación que utilice los controladores VR.

  • Cómo hacer un sistema de locomoción de movimiento artificial que utiliza los controladores VR.

  • Cómo hacer un sistema basado en RigidBody que permita agarrar, soltar y arrojar nodos RigidBody usando controladores VR.

  • Cómo crear objetos destruibles simples.

  • Cómo hacer un sistema basado en objetos :ref:`RigidBody <class_RigidBody>`que puedan destruir blancos.

Truco

Aunque este tutorial puede ser realizado por principiantes, es altamente aconsejable completar primero Tu primer juego si eres nuevo en Godot y/o en el desarrollo de juegos.

Se requiere cierta experiencia en la creación de juegos 3D antes de pasar por esta serie de tutoriales. Este tutorial asume que tienes experiencia con el editor de Godot, GDScript, y el desarrollo básico de juegos 3D. Se requieren unos auriculares listos para OpenVR y dos controladores listos para OpenVR.

Este tutorial fue escrito y probado usando un auricular y controladores de Realidad Mixta de Windows. Este proyecto también ha sido probado en el HTC Vive. Es posible que se requieran ajustes de código para otros auriculares de RV, como el Oculus Rift.

El proyecto Godot para este tutorial se encuentra en el repositorio OpenVR GitHub. Los activos iniciales de este tutorial se encuentran en la sección de publicaciones del repositorio GitHub. Los recursos de inicio contienen algunos modelos 3D, sonidos, scripts y escenas que están configurados para este tutorial.

Nota

Créditos por los recursos proporcionados:

  • El panorama del cielo fue creado por CGTuts.

  • La fuente utilizada es Titillium-Regular

    • La fuente está licenciada bajo la SIL Open Font License, Version 1.1

  • El audio usado es de varias fuentes diferentes, todas descargadas del paquete Sonniss #GameAudioGDC (Licencia PDF)

    • Las carpetas donde se almacenan los archivos de audio tienen el mismo nombre que las carpetas del paquete de audio de Sonniss.

  • El addon OpenVR fue creado por Bastiaan Olij y está liberado bajo la licencia del MIT. Se puede encontrar tanto en la Biblioteca de Bienes Godot como en GitHub. El código del tercer partido y las librerías usadas en el addon OpenVR pueden estar bajo una licencia diferente.

  • El proyecto inicial, los modelos 3D y los scripts fueron creados por TwistedTwigleg y se publica bajo la licencia del MIT.

Truco

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

Preparando todo

Si aún no lo has hecho, ve al repositorio OpenVR GitHub y descarga el archivo "Starter Assets" de los lanzamientos. Una vez que hayas descargado los activos iniciales, abre el proyecto en Godot.

Nota

Los recursos ofrecidos no son requeridos para usar los scripts provistos en este tutorial. Estos recursos incluyen escenas prefabricadas y scripts que se usarán a lo largo del tutorial.

Cuando el proyecto se cargue por primera vez, se abrirá la escena de Game.tscn. Esta será la escena principal usada para el tutorial. Incluye varios nodos y escenas ya colocadas a lo largo de la escena, algo de música de fondo, y varios nodos relacionados con la interfaz gráfica MeshInstance.


Los nodos relacionados con el GUI MeshInstance ya tienen scripts adjuntos. Estos scripts fijarán la textura de un nodo Viewport a la textura del albedo del material del nodo MeshInstance. Esto se utiliza para mostrar el texto dentro del proyecto VR. Siéntete libre de echar un vistazo al script, GUI.gd, si quieres. No vamos a repasar cómo usar los nodos Viewport para mostrar la interfaz de usuario en los nodos MeshInstance de este tutorial.

Si le interesa cómo usar los nodos Viewport para mostrar la interfaz de usuario en los nodos MeshInstance, vea el tutorial Usar un Viewport como textura. Cubre cómo usar un Viewport como una textura de renderizado, junto con cómo aplicar esa textura en un nodo MeshInstance.


Antes de comenzar con el tutorial, nos tomaremos un momento paraver cómo funcionan los nodos utilizados para VR.

El nodo ARVROrigin es el punto central del sistema de seguimiento de la RV. La posición del nodo ARVROrigin es la posición que el sistema de RV considera como el punto 'central' en el suelo. El ARVROrigin tiene una propiedad de escala mundial que afecta al tamaño del usuario dentro de la escena de RV. Para este tutorial, está configurado en 1.4, ya que el mundo era originalmente un poco más grande. Como se mencionó anteriormente, mantener la escala relativamente consistente es importante en la RV.

El ARVRCamera es el auricular del jugador y la vista en la escena. El ARVRCamera está desplazado en el eje Y por la altura del usuario del VR, lo que será importante más tarde cuando añadamos la locomoción de teletransportación. Si el sistema de RV soporta el rastreo de habitaciones, entonces el ARVRCamera se moverá a medida que el jugador se mueva. Esto significa que el ARVRCamera no está garantizado que esté en la misma posición que el nodo ARVROrigin.

El nodo ARVRController representa un controlador de RV. El ARVRController seguirá la posición y rotación del controlador VR relativo al nodo ARVROrigin. Toda la entrada de los controladores VR pasa por el nodo ARVRController. Un nodo ARVRController con un ID de 1 representa el controlador VR izquierdo, mientras que un ARVRController controlador con un ID de 2 representa el controlador VR derecho.

Para resumir:

  • El nodo ARVROrigin es el centro del sistema de seguimiento de la RV y está situado en el suelo.

  • El ARVRCamera es el auricular de RV del jugador y la vista en la escena.

  • El nodo ARVRCamera está desplazado en el eje Y por la altura del usuario.

  • Si el sistema de RV soporta el rastreo de habitaciones, entonces el nodo ARVRCamera puede ser desplazado en los ejes X y Z a medida que el jugador se mueve.

  • Los nodos ARVRController representan los controladores de RV y manejan todas las entradas de los controladores de RV.

Iniciando con la VR

Ahora que hemos repasado los nodos VR, empecemos a trabajar en el proyecto. Mientras que en Game.tscn, selecciona el nodo Game y haz un nuevo script llamado Game.gd. En el archivo Game.gd, añade el siguiente código:

extends Spatial

func _ready():
    var VR = ARVRServer.find_interface("OpenVR")
    if VR and VR.initialize():
        get_viewport().arvr = true

        OS.vsync_enabled = false
        Engine.target_fps = 90
        # Also, the physics FPS in the project settings is also 90 FPS. This makes the physics
        # run at the same frame rate as the display, which makes things look smoother in VR!
using Godot;
using System;

public class Game : Spatial
{
    public override void _Ready()
    {
        var vr = ARVRServer.FindInterface("OpenVR");
        if (vr != null && vr.Initialize())
        {
            GetViewport().Arvr = true;

            OS.VsyncEnabled = false;
            Engine.TargetFps = 90;
            // Also, the physics FPS in the project settings is also 90 FPS. This makes the physics
            // run at the same frame rate as the display, which makes things look smoother in VR!
        }
    }
}

Veamos lo que hace este código.


En la función _ready, primero obtenemos la interfaz OpenVR VR usando la función find_interface en el ARVRServer y la asignamos a una variable llamada VR`. Si ARVRServer encuentra una interfaz con el nombre OpenVR, la devolverá, de lo contrario devolverá null.

Nota

La interfaz OpenVR VR no está incluida con Godot por defecto. Necesitarás descargar el activo OpenVR de la Biblioteca de recursos o GitHub.

El código combina entonces dos condicionales, uno para comprobar si la variable VR NO es nula (if VR) y otro llama a la función de inicialización, que devuelve un booleano basado en si la interfaz OpenVR fue capaz de inicializar o no. Si ambos condicionales devuelven true, entonces podemos convertir el Godot principal Viewport en un viewport ARVR.

Si la interfaz de RV se inicializa con éxito, entonces obtenemos la raíz Viewport y ponemos la propiedad arvr como true. Esto le dirá a Godot que use la interfaz ARVR inicializada para manejar la pantalla Viewport.

Finalmente, desactivamos VSync para que el monitor de la computadora no limite los fotogramas por segundo (FPS). Después de esto le decimos a Godot que renderice a 90 fotogramas por segundo, que es el estándar para la mayoría de los auriculares VR. Sin desactivar el VSync, el monitor normal de la computadora puede limitar la velocidad de fotogramas del auricular VR a la velocidad de fotogramas del monitor de la computadora.

Nota

En la configuración del proyecto, en la pestaña Physics->Common, el FPS de física se ha establecido en "90". Esto hace que el motor de física funcione a la misma velocidad de fotograma que la pantalla de RV, lo que hace que las reacciones físicas se vean más suaves cuando se está en RV.


¡Eso es todo lo que tenemos que hacer para que Godot lance OpenVR dentro del proyecto! Adelante, inténtalo si quieres. Asumiendo que todo funcione, podrás mirar alrededor del mundo. Si tienes unos auriculares de RV con rastreo de habitaciones, entonces podrás moverte por la escena dentro de los límites del rastreo de habitaciones.

Creando los controladores

../../../_images/starter_vr_tutorial_hands.png

Ahora mismo todo lo que el usuario de RV puede hacer es quedarse parado, lo cual no es realmente lo que buscamos a menos que estemos trabajando en una película de RV. Escribamos el código para los controladores de RV. Vamos a escribir todo el código para los controladores de RV de una sola vez, por lo que el código es bastante largo. Dicho esto, una vez que hayamos terminado, podrás teletransportarte por la escena, moverte artificialmente usando el touchpad/joystick del controlador VR, y ser capaz de recoger, soltar y lanzar RigidBody nodos basados en la RV.

Primero necesitamos abrir la escena usada para los controladores VR. Left_Controller.tscn o Right_Controller.tscn. Repasemos brevemente cómo se configura la escena.

Cómo se configura la escena del controlador VR

En ambas escenas el nodo raíz es un nodo ARVRController. La única diferencia es que la escena Left_Controller tiene la propiedad Controller Id puesta a 1 mientras que la Right_Controller tiene la propiedad Controller Id puesta a 2.

Nota

El ARVRServer intenta usar estas dos identificaciones para los controladores VR izquierdo y derecho. Para los sistemas de RV que soportan más de 2 controladores/objetos rastreados, estos IDs pueden necesitar un ajuste.

El siguiente es el nodo Hand MeshInstance. Este nodo se usa para mostrar la malla de mano que se usará cuando el controlador de VR no esté sosteniendo un nodo RigidBody. La mano en la escena Left_Controller es una mano izquierda, mientras que la mano en la escena Right_Controller es una mano derecha.

El nodo llamado Raycast es un nodo Raycast que se usa para apuntar a dónde teletransportarse cuando el controlador VR se teletransporta. La longitud del Raycast se establece en -16 en el eje Y y se gira para que apunte con el dedo índice de la mano. El nodo Raycast tiene un único nodo hijo, Mesh, que es un "ref:MeshInstance <class_MeshInstance>. Esto se usa para mostrar visualmente a dónde apunta la teletransportación Raycast.

El nodo llamado Area es un nodo Area que se usará para agarrar RigidBody nodos basados en el modo de agarre del controlador VR cuando el modo de agarre del controlador VR esté configurado en AREA. El nodo Area tiene un nodo hijo único, CollisionShape, que define una esfera CollisionShape. Cuando el controlador de VR no tiene ningún objeto y se pulsa el botón de agarre, el primer nodo basado en RigidBody dentro del nodo Area será recogido.

Lo siguiente es un nodo Position3D llamado Grab_Pos. Esto se usa para definir la posición que seguirán los nodos RigidBody que son mantenidos por el controlador VR.

Un gran nodo Area llamado SleepArea se usa para deshabilitar el dormir para cualquier nodo de RigidBody dentro de su ColisionShape, simple llamado ColisionShape. Esto es necesario porque si un nodo RigidBody se duerme, entonces el controlador de VR será incapaz de agarrarlo. Usando Sleep_Area, podemos escribir código que haga que cualquier nodo RigidBody dentro de él no pueda dormir, permitiendo así que el controlador de VR lo agarre.

Un nodo AudioStreamPlayer3D llamado AudioStreamPlayer3D tiene cargado un sonido que usaremos cuando un objeto haya sido recogido, dejado o lanzado por el controlador VR. Aunque esto no es necesario para la funcionalidad del controlador de VR, hace que el agarrar y soltar objetos se sienta más natural.

Finalmente, los últimos nodos son el nodo Grab_Cast y el nodo hijo único, Mesh. El nodo Grab_Cast se usará para los nodos basados en agarrado RigidBody cuando el modo agarrar del controlador VR esté en RAYCAST. Esto permitirá al controlador VR agarrar objetos que están ligeramente fuera de alcance usando un Raycast. El nodo Mesh se utiliza para mostrar visualmente a dónde apunta la teletransportación Raycast.

Este es un rápido resumen de cómo se configuran las escenas del controlador VR, y cómo usaremos los nodos para proporcionarles la funcionalidad. Ahora que hemos visto la escena del controlador VR, vamos a escribir el código que los impulsará.

El código para los controles VR

Selecciona el nodo raíz de la escena, ya sea Right_Controller o Left_Controller, y haz un nuevo script llamado VR_Controller.gd. Ambas escenas usarán el mismo script, así que no importa cuál uses primero. Con VR_Controller.gd abierto, añade el siguiente código:

Truco

Puedes copiar y pegar el código de esta página directamente en el editor de scripts.

Si haces esto, todo el código copiado estará usando espacios en lugar de tabulaciones.

Para convertir los espacios en tabulaciones en el editor de scripts, haz clic en el menú Edit y selecciona Convert Indent To Tabs. Esto convertirá todos los espacios en pestañas. Puedes seleccionar Convert Indent To Spaces para convertir las pestañas de nuevo en espacios.

extends ARVRController

var controller_velocity = Vector3(0,0,0)
var prior_controller_position = Vector3(0,0,0)
var prior_controller_velocities = []

var held_object = null
var held_object_data = {"mode":RigidBody.MODE_RIGID, "layer":1, "mask":1}

var grab_area
var grab_raycast

var grab_mode = "AREA"
var grab_pos_node

var hand_mesh
var hand_pickup_drop_sound

var teleport_pos = Vector3.ZERO
var teleport_mesh
var teleport_button_down
var teleport_raycast

# A constant to define the dead zone for both the trackpad and the joystick.
# See https://web.archive.org/web/20191208161810/http://www.third-helix.com/2013/04/12/doing-thumbstick-dead-zones-right.html
# for more information on what dead zones are, and how we are using them in this project.
const CONTROLLER_DEADZONE = 0.65

const MOVEMENT_SPEED = 1.5

const CONTROLLER_RUMBLE_FADE_SPEED = 2.0

var directional_movement = false


func _ready():
    # Ignore the warnings the from the connect function calls.
    # (We will not need the returned values for this tutorial)
    # warning-ignore-all:return_value_discarded

    teleport_raycast = get_node("RayCast")

    teleport_mesh = get_tree().root.get_node("Game/Teleport_Mesh")

    teleport_button_down = false
    teleport_mesh.visible = false
    teleport_raycast.visible = false

    grab_area = get_node("Area")
    grab_raycast = get_node("Grab_Cast")
    grab_pos_node = get_node("Grab_Pos")

    grab_mode = "AREA"
    grab_raycast.visible = false

    get_node("Sleep_Area").connect("body_entered", self, "sleep_area_entered")
    get_node("Sleep_Area").connect("body_exited", self, "sleep_area_exited")

    hand_mesh = get_node("Hand")
    hand_pickup_drop_sound = get_node("AudioStreamPlayer3D")

    connect("button_pressed", self, "button_pressed")
    connect("button_release", self, "button_released")


func _physics_process(delta):
    if rumble > 0:
        rumble -= delta * CONTROLLER_RUMBLE_FADE_SPEED
        if rumble < 0:
            rumble = 0

    if teleport_button_down == true:
        teleport_raycast.force_raycast_update()
        if teleport_raycast.is_colliding():
            if teleport_raycast.get_collider() is StaticBody:
                if teleport_raycast.get_collision_normal().y >= 0.85:
                    teleport_pos = teleport_raycast.get_collision_point()
                    teleport_mesh.global_transform.origin = teleport_pos


    if get_is_active() == true:
        _physics_process_update_controller_velocity(delta)

    if held_object != null:
        var held_scale = held_object.scale
        held_object.global_transform = grab_pos_node.global_transform
        held_object.scale = held_scale

    _physics_process_directional_movement(delta);


func _physics_process_update_controller_velocity(delta):
    controller_velocity = Vector3(0,0,0)

    if prior_controller_velocities.size() > 0:
        for vel in prior_controller_velocities:
            controller_velocity += vel

        controller_velocity = controller_velocity / prior_controller_velocities.size()

    var relative_controller_position = (global_transform.origin - prior_controller_position)

    controller_velocity += relative_controller_position

    prior_controller_velocities.append(relative_controller_position)

    prior_controller_position = global_transform.origin

    controller_velocity /= delta;

    if prior_controller_velocities.size() > 30:
        prior_controller_velocities.remove(0)


func _physics_process_directional_movement(delta):
    var trackpad_vector = Vector2(-get_joystick_axis(1), get_joystick_axis(0))
    var joystick_vector = Vector2(-get_joystick_axis(5), get_joystick_axis(4))

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

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

    var forward_direction = get_parent().get_node("Player_Camera").global_transform.basis.z.normalized()
    var right_direction = get_parent().get_node("Player_Camera").global_transform.basis.x.normalized()

    # Because the trackpad and the joystick will both move the player, we can add them together and normalize
    # the result, giving the combined movement direction
    var movement_vector = (trackpad_vector + joystick_vector).normalized()

    var movement_forward = forward_direction * movement_vector.x * delta * MOVEMENT_SPEED
    var movement_right = right_direction * movement_vector.y * delta * MOVEMENT_SPEED

    movement_forward.y = 0
    movement_right.y = 0

    if (movement_right.length() > 0 or movement_forward.length() > 0):
        get_parent().global_translate(movement_right + movement_forward)
        directional_movement = true
    else:
        directional_movement = false


func button_pressed(button_index):
    if button_index == 15:
        _on_button_pressed_trigger()

    if button_index == 2:
        _on_button_pressed_grab()

    if button_index == 1:
        _on_button_pressed_menu()


func _on_button_pressed_trigger():
    if held_object == null:
        if teleport_mesh.visible == false:
            teleport_button_down = true
            teleport_mesh.visible = true
            teleport_raycast.visible = true
    else:
        if held_object is VR_Interactable_Rigidbody:
            held_object.interact()


func _on_button_pressed_grab():
    if teleport_button_down == true:
        return

    if held_object == null:
        _pickup_rigidbody()
    else:
        _throw_rigidbody()

    hand_pickup_drop_sound.play()


func _pickup_rigidbody():
    var rigid_body = null

    if grab_mode == "AREA":
        var bodies = grab_area.get_overlapping_bodies()
        if len(bodies) > 0:
            for body in bodies:
                if body is RigidBody:
                    if !("NO_PICKUP" in body):
                        rigid_body = body
                        break

    elif grab_mode == "RAYCAST":
        grab_raycast.force_raycast_update()
        if (grab_raycast.is_colliding()):
            var body = grab_raycast.get_collider()
            if body is RigidBody:
                if !("NO_PICKUP" in body):
                    rigid_body = body


    if rigid_body != null:

        held_object = rigid_body

        held_object_data["mode"] = held_object.mode
        held_object_data["layer"] = held_object.collision_layer
        held_object_data["mask"] = held_object.collision_mask

        held_object.mode = RigidBody.MODE_STATIC
        held_object.collision_layer = 0
        held_object.collision_mask = 0

        hand_mesh.visible = false
        grab_raycast.visible = false

        if held_object is VR_Interactable_Rigidbody:
            held_object.controller = self
            held_object.picked_up()


func _throw_rigidbody():
    if held_object == null:
        return

    held_object.mode = held_object_data["mode"]
    held_object.collision_layer = held_object_data["layer"]
    held_object.collision_mask = held_object_data["mask"]

    held_object.apply_impulse(Vector3(0, 0, 0), controller_velocity)

    if held_object is VR_Interactable_Rigidbody:
        held_object.dropped()
        held_object.controller = null

    held_object = null
    hand_mesh.visible = true

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


func _on_button_pressed_menu():
    if grab_mode == "AREA":
        grab_mode = "RAYCAST"
        if held_object == null:
            grab_raycast.visible = true

    elif grab_mode == "RAYCAST":
        grab_mode = "AREA"
        grab_raycast.visible = false


func button_released(button_index):
    if button_index == 15:
        _on_button_released_trigger()


func _on_button_released_trigger():
    if teleport_button_down == true:

        if teleport_pos != null and teleport_mesh.visible == true:
            var camera_offset = get_parent().get_node("Player_Camera").global_transform.origin - get_parent().global_transform.origin
            camera_offset.y = 0

            get_parent().global_transform.origin = teleport_pos - camera_offset

        teleport_button_down = false
        teleport_mesh.visible = false
        teleport_raycast.visible = false
        teleport_pos = null


func sleep_area_entered(body):
    if "can_sleep" in body:
        body.can_sleep = false
        body.sleeping = false


func sleep_area_exited(body):
    if "can_sleep" in body:
        # Allow the CollisionBody to sleep by setting the "can_sleep" variable to true
        body.can_sleep = true

Esto es un poco de código para revisar. Repasemos lo que el código hace paso a paso.

Explicando el código de controles VR

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

  • controller_velocity: Una variable que contiene una aproximación aproximada de la velocidad del controlador VR.

  • prior_controller_position: Una variable para mantener la última posición en el espacio del controlador VR.

  • prior_controller_velocities: Un array para mantener las últimas 30 velocidades calculadas del controlador VR. Esto se utiliza para suavizar los cálculos de velocidad a lo largo del tiempo.

  • held_object: Una variable para mantener una referencia al objeto que el controlador VR está sosteniendo. Si el controlador VR no está sosteniendo ningún objeto, esta variable será null.

  • held_object_data: Un diccionario para guardar los datos del nodo RigidBody que está siendo guardado por el controlador de VR. Se utiliza para restablecer los datos de RigidBody cuando ya no están mantenidos.

  • grab_area: Una variable que contiene el nodo Area usado para agarrar objetos con el controlador VR.

  • grab_raycast: Una variable que contiene el nodo Raycast usado para agarrar objetos con el controlador VR.

  • grab_mode: Una variable para definir el modo de agarre que el controlador VR está usando. Sólo hay dos modos de agarrar objetos en este tutorial, AREA y RAYCAST.

  • "Agarrar el nodo de la posición": Una variable para mantener el nodo que se usará para actualizar la posición y rotación de los objetos mantenidos.

  • hand_mesh: Una variable para mantener el nodo MeshInstance que contiene la malla de la mano para el controlador VR. Esta malla se mostrará cuando el controlador VR no sujete nada.

  • hand_pickup_drop_sound:Una variable para mantener el nodo AudioStreamPlayer3D que contiene el sonido pickup/drop.

  • teleport_pos: Una variable para mantener la posición a la que se teletransportará el jugador cuando el controlador VR lo haga.

  • teleport_mesh: Una variable que contiene el nodo MeshInstance usado para mostrar dónde se está teletransportando el jugador.

  • teleport_button_down:Una variable usada para rastrear si el botón de teleportación del controlador se mantiene pulsado. Esto se utilizará para detectar si este controlador VR está tratando de teleportar al jugador.

  • teleport_raycast:Una variable para mantener el nodo Raycast usado para calcular la posición de teleportación. Este nodo también tiene un MeshInstance que actúa como una 'mira láser' para apuntar.

  • CONTROLLER_DEADZONE:Una constante para definir la zona muerta tanto para el trackpad como para el joystick del controlador VR. Vea la nota de abajo para más información.

  • MOVEMENT_SPEED: Una constante para definir la velocidad a la que se mueve el jugador cuando usa el trackpad/joystick para moverse artificialmente.

  • CONTROLLER_RUMBLE_FADE_SPEED: Una constante para definir la velocidad con la que se desvanece el temblor del controlador VR.

  • directional_movement: Una variable para saber si este controlador VR está moviendo el reproductor con el touchpad/joystick.

Nota

Puedes encontrar un gran artículo explicando todo acerca de cómo manejar las zonas muertas del touchpad/joystick "aquí". <https://web.archive.org/web/20191208161810/http://www.third-helix.com/2013/04/12/doing-thumbstick-dead-zones-right.html>`__.

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

Son bastantes variables de clase. La mayoría de ellas se utilizan para mantener referencias a los nodos que necesitaremos en todo el código. A continuación vamos a empezar a ver las funciones, empezando por la función _ready.


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

Primero le decimos a Godot que silencie las advertencias sobre no usar los valores devueltos por la función connect. No necesitaremos los valores devueltos para este tutorial.

A continuación obtenemos el nodo Raycast que vamos a usar para determinar la posición para el teletransporte y lo asignamos a la variable teleport_raycast. Entonces obtenemos el nodo MeshInstance que usaremos para mostrar a dónde se teletransportará el reproductor. El nodo que usamos para teletransportarnos es un hijo de la escena Game. Hacemos esto para que el nodo malla de teleportación no se vea afectado por los cambios en el controlador VR, y así la malla de teleportación puede ser utilizada por ambos controladores VR.

Entonces la variable teleport_button_down se establece como falsa, teleport_mesh.visible se establece como false, y teleport_raycast.visible se establece como false. Esto configura las variables para teleportar el reproductor a su estado inicial, no para teleportar el reproductor.

El código entonces obtiene el nodo grab_area, el nodo grab_raycast, y el nodo grab_pos_node y los asigna todos a sus respectivas variables para su uso posterior.

A continuación, el grab_mode se establece en AREA para que el controlador VR intente agarrar objetos usando el nodo Area definido en grab_area cuando el botón de agarre del controlador VR sea presionado. También ponemos la propiedad grab_raycast del nodo visible en false para que el nodo hijo de grab_raycast no sea visible.

Después de eso, conectamos las señales body_entered y body_exited del nodo Sleep_Area en el controlador VR con las funciones sleep_area_entered y sleep_area_exited. Las funciones sleep_area_entered y sleep_area_exited se usarán para hacer que los nodos :RigidBody <class_RigidBody> no puedan dormir cuando estén cerca del controlador VR.

Luego los nodos hand_mesh y hand_pickup_drop_sound se obtienen y asignan a sus respectivas variables para uso posterior.

Finalmente, las señales button_pressed y button_release del nodo ARVRController, que extiende el controlador VR, están conectadas a las funciones button_pressed y button_released respectivamente. Esto significa que cuando un botón del controlador VR es presionado o liberado, las funciones button_pressed o button_released definidas en este script serán llamadas.

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

Primero comprobamos si la variable rumble es mayor que cero. Si la variable rumble, que es una propiedad del nodo ARVRController, es más que cero, entonces el controlador VR retumba.

Si la variable rumble es más que cero, entonces reducimos el rumble por "CONTROLLER_RUMBLE_FADE_SPEED" cada segundo restando "CONTROLLER_RUMBLE_FADE_SPEED" multiplicado por el delta. Hay entonces una condición if para comprobar si rumble es menor que cero, que pone a rumble en cero si su valor es menor que cero.

Esta pequeña sección de código es todo lo que necesitamos para reducir el ruido del controlador VR. Ahora cuando ponemos rumble a un valor, este código hará que se desvanezca automáticamente con el tiempo.


La primera sección del código comprueba si la variable teleport_button_down es igual a true, lo que significa que este controlador VR está intentando teletransportarse.

Si teleport_button_down es igual a true, forzamos al nodo teleport_raycast Raycast a actualizarse usando la función force_raycast_update. La función force_raycast_update actualizará las propiedades dentro del nodo Raycast con la última versión del mundo de la física.

El código entonces comprueba si el teleport_raycast colisionó con algo comprobando que la función de is_colliding en el teleport_raycast es verdadera. Si el Raycast colisionó con algo, entonces comprobamos si el PhysicsBody con el que colisionó el raycast es un StaticBody o no. Entonces comprobamos si el vector normal de colisión devuelto por el raycast es mayor o igual a 0.85 en el eje Y.

Nota

Lo hacemos porque no queremos que el usuario pueda teletransportarse a los nodos de RigidBody y sólo queremos que el jugador pueda teletransportarse a superficies similares al suelo.

Si se cumplen todas estas condiciones, entonces asignamos la variable teleport_pos a la función get_collision_point en teleport_raycast. Esto asignará teleport_pos a la posición en la que el raycast colisionó en el espacio mundial. Luego movemos la teleport_mesh a la posición del mundo almacenada en teleport_pos.

Esta sección del código obtendrá la posición a la que el jugador apunta con el raycast de teletransportación y actualizará la malla de teletransportación, dando una actualización visual sobre a dónde se teletransportará el usuario cuando suelte el botón de teletransportación.


La siguiente sección del código primero comprueba si el controlador VR está activo a través de la función get_is_active, que está definida por ARVRController. Si el controlador VR está activo, entonces llama a la función physics_process_update_controller_.

La función physics_process_update_controller_ calculará la velocidad del controlador VR a través de los cambios de posición. No es perfecta, pero este proceso da una idea aproximada de la velocidad del controlador de la RV, lo cual está bien para los propósitos de este tutorial.


La siguiente sección del código comprueba si el controlador VR sostiene un objeto comprobando si la variable held_object no es igual a null.

Si el controlador VR sostiene un objeto, primero guardamos su escala en una variable temporal llamada held_scale. Luego ponemos la global_transform del objeto retenido en la global_transform del nodo held_object. Esto hará que el objeto retenido tenga la misma posición, rotación y escala del nodo grab_pos_node en el espacio mundial.

Sin embargo, como no queremos que el objeto retenido cambie de escala cuando es agarrado, necesitamos poner la propiedad scale del nodo held_object de vuelta a held_scale.

Esta sección del código mantendrá el objeto retenido en la misma posición y rotación que el controlador VR, manteniéndolo sincronizado con el controlador VR.


Finalmente, la última sección del código simplemente llama a la función _physics_process_directional_movement. Esta función contiene todo el código para mover el reproductor cuando se mueve el touchpad/joystick del controlador VR.

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

Primero, esta función resetea la variable controller_velocity a un Vector3 cero.


Entonces comprobamos si hay alguna velocidad de controlador de RV almacenada/en caché guardada en el array prior_controller_velocities. Lo hacemos comprobando si la función size() devuelve un valor mayor que 0. Si hay velocidades en caché dentro de prior_controller_velocities, entonces iteramos a través de cada una de las velocidades almacenadas usando un bucle for.

Para cada una de las velocidades almacenadas, simplemente añadimos su valor a controller_velocity. Una vez que el código ha pasado por todas las velocidades en prior_controller_velocities, dividimos controller_velocity por el tamaño del array prior_controller_velocities, lo que nos dará el valor de la velocidad combinada. Esto ayuda a tener en cuenta las velocidades anteriores, haciendo que la dirección de la velocidad del controlador sea más precisa.


A continuación calculamos el cambio de posición que el controlador VR ha tomado desde la última llamada a la función physics_process. Lo hacemos restando prior_controller_position de la posición global del controlador VR, global_transform.origin. Esto nos dará un Vector3 que apunta desde la posición en prior_controller_ a la posición actual del controlador VR, que almacenamos en una variable llamada relative_controller_position.

A continuación, añadimos el cambio de posición a controller_velocity para que el último cambio de posición se tenga en cuenta en el cálculo de la velocidad. Luego añadimos relative_controller_position a prior_controller_velocities para que se tenga en cuenta en el siguiente cálculo de la velocidad del controlador de la RV.

Entonces prior_controller_position se actualiza con la posición global del controlador VR, global_transform.origin. Luego dividimos controller_velocity por delta para que la velocidad sea más alta, dando resultados como los que esperamos, sin dejar de ser relativos a la cantidad de tiempo que ha pasado. No es una solución perfecta, pero los resultados se ven decentes la mayor parte del tiempo y para los propósitos de este tutorial, es suficientemente bueno.

Por último, la función comprueba si el prior_controller_velocities tiene más de 30 velocidades en caché comprobando si la función size() devuelve un valor mayor de 30. Si hay más de 30 velocidades almacenadas en prior_controller_velocities, entonces simplemente eliminamos la velocidad almacenada más antigua llamando a la función remove y pasando a una posición índice de 0.


Lo que esta función hace en última instancia es que obtiene una idea aproximada de la velocidad del controlador VR calculando los cambios relativos de posición del controlador VR en las últimas treinta llamadas al _physics_process. Aunque esto no es perfecto, da una idea decente de la rapidez con la que el controlador de RV se mueve en el espacio tridimensional.

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

Primero esta función obtiene los ejes para el trackpad y el joystick y los asigna a las variables Vector2 llamadas trackpad_vector y joystick_vector respectivamente.

Nota

Es posible que tenga que rehacer los valores de índice del joystick y/o del touchpad dependiendo de su auricular y controlador de RV. Las entradas de este tutorial son los valores de índice de unos auriculares Windows Mixed Reality.

Entonces el trackpad_vector y joystick_vector tienen sus zonas muertas. El código para esto se detalla en el artículo a continuación, con ligeros cambios al convertir el código de C# a GDScript.

Una vez que las variables trackpad_vector y joystick_vector tienen sus zonas muertas, el código obtiene los vectores de dirección delantera y derecha relativos a la transformación global de la ref:ARVRCamera <class_ARVRCamera>. Lo que esto hace es que nos da vectores que apuntan hacia adelante y hacia la derecha relativos a la rotación de la cámara del usuario, la ARVRCamera, en el espacio mundial. Estos vectores apuntan en la misma dirección que las flechas azules y rojas cuando seleccionas un objeto en el editor de Godot con el botón local space mode activado. El vector de dirección hacia adelante se almacena en una variable llamada forward_direction, mientras que el vector de dirección hacia la derecha se almacena en una variable llamada right_direction.

A continuación el código añade las variables trackpad_vector y joystick_vector juntas y normaliza los resultados usando la función normalized. Esto nos da la dirección de movimiento combinada de ambos dispositivos de entrada, por lo que podemos usar una sola Vector2 para mover al usuario. Asignamos la dirección combinada a una variable llamada movement_vector.

Entonces calculamos la distancia que el usuario se moverá hacia adelante, en relación con la dirección de avance almacenada en forward_direction. Para calcular esto, multiplicamos la forward_direction por movement_vector.x, delta y MOVEMENT_SPEED. Esto nos dará la distancia que el usuario se moverá hacia adelante cuando el trackpad/joystick sea empujado hacia adelante o hacia atrás. Asignamos esto a una variable llamada movement_forward.

Hacemos un cálculo similar para la distancia que el usuario se moverá a la derecha, en relación con la dirección correcta almacenada en right_direction. Para calcular la distancia que el usuario se moverá a la derecha, multiplicamos right_direction por movement_vector.y, delta, and MOVEMENT_SPEED. Esto nos dará la distancia que el usuario se moverá a la derecha cuando el trackpad/joystick sea empujado a la derecha o a la izquierda. Asignamos esto a una variable llamada movement_right.

Luego eliminamos cualquier movimiento en el eje Y del movement_forward y movement_right asignando sus valores Y a 0. Hacemos esto para que el usuario no pueda volar/caer con sólo mover el trackpad o el joystick. Sin hacer esto, el jugador podría volar en la dirección en la que se encuentra.

Por último, comprobamos si la función length de movement_right o movement_forward es mayor que 0. Si lo es, entonces necesitamos mover al usuario. Para mover el usuario, realizamos una traducción global al nodo ARVROrigin usando get_parent().global_translate y le pasamos la variable movement_right con la variable movement_forward añadida. Esto moverá al jugador en la dirección en la que apunta el trackpad/joystick, en relación con la rotación del auricular VR. También ponemos la variable directional_movement en true para que el código sepa que este controlador VR está moviendo al jugador.

Si la función length en movement_right o movement_forward es menor o igual a 0, entonces simplemente ponemos la variable directional_movement en false para que el código sepa que este controlador VR no está moviendo al jugador.


Lo que esta función hace en última instancia es tomar la entrada del trackpad y el joystick del controlador VR y mover el reproductor en la dirección en la que lo está empujando. El movimiento es relativo a la rotación de los auriculares VR, así que si el jugador empuja hacia delante y gira la cabeza hacia la izquierda, se moverá hacia la izquierda.

Explicación paso a paso de la función button_pressed

Esta función comprueba si el botón VR que se acaba de pulsar es igual a uno de los botones VR utilizados en este proyecto. La variable button_index se pasa por la señal button_pressed en ARVRController, que conectamos en la función _ready.

Sólo hay tres botones que buscamos en este proyecto: el botón de disparo, el botón de agarre y el botón de menú.

Nota

Es posible que tenga que reasignar estos valores de índice de botón dependiendo de su auricular y controlador de RV. Las entradas de este tutorial son los valores de índice de un auricular de Windows Mixed Reality.

Primero comprobamos si el button_index es igual a 15, lo que debería corresponder al botón de disparo del controlador VR. Si el botón pulsado es el botón de disparo, entonces se llama a la función on_button_pressed_trigger.

Si el button_index es igual a 2, entonces el botón de agarre acaba de ser presionado. Si el botón pulsado es el botón de agarre, se llama a la función on_button_pressed_grab.

Finalmente, si el button_index es igual a 1, entonces el botón de menú fue presionado. Si el botón pulsado es el botón de menú, se llama a la función on_button_pressed_menu.

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

Primero esta función comprueba si el controlador de VR no se sostiene comprobando si held_object es igual a null. Si el controlador VR no está sosteniendo nada, entonces asumimos que la pulsación del disparador en el controlador VR fue para teletransportarse. Entonces nos aseguramos de que teleport_mesh.visible es igual a false. Usamos esto para saber si el otro controlador VR está intentando teletransportarse o no, ya que teleport_mesh será visible si el otro controlador VR se está teletransportando.

Si teleport_mesh.visible es igual a false, entonces podemos teletransportarnos con este controlador VR. Ponemos la variable teleport_button_down a true, ponemos teleport_mesh.visible a true, y ponemos teleport_raycast.visible a true. Esto le dirá al código en _physics_process que este controlador de VR se va a teleportar, hará la teleport_mesh visible para que el usuario sepa a dónde se están teleportando, y hará teleport_raycast visible para que el jugador tenga una 'mira láser' que pueda usar para apuntar la posición de teleportación.


Si held_object no es igual a null, entonces el controlador VR está sosteniendo algo. Entonces comprobamos si el objeto que está siendo retenido, held_object, extiende una clase llamada VR_Interactable_Rigidbody. No hemos hecho VR_Interactable_Rigidbody todavía, pero VR_Interactable_Rigidbody será una clase personalizada que usaremos en todos los nodos basados en special/custom RigidBody del proyecto.

Truco

¡No te preocupes, cubriremos el VR_Interactable_Rigidbody después de esta sección!

Si el held_object se extiende VR_Interactable_Rigidbody, entonces llamamos a la función interact, para que el objeto retenido pueda hacer lo que se supone que debe hacer cuando se presiona el disparador y el objeto es retenido por el controlador VR.

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

Primero esta función comprueba si teleport_button_down es igual a true. Si lo es, entonces llama a return. Hacemos esto porque no queremos que el usuario pueda recoger objetos mientras se teletransporta.

Luego comprobamos si el controlador de VR no tiene nada actualmente, comprobando si held_object es igual a null. Si el controlador VR no está sosteniendo nada, entonces se llama a la función pickup_rigidbody. Si el controlador VR está sosteniendo algo, held_object no es igual a null, entonces se llama a la función _throw_rigidbody.

Finalmente, el sonido pick up/drop se reproduce llamando a la función play en el sonido hand_pickup_drop.

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

Primero la función hace una variable llamada rigid_body, que usaremos para almacenar el RigidBody que el controlador de VR va a recoger, suponiendo que haya un RigidBody para recoger.


Entonces la función comprueba si la variable grab_mode es igual a AREA. Si lo es, entonces obtiene todos los nodos PhysicsBody dentro del grab_area usando las funciones get_overlapping_bodies. Esta función devolverá un array de nodos PhysicsBody. Asignamos el array de PhysicsBody a una nueva variable llamada bodies.

Luego comprobamos si la longitud de la variable bodies es mayor que 0. Si lo es, pasamos por cada uno de los nodos PhysicsBody en bodies usando un bucle for.

Para cada nodo PhysicsBody, comprobamos si es, o extiende, un nodo RigidBody usando if body is RigidBody, que devolverá true si el : ref:PhysicsBody <class_PhysicsBody> es o extiende el nodo RigidBody. Si el objeto es un RigidBody, entonces comprobamos que no hay una variable/constante llamada NO_PICKUP definida en el cuerpo. Hacemos esto porque si quieres tener RigidBody nodos que no pueden ser recogidos, todo lo que tienes que hacer es definir una constante/variable llamada NO_PICKUP y el controlador VR será incapaz de recogerla. Si el nodo RigidBody no tiene una variable/constante definida con el nombre NO_PICKUP, entonces asignamos la variable rigid_body al nodo RigidBody y rompemos el bucle for.

Lo que esta sección del código hace es pasar por todos los cuerpos físicos dentro del grab_area y agarra el primer RigidBody nodo que no tiene una variable/constante llamada NO_PICKUP y la asigna a la variable rigid_body para que podamos hacer algún postprocesamiento adicional más tarde en esta función.


Si la variable grab_mode no es igual a AREA, entonces comprobamos si es igual a RAYCAST en su lugar. Si es igual RAYCAST, forzamos al nodo grab_raycast a actualizarse usando la función force_raycast_update. La función force_raycast_update actualizará el Raycast con los últimos cambios en el mundo de la física. Luego comprobamos si el nodo grab_raycast colisionó con algo usando la función is_colliding, que devolverá true si el Raycast golpea algo.

Si el grab_raycast golpea algo, obtenemos el nodo PhysicsBody golpeando con la función get_collider. El código entonces comprueba si el nodo golpeado es un nodo RigidBody usando if body is RigidBody, que devolverá true si el nodo PhysicsBody es o extiende el nodo RigidBody. Entonces el código comprueba si el nodo RigidBody no tiene una variable llamada NO_PICKUP, y si no la tiene, entonces asigna el nodo RigidBody a la variable rigid_body.

Lo que esta sección del código hace es enviar el nodo grab_raycast Raycast y comprueba si ha colisionado con un nodo RigidBody que no tiene una variable/constante llamada NO_PICKUP. Si colisiona con un RigidBody sin NO_PICKUP, asigna el nodo a la variable rigid_body para que podamos hacer algún postprocesamiento adicional más tarde en esta función.


La última sección del código primero comprueba si el rigid_body no es igual al "null". Si rigid_body no es igual a null, entonces el controlador VR encontró un nodo basado en :ref: `RigidBody <class_RigidBody> `que puede ser recogido.

Si hay un controlador VR para recoger, asignamos held_object al nodo RigidBody almacenado en rigid_body. Luego guardamos el nodo RigidBody del nodo mode, collision_layer, y collision_mask en held_object_data usando mode, layer, y mask como claves para los valores respectivos. Esto es para que podamos volver a aplicarlas más tarde cuando el objeto sea soltado por el controlador de VR.

Luego ponemos el modo RigidBody's a MODE_STATIC, es collision_layer a cero, y es collision_mask a cero. Esto hará que donde el sostenido RigidBody no pueda interactuar con otros objetos del mundo de la física cuando sea sostenido por el controlador de VR.

Luego la hand_mesh MeshInstance se hace invisible poniendo la propiedad visible a false. Esto es para que la mano no se interponga en el camino del objeto sostenido. De la misma manera, la gray_raycast 'laser sight' se hace invisible poniendo la propiedad visible a false.

Entonces el código comprueba si el objeto retenido extiende una clase llamada VR_Interactable_Rigidbody. Si lo hace, entonces establece una variable llamada controller en held_object a self, y llama a la función picked_up en held_object. Aunque todavía no hemos hecho VR_Interactable_Rigidbody, lo que esto hará es decirle a la clase VR_Interactable_Rigidbody que está siendo retenida por un controlador VR, donde la referencia al controlador se almacena en la variable controller, a través de la llamada a la función picked_up.

Truco

¡No te preocupes, cubriremos el VR_Interactable_Rigidbody después de esta sección!

El código debería tener más sentido después de completar la parte 2 de esta serie de tutoriales, donde en realidad usaremos VR_Interactable_Rigidbody.

Lo que hace esta sección de código es que si se encuentra un RigidBody usando el grab Area o Raycast, lo configura para que pueda ser transportado por el controlador de VR.

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

Primero la función comprueba si el controlador VR no está sosteniendo ningún objeto, comprobando si la variable held_object es igual a null. Si lo es, entonces simplemente llama a return para que no pase nada. Aunque esto no debería ser posible, la función throw_rigidbody sólo debería ser llamada si un objeto es retenido, esta comprobación ayuda a asegurar que si algo extraño sucede, esta función reaccionará como se espera.

Después de comprobar si el controlador VR está sosteniendo un objeto, asumimos que lo está y ponemos los datos almacenados RigidBody de vuelta al objeto sostenido. Tomamos los datos mode, layer y mask almacenados en el diccionario held_object_data y los volvemos a aplicar al objeto en held_object. Esto hará que el RigidBody vuelva al estado que tenía antes de ser recogido.

Luego llamamos apply_impulse al held_object para que el RigidBody sea lanzado en la dirección de la velocidad del controlador VR, controller_velocity.

Entonces comprobamos si el objeto retenido extiende una clase llamada VR_Interactable_Rigidbody. Si lo hace, entonces llamamos a una función llamada dropped en held_object y ponemos held_object.controller en null. Aunque todavía no hemos hecho VR_Interactable_Rigidbody, lo que hará es llamar a la función dropped para que el RigidBody pueda hacer lo que sea necesario cuando se deje caer, y ponemos la variable controller en null para que el RigidBody sepa que no está siendo retenido.

Truco

¡No te preocupes, cubriremos el VR_Interactable_Rigidbody después de esta sección!

El código debería tener más sentido después de completar la parte 2 de esta serie de tutoriales, donde en realidad usaremos VR_Interactable_Rigidbody.

Independientemente de si held_object extiende VR_Interactable_Rigidbody o no, entonces ponemos held_object en null para que el controlador de VR sepa que ya no está sosteniendo nada. Debido a que el controlador de VR ya no está sosteniendo nada, hacemos la hand_mesh visible estableciendo hand_mesh.visible a true.

Finalmente, si la variable grab_mode está puesta en RAYCAST, ponemos grab_raycast.visible en true para que la 'mira láser' para el Raycast en grab_raycast sea visible.

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

Primero esta función comprueba si la variable grab_mode es igual a AREA. Si lo es, entonces establece "grab_mode" en RAYCAST. Luego comprueba si el controlador de VR no está reteniendo nada comprobando si held_object es igual a null. Si el controlador de VR no está sosteniendo nada, entonces grab_raycast.visible está configurado como true para que la 'mira láser' en el grab raycast sea visible.

Si la variable grab_mode no es igual a AREA, entonces comprueba si es igual a RAYCAST. Si lo es, entonces establece el grab_mode en AREA y establece grab_raycast.visible en false para que la 'mira laser' en el grab raycast no sea visible.

Esta sección de código simplemente cambia como el controlador de VR cogerá nodos basados en RigidBody cuando se pulse el botón de agarrar. Si grab_mode está establecido como AREA, entonces el nodo Area en grab_area será usado para detectar nodos RigidBody, mientras que si grab_mode está establecido a RAYCAST, el nodo Raycast en grab_raycast será usado para detectar nodos RigidBody.

Explicación paso a paso de la función button_released

La única sección de código en esta función comprueba si el índice del botón que acaba de ser liberado, button_index, es igual a 15, lo que debería mapear al botón de disparo en el controlador VR. La variable button_index se pasa por la señal button_release en ARVRController, que conectamos en la función _ready.

Si el botón accionador se suelta, entonces la función _on_button_released_trigger será llamada.

Explicación paso a paso de la función _on_button_released_trigger

La única sección de código de esta función primero comprueba si el controlador de VR está intentando teletransportarse, comprobando si la variable teleport_button_down es igual a true.

Si la variable teleport_button_down es igual a true, el código entonces comprueba si hay una posición de teleportación establecida y si la malla de teleportación es visible. Lo hace comprobando si teleport_pos no es igual a null y si teleport_mesh.visible es igual a true.

Si hay una posición de teleportación establecida y la malla de teleportación es visible, el código entonces calcula el desplazamiento de la cámara al nodo ARVROrigin, que se asume como el nodo padre del controlador VR. Para calcular el desplazamiento, la posición global (global_transform.origin) del nodo Player_Camera tiene la posición global del nodo ARVROrigin restada de él. Esto dará como resultado un vector que apunta desde el ARVROrigin al ARVRCamera, que almacenamos en una variable llamada camera_offset.

La razón por la que necesitamos saber el desplazamiento es porque algunos auriculares de RV usan el seguimiento de la sala, donde la cámara del jugador puede ser desplazada del nodo ARVROrigin. Por eso, cuando nos teletransportamos queremos mantener el offset creado por el room tracking para que cuando el jugador se teletransporte, no se aplique el offset creado por el room tracking. Sin esto, si te movieras en una sala y luego te teletransportaras, en lugar de aparecer en la posición en la que querías teletransportarte, tu posición estaría compensada por la cantidad de distancia que tienes del nodo ARVROrigin.

Ahora que sabemos el desplazamiento de la cámara VR al origen de la misma, necesitamos eliminar la diferencia en el eje Y. Hacemos esto porque no queremos compensar en base a la altura del usuario. Si no lo hiciéramos, al teletransportar la cabeza del jugador estaría a nivel del suelo.

Entonces podemos "teletransportar" al jugador poniendo la posición global (global_transform.origin) del nodo ARVROrigin en la posición almacenada en teleport_pos con camera_offset restado de ella. Esto teletransportará al jugador y eliminará el offset de seguimiento de la sala, de modo que el usuario aparezca exactamente donde quiere cuando se teletransporte.

Finalmente, independientemente de si el controlador VR teleportó al usuario o no, reajustamos las variables relacionadas con el teleporte. teleport_button_down está establecida en false, teleport_mesh.visible está establecida en false por lo que la malla es invisible, teleport_raycast.visible está establecida en false, y teleport_pos está establecida en null.

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

La única sección de código en esta función comprueba si el nodo PhysicsBody que entró en el nodo Sleep_Area tiene una variable llamada can_sleep. Si lo hace, entonces pone la variable can_sleep en false y pone la variable sleeping en false.

Sin hacer esto, los nodos durmientes PhysicsBody no podrían ser recogidos por el controlador de VR, incluso si el controlador de VR está en la misma posición que el nodo PhysicsBody. Para solucionar esto, simplemente 'despertamos' PhysicsBody los nodos que están cerca del controlador de VR.

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

La única sección de código en esta función comprueba si el nodo PhysicsBody que entró en el nodo Sleep_Area tiene una variable llamada can_sleep. Si lo hace, entonces pone la variable can_sleep en true.

Esto permite RigidBody nodos que dejan el Sleep_Area para dormir de nuevo, ahorrando rendimiento.


Bien, ¡wow! ¡Eso fue un montón de código! Añade el mismo script, VR_Controller.gd a la otra escena de controladores VR para que ambos controladores VR tengan el mismo script.

¡Ahora sólo tenemos que hacer una cosa antes de probar el proyecto! Ahora mismo estamos haciendo referencia a una clase llamada VR_Interactable_Rigidbody, pero aún no la hemos definido. Aunque no usaremos VR_Interactable_Rigidbody en este tutorial, vamos a crearla rápidamente para que el proyecto pueda ser ejecutado.

Creando una clase base para objetos VR interactivos

Con la pestaña Script aún abierta, crea un nuevo GDScript llamado VR_Interactable_Rigidbody.gd.

Truco

Puedes crear GDScripts en la pestaña Script presionando File -> New Script....

Una vez que tengas VR_Interactable_Rigidbody.gd abierto, agrega el siguiente código:

class_name VR_Interactable_Rigidbody
extends RigidBody

# (Ignore the unused variable warning)
# warning-ignore:unused_class_variable
var controller = null


func _ready():
    pass


func interact():
    pass


func picked_up():
    pass


func dropped():
    pass

Analicemos rápidamente el script.


Primero empezamos el script con class_name VR_Interactable_Rigidbody. Lo que esto hace es que le dice a Godot que este GDScript es una nueva clase que se llama VR_Interactable_Rigidbody. Esto nos permite comparar nodos con la clase VR_Interactable_Rigidbody en otros archivos de script sin tener que cargar el script directamente o hacer algo especial. Podemos comparar la clase como todas las clases de Godot incorporadas.

Lo siguiente es una variable de clase llamada controller. controller se usará para mantener una referencia al controlador VR que actualmente sostiene el objeto. Si un controlador VR no está sosteniendo el objeto, entonces la variable controller será null. La razón por la que necesitamos tener una referencia al controlador de VR es para que los objetos retenidos puedan acceder a los datos específicos del controlador de VR, como controller_velocity.

Finalmente, tenemos cuatro funciones. La función _ready está definida por Godot y todo lo que hacemos es simplemente tener un pass, ya que no hay nada que tengamos que hacer cuando el objeto se añade a la escena en VR_Interactable_Rigidbody.

La función interact es una función que se llamará cuando el botón de interacción del controlador VR, el disparador en este caso, sea presionado mientras el objeto es sostenido.

Truco

Una función stub es una función que está definida pero no tiene ningún código. Las funciones de stub generalmente están diseñadas para ser sobrescritas o extendidas. En este proyecto, estamos usando las funciones stub para que haya una interfaz consistente en todos los objetos interactivos RigidBody.

Las funciones picked_up y dropped son funciones de stub que serán llamadas cuando el objeto sea recogido y soltado por el controlador VR.


¡Eso es todo lo que tenemos que hacer por ahora! En la próxima parte de esta serie de tutoriales, empezaremos a hacer objetos interactivos especiales RigidBody.

Ahora que la clase base está definida, el código del controlador VR debería funcionar. Adelante, prueba el juego de nuevo, y deberías encontrar que puedes teletransportarte pulsando el panel táctil, y puedes agarrar y lanzar objetos usando los botones de agarrar/empuñar.

Ahora, puede que quieras intentar moverte usando los trackpads y/o joysticks, pero puede hacerte sentir mal por el movimiento!

Una de las principales razones por las que esto puede hacer que te sientas mal del movimiento es porque tu visión te dice que te estás moviendo, mientras que tu cuerpo no se está moviendo. Este conflicto de señales puede hacer que el cuerpo se sienta enfermo. Añadamos un shader de viñetas para ayudar a reducir el mareo mientras se mueve en RV!

Reducir el mareo por movimiento

Nota

Hay muchas maneras de reducir el mareo en la RV, y no hay una sola manera perfecta de reducir el mareo. Ver "esta página del Centro de Desarrollo de Oculus" <https://developer.oculus.com/design/latest/concepts/bp-locomotion/>`_ para más información sobre cómo implementar la locomoción y reducir el mareo.

Para ayudar a reducir el mareo al moverse, vamos a añadir un efecto de viñeta que sólo será visible mientras el jugador se mueve.

Primero, cambia rápidamente de nuevo a "Game.tscn. Bajo el nodo ARVROrigin hay un nodo hijo llamado Movement_Vignette. Este nodo va a aplicar una simple viñeta al auricular VR cuando el reproductor se mueva usando los controladores VR. Esto debería ayudar a reducir el mareo por movimiento.

Abre Movement_Vignette.tscn, que puedes encontrar en la carpeta Scenes. La escena es sólo un nodo ColorRect con un shader personalizado. Siéntete libre de mirar el shader personalizado si quieres, es sólo una versión ligeramente modificada del shader de viñetas que puedes encontrar en el repositorio ``Godot demo <https://github.com/godotengine/godot-demo-projects>`_.

Escribamos el código que hará visible el shader de viñetas cuando el jugador se mueva. Selecciona el nodo Movement_Vignette y crea un nuevo script llamado Movement_Vignette.gd. Añade el siguiente código:

extends Control

var controller_one
var controller_two


func _ready():
    yield(get_tree(), "idle_frame")
    yield(get_tree(), "idle_frame")
    yield(get_tree(), "idle_frame")
    yield(get_tree(), "idle_frame")

    var interface = ARVRServer.primary_interface

    if interface == null:
        set_process(false)
        printerr("Movement_Vignette: no VR interface found!")
        return

    rect_size = interface.get_render_targetsize()
    rect_position = Vector2(0,0)

    controller_one = get_parent().get_node("Left_Controller")
    controller_two = get_parent().get_node("Right_Controller")

    visible = false


func _process(_delta):
    if (controller_one == null or controller_two == null):
        return

    if (controller_one.directional_movement == true or controller_two.directional_movement == true):
        visible = true
    else:
        visible = false

Como este script es bastante simple, veamos rápidamente lo que hace.

Explicando el código de viñeta

Hay dos variables de clase, controller_one y controller_two. Estas variables contendrán referencias a los controladores VR izquierdo y derecho.


En la función _ready primero espera cuatro fotogramas usando yield. La razón por la que esperamos cuatro fotogramas es porque queremos asegurarnos de que la interfaz de RV está lista y accesible.

Después de esperar, la interfaz primaria de RV se recupera usando ARVRServer.primary_interface, que se asigna a una variable llamada interface. El código entonces comprueba si interfaz es igual a null. Si interfaz es igual a null, entonces _process se deshabilita usando set_process con un valor de false.

Si la interface no es null, entonces ponemos el rect_size del shader de viñetas en el tamaño de renderización de la ventana VR para que ocupe toda la pantalla. Necesitamos hacer esto porque los diferentes auriculares de la RV tienen diferentes resoluciones y relaciones de aspecto, así que tenemos que redimensionar el nodo en consecuencia. También establecemos la rect_position del shader de viñetas en cero para que esté en la posición correcta con respecto a la pantalla.

Los controladores de RV izquierdo y derecho se recuperan y se asignan a controller_one y controller_two respectivamente. Finalmente, el shader de viñetas se hace invisible de forma predeterminada al establecer su propiedad visible como false.


En _process el código primero comprueba si controller_one o controller_two es igual a null. Si cualquiera de los nodos es igual a null, entonces se llama return para que no pase nada.

Luego el código comprueba si alguno de los controladores de RV está moviendo el reproductor usando el touchpad/joystick comprobando si directional_movement es igual a true en controller_one o controller_two. Si alguno de los controladores de RV está moviendo el reproductor, entonces el shader de viñetas se hace visible estableciendo su propiedad visible como true. Si ninguno de los controladores de VR está moviendo el reproductor, entonces directional_movement es false en ambos controladores de VR, entonces el shader de viñetas se hace invisible ajustando su propiedad visible a false.


¡Ese es todo el script! Ahora que hemos escrito el código, adelante e intenta moverte con el trackpad y/o el joystick. ¡Deberíais descubrir que es menos enfermizo que antes!

Nota

Como se mencionó anteriormente, hay muchas maneras de reducir el mareo en la RV. Revisa "esta página del Centro de Desarrollo de Oculus" <https://developer.oculus.com/design/latest/concepts/bp-locomotion/>`_ para más información sobre cómo implementar la locomoción y reducir el mareo.

Notas finales

../../../_images/starter_vr_tutorial_hands.png

Ahora tienes controladores de RV completamente funcionales que pueden moverse por el entorno e interactuar con objetos basados en RigidBody. En la próxima parte de esta serie de tutoriales, crearemos algunos objetos especiales basados en RigidBody para que el jugador los use!

Advertencia

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