VR 入门教程第 1 部分

前言

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

本教程将告诉你如何在Godot中制作一个初级的VR游戏项目.

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

  • 在 VR 中,1 个单位通常被认为是 1 米。如果你围绕这个标准设计你的素材,就可以为自己省去很多麻烦。

  • 在你的三维建模程序中, 看看是否有办法测量和使用现实世界的距离. 在Blender中, 你可以使用MeasureIt插件;在Maya中, 你可以使用测量工具.

  • 您可以使用 Google Blocks 等工具制作粗略模型,然后在其他 3D 建模程序中进行优化。

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

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

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

  • 如何制作一个使用VR控制器的传送运动系统.

  • 如何制作一个使用VR控制器的人工运动的运动系统.

  • 如何创建一个 RigidBody 的系统, 允许使用VR控制器拿起, 放下和投掷RigidBody节点.

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

  • 如何创建一些特殊的 RigidBody 的对象, 可以摧毁目标.

小技巧

虽然本教程可以由初学者完成,但如果你是 Godot 和/或游戏开发的新手,强烈建议你完成 您的第一个 2D 游戏

阅读这个系列的教程需要有一定制作 3D 游戏的经验。本教程假设你有 Godot 编辑器、GDScript 和基本 3D 游戏开发的经验。需要连接一个 OpenVR 头戴设备和两个 OpenVR 控制器。

本教程是使用Windows混合现实头盔和控制器编写和测试的. 这个项目也在HTC Vive上进行了测试. 对于其他VR头盔, 如Oculus Rift, 可能需要调整代码.

本教程的 Godot 项目可以在 OpenVR GitHub 仓库找到。本教程的初始素材可以在 GitHub 仓库的 Release 部分找到。初始素材包含一些 3D 模型、声音、脚本,和为本教程配置的场景。

备注

所提供素材的致谢名单

  • 全景天空由 CGTuts 创建。

  • 使用的字体是 Titillium-Regular

    • 该字体使用 1.1 版本的 SIL 开放字体许可
  • 音频来自许多不同的地方,都是从 Sonniss #GameAudioGDC Bundle 下载的(许可协议 PDF

    • 存储音频文件的文件夹与 Sonniss 音频包中的文件夹名称相同。
  • OpenVR 插件由 Bastiaan Olij 创建,使用 MIT 许可协议发布。你可以在 Godot 素材库GitHub 上找到。OpenVR 插件中所使用的第三方代码和库可能使用的是不同的许可协议。

  • 最初的项目、3D 模型、脚本等由 TwistedTwigleg 创建,使用 MIT 许可协议发布。

小技巧

你可以在 OpenVR GitHub 仓库上找到完成的项目。

做好准备

如果还没有准备好,请在 OpenVR GitHub 仓库的 Releases(发布)中下载“Starter Assets”(初始素材)。下载好初始素材后,请在 Godot 中打开该项目。

备注

使用本教程中提供的脚本并不需要用到初始素材。初始素材中包含了几个预制场景和脚本,将在本教程中陆续使用。

当项目第一次加载时,Game.tscn场景将被打开. 这将是本教程使用的主要场景. 它包括已经放置在整个场景中的几个节点和场景, 一些背景音乐, 以及几个与GUI相关的 MeshInstance 节点.


与GUI相关的 MeshInstance 节点已经有脚本附加在它们身上. 这些脚本将把 Viewport 节点的纹理设置为 MeshInstance 节点的材质的反射纹理. 这被用来在VR项目中显示文本. 如果你想的话, 可以看一下这个脚本, GUI.gd . 在本教程中, 我们不会讨论如何使用 Viewport 节点在 MeshInstance 节点上显示用户界面.

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


在我们进入教程之前, 让我们花点时间谈谈用于VR的节点如何工作.

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

ARVRCamera 是玩家的头显和进入场景的视角. ARVRCamera 在Y轴上的偏移量为VR用户的身高, 这在后面我们添加传送定位时很重要. 如果VR系统支持房间追踪, 那么 ARVRCamera 将随着玩家的移动而移动. 这意味着 ARVRCamera 并不能保证与 ARVROrigin 节点处于同一位置.

