虚拟现实入门教程

介绍

../../_images/starter_vr_tutorial_sword.png

本教程将向您展示如何在Godot制作一个初学者虚拟现实游戏项目。

记住, 在制作虚拟现实内容时,最重要的一点是要正确调整资产规模。 !要做到这一点,可能需要大量的实践和迭代,但您可以做一些事情来简化:

  • 在虚拟现实中,1个单位通常被认为是1米。如果你按照这个标准设计你的资产,你可以省去很多麻烦。

  • 在三维建模程序中,查看是否有方法测量和使用真实世界的距离。在Blender中,可以使用MeasureIt加载项;在Maya中,可以使用Measure工具。

  • 你可以用一个工具来制作粗略的模型 Google Blocks ,然后在另一个三维建模程序中进行优化。

  • 经常测试,因为虚拟现实中的资产与平板电脑上的资产会有很大的不同!

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

  • 如何告诉Godot在虚拟现实中运行。

  • 如何制作一个移动玩家的远程传送系统。

  • 如何制作移动玩家的定向运动系统(移动)。

  • 如何使 RigidBody -基于上下车系统。

  • 如何制作可用于虚拟现实的各种物品。

注解

虽然初学者可以完成本教程,但强烈建议您完成 你的第一场比赛 ,如果你刚接触过Godot和/或游戏开发,并且有制作3D游戏的经验 之前 学习本系列教程。

本教程假设您有使用Godot编辑器的经验,有gdscript的基本编程经验,并且有基本的3D游戏开发经验。

另外,假设您有一个OpenVR就绪耳机和两个OpenVR就绪控制器!本教程是使用Windows10上的Windows混合现实耳机编写的,因此本教程是为使用该耳机编写的。它也在HTC VIVE上进行了测试。您可能需要调整代码以与其他虚拟现实耳机(如Oculus Rift)配合使用。

您可以在此处找到本教程的起始资源: VR_Starter_Tutorial_Start.zip

提供的初学者资源包含一些3D模型、声音和一些已经为本教程设置和配置的场景。

您可以随意使用这些资产!所有原始资产属于Godot社区,其他资产属于下列资产:

注解

The sky panorama was created by CGTuts (original source).

使用的字体是 Titillium-Regular ,并根据SIL开放字体许可证1.1版获得许可。

The audio used are from several different sources, all downloaded from the Sonnis #GameAudioGDC Bundle (license in PDF format). The folders where the audio files are stored have the same name as folders in the bundle.

这个 OpenVR加载项 由巴斯蒂安·奥利杰创建,并根据麻省理工学院的许可证发布。两者都能找到 on the Asset Libraryon GitHub .

其他所有内容都是原创的,并且是由TwistedTwigLeg为本教程创建的。它们是根据麻省理工学院的许可证发布的,所以您可以随意使用它们,无论您认为合适!

小技巧

您可以在本页底部找到完成的项目。

准备好一切

启动godot并打开包含在starter资产中的项目。

注解

虽然这些资源不一定需要使用本教程中提供的脚本,但它们将使教程更容易执行,因为在整个教程系列中我们将使用几个预处理场景。

首先,您可能会注意到已经有相当多的设置。这包括一个预构建的级别、放置在周围的几个实例场景、一些背景音乐和几个与GUI相关的 MeshInstances 节点。

您还可能注意到,与GUI相关的网格已经附加了一个脚本。这是用来显示里面的东西 Viewport 在网格上。如果你愿意的话,可以随意看看,但是本教程不会介绍如何使用 Viewport 制作3D图形用户界面的节点 MeshInstance 节点。

在我们开始编写代码之前,要注意的另一件事是 ARVROrigin 节点工作。它是如何工作的有点难以解释,特别是如果你以前从未使用过虚拟现实,但这里有一个要点:虚拟现实 ARVROrigin 节点是房间的中心点。如果没有房间比例跟踪,则 ARVROrigin 将直接位于播放器下方,但如果有房间比例跟踪,则 ARVROrigin 将是跟踪房间的中心。

注解

这有点简单,老实说,我对各种不同的虚拟现实耳机以及它们的工作方式还不够了解,无法给出更详细和完整的解释。像这样考虑: ARVROrigin 是虚拟现实世界的中心。如果有空间跟踪,玩家可以离开中心点, ARVROrigin 节点,但仅限于房间缩放轨迹。

如果您选择 ARVROrigin 节点,您可能会注意到世界比例设置为 1.4 . 这是因为我最初把世界弄得太大了,所以我需要稍微缩放一下虚拟现实播放器,使它们更适合这个世界。如前所述,保持天平相对恒定非常重要!

这里要注意的另一件事是我们如何在 ARVROrigin 节点。播放器摄像头是一个 ARVRCamera 表示玩家在游戏中的头部。这个 ARVRCamera 将被播放器的高度偏移,如果有房间跟踪,则相机也可以围绕三维空间移动,相对于 ARVROrigin . 这一点值得注意,尤其是在以后我们添加传送时。

注意有什么 ColorRect 调用的节点 Movement_Vignette . 这将是一个只有当玩家移动时才可见的渐晕图明暗器。我们将在虚拟现实中使用渐晕图明暗器来帮助减少运动病。它是一个孩子的原因 ARVROrigin 因为我们希望它能轻松地访问虚拟现实控制器。

最后要注意的是有两个 ARVRController 节点,这些节点表示三维空间中的左右控制器。安 ARVRController ID为1的是左手,而 ARVRController ID为2的是右手。

启动虚拟现实

首先,让我们把虚拟现实机抬起来走!同时 Game.tscn 打开,选择 Game 节点并生成一个名为 Game.gd . 添加以下代码:

extends Spatial

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

        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;
            GetViewport().Hdr = false;

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

为了让这个工作,你需要 OpenVR asset from the Asset Library . openvr资产包含在starter资产中,但可能会有更新的版本工作得更好,因此我强烈建议删除 addons 文件夹,然后转到资源库并下载最新版本。

完成之后,让我们快速回顾一下这个脚本的功能。

