VR starter tutorial part 1

简介

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

This tutorial will show you how to make a beginner VR game project in Godot.

请记住, 制作VR内容时最重要的事情之一是保证您的资源大小合适 ! 这可以通过大量练习和反复调整来实现这一目标,但是您可以采取一些措施来简化这个过程:

  • In VR, 1 unit is typically considered 1 meter. If you design your assets around that standard, you can save yourself a lot of headache.

  • In your 3D modeling program, see if there is a way to measure and use real world distances. In Blender, you can use the MeasureIt add-on; in Maya, you can use the Measure Tool.

  • 您可以使用诸如 Google Blocks 之类的工具制作粗略模型,然后在另一个3D建模程序中进行优化。

  • 经常测试,因为VR中的资源看起来与平面屏幕上的显着不同!

在本教程的整个过程中,我们将介绍:

  • 如何让Godot以VR模式运行。

  • How to make a teleportation locomotion system that uses the VR controllers.

  • How to make a artificial movement locomotion system that uses the VR controllers.

  • How to create a RigidBody-based system that allows for picking up, dropping, and throwing RigidBody nodes using the VR controllers.

  • 如何添加可销毁的目标。

  • How to create some special RigidBody-based objects that can destroy the targets.

小技巧

While this tutorial can be completed by beginners, it is highly advised to complete 您的第一个游戏, if you are new to Godot and/or game development.

**在通过本系列教程之前,需要**有一定的3D游戏制作经验。本教程假设你有Godot编辑器、GDScript和基本3D游戏开发的经验。需要连接一个OpenVR耳机和两个OpenVR控制器。

This tutorial was written and tested using a Windows Mixed Reality headset and controllers. This project has also been tested on the HTC Vive. Code adjustments may be required for other VR Headsets, such as the Oculus Rift.

本教程的Godot项目可以在`OpenVR GitHub仓库<https://github.com/GodotVR/godot_openvr_fps>`_找到。本教程的启动素材可以在GitHub仓库的发布部分找到。初始素材包含一些3D模型、声音、脚本和为本教程配置的场景。

注解

Credits for the assets provided:

  • The sky panorama was created by CGTuts.

  • 使用的字体是Titillium-Regular

    • 使用的字体是** Titillium-Regular **,并根据SIL Open Font License 1.1版获得许可

  • 所使用的音频来自几个不同的来源,都来自 Sonnis #GameAudioGDC Bundle。 声音效果的许可证包含在PDF 此处

    • 存储音频文件的文件夹与Sonniss音频包中的文件夹名称相同。

  • OpenVR插件由`Bastiaan Olij <https://github.com/BastiaanOlij>`_创建,并在MIT许可下发布。 可以在Godot素材库<https://godotengine.org/asset-library/asset/150>`_和GitHub <https://github.com/GodotVR/godot-openvr-asset上找到 >`_。 * OpenVR插件中使用的第三方代码和库可能具有不同的许可。*

  • 最初的项目、3D模型和脚本由`TwistedTwigleg <https://github.com/TwistedTwigleg>`_创建,并在MIT许可下发布。

小技巧

You can find the finished project on the OpenVR GitHub repository.

做好准备

如果你还没有下载,请到`OpenVR GitHub仓库<https://github.com/GodotVR/godot_openvr_fps>`_,并从发布的版本中下载 "Starter Assets "文件。一旦你下载了入门资产,在Godot中打开项目。

注解

The starter assets are not required to use the scripts provided in this tutorial. The starter assets include several premade scenes and scripts that will be used throughout the tutorial.

When the project is first loaded, the Game.tscn scene will be opened. This will be the main scene used for the tutorial. It includes several nodes and scenes already placed throughout the scene, some background music, and several GUI-related MeshInstance nodes.


The GUI-related MeshInstance nodes already have scripts attached to them. These scripts will set the texture of a Viewport node to the albedo texture of the material of the MeshInstance node. This is used to display text within the VR project. Feel free to take a look at the script, GUI.gd, if you want. We will not be going over how to to use Viewport nodes for displaying UI on MeshInstance nodes in this tutorial .

如果您对如何使用 Viewport 节点在 MeshInstance 节点上显示 UI 感兴趣,请参阅 使用视区作为纹理 教程。它涵盖了如何使用 Viewport 作为渲染纹理,以及如何将该纹理应用到 MeshInstance 节点上。


Before we jump into the tutorial, let's take a moment to talk about how the nodes used for VR work.

ARVROrigin <class_ARVROrigin>`节点是VR跟踪系统的中心点。:ref:`ARVROrigin <class_ARVROrigin>`的位置是VR系统认为的 "中心 "点在地面上的位置。:ref:`ARVROrigin <class_ARVROrigin>`有一个`世界缩放`属性,影响用户在VR场景中的大小。在本教程中,它被设置为`1.4,因为世界本来就有点大。如前所述,在VR中保持比例相对一致是很重要的。

The ARVRCamera is the player's headset and view into the scene. The ARVRCamera is offset on the Y axis by the VR user's height, which will be important later when we add teleportation locomotoin. If the VR system supports room tracking, then the ARVRCamera will move as the player moves. This means that the ARVRCamera is not guaranteed to be in the same position as the ARVROrigin node.

The ARVRController node represents a VR controller. The ARVRController will follow the position and rotation of the VR controller relative to the ARVROrigin node. All of the input for the VR controllers happens through the ARVRController node. An ARVRController node with an ID of 1 represents the left VR controller, while an ARVRController controller with an ID of 2 represents the right VR controller.

总而言之:

  • ARVROrigin -节点是VR跟踪系统的中心,位于地面上。

  • ARVRCamera -是玩家的VR头戴式设备,同时提供了场景的视图。

  • :ref:`ARVRCamera <class_ARVRCamera>`节点在Y轴上的偏移量为用户的高度。

  • 如果VR系统支持房间跟踪,那么 ARVRCamera 节点可能会在玩家移动时在X轴和Z轴上偏移。

  • ARVRController - 节点代表VR控制器并处理来自VR控制器的所有输入。

启动VR