ARVRController 节点代表一个VR控制器. ARVRController 将跟随VR控制器相对于 ARVROrigin 节点的位置和旋转. 所有的VR控制器的输入都是通过 ARVRController 节点进行的. 一个 ARVRController 节点的 ID1 , 代表左边的VR控制器, 而一个 ARVRController 控制器的 ID2 , 代表右边的VR控制器.

总而言之:

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

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

  • ARVRCamera 节点在Y轴上的偏移量为用户的高度.

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

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

启动 VR

现在我们已经看过了VR节点, 让我们开始在项目中工作. 在 Game.tscn 中, 选择 Game 节点, 制作一个新的脚本, 名为 Game.gd . 在 Game.gd 文件中, 添加以下代码:

GDScriptC#

  1. extends Spatial
  2. func _ready():
  3. var VR = ARVRServer.find_interface("OpenVR")
  4. if VR and VR.initialize():
  5. get_viewport().arvr = true
  6. OS.vsync_enabled = false
  7. Engine.target_fps = 90
  8. # Also, the physics FPS in the project settings is also 90 FPS. This makes the physics
  9. # run at the same frame rate as the display, which makes things look smoother in VR!
  1. using Godot;
  2. using System;
  3. public class Game : Spatial
  4. {
  5. public override void _Ready()
  6. {
  7. var vr = ARVRServer.FindInterface("OpenVR");
  8. if (vr != null && vr.Initialize())
  9. {
  10. GetViewport().Arvr = true;
  11. OS.VsyncEnabled = false;
  12. Engine.TargetFps = 90;
  13. // Also, the physics FPS in the project settings is also 90 FPS. This makes the physics
  14. // run at the same frame rate as the display, which makes things look smoother in VR!
  15. }
  16. }
  17. }

让我们回顾一下这段代码的作用.


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

备注

Godot 中默认不包含 OpenVR 的 VR 接口。你需要从素材库GitHub 下载 OpenVR 素材。

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

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

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

备注

在项目设置中, 在 Physics->Common 标签下, 物理FPS已经被设置为 90 . 这使得物理引擎以与VR显示器相同的帧率运行, 使得物理反应在VR中看起来更加平滑.


这就是我们需要为Godot在项目中启动OpenVR所做的全部工作, 如果你想的话, 就去试一试吧. 假设一切正常, 你将能够环视这个世界. 如果你有一个带有房间追踪功能的VR头盔, 那么你将能够在房间追踪范围内在场景中移动.

创建控制器

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

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

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

如何设置VR控制器的场景

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

备注

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

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

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

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

接下来是一个名为 Grab_PosPosition3D 节点. 这是用来定义抓取的 RigidBody 节点跟随的位置, 它们被VR控制器持有.

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

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

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

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

VR控制器的代码

选择场景的根节点, Right_ControllerLeft_Controller , 然后制作一个新的脚本, 叫做 VR_Controller.gd . 两个场景将使用同一个脚本, 所以你先用哪个并不重要. 打开 VR_Controller.gd , 添加以下代码:

小技巧

你可以直接将本页的代码复制并粘贴到脚本编辑器中.

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

要在脚本编辑器中把空格转换成制表符, 请点击 Edit 菜单, 选择 Convert Indent To Tabs 把缩进转换成制表符. 这将把所有的空格转换为制表符. 你可以选择 Convert Indent To Spaces 来将制表符转换为空格.

