VR starter tutorial part 2¶
简介¶

在这部分VR入门系列教程中,我们将增加一些特殊的,基于 RigidBody 的节点来用于VR中。
这将继续我们在上一个教程部分的内容,我们刚刚完成了VR控制器的工作,并定义了一个名为 VR_Interactable_Rigidbody
的自定义类。
小技巧
You can find the finished project on the OpenVR GitHub repository.
添加可销毁的目标¶
在我们制作任何一个特殊的以 RigidBody 为基础的节点之前,我们需要一些东西来让它们去执行。让我们做一个简单的球体目标,当它被摧毁时,会破裂成一堆碎片。
Open up Sphere_Target.tscn
, which is in the Scenes
folder. The scene is fairly simple, with just a StaticBody with a sphere shaped
CollisionShape, a MeshInstance node displaying a sphere mesh, and an AudioStreamPlayer3D node.
特殊的 RigidBody 节点将处理对球体的损坏,这就是为什么我们使用 StaticBody 节点,而不是诸如 Area 或 RigidBody 一类的节点。除此以外,就没什么好说的了,所以让我们直接进入写代码的阶段。
Select the Sphere_Target_Root
node and make a new script called Sphere_Target.gd
. Add the following code:
extends Spatial
var destroyed = false
var destroyed_timer = 0
const DESTROY_WAIT_TIME = 80
var health = 80
const RIGID_BODY_TARGET = preload("res://Assets/RigidBody_Sphere.scn")
func _ready():
set_physics_process(false)
func _physics_process(delta):
destroyed_timer += delta
if destroyed_timer >= DESTROY_WAIT_TIME:
queue_free()
func damage(damage):
if destroyed == true:
return
health -= damage
if health <= 0:
get_node("CollisionShape").disabled = true
get_node("Shpere_Target").visible = false
var clone = RIGID_BODY_TARGET.instance()
add_child(clone)
clone.global_transform = global_transform
destroyed = true
set_physics_process(true)
get_node("AudioStreamPlayer").play()
get_tree().root.get_node("Game").remove_sphere()
Let's go over how this script works.
Explaining the Sphere Target code¶
First, let's go through all the class variables in the script:
destroyed
: A variable to track whether the sphere target has been destroyed.destroyed_timer
: A variable to track how long the sphere target has been destroyed.DESTROY_WAIT_TIME
: A constant to define the length of time the target can be destroyed for before it frees/deletes itself.health
: A variable to store the amount of health the sphere target has.RIGID_BODY_TARGET
:一个常数,用于储存被摧毁的球体目标的场景。
``_ready``函数的逐步说明¶
All the _ready
function does is that it stops the _physics_process
from being called by calling set_physics_process
and passing false
.
The reason we do this is because all the code in _physics_process
is for destroying this node when enough time has passed, which we only want to
do when the target has been destroyed.
_physics_process
function step-by-step explanation¶
首先,这个函数将时间 delta
添加到 destroyed_timer
变量中。然后检查 destroyed_timer
是否大于或等于 DESTROY_WAIT_TIME
。如果 destroyed_timer
大于或等于 DESTROY_WAIT_TIME
,那么球体目标将通过调用 queue_free
函数来释放/删除自己。
damage
function step-by-step explanation¶
damage
函数将被特殊的 RigidBody 节点调用,它将传递对目标造成的伤害量,这是一个名为 damage
的函数参数变量。 damage
变量将保存特殊的 RigidBody 节点对球体目标造成的伤害量。
首先,这个函数通过检查 destroyed
变量是否等于 true
来检查目标是否已经被销毁。如果 destroyed
等于 true
,那么这个函数就会调用 return
,所以其他代码都不会被调用。这只是一个安全检查,如果两个东西同时损坏目标,则目标不能被破坏两次。
接下来,该函数从目标的健康状况 health
中删除受到的伤害量 damage
。然后检查 health
是否等于零或更少,这意味着目标刚刚已被摧毁。
如果目标刚刚被摧毁,那么我们将 CollisionShape <class_CollisionShape>`的 ``disabled` 属性设置为 true
,将 Sphere_Target
MeshInstance 的 visible
属性设置为 false
,使 Sphere_Target
MeshInstance 不可见,这样做,是为了让目标不能再影响物理世界,所以不可破碎的目标网格是不可见的。
之后,函数将实例化``RIGID_BODY_TARGET``场景,并将其添加为目标的子场景。然后,它将新实例化的场景的``global_transform``(称为``clone``)设置为未被破坏的目标的``global_transform``。这使得被破坏的目标与未被破坏的目标在相同的位置开始,并具有相同的旋转和比例。
然后该函数将``destroyed``变量设置为``true``,这样目标就知道它已经被破坏了,并调用``set_physics_process``函数并传递``true``。这将开始执行``_physics_process``中的代码,这样在``DESTROY_WAIT_TIME``秒过后,球体目标将释放/销毁自己。
The function then gets the AudioStreamPlayer3D node and calls the play
function so it plays its sound.
最后,在``Game.gd``中调用``remove_sphere``函数。为了得到``Game.gd``,代码使用场景树,从场景树的根部到``Game.tscn``场景的根部。
将 remove_sphere
函数添加到 Game.gd
¶
You may have noticed we are calling a function in Game.gd
, called remove_sphere
, that we have not defined yet. Open up Game.gd
and
add the following additional class variables:
var spheres_left = 10
var sphere_ui = null
spheres_left
。游戏世界里剩余的球体目标数量。在提供的``游戏``场景中,有``10``个球体,所以这是初始值。sphere_ui
:对球体UI的引用。我们之后会在教程中用到这个,来显示世界中剩余球体的数量。
With these variables defined, we can now add the remove_sphere
function. Add the following code to Game.gd
:
func remove_sphere():
spheres_left -= 1
if sphere_ui != null:
sphere_ui.update_ui(spheres_left)
Let's go through what this function does real quick:
First, it removes one from the spheres_left
variable. It then checks to see if the sphere_ui
variable is not equal to null
, and if it is not
equal to null
it calls the update_ui
function on sphere_ui
, passing in the number of spheres as an argument to the function.
注解
我们将在后面的教程中添加 sphere_ui
的代码!
现在 Sphere_Target
已经可以使用了,但是我们没有办法破坏它。让我们通过添加一些特殊的基于 RigidBody 的节点来解决这个问题,这些节点可以破坏目标。
加一把手枪¶
Let's add a pistol as the first interactable RigidBody node. Open up Pistol.tscn
, which you can find in the Scenes
folder.
在添加代码之前,我们先来快速了解一下 Pistol.tscn
中的一些注意事项。
``Pistol.tscn``中的所有节点(除了根节点)都是旋转的。这是为了让手枪在拿起时相对于VR控制器处于正确的旋转状态。根节点是一个:ref:`RigidBody <class_RigidBody>`节点,我们需要这个节点,因为我们将使用我们在本系列教程最后一部分创建的``VR_Interactable_Rigidbody``类。
有一个 MeshInstance 节点叫做``Pistol_Flash``,这是一个简单的网格,我们将用它来模拟手枪枪管末端的枪口闪光。一个名为``LaserSight``的 MeshInstance <class_MeshInstance>`节点用来作为手枪瞄准的指南,它遵循 :ref:`Raycast <class_Raycast>`节点的方向,名为``Raycast`,手枪用来检测它的'子弹'是否击中了什么东西。最后,在手枪的尾部有一个:ref:`AudioStreamPlayer3D <class_AudioStreamPlayer3D>`节点,我们将用它来播放手枪射击的声音。
Feel free to look at the other parts of the scene if you want. Most of the scene is fairly straightforward, with the major changes mentioned above. Select the RigidBody
node called Pistol
and make a new script called Pistol.gd
. Add the following code:
extends VR_Interactable_Rigidbody
var flash_mesh
const FLASH_TIME = 0.25
var flash_timer = 0
var laser_sight_mesh
var pistol_fire_sound
var raycast
const BULLET_DAMAGE = 20
const COLLISION_FORCE = 1.5
func _ready():
flash_mesh = get_node("Pistol_Flash")
flash_mesh.visible = false
laser_sight_mesh = get_node("LaserSight")
laser_sight_mesh.visible = false
raycast = get_node("RayCast")
pistol_fire_sound = get_node("AudioStreamPlayer3D")
func _physics_process(delta):
if flash_timer > 0:
flash_timer -= delta
if flash_timer <= 0:
flash_mesh.visible = false
func interact():
if flash_timer <= 0:
flash_timer = FLASH_TIME
flash_mesh.visible = true
raycast.force_raycast_update()
if raycast.is_colliding():
var body = raycast.get_collider()
var direction_vector = raycast.global_transform.basis.z.normalized()
var raycast_distance = raycast.global_transform.origin.distance_to(raycast.get_collision_point())
if body.has_method("damage"):
body.damage(BULLET_DAMAGE)
elif body is RigidBody:
var collision_force = (COLLISION_FORCE / raycast_distance) * body.mass
body.apply_impulse((raycast.global_transform.origin - body.global_transform.origin).normalized(), direction_vector * collision_force)
pistol_fire_sound.play()
if controller != null:
controller.rumble = 0.25
func picked_up():
laser_sight_mesh.visible = true
func dropped():
laser_sight_mesh.visible = false
Let's go over how this script works.
Explaining the pistol code¶
首先,请注意,我们有``extends RigidBody``,而不是``extends VR_Interactable_Rigidbody``。这使得手枪脚本在哪里扩展了``VR_Interactable_Rigidbody``类,这样VR控制器就知道这个对象可以被交互,当这个对象被VR控制器持有时,``VR_Interactable_Rigidbody``中定义的函数可以被调用。
接下来,我们来看看类变量:
flash_mesh
: A variable to hold the MeshInstance node that is used to simulate muzzle flash on the pistol.FLASH_TIME
: 一个常数,用于定义枪口闪光的可见时间。这也将定义手枪的射击速度。flash_timer
: A variable to hold the amount of time the muzzle flash has been visible for.laser_sight_mesh
: A variable to hold the MeshInstance node that acts as the pistol's 'laser sight'.pistol_fire_sound
: A variable to hold the AudioStreamPlayer3D node used for the pistol's firing sound.raycast
: A variable to hold the Raycast node that is used for calculating the bullet's position and normal when the pistol is fired.BULLET_DAMAGE
: A constant to define the amount of damage a single bullet from the pistol does.COLLISION_FORCE
: 一个常数,用于定义手枪子弹碰撞时对 RigidBody 节点施加的力。
``_ready``函数的逐步说明¶
该函数获取节点并将其分配给适当的变量。对于 flash_mesh
和 laser_sight_mesh
节点,它们的 visible
属性都设置为 false
,所以它们最初是不可见的。
_physics_process
function step-by-step explanation¶
_physics_process
函数首先检查手枪的枪口闪光是否可见——检查 flash_timer
是否大于零。如果 flash_timer
大于零,那么我们就从中去掉时间 delta
。接下来,我们检查 flash_timer
变量是否为零或更小,因为我们已经从中减去了 delta
。如果是的话,那么这说明手枪枪口闪光计时器刚刚结束,所以我们需要将 flash_mesh
的 visible
属性设置为 false
,使 flash_mesh
不可见。
interact
function step-by-step explanation¶
interact函数首先通过检查 flash_timer
是否小于或等于0来检查手枪的枪口闪光是否隐形。我们这样做是为了将手枪的射击速度限制在枪口闪光可见的时间长度内,这是限制玩家射击速度的一个简单解决方案。
如果 flash_timer
为0或更少,我们就把 flash_timer
设置为 FLASH_TIME
,这样在手枪再次射击之前就会有一个延迟。之后我们将 flash_mesh.visible
设置为 true
,这样在 flash_timer
大于零的时候,就可以确保手枪尾部的枪口闪光是可见的。
接下来,我们在 raycast
中的 Raycast 节点上调用 force_raycast_update
函数,这样它就能从物理世界获得最新的碰撞信息。然后我们通过检查 is_colliding
函数是否等于 true
来检查 raycast
是否撞到了什么东西。
如果 raycast
撞到了什么东西,那么我们通过 get_collider
函数得到与它碰撞的 PhysicsBody 。我们把撞到的 PhysicsBody 分配给一个叫 body
的变量。
然后,我们通过从 raycast
节点的 global_transform
上的 Raycast 得到它的正 Z
方向轴。这将为我们提供raycast在Z轴上的指向,这与在Godot编辑器中启用 "局部空间模式 "时 Spatial 小工具上的蓝色箭头的方向相同。我们将这个方向存储在一个名为 direction_vector
的变量中。
接下来我们通过获取全局位置``global_transform.origin``到:ref:Raycast <class_Raycast>`碰撞点的距离,``global_transform.原点到:ref:`Raycast <class_Raycast>`的碰撞点的距离,``raycast.get_collision_point`,使用``distance_to``函数。这将给我们提供:ref:`Raycast <class_Raycast>`在碰撞之前所走过的距离,我们将其存储在一个名为``raycast_distance``的变量中。
然后代码检查 PhysicsBody , body
, 是否有一个叫做 damage
的函数/方法,使用 has_method
函数。如果 PhysicsBody 有一个叫做 damage
的函数/方法,那么我们就调用 damage
函数,并传递 BULLET_DAMAGE
,这样它就会受到子弹碰撞到它的伤害。
不管 PhysicsBody 是否有 damage
函数,我们都要检查 body
是否是基于 RigidBody 的节点。如果 body
是一个以 RigidBody 为基础的节点,那么我们要在子弹碰撞时推它。
要计算所受的力,我们只需用``COLLISION_FORCE``除以``raycast_distance``,然后再乘以``body.mass``。我们把这个计算结果存储在一个叫``collision_force``的变量中。这将使距离较短的碰撞比距离较长的碰撞施加移动力,给出一个略微*真实的碰撞响应。
然后我们用``apply_impulse``函数推送:ref:RigidBody <class_RigidBody>,其中位置是一个零向量的三维数组,所以力是从中心开始施加的,碰撞力是我们计算出来的``collision_force``变量。
不管 raycast
变量是否击中了什么东西,我们都要通过调用 pistol_fire_sound
变量上的 play
函数来播放手枪的射击声。
最后,我们通过检查 controller
变量是否不等于 null
来检查手枪是否被VR控制器所持有。如果不等于 null
,我们就把VR控制器的 rumble
属性设置为 0.25
,这样手枪射击时就会有轻微的响声。
picked_up
function step-by-step explanation¶
这个函数只是通过设置 visible
属性为 true
,使 laser_sight_mesh
MeshInstance 可见。
``dropped``函数的逐步说明¶
这个函数只是通过设置 visible
属性为 false
,使 laser_sight_mesh
MeshInstance 不可见。
手枪完毕¶