Now that we have gone over the VR nodes, let's start working on the project. While in Game.tscn, select the Game node and make a new script called Game.gd. In the Game.gd file, add the following code:

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!
        }
    }
}

Let's go over what this code does.


在``_ready``函数中,我们首先使用 ARVRServer <class_ARVRServer>`中的``find_interface``函数获取OpenVR VR接口,并将其分配给一个名为`VR`的变量。如果 :ref:`ARVRServer <class_ARVRServer>`找到一个名称为OpenVR的接口,就会返回,否则就会返回``null`

注解

The OpenVR VR interface is not included with Godot by default. You will need to download the OpenVR asset from the Asset Library or GitHub.

然后,这段代码结合了两个条件,一个是检查`VR`变量是否为NOT空(if VR),另一个是调用initialize函数,根据OpenVR接口是否能够初始化,返回一个布尔值。如果这两个条件都返回true,那么我们就可以把主Godot :ref:`Viewport <class_Viewport>`变成一个ARVR Viewport视图。

如果VR接口初始化成功,我们就得到根 Viewport,并将`arvr`属性设置为``true``。这将告诉Godot使用初始化的ARVR接口来驱动:ref:`Viewport <class_Viewport>`的显示。

最后,我们禁用VSync,这样每秒帧数(FPS)就不会被电脑显示器限制。之后我们告诉Godot以每秒``90``帧的速度渲染,这是大多数VR头显的标准。如果不禁用VSync,普通电脑显示器可能会将VR头显的帧率限制在电脑显示器的帧率上。

注解

In the project settings, under the Physics->Common tab, the physics FPS has been set to 90. This makes the physics engine run at the same frame rate as the VR display, which makes physics reactions look smoother when in VR.


That is all we need to do for Godot to launch OpenVR within the project! Go ahead and give it a try if you want. Assuming everything works, you will be able to look around the world. If you have a VR headset with room tracking, then you will be able to move around the scene within the limits of the room tracking.

Creating the controllers

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

现在,VR用户能做的就是站在周围,这并不是真正要做的,除非正在制作一部VR电影。来编写VR控制器的代码。要一次性写完所有VR控制器的代码,所以代码比较长。也就是说,一旦我们完成了,你将能够在场景中进行传送,使用VR控制器上的触摸板/操纵杆进行人工移动,并且能够拾取、丢弃和抛出:ref:`RigidBody <class_RigidBody>`类型节点。

首先需要打开VR控制器使用的场景,Left_Controller.tscn``或者``Right_Controller.tscn。简单介绍一下场景是如何设置的。

如何设置VR控制器的场景

在这两个场景中,根节点都是ARVRController节点,唯一不同的是,Left_Controller``场景的 ``Controller Id``属性设置为 ``1,而 Right_Controller``的``Controller Id``属性设置为``2

注解

:ref:`ARVRServer <class_ARVRServer>`试图将这两个ID用于左、右VR控制器。对于支持2个以上控制器/跟踪对象的VR系统,这些ID可能需要调整。

接下来是``Hand`` :ref:`MeshInstance <class_MeshInstance>`节点。这个节点是用来显示手部网格的,当VR控制器没有握住一个:ref:`RigidBody <class_RigidBody>`节点时,将使用这个节点。``Left_Controller``场景中的手是左手,而``Right_Controller``场景中的手是右手。

名为``Raycast``的节点是一个:ref:Raycast <class_Raycast>`节点,用于VR控制器传送时瞄准传送到哪里。:ref:`Raycast <class_Raycast>`的长度在Y轴上设置为`-16``,并旋转使其指向手的指针外。Raycast``节点有一个子节点,``Mesh,是一个:ref:MeshInstance <class_MeshInstance>。它用于直观地显示传送:ref:`Raycast <class_Raycast>`的目标位置。

名为``Area``的节点是一个:ref:Area <class_Area>`节点,将用于在VR控制器抓取模式设置为``AREA``时,抓取基于:ref:`RigidBody <class_RigidBody>`的节点。``Area``节点有一个子节点``CollisionShape`,定义了一个球体:ref:CollisionShape <class_CollisionShape>。当VR控制器没有握住任何物体,按下抓取按钮时,在``Area``节点内的第一个:ref:`RigidBody <class_RigidBody>`类型的节点将被拾取。

接下来是一个名为``Grab_Pos``的 :ref:`Position3D <class_Position3D>`节点。这是用来定义抓取的 :ref:`RigidBody <class_RigidBody>`节点跟随的位置,它们被VR控制器持有。

一个大的:ref:Area <class_Area>`节点称为``Sleep_Area`,用于禁止其:ref:CollisionShape <class_CollisionShape>`内的任何RigidBody节点休眠,简称为``CollisionShape`。之所以需要这样做,是因为如果一个:ref:RigidBody <class_RigidBody>`节点陷入休眠,那么VR控制器将无法抓住它。通过使用``Sleep_Area`,我们可以编写代码,使其中的任何:ref:`RigidBody <class_RigidBody>`节点无法进入休眠状态,以允许VR控制器抓取它。

一个名为 "AudioStreamPlayer3D <class_AudioStreamPlayer3D>"的 :ref:`AudioStreamPlayer3D<class_AudioStreamPlayer3D>`节点加载了一个声音,当一个物体被VR控制器抓起、掉落或抛出时,我们将使用这个声音。虽然这对于VR控制器的功能来说并不是必须的,但它让抓取和丢弃物体的感觉更加自然。