首先,我们从ARVR服务器找到一个虚拟现实接口。我们这样做是因为默认情况下,Godot不包含任何VR接口,而是公开API,所以任何人都可以与GDNative /C++进行AR/VR接口。接下来,我们检查是否找到OpenVR接口,然后初始化它。

假设初始化没有任何问题,然后我们将主 Viewport 通过设置 arvrtrue . 我们还将HDR设置为 false ,因为您不能在OpenVR中使用HDR。

然后,我们禁用v-sync并将目标fps设置为每秒90帧。大多数虚拟现实耳机的运行频率为90赫兹,由于游戏将同时显示在虚拟现实耳机和电脑显示器上,我们希望禁用v-sync并手动设置目标fps,因此电脑显示器不会将虚拟现实显示器拖至60 fps。

注解

还有一件事需要注意,物理fps也设置为90!这使得物理运行的帧速率与显示器相同,这使得虚拟现实中的事情看起来更平滑。

../../_images/starter_vr_tutorial_hands.png

完成后,继续玩游戏吧!如果一切顺利,你现在就能环顾世界了!如果你有一个带有房间追踪功能的虚拟现实耳机,你将能够在房间追踪功能允许的范围内移动。

对控制器进行编码

如果我们拍一部虚拟现实电影也许很有趣,但我们真正想做的不仅仅是站在那里看。目前,我们无法移动到房间跟踪边界之外(假设您的虚拟现实耳机有房间跟踪),我们无法与任何东西交互!让我们改变一下!

您可能已经注意到控制器后面有一对绿色和黑色的手。让我们为这些控制器编写代码,这将允许玩家在世界各地传送,并允许玩家抓取和释放 RigidBody 节点。

打开任意一个 Left_Controller.tscnRight_Controller.tscn . 请随意看看场景是如何设置的;只有几个要点需要指出。

首先,注意有几个 Raycast 节点。我们要用一个 Raycast 在游戏世界中传送 (Raycast )我们会用另一个来捡东西 (GrabCast )如果玩家正在使用 Raycast 用于拾取对象的节点。

另一个需要注意的是 Area 打电话 Area 那是手掌中的一个小球体。这将用于检测玩家在使用时可以用手捡起的物体。 Area 用于拾取对象的节点。

我们还有一个更大的 Area 打电话 Sleep_Area ,用于唤醒 RigidBody 手靠近时的节点。

选择根节点,或者 Left_ControllerRight_Controller 根据您选择的场景,并创建一个名为 VR_Controller.gd . 将以下内容添加到 VR_Controller.gd

extends ARVRController

onready var grab_area = $Area
onready var grab_raycast = $GrabCast
onready var grab_pos_node = $Grab_Pos
onready var hand_mesh = $Hand
onready var teleport_raycast = $RayCast

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_mode = "AREA"
var teleport_pos
var teleport_mesh
var teleport_button_down

const CONTROLLER_DEADZONE = 0.65

const MOVEMENT_SPEED = 1.5

var directional_movement = false

func _ready():
    teleport_mesh = get_tree().root.get_node("Game/Teleport_Mesh")
    teleport_button_down = false

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

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


func _physics_process(delta):

    if teleport_button_down:
        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


    # Controller velocity
    # --------------------
    if get_is_active():
        controller_velocity = Vector3(0, 0, 0)

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

            # Get the average velocity, instead of just adding them together.
            controller_velocity = controller_velocity / prior_controller_velocities.size()

        prior_controller_velocities.append((global_transform.origin - prior_controller_position) / delta)

        controller_velocity += (global_transform.origin - prior_controller_position) / delta
        prior_controller_position = global_transform.origin

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

    # --------------------

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


    # Directional movement
    # --------------------
    # NOTE: you may need to change this depending on which VR controllers
    # you are using and which OS you are on.
    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()

    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().translate(movement_right + movement_forward)
        directional_movement = true
    else:
        directional_movement = false
    # --------------------


func button_pressed(button_index):

    # If the trigger is pressed...
    if button_index == 15:
        if held_object:
            if held_object.has_method("interact"):
                held_object.interact()

        else:
            if not teleport_mesh.visible and not held_object:
                teleport_button_down = true
                teleport_mesh.visible = true
                teleport_raycast.visible = true


    # If the grab button is pressed...
    if button_index == 2:
        if teleport_button_down:
            return

        if not held_object:

            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 not "NO_PICKUP" in body:
                                rigid_body = body
                                break

            elif grab_mode == "RAYCAST":
                grab_raycast.force_raycast_update()
                if grab_raycast.is_colliding():
                    if grab_raycast.get_collider() is RigidBody and not "NO_PICKUP" in grab_raycast.get_collider():
                        rigid_body = grab_raycast.get_collider()


            if rigid_body:

                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.has_method("picked_up"):
                    held_object.picked_up()
                if "controller" in held_object:
                    held_object.controller = self


        else:

            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.has_method("dropped"):
                held_object.dropped()

            if "controller" in held_object:
                held_object.controller = null

            held_object = null
            hand_mesh.visible = true

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


        get_node("AudioStreamPlayer3D").play(0)


    # If the menu button is pressed...
    if button_index == 1:
        if grab_mode == "AREA":
            grab_mode = "RAYCAST"

            if not held_object:
                grab_raycast.visible = true
        elif grab_mode == "RAYCAST":
            grab_mode = "AREA"
            grab_raycast.visible = false


func button_released(button_index):

    # If the trigger button is released...
    if button_index == 15:

        if teleport_button_down:

            if teleport_pos and teleport_mesh.visible:
                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:
        body.can_sleep = true