GDScript

  1. extends ARVRController
  2. var controller_velocity = Vector3(0,0,0)
  3. var prior_controller_position = Vector3(0,0,0)
  4. var prior_controller_velocities = []
  5. var held_object = null
  6. var held_object_data = {"mode":RigidBody.MODE_RIGID, "layer":1, "mask":1}
  7. var grab_area
  8. var grab_raycast
  9. var grab_mode = "AREA"
  10. var grab_pos_node
  11. var hand_mesh
  12. var hand_pickup_drop_sound
  13. var teleport_pos = Vector3.ZERO
  14. var teleport_mesh
  15. var teleport_button_down
  16. var teleport_raycast
  17. # A constant to define the dead zone for both the trackpad and the joystick.
  18. # See https://web.archive.org/web/20191208161810/http://www.third-helix.com/2013/04/12/doing-thumbstick-dead-zones-right.html
  19. # for more information on what dead zones are, and how we are using them in this project.
  20. const CONTROLLER_DEADZONE = 0.65
  21. const MOVEMENT_SPEED = 1.5
  22. const CONTROLLER_RUMBLE_FADE_SPEED = 2.0
  23. var directional_movement = false
  24. func _ready():
  25. # Ignore the warnings the from the connect function calls.
  26. # (We will not need the returned values for this tutorial)
  27. # warning-ignore-all:return_value_discarded
  28. teleport_raycast = get_node("RayCast")
  29. teleport_mesh = get_tree().root.get_node("Game/Teleport_Mesh")
  30. teleport_button_down = false
  31. teleport_mesh.visible = false
  32. teleport_raycast.visible = false
  33. grab_area = get_node("Area")
  34. grab_raycast = get_node("Grab_Cast")
  35. grab_pos_node = get_node("Grab_Pos")
  36. grab_mode = "AREA"
  37. grab_raycast.visible = false
  38. get_node("Sleep_Area").connect("body_entered", self, "sleep_area_entered")
  39. get_node("Sleep_Area").connect("body_exited", self, "sleep_area_exited")
  40. hand_mesh = get_node("Hand")
  41. hand_pickup_drop_sound = get_node("AudioStreamPlayer3D")
  42. connect("button_pressed", self, "button_pressed")
  43. connect("button_release", self, "button_released")
  44. func _physics_process(delta):
  45. if rumble > 0:
  46. rumble -= delta * CONTROLLER_RUMBLE_FADE_SPEED
  47. if rumble < 0:
  48. rumble = 0
  49. if teleport_button_down == true:
  50. teleport_raycast.force_raycast_update()
  51. if teleport_raycast.is_colliding():
  52. if teleport_raycast.get_collider() is StaticBody:
  53. if teleport_raycast.get_collision_normal().y >= 0.85:
  54. teleport_pos = teleport_raycast.get_collision_point()
  55. teleport_mesh.global_transform.origin = teleport_pos
  56. if get_is_active() == true:
  57. _physics_process_update_controller_velocity(delta)
  58. if held_object != null:
  59. var held_scale = held_object.scale
  60. held_object.global_transform = grab_pos_node.global_transform
  61. held_object.scale = held_scale
  62. _physics_process_directional_movement(delta);
  63. func _physics_process_update_controller_velocity(delta):
  64. controller_velocity = Vector3(0,0,0)
  65. if prior_controller_velocities.size() > 0:
  66. for vel in prior_controller_velocities:
  67. controller_velocity += vel
  68. controller_velocity = controller_velocity / prior_controller_velocities.size()
  69. var relative_controller_position = (global_transform.origin - prior_controller_position)
  70. controller_velocity += relative_controller_position
  71. prior_controller_velocities.append(relative_controller_position)
  72. prior_controller_position = global_transform.origin
  73. controller_velocity /= delta;
  74. if prior_controller_velocities.size() > 30:
  75. prior_controller_velocities.remove(0)
  76. func _physics_process_directional_movement(delta):
  77. var trackpad_vector = Vector2(-get_joystick_axis(1), get_joystick_axis(0))
  78. var joystick_vector = Vector2(-get_joystick_axis(5), get_joystick_axis(4))
  79. if trackpad_vector.length() < CONTROLLER_DEADZONE:
  80. trackpad_vector = Vector2(0,0)
  81. else:
  82. trackpad_vector = trackpad_vector.normalized() * ((trackpad_vector.length() - CONTROLLER_DEADZONE) / (1 - CONTROLLER_DEADZONE))
  83. if joystick_vector.length() < CONTROLLER_DEADZONE:
  84. joystick_vector = Vector2(0,0)
  85. else:
  86. joystick_vector = joystick_vector.normalized() * ((joystick_vector.length() - CONTROLLER_DEADZONE) / (1 - CONTROLLER_DEADZONE))
  87. var forward_direction = get_parent().get_node("Player_Camera").global_transform.basis.z.normalized()
  88. var right_direction = get_parent().get_node("Player_Camera").global_transform.basis.x.normalized()
  89. # Because the trackpad and the joystick will both move the player, we can add them together and normalize
  90. # the result, giving the combined movement direction
  91. var movement_vector = (trackpad_vector + joystick_vector).normalized()
  92. var movement_forward = forward_direction * movement_vector.x * delta * MOVEMENT_SPEED
  93. var movement_right = right_direction * movement_vector.y * delta * MOVEMENT_SPEED
  94. movement_forward.y = 0
  95. movement_right.y = 0
  96. if movement_right.length() > 0 or movement_forward.length() > 0:
  97. get_parent().global_translate(movement_right + movement_forward)
  98. directional_movement = true
  99. else:
  100. directional_movement = false
  101. func button_pressed(button_index):
  102. if button_index == 15:
  103. _on_button_pressed_trigger()
  104. if button_index == 2:
  105. _on_button_pressed_grab()
  106. if button_index == 1:
  107. _on_button_pressed_menu()
  108. func _on_button_pressed_trigger():
  109. if held_object == null:
  110. if teleport_mesh.visible == false:
  111. teleport_button_down = true
  112. teleport_mesh.visible = true
  113. teleport_raycast.visible = true
  114. else:
  115. if held_object is VR_Interactable_Rigidbody:
  116. held_object.interact()
  117. func _on_button_pressed_grab():
  118. if teleport_button_down == true:
  119. return
  120. if held_object == null:
  121. _pickup_rigidbody()
  122. else:
  123. _throw_rigidbody()
  124. hand_pickup_drop_sound.play()
  125. func _pickup_rigidbody():
  126. var rigid_body = null
  127. if grab_mode == "AREA":
  128. var bodies = grab_area.get_overlapping_bodies()
  129. if len(bodies) > 0:
  130. for body in bodies:
  131. if body is RigidBody:
  132. if !("NO_PICKUP" in body):
  133. rigid_body = body
  134. break
  135. elif grab_mode == "RAYCAST":
  136. grab_raycast.force_raycast_update()
  137. if grab_raycast.is_colliding():
  138. var body = grab_raycast.get_collider()
  139. if body is RigidBody:
  140. if !("NO_PICKUP" in body):
  141. rigid_body = body
  142. if rigid_body != null:
  143. held_object = rigid_body
  144. held_object_data["mode"] = held_object.mode
  145. held_object_data["layer"] = held_object.collision_layer
  146. held_object_data["mask"] = held_object.collision_mask
  147. held_object.mode = RigidBody.MODE_STATIC
  148. held_object.collision_layer = 0
  149. held_object.collision_mask = 0
  150. hand_mesh.visible = false
  151. grab_raycast.visible = false
  152. if held_object is VR_Interactable_Rigidbody:
  153. held_object.controller = self
  154. held_object.picked_up()
  155. func _throw_rigidbody():
  156. if held_object == null:
  157. return
  158. held_object.mode = held_object_data["mode"]
  159. held_object.collision_layer = held_object_data["layer"]
  160. held_object.collision_mask = held_object_data["mask"]
  161. held_object.apply_impulse(Vector3(0, 0, 0), controller_velocity)
  162. if held_object is VR_Interactable_Rigidbody:
  163. held_object.dropped()
  164. held_object.controller = null
  165. held_object = null
  166. hand_mesh.visible = true
  167. if grab_mode == "RAYCAST":
  168. grab_raycast.visible = true
  169. func _on_button_pressed_menu():
  170. if grab_mode == "AREA":
  171. grab_mode = "RAYCAST"
  172. if held_object == null:
  173. grab_raycast.visible = true
  174. elif grab_mode == "RAYCAST":
  175. grab_mode = "AREA"
  176. grab_raycast.visible = false
  177. func button_released(button_index):
  178. if button_index == 15:
  179. _on_button_released_trigger()
  180. func _on_button_released_trigger():
  181. if teleport_button_down == true:
  182. if teleport_pos != null and teleport_mesh.visible == true:
  183. var camera_offset = get_parent().get_node("Player_Camera").global_transform.origin - get_parent().global_transform.origin
  184. camera_offset.y = 0
  185. get_parent().global_transform.origin = teleport_pos - camera_offset
  186. teleport_button_down = false
  187. teleport_mesh.visible = false
  188. teleport_raycast.visible = false
  189. teleport_pos = null
  190. func sleep_area_entered(body):
  191. if "can_sleep" in body:
  192. body.can_sleep = false
  193. body.sleeping = false
  194. func sleep_area_exited(body):
  195. if "can_sleep" in body:
  196. # Allow the CollisionBody to sleep by setting the "can_sleep" variable to true
  197. body.can_sleep = true

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