最后一个节点是``Grab_Cast``节点和它唯一的子节点``Mesh``。当VR控制器抓取模式设置为``RAYCAST``时,``Grab_Cast``节点将用于抓取:ref:`RigidBody <class_RigidBody>``类型节点。这将允许VR控制器使用Raycast来抓取那些稍微够不到的物体。``Mesh``节点用于直观地显示传送:ref:`Raycast <class_Raycast>`的目标位置。

这是对VR控制器场景如何设置的快速概述,以及将如何使用节点为它们提供功能。我们已经看了VR控制器场景,来编写驱动它们的代码。

The code for the VR controllers

Select the root node of the scene, either Right_Controller or Left_Controller, and make a new script called VR_Controller.gd. Both scenes will be using the same script, so it doesn't matter which you use first. With VR_Controller.gd opened, add the following code:

小技巧

You can copy and paste the code from this page directly into the script editor.

如果这么做,所有复制的代码将使用空格而不是制表符。

To convert the spaces to tabs in the script editor, click the Edit menu and select Convert Indent To Tabs. This will convert all the spaces into tabs. You can select Convert Indent To Spaces to convert tabs back into spaces.

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

这段代码挺多的,让我们一步步来看看这段代码的作用。

Explaining the VR controller code

First, let's go through all the class variables in the script:

  • controller_velocity:一个变量,用于保存VR控制器速度的近似值。

  • prior_controller_position:一个变量,用于保存VR控制器在3D空间中的最后位置。

  • prior_controller_velocities: An Array to hold the last 30 calculated VR controller velocities. This is used to smooth the velocity calculations over time.

  • held_object:一个变量,用于保存VR控制器所持有的对象的引用,如果VR控制器没有持有任何对象,这个变量将是``null``。

  • held_object_data:一个字典,用于保存VR控制器持有的:ref:`RigidBody <class_RigidBody>`节点的数据。当:ref:`RigidBody <class_RigidBody>`节点不再被持有时,用于重置该节点的数据。

  • grab_area: A variable to hold the Area node used to grab objects with the VR controller.

  • grab_raycast: A variable to hold the Raycast node used to grab objects with the VR controller.

  • grab_mode:用于定义VR控制器使用的抓取模式的变量。本教程中只有两种抓取对象的模式,AREA``和``RAYCAST

  • grab_pos_node。一个变量,用于保存用于更新所持物体的位置和旋转的节点。

  • hand_mesh。一个变量,用于保存:ref:`MeshInstance <class_MeshInstance>`节点,其中包含VR控制器的手部网格。当VR控制器没有拿着任何东西时,这个网格将被显示出来。

  • hand_pickup_drop_sound: A variable to hold the AudioStreamPlayer3D node that contains the pickup/drop sound.

  • teleport_pos:一个变量,用于在VR控制器传送玩家时保存玩家被传送到的位置。

  • teleport_mesh: A variable to hold the MeshInstance node used to show where the player is teleporting to.

  • teleport_button_down: A variable used to track whether the controller's teleport button is held down. This will be used to detect if this VR controller is trying to teleport the player.

  • teleport_raycast: A variable to hold the Raycast node used to calculate the teleport position. This node also has a MeshInstance that acts as a 'laser sight' for aiming.

  • CONTROLLER_DEADZONE: A constant to define the deadzone for both the trackpad and the joystick on the VR controller. See the note below for more information.

  • MOVEMENT_SPEED: A constant to define the speed the player moves at when using the trackpad/joystick to move artificially.

  • CONTROLLER_RUMBLE_FADE_SPEED:一个常数,用于定义VR控制器隆隆声衰减的速度。

  • directional_movement: A variable to hold whether this VR controller is moving the player using the touchpad/joystick.

注解

您可以在这里找到一篇很棒的文章解释如何处理游戏手柄/控制器死区:http://www.third-helix.com/2013/04/12/doing-thumbstick-dead-zones-right.html

We are using a translated version of the scaled radial dead zone code provided in that article for the VR controller's joystick/touchpad. The article is a great read, and I highly suggest giving it a look!

这是相当多的类变量。它们中的大部分都是用来保存在整个代码中需要的节点的引用。接下来我们开始查看函数,从``_ready``函数开始。


``_ready``函数的逐步说明

首先,我们告诉Godot关闭关于不使用``connect``函数返回值的警告,在本教程中,将不需要返回值。

接下来我们获得 Raycast <class_Raycast>`节点,将使用它来确定传送的位置,并将其分配给 ``teleport_raycast` 变量。然后我们获得 MeshInstance <class_MeshInstance>`节点,用其显示玩家被传送到哪里。我们用来传送的节点是 ``Game` 场景的一个子节点,这样做是为了让传送网格节点不受VR控制器变化的影响,传送网格可以被两个VR控制器使用。

然后将 teleport_button_down 变量设置为false, teleport_mesh.visible 设置为 falseteleport_raycast.visible 设置为 false ,这样就设置了传送玩家进入初始状态,而不是传送玩家的变量。

The code then gets the grab_area node, the grab_raycast node, and the grab_pos_node node and assigns them all to their respective variables for use later.

接下来将 grab_mode 设置为 AREA ,这样VR 控制器将尝试在按下 VR 控制器的抓取/握持按钮时使用 grab_area 中定义的 区域(Area) 节点抓取对象。我们还将 grab_raycast 节点的 visible 属性设置为 false ,这样 grab_raycast 的 ‘激光瞄准器(laser sight)’子节点就不可见了。

之后我们将VR控制器中的 "Sleep_Area "节点的 "body_entered "和 "body_exited "信号连接到 "sleep_area_entered "和 "sleep_area_exited "函数中。`sleep_area_entered``和`sleep_area_exited``函数将用于使:ref:`RigidBody <class_RigidBody>`节点在VR控制器附近时无法休眠。

Then the hand_mesh and hand_pickup_drop_sound nodes are gotten and assigned them to their respective variables for use later.

最后,VR控制器扩展的:ref:`ARVRController <class_ARVRController>`节点中的`button_pressed``和`button_release``信号分别与`button_pressed``和`button_released``函数相连。这意味着当VR控制器上的某个按钮被按下或释放时,本脚本中定义的 "button_pressed "或 "button_released "函数将被调用。

_physics_process function step-by-step explanation

首先我们检查``rumble``变量是否大于零。如果``rumble``变量,也就是:ref:`ARVRController <class_ARVRController>`节点的一个属性,大于零,那么VR控制器就会发出隆隆声。

如果``rumble``变量大于零,那么我们每隔一秒用``CONTROLLER_RUMBLE_FADE_SPEED``减去``CONTROLLER_RUMBLE_FADE_SPEED``乘以delta,就可以减少rumble``。然后有一个``if``条件来检查``rumble``是否小于零,如果其值小于零,则将``rumble``设置为零。

这一小段代码是我们减少VR控制器的隆隆声所需要的全部内容。现在,当我们将``rumble``设置为一个值时,这段代码将自动使其随着时间的推移而逐渐消失。


第一段代码检查``teleport_button_down``变量是否等于``true``,这意味着这个VR控制器正在尝试传送。

If teleport_button_down is equal to true, we force the teleport_raycast Raycast node to update using the force_raycast_update function. The force_raycast_update function will update the properties within the Raycast node with the latest version of the physics world.

这段代码通过检查`teleport_raycast``中的`is_colliding``函数是否为真,来检查`teleport_raycast``是否与任何东西相撞。如果:ref:Raycast <class_Raycast>`与某物相撞,我们就检查:ref:`PhysicsBody <class_PhysicsBody>`与Raycast相撞的:ref:`StaticBody <class_StaticBody>`是否为:ref:`StaticBody <class_StaticBody>。然后我们检查raycast返回的碰撞法向量在Y轴上是否大于或等于``0.85``。