这是一段很长的代码,所以让我们一点一点地把它分解。让我们从类变量开始,它是任何/所有函数之外的变量。

  • controller_velocity :控制器移动的速度。我们将通过每个物理框架的位置变化来计算这个。

  • prior_controller_position :控制器的上一个位置。我们将用这个来计算控制器的速度。

  • prior_controller_velocities :最后30个计算的速度(假设游戏以90 fps的速度运行,第二个速度值的1/3)。

  • held_object :当前持有的对象,a RigidBody ,如果有。

  • held_object_data :当前保留对象的数据,用于在不再保留对象时重置该对象。

  • grab_areaArea 用于抓取对象的节点。

  • grab_pos_node :放置物体的位置。

  • hand_mesh :手网,用于表示玩家的手,当他们不拿任何东西时。

  • teleport_pos :传送的位置 Raycast 瞄准了。

  • teleport_mesh :用于表示传送位置的网格。

  • teleport_button_down :跟踪传送按钮是否被按下的变量。

  • teleport_raycast :传送 Raycast 节点,用于计算远程传输位置。

  • CONTROLLER_DEADZONE :触摸板和操纵杆的死区。

  • MOVEMENT_SPEED :使用触摸板和/或操纵杆移动时播放机的移动速度。

  • directional_movement :使用此控制器跟踪播放机是否正在移动的布尔值。


接下来,让我们通过 _ready .

首先,我们得到传送 Raycast 节点并将其分配给 teleport_raycast .

接下来,我们得到传送网;注意我们是如何得到它的。 Game/Teleport_Mesh 使用 get_tree().root . 这是因为我们需要传送网格与控制器分离,所以移动和旋转控制器不会影响传送网格的位置和旋转。

然后我们到达抓取区,抓取 Raycast ,定位节点并将其分配给适当的变量。

我们将默认抓取模式设置为 AREA 所以它使用 Area 默认情况下用于抓取对象的节点。

然后我们连接 body_enteredbody_exited 信号来自睡眠区节点,我们得到手网格并分配给它适当的变量,最后我们连接 button_pressedbutton_released 来自的信号 ARVRController .


现在让我们通过 _physics_process .

首先,我们检查一下传送按钮是否关闭。如果传送按钮关闭,我们就强制传送 Raycast 更新,这将给我们帧完美的碰撞检测。然后我们检查 Raycast 与任何东西碰撞。

接下来,我们检查碰撞体是否 Raycast 与是碰撞 StaticBody . 我们这样做是为了确保玩家只能传送 StaticBody 节点。然后我们检查 Y 返回的值 Raycastget_collision_normal 函数大于 0.85 ,主要是指向正上方。这只允许玩家在相当平坦的面朝上传送。

如果所有这些都检查传送 Raycast 返回true,然后设置 teleport_pos 到了碰撞点,我们将传送网格移动到 teleport_pos .

接下来我们要检查的是 ARVRController 是否激活。如果 ARVRController 是活动的,这意味着有一个控制器并且正在被跟踪。如果控制器处于活动状态,我们将重置 controller_velocity 到一个空的 Vector3 .

然后我们将所有先前的速度计算添加到 prior_controller_velocitiescontroller_velocity . 通过前面的计算,我们获得了更流畅的投掷/接球体验,尽管这并不完美。我们想要得到这些速度的平均值,否则我们会得到疯狂的,不现实的高速数字。

接下来,我们从控制器当前的位置,从控制器所在的位置计算速度。我们可以利用这个位置差来帮助跟踪控制器的速度。

然后我们将控制器中的速度,这个物理帧和最后一个物理帧添加到 controller_velocity . 然后我们更新 prior_controller_position 到目前的位置,所以我们可以用它来计算下一个物理框架的速度。

注解

我们计算速度的方法并不完美,因为它依赖于每秒一致的帧数量。理想情况下,我们可以直接从虚拟现实控制器中找到速度,但目前在Open虚拟现实中,无法访问控制器的速度。但是,通过比较帧之间的位置,我们可以非常接近实际速度,这对于这个项目来说是很好的。

然后,我们检查是否有超过30个存储速度(超过三分之一秒)。如果超过30,我们就从 prior_controller_velocities .

接下来,我们检查是否有一个被扣留的物体。如果有的话,我们会将所持对象的位置和旋转更新为 grab_pos_node . 由于缩放的工作方式,我们需要临时存储缩放,然后在更新转换后重置缩放;否则,缩放将始终与控制器相同,如果玩家抓取缩放对象,这将破坏沉浸。

我们要做的最后一件事 _physics_process 移动虚拟现实控制器上的触摸板/操纵杆时移动播放器。

首先,我们将轴值转换为 Vector2 变量,所以我们可以处理它们。我们反转X轴,因此向左移动触摸板/操纵杆将使播放器向左移动。

注解

根据您的虚拟现实控制器和操作系统,您可能需要更改代码,以便获得正确的轴值!

接下来,我们将说明触摸板和操纵杆上的死区。执行此操作的代码改编自下面的链接,我强烈建议您查看它。

小技巧

你可以找到一篇很好的文章来解释操纵杆死区 on Third Helix .

有一件事需要注意,我们正在形成多大的死区。我们之所以使用如此大的死区,是因为玩家不能通过将手指放在触摸板/操纵杆的中心来意外移动自己,如果他们没有预料到,这会使玩家体验运动病。

接下来,我们从虚拟现实相机中得到正向和向右的方向向量。我们需要这些,这样我们就可以根据玩家当前的位置向前/向后和向右/向左移动。

然后,我们通过将轨迹板和操纵杆向量加在一起并对它们进行规格化来计算玩家的移动量。

接下来,我们通过将虚拟现实相机的方向向量乘以组合的轨迹板/操纵杆向量来计算播放器向前/向后和向右/向左的移动距离。

然后我们移除Y轴上的移动,这样玩家就不能通过使用触摸板/操纵杆移动来飞行/坠落。

最后,如果有向前/向后或向右/向左的移动,我们会移动玩家。如果我们移动玩家,我们就设置 directional_movement 因此。


现在,让我们看看 button_pressed .

如果按下的按钮是按钮15,这对于Windows混合现实控制器是触发按钮,我们将与持有的对象交互假设控制器持有一个,如果播放器不持有一个对象,我们将尝试开始传送。

如果控制器持有一个对象,并且持有的对象有一个调用的方法/函数 interact 我们称之为 interact 作用于被保持的对象。