解释VR控制器的代码

首先, 让我们浏览一下脚本中所有的类变量:

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

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

  • prior_controller_velocities: 一个数组, 用于保存最近30次计算的VR控制器的速度. 这是用来平滑速度计算的时间.

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

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

  • grab_area: 一个变量用来保存 Area 节点, 用于VR控制器抓取物体.

  • grab_raycast: 一个变量用来保存 Raycast 节点, 用于VR控制器抓取物体.

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

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

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

  • hand_pickup_drop_sound: 一个变量, 用来保存包含拾取/放下声音的 AudioStreamPlayer3D 节点.

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

  • teleport_mesh: 一个变量, 用来保存 MeshInstance 节点, 用于显示玩家的传送位置.

  • teleport_button_down: 一个变量, 用于跟踪控制器的传送按钮是否被按下. 这将被用来检测这个VR控制器是否在试图传送玩家.

  • teleport_raycast: 一个变量, 用来保存 Raycast 节点, 用来计算传送的位置. 这个节点也有一个 MeshInstance, 作为瞄准的 “激光瞄准器”.

  • CONTROLLER_DEADZONE: 一个常数, 用于定义VR控制器上的触控板和操纵杆的死区. 更多信息请参见下面的说明.

  • MOVEMENT_SPEED: 一个常数, 用于定义玩家在使用触控板/操纵杆进行人工移动时的移动速度.

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

  • directional_movement : 一个变量, 用于保持该VR控制器是否使用触摸板/操纵杆移动玩家.