注解

我们这样做是因为我们不希望用户能够传送到RigidBody节点上,我们只希望玩家能够在类似地板的表面进行传送。

如果所有这些条件都得到满足,那么我们就将``teleport_pos``变量分配给``teleport_raycast``中的``get_collision_point``函数。这将把``teleport_pos``分配给射线广播在世界空间中碰撞的位置。然后我们将``teleport_mesh``移动到``teleport_pos``中存储的世界位置。

这段代码将通过传送射线广播获得玩家瞄准的位置,并更新传送网,在释放传送按钮时,直观地更新用户将传送到哪里。


下一节代码首先通过``get_is_active``函数检查VR控制器是否处于活动状态,该函数由:ref:`ARVRController <class_ARVRController>`定义。如果VR控制器是活动的,那么它就会调用``_physics_process_update_controller_velocity``函数。

``_physics_process_update_controller_velocity``函数将通过位置的变化来计算VR控制器的速度。它并不完美,但这个过程可以得到VR控制器的速度的一个大致概念,对于本教程的目的来说,这是很好的。


下一段代码通过检查``held_object``变量是否不等于``null``来检查VR控制器是否持有一个对象。

如果VR控制器持有一个对象,我们首先将它的比例存储在一个名为``held_scale``的临时变量中。然后我们将持有对象的``global_transform``设置为``held_object``节点的``global_transform``。这将使持有的对象在世界空间中具有与``grab_pos_node``节点相同的位置、旋转和比例。

但是,由于我们不希望被抓取的对象在被抓取时的比例发生变化,我们需要将``held_object``节点的``scale``属性设置为``held_scale``。

这段代码将使持有的物体与VR控制器保持相同的位置和旋转,使其与VR控制器保持同步。


最后,最后一段代码只是简单地调用``_physics_process_directional_movement``函数。这个函数包含了当VR控制器上的触摸板/操纵杆移动时移动玩家的所有代码。

_physics_process_update_controller_velocity 函数步骤解释

First this function resets the controller_velocity variable to zero Vector3.


然后我们检查是否有任何存储/缓存的VR控制器速度保存在``prior_controller_velocities``数组中。我们通过检查``size()``函数是否返回一个大于``0``的值。如果在``prior_controller_velocities``中存在缓存的速度,那么我们就使用``for``循环对每个存储的速度进行迭代。

对于每一个缓存的速度,我们只需将其值添加到``controller_velocity``中。一旦代码通过了``prior_controller_velocities``中的所有缓存速度,我们将``controller_velocity``除以``prior_controller_velocities``数组的大小,就会得到综合速度值。这有助于将之前的速度考虑在内,使控制器的速度方向更加准确。


接下来我们计算VR控制器自上次``_physics_process``函数调用后的位置变化。我们通过从VR控制器的全局位置``global_transform.origin``中减去``prior_controller_position``来计算。这将给我们一个:ref:`Vector3 <class_Vector3>`从``prior_controller_position``中的位置指向VR控制器的当前位置,我们将其存储在一个名为``relative_controller_position``的变量中。

接下来我们将位置的变化添加到``controller_velocity``中,这样在计算速度时就会考虑到最新的位置变化。然后我们将``relative_controller_position``添加到``prior_controller_velocities``中,这样在下一次计算VR控制器的速度时就可以将其考虑进去。

然后``prior_controller_position``用VR控制器的全局位置``global_transform.origin``更新。然后我们将``controller_velocity``除以``delta``,这样速度就会更高,得到的结果就像我们期望的那样,同时还是相对于已经过去的时间量。这不是一个完美的解决方案,但大多数时候结果看起来还不错,就本教程而言,这已经足够了。

后,函数检查``prior_controller_velocities``是否有超过``30``的速度缓存,检查``size()``函数是否返回一个大于``30``的值。如果在``prior_controller_velocities``中存储了超过``30``的缓存速度,那么我们只需通过调用``remove``函数并传递一个``0``的索引位置来删除最老的缓存速度。


这个函数最终要做的是通过计算VR控制器在过去三十次``_physics_process``的相对位置变化,得到VR控制器速度的一个大概。虽然这并不完美,但它可以很好地了解VR控制器在3D空间中的移动速度。

_physics_process_directional_movement 函数分步解释

首先,这个函数获取触控板和操纵杆的轴,并将它们分配给:ref:Vector2 <class_Vector2>`变量,分别称为``trackpad_vector``和``joystick_vector`

注解

您可能需要根据您的VR头显和控制器重新映射操纵杆和/或触摸板的索引值。本教程中的输入是Windows混合现实耳机的索引值。

然后``trackpad_vector``和``joystick_vector``具有它们的盲区。这方面的代码在下面的文章中详细介绍了,随着代码从C#转换到GDScript,略有变化。