如果控制器没有拿着一个物体,我们会检查确保隐形传送网格不可见。此项检查确保玩家不能同时用双手/控制器传送。如果隐形传送网不可见,我们设置 teleport_button_downtrue 制作 teleport_mesh 可见,并使远程传输光线投射可见。这使得隐形传送网将跟随 Raycast 来自手的指针。

如果按下的按钮是按钮2,对于Windows混合现实控制器,它是抓取/抓取按钮,我们将抓取/扔一个对象。

首先,我们要确保玩家不会尝试传送,因为我们不希望玩家在传送过程中能够抓到东西。

然后,我们检查控制器是否已经持有一个对象。

如果控制器没有固定一个物体,我们会检查玩家使用的抓取模式。

如果玩家正在使用 AREA 抓取模式,然后让所有的身体重叠抓取 Area . 我们把抓住的所有尸体都检查一遍 Area 看看有没有 RigidBody . 我们也会检查以确保 RigidBody 中的节点 Area 没有名为 NO_PICKUP ,因为我们不希望能够使用该变量拾取节点。

假设有一个 RigidBody 抓取内节点 Area 没有调用的变量 NO_PICKUP ,我们将其分配给 rigid_body 用于附加处理。

如果玩家正在使用 RAYCAST 抓取模式,我们先强制 Raycast 更新。然后我们检查 Raycast 与某物相撞。

如果 Raycast 与某物发生碰撞,然后我们检查它与之碰撞的是不是 RigidBody ,并且它没有一个名为 NO_PICKUP . 如果 Raycast 正在与 RigidBody ,并且它没有一个名为 NO_PICKUP ,我们将其分配给 rigid_body 用于附加处理。

如果 rigid_body 不是 null ,这意味着我们发现了 RigidBody 在抓取中 Area ,我们分配 held_object 为了它。然后我们储存现在持有的 RigidBody 的信息 held_object_data . 我们正在储存 RigidBody 模式、图层和遮罩,所以稍后,当我们删除它时,我们可以将所有这些变量重置为它们在我们获取之前的状态。 RigidBody .

然后,我们将所持物体的 RigidBody 模式到 MODE_STATIC 并将碰撞层和遮罩设置为0,这样它就不会与任何其他物理体碰撞。

我们使手网格不可见,这样它就不会妨碍我们所持的对象(也因为我不想为手设置动画)。我们也抓住了 Raycast 不可见,因此用于显示 Raycast 不再可见。

如果 RigidBody 我们接的有 picked_up 方法/函数,我们称之为。如果 RigidBody 我们得到了一个变量 controller ,我们将其设置为该控制器。

如果控制器没有固定住一个物体,并且按下的按钮是2,我们要放下/扔下固定的物体。

首先,我们设置了 RigidBody 的模式、图层和遮罩恢复到我们拾取对象时的状态。然后,我们用控制器的速度作为力,对被抓住的物体施加一个脉冲。

如果之前持有 RigidBody 有一个函数调用 dropped 我们称之为。如果 RigidBody 有一个变量调用 controller ,我们设置为 null .

然后,我们开始 held_objectnull ,因为我们不再持有任何对象,我们使手网格再次可见。

如果我们使用 RAYCAST 抓取模式,我们做 Raycast 可见,因此我们可以看到用于显示抓取的网格 Raycast .

最后,不管我们是抓住一个物体还是释放它,我们都会播放加载到 AudioStreamPlayer3D ,这是一种拾/落噪声。

我们做的最后一件事 button_pressed 正在检查按下的按钮是否为1,对于Windows混合现实控制器,这是菜单按钮。

如果按下菜单按钮,我们将更改抓取模式,并设置抓取的可见性。 Raycast 所以只有在使用 RAYCAST 作为抓取模式。


让我们来看一看 button_released 下一步。

如果释放的按钮是触发按钮15,那么我们可能想要传送。

首先,我们检查一下 teleport_button_downtrue . 如果是,这意味着玩家打算传送,而如果是 false ,玩家在持有物体时释放了触发器。

然后我们检查这个控制器是否有传送位置,我们检查以确保传送网格可见。

如果这两个条件都是 true 然后我们计算偏移量 ARVRCamera 来自 ARVROrigin . 我们这么做是因为 ARVRCameraARVROrigin 使用房间比例跟踪。

因为我们想将玩家在当前位置传送到传送位置,并且记住,由于房间比例跟踪,他们的当前位置可以从原点偏移,我们必须找出偏移量,这样当我们传送时,我们可以移除它,这样玩家的当前位置就是传送到传送位置。

我们将相机的Y值“偏移”设置为零,因为我们不想考虑玩家身高的偏移。

然后,我们传送 ARVROrigin 到传送位置,应用相机偏移。

不管我们是否传送,我们都会重置所有传送相关的变量,这样控制器就必须在再次传送之前获得新的变量。


最后,让我们看看 sleep_area_enteredsleep_area_exited .

当身体进入或存在睡眠区时,我们检查它是否有一个变量 can_sleep . 如果有,我们就把它设为 false 如果进入睡眠区,唤醒身体;如果退出,我们将其设置为 true 所以 RigidBody 节点可以休眠(这可以降低CPU使用率)。


好的,哇!那是很多代码!添加相同的脚本, VR_Controller.gd 以使两个控制器具有相同的脚本。

现在继续尝试游戏,你会发现你可以通过按触摸板来传送,还可以使用抓取/抓取按钮来抓取和投掷物体。

现在,您可能想尝试使用触摸板和/或操纵杆移动,但是 它可能会让你运动不适!

这会让你感到运动不适的一个主要原因是你的视觉告诉你你在运动,而你的身体不在运动。这种信号冲突会让身体感觉不舒服,所以我们来解决它吧!

减少运动病

注解

在虚拟现实中有很多方法可以减少运动病,而且没有一种完美的方法可以减少运动病。见 this page on the Oculus Developer Center 更多关于如何实施运动和减少运动病的信息。