备注

您可以在这里找到一篇很棒的文章解释如何处理游戏手柄/控制器死区 这里.

我们正在使用该文章中所提供的缩放比例的径向死区代码的翻译版本,用于 VR 控制器的操纵杆/触摸板。这篇文章读起来很棒,我强烈建议您看一下!

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


_ready 函数的逐步说明

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

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

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

然后, 代码获取 grab_area 节点, grab_raycast 节点和 grab_pos_node 节点, 并将它们全部分配给各自的变量, 以供以后使用.

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

之后我们将VR控制器中的 Sleep_Area 节点的 body_enteredbody_exited 信号连接到 sleep_area_enteredsleep_area_exited 函数中. sleep_area_enteredsleep_area_exited 函数将用于使 RigidBody 节点在 VR 控制器附近时无法休眠.

然后得到 hand_meshhand_pickup_drop_sound 节点, 并将它们分配给各自的变量, 以便以后使用.

最后,VR控制器扩展的 ARVRController 节点中的 button_pressedbutton_release 信号分别与 button_pressedbutton_released 函数相连. 这意味着当VR控制器上的某个按钮被按下或释放时, 本脚本中定义的 “button_pressed” 或 “button_released” 函数将被调用.

_physics_process 函数分步说明

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

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

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


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

如果 teleport_button_down 等于 true , 我们将使用 force_raycast_update 函数强制更新 teleport_raycast Raycast 节点. force_raycast_update 函数将用物理世界的最新版本更新 Raycast 节点内的属性.

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

备注

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

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

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


下一节代码首先通过 get_is_active 函数检查VR控制器是否处于活动状态, 该函数由 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 函数步骤解释

首先, 这个函数将 controller_velocity 变量重置为零 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 来计算. 这将给我们一个 Vector3prior_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 函数分步解释

首先, 这个函数获取触控板和操纵杆的轴, 并将它们分配给 Vector2 变量, 分别称为 trackpad_vectorjoystick_vector.

备注

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

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

一旦 trackpad_vectorjoystick_vector 变量的死区被计算在内,代码就会得到相对于 ARVRCamera 的全局变换的前进和右转方向的向量。这样做的目的是给我们提供相对于用户相机的旋转,即 ARVRCamera,在世界空间中向前和向右指向的向量。当您在 Godot 编辑器中选择一个对象并启用 局部空间模式 按钮时,这些向量与蓝色和红色箭头的方向相同。前进方向向量存储在一个名为 forward_direction 的变量中,而右侧方向向量存储在一个名为 right_direction 的变量中。

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

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

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

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

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

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


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