一旦 "trackpad_vector "和 "joystick_vector "变量的盲区被计算在内,代码就会得到相对于 ARVRCamera,在世界空间中向前和向右指向的向量。当您在Godot编辑器中选择一个对象并启用 "局部空间模式 "按钮时,这些向量与蓝色和红色箭头的方向相同。前进方向向量存储在一个名为 "forward_direction "的变量中,而右侧方向向量存储在一个名为 "right_direction "的变量中。

接下来,代码将``trackpad_vector``和``joystick_vector``变量加在一起,并使用``normalized``函数对结果进行标准化。这样我们就得到了两个输入设备的组合移动方向,所以我们可以使用一个:ref:`Vector2 <class_Vector2>`来移动用户。我们将组合方向分配给一个名为``movement_vector``的变量。

然后我们计算用户将朝 forward_direction 方向前进的距离大小。为了计算它,我们将 forward_direction 乘以 movement_vector.xdeltaMOVEMENT_SPEED 。这将给我们提供当触控板/操纵杆被向前或向后推时用户将向前移动的距离。我们将其分配给名为 movement_forward 的变量。

我们对用户向右移动的距离进行类似的计算,相对于存储在``right_direction``中的正确方向。为了计算用户向右移动的距离,我们将``right_direction``乘以``movement_vector.y``、delta``和``MOVEMENT_SPEED。这将给我们提供当触控板/操纵杆被向右或向左推时,用户将向右移动的距离。我们将其分配给一个名为``movement_right``的变量。

接下来,我们将 "向前移动 "和 "向右移动 "的 "Y "轴上的任何移动,将它们的 "Y "值赋值为 "0"。我们这样做是为了让用户不能仅仅通过移动触控板或操纵杆来飞行/下降。如果不这样做,玩家可能会朝着他们所面对的方向飞行。

最后,我们检查``movement_right``或``movement_forward``上的``length``函数是否大于``0```。如果是,那么我们需要移动用户。要移动用户,我们使用``get_parent().global_translate``对:ref:ARVROrigin <class_ARVROrigin>`节点进行全局翻译,并将``movement_right``变量与``movement_forward``变量一起传递给它。这将使玩家沿着触控板/操纵杆指向的方向移动,相对于VR头显的旋转。我们还将``directional_movement``变量设置为``true`,这样代码就知道这个VR控制器在移动玩家。

如果``movement_right``或``movement_forward``上的``length``函数小于或等于``0``,那么我们只需将``directional_movement``变量设置为``false``,这样代码就知道这个VR控制器没有移动玩家。


这个功能最终要做的是接受VR控制器的触控板和操纵杆的输入,并按照玩家推动它们的方向移动。移动是相对于VR头显的旋转而言的,所以如果玩家向前推并向左转头,它们就会向左移动。

button_pressed function step-by-step explanation

这个函数检查刚刚按下的VR按钮是否等于本项目中使用的一个VR按钮。`button_index``变量是由:ref:`ARVRController <class_ARVRController>`中的`button_pressed``信号传递进来的,我们在``_ready``函数中连接了这个信号。

在这个项目中,我们要找的按钮只有三个:触发按钮、抓取/握持按钮和菜单按钮。

注解

您可能需要根据您的VR头显和控制器重新映射这些按钮索引值。本教程中的输入是Windows混合现实头盔的索引值。

首先我们检查``button_index``是否等于``15``,这应该映射到VR控制器的触发按钮。如果按下的按钮是触发按钮,那么就会调用``_on_button_pressed_trigger``函数。

如果``button_index``等于``2``,那么抓取按钮刚刚被按下。如果按下的是抓取按钮,则调用``_on_button_pressed_grab``函数。

最后,如果``button_index``等于``1``,则表示刚刚按下了菜单键,如果按下的是菜单键,则调用``_on_button_pressed_menu``函数。如果按下的按钮是菜单按钮,则调用``_on_button_pressed_menu``函数。

_on_button_pressed_trigger 函数的逐步说明

首先这个函数通过检查``held_object``是否等于``null``来检查VR控制器是否没有拿着。如果VR控制器没有持有任何东西,那么我们假设VR控制器上的触发按压是为了传送。然后我们确保``teleport_mesh.visible``等于``false``。我们用这个来判断另一个VR控制器是否在尝试传送,因为如果另一个VR控制器在传送,那么``teleport_mesh``将是可见的。

如果``teleport_mesh.visible``等于``false``,那么就可以用这个VR控制器进行远程传输。将``teleport_button_down``变量设置为``true``,teleport_mesh.visible``设置为true,``teleport_raycast.visible```设置为``true,将告诉``_physics_process``中的代码,这个VR控制器将进行传送,使``teleport_mesh``可见,这样用户就知道传送到哪里,并使``teleport_raycast``可见,这样玩家就可以用激光瞄准器来瞄准传送位置。


如果``held_object``不等于``null``,那么VR控制器就会持有一些东西。然后检查被持有的对象``held_object``是否扩展自``VR_Interactable_Rigidbody``类,虽然还没有做``VR_Interactable_Rigidbody``,但是将自定义一个``VR_Interactable_Rigidbody``的类,将在项目中所有基于特殊的或自定义的:ref:`RigidBody <class_RigidBody>`节点上使用。

小技巧

Don't worry, we will cover VR_Interactable_Rigidbody after this section!

如果``held_object``扩展自``VR_Interactable_Rigidbody``,那么我们就调用``interact``函数,这样,当触发器被按下,对象被VR控制器握住时,被握住的对象就可以做任何它应做的事情。

_on_button_pressed_grab function step-by-step explanation

首先,这个函数检查``teleport_button_down``是否等于``true``,如果是,则调用``return``,这样做是因为我们不希望用户在传送时能够拾取对象。

Then we check to see if the VR controller is currently not holding anything by checking if held_object is equal to null. If the VR controller is not holding anything, then the _pickup_rigidbody function is called. If the VR controller is holding something, held_object is not equal to null, then the _throw_rigidbody function is called.

Finally, the pick-up/drop sound is played by calling the play function on hand_pickup_drop_sound.

_pickup_rigidbody function step-by-step explanation