为了减少移动时的晕车,我们将添加一个只有在玩家移动时才可见的小插页效果。

打开 Movement_Vignette.tscn ,您可以在 Scenes 文件夹。注意它只是一个 ColorRect 具有自定义明暗器的节点。如果您愿意的话,可以随意查看定制的明暗器,它只是您可以在Godot演示库中找到的vignette明暗器的一个稍微修改过的版本。

Movement_Vignette 选定,生成一个名为 Movement_Vignette.gd . 将以下代码添加到 Movement_Vignette.gd

extends ColorRect

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.get_primary_interface()

    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 not controller_one or not controller_two:
        return

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

因为这个脚本相当简短,让我们快速回顾一下它的作用。

_ready ,我们等待四帧。我们这样做是为了确保虚拟现实界面已经准备好并投入使用。

接下来,我们得到当前的虚拟现实界面,并调整 ColorRect 节点的大小和位置,以便在虚拟现实中覆盖整个视图。

然后,我们得到左右控制器,将它们分配给 controller_onecontroller_two .

然后默认情况下,我们将使小插曲不可见。

_process ,我们通过检查来查看是否有任何一个控制器正在移动播放器。 directional_movement . 如果任何一个控制器移动播放器,我们都会使小插曲可见,而如果两个控制器都不移动播放器,我们会使小插曲不可见。


完成后,继续尝试使用操纵杆和/或触摸板四处移动。你应该发现它比以前更不容易诱发运动病!

让我们加点特别的 RigidBody 我们可以与下一个节点交互。

添加可摧毁目标

首先,让我们从制造一些目标开始,我们将用各种特殊的方法以各种方式摧毁目标。 RigidBody 节点。

打开 Sphere_Target.tscn ,您可以在 Scenes 文件夹。 Sphere.tscn 只是一个 StaticBody 用一个 CollisionShape 一个网格和一个音频播放器。

选择 Sphere_Target 根节点, StaticBody 节点,并生成一个名为 Sphere_Target.gd . 将以下内容添加到 Sphere_Target.gd

extends StaticBody

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

var health = 80

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

func _ready():
    set_physics_process(false)

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


func damage(bullet_global_transform, damage):

    if destroyed:
        return

    health -= damage

    if health <= 0:

        $CollisionShape.disabled = true
        $Sphere_Target.visible = false

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

        destroyed = true
        set_physics_process(true)

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

让我们从类变量开始,来看看这个脚本是如何工作的。

  • destroyed :跟踪此目标是否被销毁的变量。

  • destroyed_timer :跟踪目标被摧毁时间的变量。

  • DESTROY_WAIT_TIME :一个常量,用于告诉球体目标在销毁/删除自身之前要等待多长时间。

  • health :目标的运行状况。

  • RIGID_BODY_TARGET :目标分成几个较小的 RigidBody 节点。


我们过去吧 _ready .

我们所做的一切 _ready 正在设置 _physics_processfalse . 这是因为我们只使用 _physics_process 为了摧毁目标,所以我们不想在目标被摧毁之前调用它。


下一步,让我们过去 _physics_process .

首先,我们增加时间 destroyed_timer . 然后我们检查是否已经过了足够的时间,我们可以摧毁目标。如果经过足够的时间,我们将使用 queue_free .


最后,让我们过去 damage .

首先,我们检查确保目标没有被摧毁。

然后,我们消除目标对健康造成的伤害。

如果目标的生命值为零或更低,那么它已经受到了足够的伤害。

首先,禁用碰撞形状,使整个目标网格不可见。接下来,我们生成/实例 RigidBody 目标的版本,并将其实例化到该目标的位置。

然后,我们开始 destroyedtrue 然后开始处理 _physics_process . 最后,我们播放一个声音,并从 Game.gd 通过呼叫 remove_sphere .


现在,您可能已经注意到我们正在调用 Game.gd 我们还没做,所以我们来解决这个问题吧!

首先,开放 Game.gd 并添加以下附加类变量:

var spheres_left = 10
var sphere_ui = null
  • spheres_left :游戏世界中剩余的球体目标数量。

  • sphere_ui :对球体用户界面的引用。我们稍后再使用!

接下来,我们需要添加 remove_sphere 功能。将以下内容添加到 Game.gd

func remove_sphere():
    spheres_left -= 1

    if sphere_ui:
        sphere_ui.update_ui(spheres_left)

这个函数的作用是从 spheres_left .

然后,它检查是否 sphere_ui 不为空,如果不为空,则调用其 update_ui 函数,传入剩余的球体数量。稍后我们将在本部分中添加UI代码。

现在我们有了可摧毁的目标,我们需要一种方法来摧毁它们!

添加手枪

好吧,我们加一把手枪。打开 Pistol.tscn ,您可以在 Scenes 文件夹找到。

这里有几点需要注意。首先要注意的是所有东西是如何旋转的。这是为了让玩家抓到手枪时手枪能正确旋转。另一件要注意的事情是如何有一个激光瞄准网,和一个闪光网;这两个都做你所期望的:分别作为激光指向器和枪口闪光。

另一个需要注意的是 Raycast 手枪末端的节点。这就是我们用来计算子弹撞击位置的方法。

既然我们已经看过了这个场景,我们来编写代码。选择 Pistol 根节点, RigidBody 节点,并生成一个名为 Pistol.gd . 将以下代码添加到 Pistol.gd

extends RigidBody

onready var flash_mesh = $Pistol_Flash
onready var laser_sight_mesh = $LaserSight
onready var raycast = $RayCast

const FLASH_TIME = 0.25
var flash_timer = 0

var BULLET_DAMAGE = 20

func _ready():
    flash_mesh.visible = false
    laser_sight_mesh.visible = false

func _physics_process(delta):
    if flash_timer > 0:
        flash_timer -= delta
        # If the flash has been visible enough, then make the flash mesh invisible.
        if flash_timer <= 0:
            flash_mesh.visible = false