这就是我们需要做的所有工作 手枪在项目中!继续运行项目吧。如果你爬上楼梯,拿起手枪,你就可以用VR控制器上的扳机按钮向场景中的球体目标射击!如果你向目标射击的时间足够长,它们就会碎裂成碎片。
添加霰弹枪¶
接下来让我们往VR项目中添加一杆霰弹枪。
Adding a special shotgun RigidBody should be fairly straightforward, as almost everything with the shotgun is the same as the pistol.
Open up Shotgun.tscn
, which you can find in the Scenes
folder and take a look at the scene. Almost everything is the same as in Pistol.tscn
.
The only thing that is different, beyond name changes, is that instead of a single Raycast, there are five Raycast nodes.
This is because a shotgun generally fires in a cone shape, so we are going to emulate that effect by having several Raycast nodes that will rotate
randomly in a cone shape when the shotgun fires.
除此之外,一切都和 Pistol.tscn
差不多。
Let's write the code for the shotgun. Select the RigidBody node called Shotgun
and make a new script called Shotgun.gd
. Add the following code:
extends VR_Interactable_Rigidbody
var flash_mesh
const FLASH_TIME = 0.25
var flash_timer = 0
var laser_sight_mesh
var shotgun_fire_sound
var raycasts
const BULLET_DAMAGE = 30
const COLLISION_FORCE = 4
func _ready():
flash_mesh = get_node("Shotgun_Flash")
flash_mesh.visible = false
laser_sight_mesh = get_node("LaserSight")
laser_sight_mesh.visible = false
raycasts = get_node("Raycasts")
shotgun_fire_sound = get_node("AudioStreamPlayer3D")
func _physics_process(delta):
if flash_timer > 0:
flash_timer -= delta
if flash_timer <= 0:
flash_mesh.visible = false
func interact():
if flash_timer <= 0:
flash_timer = FLASH_TIME
flash_mesh.visible = true
for raycast in raycasts.get_children():
if not raycast is RayCast:
continue
raycast.rotation_degrees = Vector3(90 + rand_range(10, -10), 0, rand_range(10, -10))
raycast.force_raycast_update()
if raycast.is_colliding():
var body = raycast.get_collider()
var direction_vector = raycasts.global_transform.basis.z.normalized()
var raycast_distance = raycasts.global_transform.origin.distance_to(raycast.get_collision_point())
if body.has_method("damage"):
body.damage(BULLET_DAMAGE)
if body is RigidBody:
var collision_force = (COLLISION_FORCE / raycast_distance) * body.mass
body.apply_impulse((raycast.global_transform.origin - body.global_transform.origin).normalized(), direction_vector * collision_force)
shotgun_fire_sound.play()
if controller != null:
controller.rumble = 0.25
func picked_up():
laser_sight_mesh.visible = true
func dropped():
laser_sight_mesh.visible = false
这段代码的大部分内容与手枪的代码完全相同,只有一些*小的改动,主要是名称不同而已。由于这些脚本的相似度很高,我们只关注一下这些变化。
Explaining the shotgun code¶
和手枪一样,霰弹枪也扩展了 VR_Interactable_Rigidbody
,所以VR控制器知道这个对象可以与之交互,以及有哪些功能。
There is only one new class variable:
raycasts
: A variable to hold the node that has all of the Raycast nodes as its children.
新的类变量取代了``Pistol.gd``中的``raycast``变量,因为对于霰弹枪,我们需要处理多个:ref:`Raycast <class_Raycast>`节点,而不是只有一个。所有其他类变量与``Pistol.gd``相同,功能也相同,只是有些变量被重新命名为非手枪专用变量。
interact
function step-by-step explanation¶
interact函数首先通过检查 flash_timer
是否小于或等于0来检查霰弹枪的枪口闪光是否隐形。我们这样做是为了将霰弹枪的射击速度限制在枪口闪光可见的时间长度内,这是限制玩家射击速度的一个简单解决方案。
如果 flash_timer
为0或更少,我们再将 flash_timer
设置为 FLASH_TIME
,这样在霰弹枪再次开火之前就会有一个延迟。之后我们将 flash_mesh.visible
设置为 true
,这样在 flash_timer
大于零的时候,霰弹枪尾部的枪口闪光将是可见的。
接下来,我们在 raycast
中的 Raycast 节点上调用 force_raycast_update
函数,这样它就能从物理世界获得最新的碰撞信息。然后我们通过检查 is_colliding
函数是否等于 true
来检查 raycast
是否撞到了什么东西。
接下来我们使用for循环来检查 raycasts
变量的每个子节点。这样代码就会遍历 Raycast 的每一个节点,这些节点都是 raycasts
变量的子节点。
对于每个节点,我们检查 raycast
是否 不是一个 Raycast 节点。如果这个节点不是 Raycast 节点,我们就简单地使用 continue
跳过它。
接下来,我们通过设置 raycast
的 rotation_degrees
变量为Vector3,其中X轴和Z轴为 -10
至 10
的随机数,将 raycast
节点围绕一个小于 10
度锥体随机旋转。这个随机数是用 rand_range
函数选择的。
然后我们在 raycast
中的 Raycast 节点上调用 force_raycast_update
函数,这样它就能从物理世界获得最新的碰撞信息。然后我们通过检查 is_colliding
函数是否等于 true
来检查 raycast
是否撞到了什么东西。
The rest of the code is exactly the same, but this process is repeated for each Raycast node that is a child of the raycasts
variable.
如果 raycast
撞到了什么东西,那么我们通过 get_collider
函数得到与它碰撞的 PhysicsBody 。我们把撞到的 PhysicsBody 分配给一个叫 body
的变量。
然后,我们从 raycast
节点的 global_transform
上的 Basis 得到射线广播的正 Z
方向轴,这提供raycast在Z轴上的指向,与在Godot编辑器中启用 Local space mode
[局部空间模式] 时 Spatial 小工具上的蓝色箭头的方向相同。我们将这个方向存储在一个名为 direction_vector
的变量中。
接下来,我们通过使用 distance_to
函数获取 raycast
节点的全局位置 global_transform.origin
到raycast的碰撞点 raycast.get_collision_point
的距离,得到从raycast原点到raycast碰撞点的距离。这提供了 Raycast <class_Raycast>`在碰撞前走过的距离,将其存储在一个名为 ``raycast_distance` 的变量中。
然后代码检查 PhysicsBody , body
, 是否有一个叫做 damage
的函数/方法,使用 has_method
函数。如果 PhysicsBody 有一个叫做 damage
的函数/方法,那么我们就调用 damage
函数,并传递 BULLET_DAMAGE
,这样它就会受到子弹碰撞到它的伤害。
不管 PhysicsBody 是否有 damage
函数,我们都要检查 body
是否是基于 RigidBody 的节点。如果 body
是一个以 RigidBody 为基础的节点,那么我们要在子弹碰撞时推它。
要计算所受的力,我们只需用``COLLISION_FORCE``除以``raycast_distance``,然后再乘以``body.mass``。我们把这个计算结果存储在一个叫``collision_force``的变量中。这将使距离较短的碰撞比距离较长的碰撞施加移动力,给出一个略微*真实的碰撞响应。
然后我们用``apply_impulse``函数推送:ref:RigidBody <class_RigidBody>,其中位置是一个零向量的三维数组,所以力是从中心开始施加的,碰撞力是我们计算出来的``collision_force``变量。
一旦所有的 Raycast 中的 raycast
变量被迭代过后,通过调用 play
中 shotgun_fire_sound
变量来播放枪声。
最后,我们通过检查 controller
变量是否不等于 null
来检查霰弹枪是否由VR控制器持有。如果不等于 null
,我们就把VR控制器的 rumble
属性设置为 0.25
,这样猎枪射击时就会有轻微的轰鸣声。
霰弹枪完毕¶
其他的一切都和手枪完全一样,最多只是一些简单的名称变化。
Now the shotgun is finished! You can find the shotgun in the sample scene by looking around the back of one of the walls (not in the building though!).
添加炸弹¶
好吧,让我们添加一个不同的特殊 RigidBody 。与其添加一些会射击的东西,不如添加一些我们可以投掷的东西--炸弹!
Open up Bomb.tscn
, which is in the Scenes
folder.
根节点是一个 RigidBody <class_RigidBody>`节点,我们将扩展使用 ``VR_Interactable_Rigidbody` ,它有一个 CollisionShape,就像我们到目前为止所做的其他特殊 RigidBody 节点一样。同样,有一个 MeshInstance 叫做 Bomb
,用来显示炸弹的网格。
然后我们有一个 Area 节点,简单地称为``Area``,它有一个大的 CollisionShape 作为它的子节点。当炸弹爆炸时,将使用这个 Area 节点将是炸弹的爆炸半径。
还有几个 Particles 节点,其中一个 Particles 节点是炸弹引信中冒出的烟雾,而另一个是爆炸。如果你想看的话,可以看一下 ParticlesMaterial 资源,它定义了粒子工作方式。不会在本教程中介绍粒子如何工作,因为它不在本教程的范围内。
There is one thing with the Particles nodes that we need to make note of. If you select the Explosion_Particles
node, you'll find that its lifetime
property
is set to 0.75
and that the one shot
checkbox is enabled. This means that the particles will only play once, and the particles will last for 0.75
seconds.
We'll need to know this so we can time the removal of the bomb with the end of the explosion Particles.
Let's write the code for the bomb. Select the Bomb
RigidBody node and make a new script called Bomb.gd
. Add the following code:
extends VR_Interactable_Rigidbody
var bomb_mesh
const FUSE_TIME = 4
var fuse_timer = 0
var explosion_area
const EXPLOSION_DAMAGE = 100
const EXPLOSION_TIME = 0.75
var explosion_timer = 0
var exploded = false
const COLLISION_FORCE = 8
var fuse_particles
var explosion_particles
var explosion_sound
func _ready():
bomb_mesh = get_node("Bomb")
explosion_area = get_node("Area")
fuse_particles = get_node("Fuse_Particles")
explosion_particles = get_node("Explosion_Particles")
explosion_sound = get_node("AudioStreamPlayer3D")
set_physics_process(false)
func _physics_process(delta):
if fuse_timer < FUSE_TIME:
fuse_timer += delta
if fuse_timer >= FUSE_TIME:
fuse_particles.emitting = false
explosion_particles.one_shot = true
explosion_particles.emitting = true
bomb_mesh.visible = false
collision_layer = 0
collision_mask = 0
mode = RigidBody.MODE_STATIC
for body in explosion_area.get_overlapping_bodies():
if body == self:
pass
else:
if body.has_method("damage"):
body.damage(EXPLOSION_DAMAGE)
if body is RigidBody:
var direction_vector = body.global_transform.origin - global_transform.origin
var bomb_distance = direction_vector.length()
var collision_force = (COLLISION_FORCE / bomb_distance) * body.mass
body.apply_impulse(Vector3.ZERO, direction_vector.normalized() * collision_force)
exploded = true
explosion_sound.play()
if exploded:
explosion_timer += delta
if explosion_timer >= EXPLOSION_TIME:
explosion_area.monitoring = false
if controller != null:
controller.held_object = null
controller.hand_mesh.visible = true
if controller.grab_mode == "RAYCAST":
controller.grab_raycast.visible = true
queue_free()
func interact():
set_physics_process(true)
fuse_particles.emitting = true
Let's go over how this script works.
解释炸弹代码¶
和其他特殊的 RigidBody 节点一样,炸弹也扩展了 VR_Interactable_Rigidbody
,这样VR控制器就知道这个对象可以被交互,当这个对象被VR控制器持有时, VR_Interactable_Rigidbody
中定义的函数可以被调用。
接下来,我们来看看类变量:
bomb_mesh
: A variable to hold the MeshInstance node that is used for the non-exploded bomb.FUSE_TIME
:一个常数,用于定义炸弹爆炸前引信将 '燃烧' 多长时间fuse_timer
: A variable to hold the length of time that has passed since the bomb's fuse has started to burn.explosion_area
: A variable to hold the Area node used to detect objects within the bomb's explosion.EXPLOSION_DAMAGE
: A constant to define how much damage is applied with the bomb explodes.EXPLOSION_TIME
: 一个常数,用于定义炸弹爆炸后在场景中持续时长,该值应与爆炸的 ``lifetime``属性相同 Particles 节点。explosion_timer
A variable to hold the length of time that has passed since the bomb exploded.exploded
: A variable to hold whether the bomb has exploded or not.COLLISION_FORCE
: 一个常数,定义炸弹爆炸时施加在 RigidBody 节点上的力的大小。fuse_particles
: A variable to hold a reference to the Particles node used for the bomb's fuse.explosion_particles
: A variable to hold a reference to the Particles node used for the bomb's explosion.explosion_sound
: A variable to hold a reference to the AudioStreamPlayer3D node used for the explosion sound.
``_ready``函数的逐步说明¶
The _ready
function first gets all of the nodes from the bomb scene and assigns them to their respective class variables for later use.
然后我们调用 set_physics_process
并传递 false
,这样 _physics_process
就不会被执行。这样做是因为 _physics_process
中的代码会开始燃烧导火索并爆炸炸弹,而我们只想在用户与炸弹交互时这样做。如果我们不禁用 _physics_process
,炸弹的引信会在用户有机会接近炸弹之前就开始燃烧。
_physics_process
function step-by-step explanation¶
The _physics_process
function first checks to see if fuse_timer
is less than FUSE_TIME
. If it is, then the bomb's fuse is still burning.
如果炸弹的引信仍在燃烧,我们就在 fuse_timer
变量中加入时间 delta
,然后检查 fuse_timer
是否大于或等于 FUSE_TIME
。如果 fuse_timer
大于或等于 FUSE_TIME
,那么引信刚刚完成,我们需要引爆炸弹。
为了使炸弹爆炸,我们首先在 fuse_particles
上将 emitting
设置为 false
,停止为引信发射粒子。然后告诉爆炸 Particles <class_Particles>`节点 ``explosion_particles` ,通过设置 one_shot
为 true
,让它一次发射所有的粒子。之后,在 explosion_particles
上设置 emitting
为 true
,这样看起来就像炸弹已经爆炸。为了让炸弹看起来像爆炸,通过设置 bomb_mesh.visible
为 false
来隐藏炸弹 MeshInstance 节点。
为了防止炸弹与物理世界中的其他物体发生碰撞,将炸弹的 collision_layer
和 collision_mask
属性设置为 0
。将 RigidBody 模式改为 MODE_STATIC
,这样炸弹 :ref:`RigidBody <class_RigidBody>`就不会移动。
然后,需要获取 explosion_area
节点内的所有 PhysicsBody 节点。要做到这一点,在for循环中使用 get_overlapping_bodies
。 get_overlapping_bodies
函数将返回一个 PhysicsBody 节点内的 Area 节点的 PhysicsBody 数组,这正是我们要的。
对于每个 PhysicsBody 节点(我们将其存储在一个名为 body
的变量中),我们检查它是否等于 self
。这样做是为了使炸弹不会意外地自己爆炸,因为 explosion_area
[爆炸区]可能会检测到 Bomb
[炸弹] :ref:`RigidBody <class_RigidBody>`是爆炸区的一个物理体。
如果 PhysicsBody <class_PhysicsBody>`节点 ``body` 不是炸弹,那么首先检查 PhysicsBody 节点是否有一个叫做 damage
的函数。如果 PhysicsBody <class_PhysicsBody>`节点有一个叫做 ``damage` 的函数,就调用它,并把 EXPLOSION_DAMAGE
传给它,使它受到爆炸的伤害。
接下来我们检查一下 PhysicsBody 节点是否是 RigidBody 。如果 body
是一个 RigidBody,要在炸弹爆炸时移动它。
要在炸弹爆炸时移动 RigidBody 节点,首先需要计算从炸弹到 RigidBody 节点的方向。为此,从 RigidBody 的全局位置中减去炸弹的全局位置 global_transform.origin
。这将给出一个 Vector3,从炸弹指向 RigidBody <class_RigidBody>`节点。将这个 :ref:`Vector3 <class_Vector3>`存储在一个名为 ``direction_vector` 的变量中。
然后,使用 rediction_vector
上的 length
函数计算 RigidBody 离炸弹的距离。将距离存储在一个名为 bomb_distance
的变量中。
然后,计算炸弹爆炸时炸弹对 RigidBody 节点的作用力,方法是将 COLLISION_FORCE
除以 bomb_distance
,再乘以 collision_force
。这样,如果 RigidBody 节点离炸弹更近,它就会被推得更远。
最后,用 apply_impulse
函数推动 RigidBody <class_RigidBody>`节点, :ref:`Vector3 <class_Vector3>`位置为零, ``collision_force` 乘以 direction_vector.normalized
作为力。这样,当炸弹爆炸时,就会把 :ref:`RigidBody <class_RigidBody>`节点炸飞。
当循环浏览了 explosion_area
内的所有 PhysicsBody <class_PhysicsBody>`节点后,将 ``exploded` 变量设置为 true
,这样代码就知道炸弹爆炸了,并调用 explosion_sound
上的 play
,播放爆炸声。
好了,下一部分代码开始,首先检查 exploded
是否等于 true
。
如果 exploded
等于 true
,那么这意味着炸弹在释放或销毁自己之前,正在等待爆炸粒子完成。在 explosion_timer
中加入时间 delta
,这样就可以跟踪炸弹爆炸后的时间。
If explosion_timer
is greater than or equal to EXPLOSION_TIME
after we added delta
, then the explosion timer just finished.
如果爆炸计时器刚刚结束,将 explosion_area.monitoring
设置为 false
。这样做的原因是有一个bug,当 monitoring
属性为真时,当你释放或删除一个 Area 节点时,会打印一个错误。为了确保这种情况不会发生,只需在 explosion_area
上将 monitoring
设置为false。
接下来检查炸弹是否被VR控制器持有,检查 controller
变量是否不等于 null
。如果炸弹被VR控制器持有,就把VR控制器的 controller
属性 held_object
设置为 null
。因为VR控制器不再持有任何东西,所以将 controller.hand_mesh.visible
设置为``true``,使VR控制器的手部网格可见。然后检查VR控制器的抓取模式是否是 RAYCAST
,如果是,将 controller.grab_raycast.visible
设置为 true
,这样抓取raycast的 '激光瞄准器' 就可见了。
最后,不管炸弹是否被VR控制器持有,调用 queue_free
,这样炸弹场景就会被释放或从场景中移除。
interact
function step-by-step explanation¶
首先, interact
函数调用 set_physics_process
并传递 true
,这样 _physics_process
中的代码就开始执行。这将启动炸弹的引信,最终导致炸弹爆炸。
最后,通过将 fuse_particles.visible
设置为 true
来启动引信粒子。
炸弹完毕¶
现在炸弹已经准备好了!你可以在橙色建筑中找到炸弹。
Because of how we are calculating the VR controller's velocity, it is easiest to throw the bombs using a thrusting-like motion instead of a more natural throwing-like motion. The smooth curve of a throwing-like motion is harder to track with the code we are using for calculating the velocity of the VR controllers, so it does not always work correctly and can lead inaccurately calculated velocities.
加一把剑¶
让我们添加最后一个特殊的 RigidBody —— 能够破坏目标的基础节点。让我们添加一把剑来砍穿目标!
Open up Sword.tscn
, which you can find in the Scenes
folder.
这里并没有发生很多事情。所有根 Sword
的子节点 RigidBody 节点都被旋转,当VR控制器拾取它们时,位置是正确的,有一个 MeshInstance 节点用于显示剑,还有一个 AudioStreamPlayer3D 节点用于保存剑与某物碰撞时的声音。
但有一点略有不同。有一个 KinematicBody <class_KinematicBody>`节点叫做 ``Damage_Body` 。如果你看看它,你会发现它不在任何碰撞层上,而只在一个碰撞掩码上。这是为了让 KinematicBody 不会影响场景中的其他 PhysicsBody 节点,但它仍然会被 PhysicsBody 节点影响。
使用 Damage_Body
:ref:`KinematicBody <class_KinematicBody>`节点来检测剑与场景中的东西碰撞时的碰撞点和法线。
小技巧
虽然从性能的角度来看,这可能不是获得碰撞信息的最佳方式,但它确实给了我们很多信息,可以用来进行后处理!使用 KinematicBody 节点碰撞的位置。
That is really the only thing note worthy about the sword scene. Select the Sword
RigidBody node and make a new script called Sword.gd
.
Add the following code:
extends VR_Interactable_Rigidbody
const SWORD_DAMAGE = 2
const COLLISION_FORCE = 0.15
var damage_body = null
func _ready():
damage_body = get_node("Damage_Body")
damage_body.add_collision_exception_with(self)
sword_noise = get_node("AudioStreamPlayer3D")
func _physics_process(_delta):
var collision_results = damage_body.move_and_collide(Vector3.ZERO, true, true, true);
if (collision_results != null):
if collision_results.collider.has_method("damage"):
collision_results.collider.damage(SWORD_DAMAGE)
if collision_results.collider is RigidBody:
if controller == null:
collision_results.collider.apply_impulse(
collision_results.position,
collision_results.normal * linear_velocity * COLLISION_FORCE)
else:
collision_results.collider.apply_impulse(
collision_results.position,
collision_results.normal * controller.controller_velocity * COLLISION_FORCE)
sword_noise.play()
让我们回顾一下这个脚本是如何运作的!
Explaining the sword code¶
和其他特殊的 RigidBody <class_RigidBody>`节点一样,剑扩展了 ``VR_Interactable_Rigidbody` ,这样VR控制器就知道这个对象可以被交互,当这个对象被VR控制器持有时, VR_Interactable_Rigidbody
中定义的函数可以被调用。
接下来,我们来看看类变量:
SWORD_DAMAGE
:一个常数,用于定义剑的伤害量。每次调用"_physics_process
时,被剑碰到的每个对象都会受到伤害COLLISION_FORCE
:一个常数,定义当剑与 RigidBody 节点相撞时,施加在 PhysicsBody 节点上的力的大小。damage_body
:一个变量,用于存放 KinematicBody 节点,用于检测剑是否刺中了 PhysicsBody 节点。sword_noise
:一个变量,用来存放 AudioStreamPlayer3D 节点,当剑与某物碰撞时,用来播放声音。
``_ready``函数的逐步说明¶
我们在 _ready
函数中所做的就是获取 Damage_Body
KinematicBody <class_KinematicBody>`节点,并将其分配给 ``damage_body` 。因为我们不想让剑检测到与剑的根部 RigidBody 节点的碰撞,所以我们在 damage_body
上调用 add_collision_exception_with
,并传递 self
,这样剑的根部就不会被检测到。
Finally, we get the AudioStreamPlayer3D node for the sword collision sound and apply it to the sword_noise
variable.
_physics_process
function step-by-step explanation¶
首先,我们需要确定剑是否与某物相撞。为此,使用 damage_body
节点的 move_and_collide
函数。与通常使用 move_and_collide
不同的是,没有传递速度,而是传递一个空的 Vector3。因为不想让 damage_body
节点移动,所以将 test_only
参数(第四个参数)设置为 true
,这样 :ref:`KinematicBody <class_KinematicBody>`就会生成碰撞信息,而不会在碰撞世界中造成任何碰撞。
The move_and_collide
function will return a KinematicCollision class that has all of the information we need for detecting collisions
on the sword. We assign the return value of move_and_collide
to a variable called collision_results
.
接下来我们检查 collision_results
是否不等于 null
。如果 collision_results
不等于 null
,那么我们就知道这把剑与某物相撞了。
然后,使用 has_method
函数检查与剑相撞的 PhysicsBody 是否有一个叫做 damage
的函数或方法。如果 PhysicsBody 有一个叫做 damage_body
的函数,就调用它,并把剑的伤害量 SWORD_DAMAGE
传递给它。
接下来检查剑碰撞的 PhysicsBody 是否是一个 RigidBody。如果剑碰撞的是一个 RigidBody 节点,再通过检查 controller
是否等于 null
来查看剑是否被VR控制器所持有。
如果VR控制器没有握住剑, controller
等于 null
,那么就使用 apply_impulse
函数移动剑碰撞的 RigidBody 节点。对于 apply_impulse
函数中的 position
,使用 collision_results
中 KinematicCollision 类中存储的 collision_position
变量。对于 apply_impulse
函数中的 velocity
,使用 collision_normal
乘以剑的 RigidBody 节点的 linear_velocity
乘以 COLLISION_FORCE
。
如果剑被VR控制器握着, controller
不等于 null
,那么就使用 apply_impulse
函数移动剑碰撞的 RigidBody 节点。对于 apply_impulse
函数中的 position
,使用 collision_results
中 KinematicCollision 类中存储的 collision_position
变量。对于 apply_impulse
函数的 velocity
,使用 collision_normal
乘以VR控制器的速度乘以 COLLISION_FORCE
。
最后,不管 PhysicsBody,通过调用 sword_noise
上的 play
来播放剑与物品碰撞的声音。
更新目标UI¶
Let's update the UI as the sphere targets are destroyed.
打开 Main_VR_GUI.tscn
,可以在 Scenes
文件夹中找到它。如果想了解场景是如何设置的,但为了不让本教程变得太长,不在本教程中介绍。
Expand the GUI
Viewport node and then select the Base_Control
node. Add a new script called Base_Control.gd
, and add the following:
extends Control
var sphere_count_label
func _ready():
sphere_count_label = get_node("Label_Sphere_Count")
get_tree().root.get_node("Game").sphere_ui = self
func update_ui(sphere_count):
if sphere_count > 0:
sphere_count_label.text = str(sphere_count) + " Spheres remaining"
else:
sphere_count_label.text = "No spheres remaining! Good job!"
Let's go over how this script works real quick.
First, in _ready
, we get the Label that shows how many spheres are left and assign it to the sphere_count_label
class variable.
Next, we get Game.gd
by using get_tree().root
and assign sphere_ui
to this script.
In update_ui
, we change the sphere Label's text. If there is at least one sphere remaining, we change the text to show how many spheres are still
left in the world. If there are no more spheres remaining, we change the text and congratulate the player.
添加最终的特殊RigidBody¶
最后,在我们完成本教程之前,让我们添加一种在VR中重置游戏的方法。
Open up Reset_Box.tscn
, which you will find in Scenes
. Select the Reset_Box
RigidBody node and make a new script called Reset_Box.gd
.
Add the following code:
extends VR_Interactable_Rigidbody
var start_transform
var reset_timer = 0
const RESET_TIME = 10
const RESET_MIN_DISTANCE = 1
func _ready():
start_transform = global_transform
func _physics_process(delta):
if start_transform.origin.distance_to(global_transform.origin) >= RESET_MIN_DISTANCE:
reset_timer += delta
if reset_timer >= RESET_TIME:
global_transform = start_transform
reset_timer = 0
func interact():
# (Ignore the unused variable warning)
# warning-ignore:return_value_discarded
get_tree().change_scene("res://Game.tscn")
func dropped():
global_transform = start_transform
reset_timer = 0
Let's quickly go over how this script works.
Explaining the reset box code¶
Like with the other special RigidBody-based objects we've created, the reset box extends VR_Interactable_Rigidbody
.
start_transform
类变量将存储游戏开始时重置框的全局变换, reset_timer
类变量保存重置框位置移动后的时长, RESET_TIME
常量定义了重置框在被重置前需要等待的时长, RESET_MIN_DISTANCE
常量定义了重置框在重置计时器启动前需要离开初始位置多远。
在 _ready
函数中,我们所做的只是在场景开始时存储重置位置的 global_transform
。这样就可以在时间足够长时,将重置框对象的位置、旋转和比例重置为这个初始变换。
在 _physics_process
函数中,代码检查重置框的初始位置到重置框的当前位置是否比 RESET_MIN_DISTANCE
远。如果远,那么它就开始增加 reset_timer
时间 delta
。一旦 reset_timer
大于或等于 reset_TIME
,就把 global_transform
重置为 start_transform
,这样复位框就回到了初始位置。然后将 reset_timer
设置为 0
。
interact
函数只是使用 get_tree().change_scene
重新加载 Game.tscn
场景,这将重新加载游戏场景,重置所有。
最后, dropped
函数将 global_transform
重设为 start_transform
中的初始变换,这样复位框就有了初始位置旋转。然后将 reset_timer
设置为 0
,这样就复位了计时器。
Reset box finished¶
With that done, when you grab and interact with the reset box, the entire scene will reset/restart and you can destroy all the targets again!
注解
在没有任何形式过渡的情况下,突然重置场景会导致VR中的不适感。
最后的笔记¶

呼! 工作量还不小。
Now you have a fully working VR project with multiple different types of special RigidBody-based nodes that can be used and extended. Hopefully this will help serve as an introduction to making fully-featured VR games in Godot! The code and concepts detailed in this tutorial can be expanded on to make puzzle games, action games, story-based games, and more!
警告
你可以在 `OpenVR GitHub 仓库<https://github.com/GodotVR/godot_openvr_fps>`_ ,所发布标签中下载本系列教程的成品项目!