首先函数定义了一个名为``rigid_body``的变量,我们要用它来存储VR控制器要拾取的:ref:RigidBody <class_RigidBody>,假设要拾取一个RigidBody。


然后函数检查``grab_mode``变量是否等于``AREA``,如果等于,则使用``get_overlapping_bodies``函数获取``grab_area``内的所有 PhysicsBody <class_PhysicsBody>`节点。该函数将返回一个:ref:`PhysicsBody <class_PhysicsBody>`节点的数组。我们将:ref:`PhysicsBody <class_PhysicsBody>`的数组分配给一个新的变量``bodies`

然后我们检查``bodies``变量的长度是否大于``0``,如果是,我们使用for循环来检查``bodies``中的:ref:`PhysicsBody <class_PhysicsBody>`节点。

对于每个 PhysicsBody 节点,我们使用``if body is RigidBody``检查它是否是 RigidBody 节点,如果 .ref:PhysicsBody <class_PhysicsBody> 节点是 RigidBody 节点或其扩展,将返回``true``。如果对象是 RigidBody,那么我们检查在body中有没有定义一个名为``NO_PICKUP``的变量或常量。之所以这样做,是因为如果你想让:ref:`RigidBody <class_RigidBody>`节点无法被拾取,只需要定义一个名为``NO_PICKUP``的常量或变量,VR控制器就会无法拾取它。如果:ref:`RigidBody <class_RigidBody>`节点没有定义一个名称为``NO_PICKUP``的变量或常量,那么将``rigid_body``变量赋值给:ref:`RigidBody <class_RigidBody>`节点,并中断for循环。

这部分代码要做的是遍历``grab_area``内的所有物理实体,并获取第一个没有变量或常量 NO_PICKUPRigidBody 节点,并且将其分配给 rigid_body 变量,以便我们稍后可以在此函数中进行一些额外的后期处理。


如果 grab_mode 变量不等于 AREA 我们就检查它是否等于 RAYCAST 。如果它等于 RAYCAST ,我们就使用 force_raycast_update 函数强制更新 grab_raycast 节点。force_raycast_update 函数将用物理世界的最新变化更新 Raycast 。然后我们使用 is_colliding 函数检查 grab_raycast 节点是否与某物相撞,如果 Raycast 撞到了某物,则返回true。

如果 grab_raycast 命中了什么东西,我们就会使用 get_collider 函数得到 PhysicsBody 命中的节点。然后,代码使用 if body is RigidBody 检查节点是否是 RigidBody 节点,如果 PhysicsBody <class_PhysicsBody>`节点是 :ref:`RigidBody <class_RigidBody>`节点或扩展了 :ref:`RigidBody <class_RigidBody>`节点,代码将返回 ``true` 。然后代码检查 RigidBody 节点是否没有名为 NO_PICKUP 的变量,如果没有,则将 RigidBody 节点赋值给 rigid_body 变量。

What this section of code does is sends the grab_raycast Raycast node out and checks if it collided with a RigidBody node that does not have a variable/constant named NO_PICKUP. If it collided with a RigidBody without NO_PICKUP, it assigns the node to the rigid_body variable so we can do some additional post processing later in this function.


最后一段代码首先检查 rigid_body 是否不等于 null 。如果 rigid_body 不等于 null ,那么VR控制器找到了一个可以拾取的 RigidBody 类型节点。

如果有一个VR控制器要拾取,我们将 held_object 分配给存储在 rigid_body 中的 RigidBody 节点。然后将 RigidBody 节点的 modecollision_layercollision_maskmodelayermask 作为各自值的键存储在 held_object_data 中。这是为了以后当对象被VR控制器丢弃时,可以重新应用它们。

We then set the RigidBody's mode to MODE_STATIC, it's collision_layer to zero, and it's collision_mask to zero. This will make it where the held RigidBody cannot interact with other objects in the physics world when held by the VR controller.

接下来将 hand_mesh MeshInstance 通过设置 visible 属性为 false 而使 hand_mesh 变得不可见。这样,手就不会挡住所持对象的去路。同样 grab_raycast '激光瞄准器' 也是通过设置 visible 属性为 false 而变得不可见的。

然后,代码检查持有的对象是否扩展了一个叫做``VR_Interactable_Rigidbody``的类。如果是,那么就在``held_object``上设置``controller``的变量为``self``,然后在``held_object``上调用``picked_up```函数。虽然我们还没有做``VR_Interactable_Rigidbody``,但这样做的作用是通过调用``picked_up``函数,设置告诉``VR_Interactable_Rigidbody``类,它被一个VR控制器持有,控制器的引用存储在``controller``变量中。

小技巧

Don't worry, we will cover VR_Interactable_Rigidbody after this section!

在完成本系列教程的第二部分后,代码应该会更有意义,在那里我们将实际使用``VR_Interactable_Rigidbody``。

这段代码的作用是,如果使用抓取 Area,它就会将其设置为可以被VR控制器携带。

_throw_rigidbody function step-by-step explanation

首先,该函数通过检查``held_object``变量是否等于``null``来检查VR控制器是否没有持有任何对象。如果是,那么它只是调用``return``,所以什么都不会发生。虽然这应该是不可能的,但``_throw_rigidbody``函数应该只在对象被持有的情况下被调用,这种检查有助于确保如果发生了一些奇怪的事情,这个函数将按照预期的方式做出反应。

在检查VR控制器是否持有对象后,我们假设它是,并将存储的:ref:RigidBody <class_RigidBody>`数据设置回持有对象。我们将存储在``held_object_data``字典中的``mode```layer``和``mask``数据重新应用到``held_object``中的对象。这将把:ref:`RigidBody <class_RigidBody>`设置回被拾取之前的状态。

然后我们在``held_object``上调用``apply_impulse``,这样:ref:RigidBody <class_RigidBody>`就会被抛向VR控制器的速度方向,``controller_velocity`

然后,我们检查持有的对象是否扩展了一个叫做``VR_Interactable_Rigidbody``的类。如果是,那么我们在``held_object``中调用一个叫做``dropped``的函数,并将``held_object.controller``设置为``null``。虽然我们还没有做``VR_Interactable_Rigidbody``,但是这样做的目的是调用``droppped``函数,这样:ref:RigidBody <class_RigidBody>`就可以在drop时做任何需要做的事情,我们将``controller``变量设置为``null``,这样:ref:`RigidBody <class_RigidBody>`就知道它没有被持有。