# Called when the interact button is pressed while the object is held.
func interact():

    if flash_timer <= 0:

        flash_timer = FLASH_TIME
        flash_mesh.visible = true

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

            var body = raycast.get_collider()

            if body.has_method("damage"):
                body.damage(raycast.global_transform, BULLET_DAMAGE)
            elif body.has_method("apply_impulse"):
                var direction_vector = raycast.global_transform.basis.z.normalized()
                body.apply_impulse((raycast.global_transform.origin - body.global_transform.origin).normalized(), direction_vector * 1.2)

        $AudioStreamPlayer3D.play()


# Called when the object is picked up.
func picked_up():
    laser_sight_mesh.visible = true


# Called when the object is dropped.
func dropped():
    laser_sight_mesh.visible = false

让我们来看看这个脚本的作用,从类变量开始:

  • flash_mesh :用来使枪口闪光的网格。

  • FLASH_TIME :枪口闪光可见的时间长度。

  • flash_timer :跟踪枪口闪光可见时间的变量。

  • laser_sight_mesh :用于激光瞄准的长矩形网格。

  • raycast :用于手枪射击的光线投射节点。

  • BULLET_DAMAGE :单个子弹造成的伤害。


我们过去吧 _ready .

我们在这里所做的就是获取节点并将它们分配给适当的变量。我们还确保闪光和激光视距网格不可见。


接下来,让我们看看 _physics_process .

首先,我们检查闪光灯是否可见。我们通过检查 flash_timer 大于零。这是因为 flash_timer 将是一个倒计时,一个倒计时而不是倒计时。

如果 flash_timer 大于零,我们减去 delta 然后检查它是否等于零或小于零。如果是,我们会使闪光网格不可见。

这使得闪光网格在 FLASH_TIME 许多秒过去了。


现在,让我们看看 interact ,当按下虚拟现实控制器上的触发按钮,手枪被握住时调用。

首先,我们检查闪光定时器是否小于或等于零。这项检查使得我们在闪光可见时无法开火的地方,限制了手枪开火的频率。

如果我们能开火,我们就重置 flash_timer 设置为 FLASH_TIME 我们让闪光网可见。

然后我们更新 Raycast 检查它是否与任何东西碰撞。

如果 Raycast 撞到什么东西了,我们找到了对撞机。我们检查一下对撞机是否有 damage 函数,如果有,我们就称它为函数。如果没有,我们就检查一下对撞机是否有 apply_impulse 函数,如果它是这样的话,我们在从 Raycast 去对撞机。

最后,不管手枪是否打中什么东西,我们都会弹手枪的射击声。


最后,让我们看看 picked_updropped 当手枪被拾起和落下时分别称之为。

在这些功能中,我们所要做的就是在拿起手枪时使激光指向器可见,在放下手枪时使其不可见。


../../_images/starter_vr_tutorial_pistol.png

完成后,继续玩游戏吧!如果你爬上楼梯,拿上手枪,你应该能向球开火,球会碎的!

添加猎枪

让我们添加一种不同类型的武器 RigidBody :猎枪。这很简单,因为几乎所有的东西都和手枪一样。

打开 Shotgun.tscn ,您可以在中找到 Scenes . 注意所有的东西或多或少是一样的,但不是单一的 Raycast 有五个,没有激光指向器。这是因为猎枪通常以锥形发射,所以我们将通过使用几个 Raycast 节点,都是以锥形随机旋转的,我移除了激光指向器,这样玩家就可以在不知道猎枪指向哪里的情况下瞄准。

好的,选择 Shotgun 根节点, RigidBody 制作一个新的脚本 Shotgun.gd . 将以下内容添加到 Shotgun.gd

extends RigidBody

onready var flash_mesh = $Shotgun_Flash
onready var raycasts = $Raycasts

const FLASH_TIME = 0.25
var flash_timer = 0

var BULLET_DAMAGE = 30

func _ready():
    flash_mesh.visible = false

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


# Called when the interact button is pressed while the object is held.
func interact():

    if flash_timer <= 0:

        flash_timer = FLASH_TIME
        flash_mesh.visible = true

        for raycast in raycasts.get_children():

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

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

                var body = raycast.get_collider()

                # If the body has the damage method, then use that; otherwise, use apply_impulse.
                if body.has_method("damage"):
                    body.damage(raycast.global_transform, BULLET_DAMAGE)
                elif body.has_method("apply_impulse"):
                    var direction_vector = raycast.global_transform.basis.z.normalized()
                    body.apply_impulse((raycast.global_transform.origin - body.global_transform.origin).normalized(), direction_vector * 4)

        $AudioStreamPlayer3D.play()


func picked_up():
    pass


func dropped():
    pass

你可能已经注意到这几乎和手枪一模一样,而且确实如此,所以让我们来看看到底发生了什么变化。

  • raycasts :容纳全部五个节点的节点 Raycast 用于猎枪射击的节点。

_ready ,我们得到 Raycasts 节点,而不仅仅是一个 Raycast .

唯一的变化,除了 picked_updropped ,在中 interact .

现在我们逐一检查 Raycast 在里面 raycasts . 然后在x和z轴上旋转,使其在10到 -10 圆锥体。从那里,我们处理每一个 Raycast 就像我们单曲一样 Raycast 在手枪里,什么都没变,我们只做了五次,每次一次 Raycast 在里面 raycasts .


现在你也可以找到并发射猎枪了!猎枪位于其中一面墙后面的后面(但不在建筑物内!).

添加炸弹

虽然这两个都很好,但让我们添加下一个可以扔的东西——炸弹!

打开 Bomb.tscn ,您可以在 Scenes 文件夹。

首先,请注意 Area 节点。这是炸弹的爆炸半径。里面有什么 Area 当炸弹爆炸时会受到爆炸的影响。

另一个需要注意的是如何有两组 Particles :一个用于从保险丝中冒出烟雾,另一个用于爆炸本身。请随便看看 Particles 节点,如果你想!

唯一需要注意的是爆炸持续了多长时间 Particles 节点将持续,其生命周期为0.75秒。我们需要知道这一点,这样我们才能在爆炸结束时确定拆除炸弹的时间。 Particles .