button_pressed 函数分步说明

这个函数检查刚刚按下的VR按钮是否等于本项目中使用的一个VR按钮. button_index 变量是由 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_objectnull ,那么 VR 控制器就正在握着什么东西,我们就去检查被握对象 held_object 是否扩展自 VR_Interactable_Rigidbody 类。虽然我们还没有创建 VR_Interactable_Rigidbody 类,但是接下来会把 VR_Interactable_Rigidbody 用于项目中所有特殊(自定义)的基于 RigidBody 的节点。

小技巧

别担心, 我们将在本节之后介绍 VR_Interactable_Rigidbody !

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

_on_button_pressed_grab 函数分步说明

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

然后我们通过检查 held_object 是否等于 null 来检查VR控制器当前是否没有持有任何东西. 如果VR控制器没有持有任何东西, 则调用 _pickup_rigidbody 函数. 如果VR控制器有拿着东西, held_object 不等于 null , 则调用 _throw_rigidbody 函数.

最后, 通过调用 hand_pickup_drop_soundplay 函数来播放拾取/放下的声音.

_pickup_rigidbody 函数的分步说明

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


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

然后我们检查 bodies 变量的长度是否大于 0, 如果是, 我们使用for循环来检查 bodies 中的 PhysicsBody 节点.

对于每个 PhysicsBody 节点, 我们使用``if body is RigidBody``检查它是否是 RigidBody 节点, 如果 PhysicsBody 节点是 RigidBody 节点或其扩展, 将返回 true. 如果对象是 RigidBody, 那么我们检查在body中有没有定义一个名为 NO_PICKUP 的变量或常量. 之所以这样做, 是因为如果你想让 RigidBody 节点无法被拾取, 只需要定义一个名为 NO_PICKUP 的常量或变量,VR控制器就会无法拾取它. 如果 RigidBody 节点没有定义一个名称为 NO_PICKUP 的变量或常量, 那么将 rigid_body 变量赋值给 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 节点是 RigidBody 或其派生节点,代码将返回 true。然后代码会检查该 RigidBody 节点是否没有名为 NO_PICKUP 的变量,如果没有,则将该 RigidBody 节点赋值给 rigid_body 变量。

这段代码的作用是将 grab_raycast Raycast 节点发送出去,并检查它是否与一个没有 NO_PICKUP 变量/常量的 RigidBody 节点碰撞。如果它与一个没有 NO_PICKUP 的 RigidBody 碰撞,它将该节点分配给 rigid_body 变量,这样我们就可以在这个函数的后面做一些额外的后期处理。


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

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

然后我们将 RigidBody 的模式设置为 MODE_STATIC , 它的 collision_layercollision_mask 为零. 这将使被持有的 RigidBody 在被VR控制器持有时不能与物理世界中的其他物体互动.

接下来将 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 变量中.

小技巧

别担心, 我们将在本节之后介绍 VR_Interactable_Rigidbody !

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

这段代码的作用是, 如果使用抓取 AreaRaycast 找到了 RigidBody, 它就会将其设置为可以被VR控制器携带.

_throw_rigidbody 函数的分步说明

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

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

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

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

小技巧

别担心, 我们将在本节之后介绍 VR_Interactable_Rigidbody !

在完成本系列教程的第二部分后, 代码应该会更有意义, 在那里我们将实际使用 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 中的 Raycast激光视线 是可见的.

_on_button_pressed_menu 函数的分步说明

首先这个函数检查是否 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上的 “激光瞄准器” 将会不可见.

这一段代码简单地改变了VR控制器在按下 grab/grip 抓取按钮时如何抓取 RigidBody 的节点. 如果 grab_mode 设置为 AREA, 那么 grab_area 中的 Area 节点将被用于检测 RigidBody 节点, 如果 grab_mode 设置为 RAYCAST 那么 grab_raycast 中的 Raycast 节点将被用于检测 RigidBody 节点.

button_released 函数的分步说明

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

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