小技巧

Don't worry, we will cover VR_Interactable_Rigidbody after this section!

在完成本系列教程的第二部分后,代码应该会更有意义,在那里我们将实际使用``VR_Interactable_Rigidbody``。

无论``held_object``是否扩展了``VR_Interactable_Rigidbody``,我们都要将``held_object``设置为``null``,这样VR控制器就知道它不再拿着任何东西了。因为VR控制器不再持有任何东西,所以我们将``hand_mesh.visible``设置为true,使``hand_mesh``可见。

最后,如果``grab_mode``变量设置为``RAYCAST``,我们将``grab_raycast.visible``设置为``true```,这样``grab_raycast``中的:ref:`Raycast <class_Raycast>``的'激光视线'是可见的。

_on_button_pressed_menu function step-by-step explanation

首先这个函数检查是否``grab_mode``变量等于``AREA``。如果是,则将“ grab_mode”设置为“ RAYCAST”。然后,它会检查该 VR控制器是否未持有任何东西,如果``held_object``等于``null``,则为未持有。如果该 VR控制器未持有任何东西,那么``grab_raycast.visible``的值被设置为``True``,则“激光瞄准器”的抓取光线投射可见。

如果 grab_mode 变量不等于``AREA``, 将会检查它是否等于``RAYCAST``. 如果是的话,这将把 grab_mode 设置为 AREA 并且将 grab_raycast.visible 设置为 false ,因此grab raycast上的"激光瞄准器"将会不可见。

This section of code simply changes how the VR controller will grab RigidBody-based nodes when the grab/grip button is pressed. If grab_mode is set to AREA, then the Area node in grab_area will be used for detecting RigidBody nodes, while if grab_mode is set to RAYCAST the Raycast node in grab_raycast will be used for detecting RigidBody nodes.

button_released function step-by-step explanation

这个函数中唯一的一段代码是检查刚刚释放的按钮的索引 button_index 是否等于 15 ,它应该映射到VR控制器上的触发按钮。 button_index 变量是由 ARVRController 中的 button_release 信号传递进来的,我们之前在``_ready``函数中连接了这个信号。

如果触发器按钮被松开, 那么 _on_button_released_trigger 函数将会被调用。

_on_button_released_trigger function step-by-step explanation

该函数中唯一的一段代码首先通过检查``teleport_button_down``变量是否等于``true``来检查VR控制器是否在尝试传送。