好吧,现在让我们写下炸弹的代码。选择 Bomb RigidBody 节点并生成一个名为 Bomb.gd . 将以下代码添加到 Bomb.gd

extends RigidBody

onready var bomb_mesh = $Bomb
onready var explosion_area = $Area
onready var fuse_particles = $Fuse_Particles
onready var explosion_particles = $Explosion_Particles

const FUSE_TIME = 4
var fuse_timer = 0

var EXPLOSION_DAMAGE = 100
var EXPLOSION_TIME = 0.75
var explosion_timer = 0
var explode = false
var controller = null

func _ready():
    set_physics_process(false)

func _physics_process(delta):

    if fuse_timer < FUSE_TIME:

        fuse_timer += delta

        if fuse_timer >= FUSE_TIME:

            fuse_particles.emitting = false
            explosion_particles.one_shot = true
            explosion_particles.emitting = true
            bomb_mesh.visible = false

            collision_layer = 0
            collision_mask = 0
            mode = RigidBody.MODE_STATIC

            for body in explosion_area.get_overlapping_bodies():
                if body == self:
                    pass
                else:
                    if body.has_method("damage"):
                        body.damage(global_transform.looking_at(body.global_transform.origin, Vector3(0, 1, 0)), EXPLOSION_DAMAGE)
                    elif body.has_method("apply_impulse"):
                        var direction_vector = body.global_transform.origin - global_transform.origin
                        body.apply_impulse(direction_vector.normalized(), direction_vector.normalized() * 1.8)

            explode = true
            $AudioStreamPlayer3D.play()


    if explode:

        explosion_timer += delta
        if explosion_timer >= EXPLOSION_TIME:

            explosion_area.monitoring = false

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

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

            queue_free()


func interact():
    set_physics_process(true)
    fuse_particles.emitting = true


func picked_up():
    pass

func dropped():
    pass