_on_button_released_trigger 函数的分步说明

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

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

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

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

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

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

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

sleep_area_entered 函数的分步说明

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

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

sleep_area_exited 函数的分步说明

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

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


好吧, 哇!那是很多代码!将相同的脚本 VR_Controller.gd 添加到其他VR控制器场景中, 以便两个VR控制器具有相同的脚本.

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

为可交互的 VR 对象创建基类

Script 选项卡仍然打开的情况下,创建一个新的 GDScript,名为 VR_Interactable_Rigidbody.gd

小技巧

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

打开 VR_Interactable_Rigidbody.gd 后, 添加以下代码:

GDScript

  1. class_name VR_Interactable_Rigidbody
  2. extends RigidBody
  3. # (Ignore the unused variable warning)
  4. # warning-ignore:unused_class_variable
  5. var controller = null
  6. func _ready():
  7. pass
  8. func interact():
  9. pass
  10. func picked_up():
  11. pass
  12. func dropped():
  13. pass

让我们快速浏览一下这个脚本.


首先, 我们用 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 中, 当对象被添加到场景中时, 我们并不需要做什么.

interact 功能是一个桩函数, 当按住VR控制器上的交互按钮(在这种情况下为触发器)时将调用该函数.

小技巧

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

picked_updropped 函数是存根函数, 当VR控制器拾取和放置对象时将调用它们.


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

现在已经定义了基类,VR控制器中的代码应该可以工作了. 继续尝试游戏, 您会发现您可以通过按触摸板进行四处移动, 并可以使用 “抓/握” 按钮抓取和扔出物体.

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

这可能使你感到晕车的主要原因之一,你的视觉告诉你正在移动,而你的身体却没有移动。这种信号的冲突会使身体感到不适。让我们添加一个晕影着色器来帮助减少在 VR 中移动时的晕动症吧!

减轻晕动病

备注

有很多方法可以减轻 VR 中的晕动病,但没有减轻晕动病的完美方法。有关如何实施运动和减轻晕动病的更多信息,请参阅 Oculus 开发者中心的这个页面

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

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

打开 Scenes 文件夹中的 Movement_Vignette.tscn。这个场景只是一个带自定义着色器的 ColorRect 节点。如果你想的话,可以随意看看这个自定义着色器,它只是 Vignette 着色器的一个略微修改的版本,你可以在 Godot 演示资源库找到。

让我们来编写代码,使玩家在移动时可以看到暗角晕影着色器。选中“Movement_Vignette”节点并创建一个名为“Movement_Vignette.gd”的新脚本。添加以下代码:

GDScript

  1. extends Control
  2. var controller_one
  3. var controller_two
  4. func _ready():
  5. yield(get_tree(), "idle_frame")
  6. yield(get_tree(), "idle_frame")
  7. yield(get_tree(), "idle_frame")
  8. yield(get_tree(), "idle_frame")
  9. var interface = ARVRServer.primary_interface
  10. if interface == null:
  11. set_process(false)
  12. printerr("Movement_Vignette: no VR interface found!")
  13. return
  14. rect_size = interface.get_render_targetsize()
  15. rect_position = Vector2(0,0)
  16. controller_one = get_parent().get_node("Left_Controller")
  17. controller_two = get_parent().get_node("Right_Controller")
  18. visible = false
  19. func _process(_delta):
  20. if controller_one == null or controller_two == null:
  21. return
  22. if controller_one.directional_movement == true or controller_two.directional_movement == true:
  23. visible = true
  24. else:
  25. visible = false

因为这个脚本相当简短, 让我们快速浏览一下它的作用.

解释小插图代码

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


_ready 函数中首先使用 yield 等待四帧. 我们之所以要等待四帧, 是因为要确保VR界面已经准备好并可以访问.

等待之后, 使用 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 来使自己可见.


这就是整个脚本!现在我们已经写好了代码, 去试试用触控板和/或操纵杆移动吧. 你应该发现, 这时的运动不适感比以前少了很多!

备注

如前所述,有很多方法可以减轻 VR 晕动症。请查看 Oculus 开发者中心的这个页面,了解更多关于如何实现运动和减轻晕动病的信息。

最后的说明

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

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

警告

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