如果`teleport_button_down```变量等于``true``,代码就会检查是否设置了传送位置,以及传送网格是否可见。这是通过检查``teleport_pos``是否不等于``null``和``teleport_mesh.visible``是否等于``true```来实现的。

如果有一个传送位置设置,并且传送网格是可见的,那么代码就会计算从摄像机到:ref:ARVROrigin <class_ARVROrigin>`节点的偏移量,该节点被假定为VR控制器的父节点。为了计算偏移量,``Player_Camera``节点的全局位置(``global_transform.origin`)将会减去:ref:`ARVROrigin <class_ARVROrigin>`节点的全局位置。这将产生一个从 :ref:`ARVROrigin <class_ARVROrigin>`指向 :ref:`ARVRCamera <class_ARVRCamera>`的向量,我们将其存储在一个名为``camera_offset``的变量中。

我们之所以需要知道偏移量,是因为一些VR头显使用了房间追踪,玩家的摄像头可以从:ref:`ARVROrigin <class_ARVROrigin>`节点上进行偏移。正因为如此,当我们传送时,我们希望保留房间追踪产生的偏移,这样当玩家传送时,房间追踪产生的偏移就不会被应用。如果没有这个功能,如果你在一个房间里移动,然后传送,你的位置就不会出现在你想传送的位置,而是会被你与:ref:`ARVROrigin <class_ARVROrigin>`节点的距离所抵消。

现在我们知道了从VR摄像机到VR原点的偏移量,我们需要去除``Y``轴上的差异。我们之所以这样做,是因为我们不希望根据用户的身高进行偏移。如果不这样做的话,当传送时,玩家的头部将与地面持平。

然后,我们便可以通过将ARVROrigin节点的全局位置(global_transform.origin)设置为``teleport_pos``中存储的位置,并从中减去``camera_offset`` 来传送玩家。这将会在传送玩家的同时,移除房间跟踪的偏移。因此保证用户在传送时,将会出现在他们想要的地方。

最后,不管VR控制器是否对用户进行了传送,我们都要重置传送相关的变量。我们把 teleport_button_down```设置为``false`teleport_mesh.visible```设置为``false```以便让传送网格不可见,`teleport_raycast.visible```设置为``false``,然后把 teleport_pos```设置为``null``

sleep_area_entered function step-by-step explanation

这个函数中唯一的一段代码是检查进入``Sleep_Area``节点的:ref:PhysicsBody <class_PhysicsBody>`节点是否有一个叫``can_sleep``的变量。如果有,则将``can_sleep``变量设为``false`,并将``sleeping``变量设为``false``。

如果不这样做,睡眠状态的 PhysicsBody 节点将无法被 VR 控制器拿起,即使 VR 控制器与 PhysicsBody 节点处于同一位置。为了解决这个问题,我们只需 "唤醒 "靠近VR控制器的 :ref:`PhysicsBody <class_PhysicsBody>`节点即可。

sleep_area_exited function step-by-step explanation

这个函数中唯一的一段代码是检查进入``Sleep_Area``节点的:ref:PhysicsBody <class_PhysicsBody>`节点是否有一个叫``can_sleep``的变量。如果有,则将``can_sleep``变量设置为``true`

这将允许离开 Sleep_AreaRigidBody 节点再次睡眠,以便节省性能。


Okay, whew! That was a lot of code! Add the same script, VR_Controller.gd to the other VR controller scene so both VR controllers have the same script.

现在,我们只需要在测试项目之前再做一件事!现在我们引用了一个叫做``VR_Interactable_Rigidbody``的类,但是我们还没有定义它。虽然我们在本教程中不会使用``VR_Interactable_Rigidbody``,但我们还是要快速创建它,以便项目能够运行。

为可交互式VR对象创建基础类

Script 标签页仍然打开的情况下,创建一个新的GDScript,名为 VR_Interactable_Rigidbody.gd

小技巧

你可以在 Script 选项卡中通过点击 File -> New Script... 来新建一个GDScripts脚本。

Once you have VR_Interactable_Rigidbody.gd open, add the following code:

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

Let's quickly go through what this script.


首先,我们用 class_name VR_Interactable_Rigidbody 作为脚本的开头。这样做的目的是告诉Godot这个GDScript是一个新的类,叫做 VR_Interactable_Rigidbody 。这使我们可以将节点与其他脚本文件中的 VR_Interactable_Rigidbody 类进行比较,而不必直接加载脚本或做任何特殊的事情。我们可以像所有内置的Godot类一样对该类进行比较。

接下来是一个名为 controller 的类变量。 controller 将被用来保存对当前持有物品的VR控制器的引用。如果一个VR控制器没有持有物品,那么 controller 变量的值将为 null 。我们需要对VR控制器有一个引用的原因,是为了让被持有的物品能够访问VR控制器的特定数据,比如 controller_velocity

最后,我们有四个函数。 _ready 函数是由Godot定义的,我们要做的只是将其 pass 。因为在 VR_Interactable_Rigidbody 中,当对象被添加到场景中时,我们并不需要做什么。

The interact function is a stub function that will be called when the interact button on the VR controller, the trigger in this case, is pressed while the object is held.

小技巧

存根函数指的是一个定义了,但其中没有任何代码的函数。存根函数一般被设计成可以被覆盖或扩展。在这个项目中,我们使用了存根函数,因此在所有可交互的 RigidBody 对象中都将有一个一致的接口。

The picked_up and dropped functions are stub functions that will be called when the object is picked up and dropped by the VR controller.


这就是我们现在需要做的所有事情!在本系列教程的下一部分,我们将开始制作特殊的可交互的 RigidBody 对象。

Now that the base class is defined, the code in the VR controller should work. Go ahead and try the game again, and you should find you can teleport around by pressing the touch pad, and can grab and throw objects using the grab/grip buttons.

现在,您可能想尝试使用触控板和/或操纵杆移动,但**它可能会让您运动生病!**

One of the main reasons this can make you feel motion sick is because your vision tells you that you are moving, while your body is not moving. This conflict of signals can make the body feel sick. Let's add a vignette shader to help reduce motion sickness while moving in VR!

减少晕动病

注解

有很多方法可以减少VR中的晕动病,并且没有一种方法可以减少晕动病。 有关如何实施运动和减少晕动病的更多信息,请参阅Oculus开发人员中心的 这个页面

为了帮助减少移动时的晕车,我们将添加一个只有在游戏角色移动时才能看到的晕影效果。

首先,让我们迅速切换回 Game.tscn 。在 ARVROrigin 节点下有一个子节点叫 Movement_Vignette 。当玩家使用VR控制器移动时,这个节点将在VR头显上应用一个简单的晕影。这应该有助于减少晕动症。

Open up Movement_Vignette.tscn, which you can find in the Scenes folder. The scene is just a ColorRect node with a custom shader. Feel free to look at the custom shader if you want, it is just a slightly modified version of the vignette shader you can find in the Godot demo repository.

Let's write the code that will make the vignette shader visible when the player is moving. Select the Movement_Vignette node and create a new script called Movement_Vignette.gd. Add the following code:

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

Because this script is fairly brief, let's quickly go over what it does.

Explaining the vignette code

这里有两个类变量, controller_onecontroller_two 。这两个变量将分别保存对左、右VR控制器的引用。


In the _ready function first waits for four frames using yield. The reason we are waiting four frames is because we want to ensure the VR interface is ready and accessible.

等待之后,使用 ARVRServer.primary_interface 检索主VR接口,该接口被分配给一个名为 interface 的变量。然后代码会检查 interface 是否等于 null ,如果 interface 等于 null ,则使用 set_process 禁用 _process ,值为 false

如果 interface 不是 null ,那么我们将vignette shader的 rect_size 设置为VR视口的渲染大小,这样它就会占据整个屏幕。我们需要这样做,因为不同的VR头显有不同的分辨率和纵横比,所以需要相应地调整节点的大小,还要将vignette shader的 rect_position 设置为0,这样相对于屏幕的位置处于正确。

然后检索左和右VR控制器,并分别分配给 controller_onecontroller_two 变量。最后,通过将vignette shader的 visible 属性设置为 false ,使其默认为不可见。


_process 中,代码首先检查 controller_onecontroller_two 是否等于 null 。如果其中一个节点等于 null ,则调用 return 来退出函数,这样就不会发生任何事情。

然后代码会通过检查 controller_onecontroller_two 中的 directional_movement 是否等于 true 来检查VR控制器中的任何一个是否在使用触摸板/摇杆移动玩家。如果VR控制器中的任何一个在移动玩家,那么vignette shader将通过设置它的 visible 属性为 true 来使自己可见。


That is the whole script! Now that we have written the code, go ahead and try moving around with the trackpad and/or joystick. You should find that it is less motion sickness-inducing then before!

注解

As previously mentioned, there are plenty of ways to reduce motion sickness in VR. Check out this page on the Oculus Developer Center for more information on how to implement locomotion and reducing motion sickness.

最后的笔记

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

现在你已经拥有了完全可以工作的VR控制器,可以在环境中移动,并与基于 RigidBody 的对象进行交互。在本系列教程的下一部分,我们将创建一些特殊的,基于 RigidBody 的对象供玩家使用!

警告

你可以在Godot OpenVR GitHub仓库的Release标签中下载本系列教程的成品项目!