让我们来看看这个脚本的功能,从类变量开始:

  • bomb_meshMeshInstance 用于炸弹网。

  • FUSE_TIME :保险丝烧断的时间长度。

  • fuse_timer :用于跟踪保险丝燃烧时间的变量。

  • explosion_areaArea 用于检测爆炸中的节点的节点。

  • EXPLOSION_DAMAGE :爆炸造成的损坏量。

  • EXPLOSION_TIME :爆炸的时间长度 Particles 取(你可以通过乘以粒子来计算这个数字 lifetime 由其 speed scale

  • explosion_timer :用于跟踪爆炸持续时间的变量。

  • explode :用于跟踪炸弹是否爆炸的布尔值。

  • fuse_particles :保险丝 Particles 节点。

  • explosion_particles :爆炸 Particles 节点。

  • controller :当前持有炸弹的控制器(如果有)。这是由控制器设置的,因此我们不需要检查任何超出检查范围的内容。 null .


我们过去吧 _ready .

首先,我们获取所有的节点,并将它们分配给适当的变量以供以后使用。

然后,我们确定 _physics_process 不会被叫来的。我们这样做是因为我们将使用 _physics_process 只是为了引信和摧毁炸弹,所以我们不想过早触发,我们只想在玩家拿着炸弹进行互动时启动引信。


现在,让我们看看 _physics_process .

首先我们检查一下 fuse_timer 小于 FUSE_TIME .如果 fuse_timer 小于 FUSE_TIME 那么炸弹一定是烧断了导火索。

然后我们增加时间 fuse_timer 检查炸弹是否等得够久,烧穿了整个导火线。

如果炸弹等得够久,我们就得引爆炸弹。我们先阻止烟雾 Particles 从发射到爆炸 Particles 发出。我们还隐藏炸弹网,使其不再可见。

接下来,我们将碰撞层和遮罩设置为零,并将 RigidBody 模式变为静态。这使得现在爆炸的原子弹无法与物理世界相互作用,因此它将保持原位。

然后,我们要检查爆炸内部的一切 Area . 我们要确保爆炸中的尸体 Area 不是炸弹本身,因为我们不想用它自己引爆炸弹。然后我们检查尸体是否有 damage 方法/函数,如果有,我们调用它,如果没有,我们检查它是否有 apply_impulse 方法/函数,并改为调用它。

然后,我们开始 explode 这是真的,因为炸弹爆炸了,我们会播放一个声音。

接下来,我们检查炸弹是否爆炸,因为我们需要等到爆炸发生。 Particles 完成了。

如果炸弹爆炸,我们会增加时间 explosion_timer . 然后我们检查爆炸情况 Particles 完成了。如果是的话,我们就引爆了 Area 的监视属性 false 为了确保我们不会在调试程序中发现任何错误,我们让控制器放下炸弹,如果它抓住了它,我们就进行抓取。 Raycast 如果抓取模式为 RAYCAST 我们用 queue_free .


最后,让我们看看 interact .

我们要做的就是把它放在哪里 _physics_process 将被调用,这将启动保险丝。我们也做保险丝 Particles 开始散发,所以烟从炸弹顶部冒出来。


完成后,炸弹就准备好了!你可以在橙色的大楼里找到它们。因为我们是如何计算速度的,所以与更自然的投掷运动相比,以类似信任的方式投掷炸弹是最容易的。像投掷一样运动的平滑曲线很难跟踪,而且由于我们跟踪速度的方式,它并不总是有效的。

添加一把剑

最后,让我们添加一把剑,这样我们可以切开东西!

打开 Sword.tscn ,您可以在中找到 Scenes .

这里没有什么值得注意的,但只有一件事,那就是剑的刀刃长度是如何被分成几个小的 Area 节点。这是因为我们需要大致了解剑在刀刃上的碰撞位置,这是我能想到的最简单(也是唯一)的方法。

小技巧

如果你知道如何找到 Area 和A CollisionObject 会见,请让我知道和/或做一个Godot文件公关!这种方法使用几个小 Area 节点工作正常,但不理想。

除此之外,实际上没有太多的注释,所以让我们编写代码。选择 Sword 根节点, RigidBody 制作一个新的脚本 Sword.gd . 将以下代码添加到 Sword.gd

extends RigidBody

const SWORD_DAMAGE = 20

var controller

func _ready():
    $Damage_Area_01.connect("body_entered", self, "body_entered_sword", ["01"])
    $Damage_Area_02.connect("body_entered", self, "body_entered_sword", ["02"])
    $Damage_Area_03.connect("body_entered", self, "body_entered_sword", ["03"])
    $Damage_Area_04.connect("body_entered", self, "body_entered_sword", ["04"])


# Called when the interact button is pressed while the object is held.
func interact():
    pass


# Called when the object is picked up.
func picked_up():
    pass


# Called when the object is dropped.
func dropped():
    pass


func body_entered_sword(body, number):
    if body == self:
        pass
    else:

        var sword_part = null
        if number == "01":
            sword_part = get_node("Damage_Area_01")
        elif number == "02":
            sword_part = get_node("Damage_Area_02")
        elif number == "03":
            sword_part = get_node("Damage_Area_03")
        elif number == "04":
            sword_part = get_node("Damage_Area_04")

        if body.has_method("damage"):
            body.damage(sword_part.global_transform.looking_at(body.global_transform.origin, Vector3(0, 1, 0)), SWORD_DAMAGE)

            get_node("AudioStreamPlayer3D").play()

       elif body.has_method("apply_impulse"):

            var direction_vector = sword_part.global_transform.origin - body.global_transform.origin

            if not controller:
                body.apply_impulse(direction_vector.normalized(), direction_vector.normalized() * self.linear_velocity)
            else:
                body.apply_impulse(direction_vector.normalized(), direction_vector.normalized() * controller.controller_velocity)

            $AudioStreamPlayer3D.play()

让我们从两个类变量开始,回顾一下这个脚本的功能:

  • SWORD_DAMAGE :单个剑片造成的伤害。

  • controller :持有剑的控制器,如果有。这是由控制器设置的,所以我们不需要在这里设置,在 Sword.gd .


我们过去吧 _ready 下一步。

我们在这里所做的就是连接 Area 结点 body_entered 向发送信号 body_entered_sword 函数,传入一个附加参数,该参数将是损坏的数量 Area 所以我们可以弄清楚尸体在剑上的碰撞点。


现在让我们过去 body_entered_sword .

首先,我们要确保剑与之碰撞的身体不是它本身。

然后我们用传入的数字计算出剑的哪一部分与身体相撞。

接下来,我们检查一下剑与之碰撞的身体是否有 damage 函数,如果它可以,我们就称它为并播放声音。

如果它没有损坏功能,那么我们检查它是否具有 apply_impulse 功能。如果是这样的话,我们就可以计算出物体与剑的碰撞方向。然后我们检查剑是否被持有。

如果剑不被持有,我们使用 RigidBody 力的速度 apply_impulse 当剑被握住时,我们用控制器的速度作为冲力。

最后,我们播放一个声音。


../../_images/starter_vr_tutorial_sword.png

完成后,你现在可以切开目标了!你可以在猎枪和手枪之间的角落里找到那把剑。

更新目标用户界面

好吧,让我们在球体目标被破坏时更新UI。

打开 Game.tscn 然后展开 GUI MeshInstance . 从那里,展开 GUI Viewport 节点,然后选择 Base_Control 节点。添加名为的新脚本 Base_Control ,并添加以下内容:

extends Control

var sphere_count_label = $Label_Sphere_Count

func _ready():
    get_tree().root.get_node("Game").sphere_ui = self

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

让我们看一下这个脚本的作用。

第一,在 _ready ,我们得到 Label 它显示还剩多少个球体并将其分配给 sphere_count_label 类变量。接下来,我们 Game.gd 通过使用 get_tree().root 并赋值 sphere_ui 到这个脚本。

update_ui 我们改变了球体 Label 的文本。如果还有至少一个球体,我们将更改文本以显示世界上还有多少个球体。如果没有多余的球体,我们将更改文本并祝贺玩家。

添加最终的特殊刚体

最后,在我们完成本教程之前,让我们添加一种在虚拟现实中重置游戏的方法。

打开 Reset_Box.tscn ,您可以在中找到 Scenes . 选择 Reset_Box RigidBody 节点并生成一个名为 Reset_Box.gd . 将以下代码添加到 Reset_Box.gd

extends RigidBody

var start_transform

var reset_timer = 0
const RESET_TIME = 120


func _ready():
    start_transform = global_transform


func _physics_process(delta):
    reset_timer += delta
    if reset_timer >= RESET_TIME:
        global_transform = start_transform
        reset_timer = 0


# Called when the interact button is pressed while the object is held.
func interact():
    get_tree().change_scene("res://Game.tscn")


# Called when the object is picked up.
func picked_up():
    pass


# Called when the object is dropped.
func dropped():
    global_transform = start_transform
    reset_timer = 0

我们来看看这是怎么回事。

首先,我们得到了全球首创的 Transform 在里面 _ready ,并将其分配给 start_transform . 我们会经常用这个来重置重置框的位置。

_physics_process ,我们检查是否有足够的时间来重置。如果有,我们重新设置盒子的 Transform 然后重置计时器。

如果玩家在按住重置框时进行交互,我们将通过调用 get_tree().change_scene 并通过路径到达当前场景。这将完全重置/重新启动场景。

当重置框丢失时,我们重置 Transform 还有计时器。


完成后,当您抓取并与重置框交互时,整个场景将重置/重新启动,您可以再次摧毁所有目标!

最后的注释

../../_images/starter_vr_tutorial_sword.png

唷!这是很多工作。现在你有一个虚拟现实项目!

警告

如果你迷路了,一定要再读一遍代码!

您可以在此处下载此部分的已完成项目: VR_Starter_Tutorial_Complete.zip

希望这能作为在Godot制作全功能虚拟现实游戏的介绍!这里编写的代码可以扩展到制作益智游戏、动作游戏、基于故事的游戏等等!