VR starter tutorial part 2

Introduction

RigidBody-based nodes that can be used in VR. VR_Interactable_Rigidbody. Tip OpenVR GitHub repository.

Adding destroyable targets

RigidBody-based nodes, we need something for them to do. Let’s make a simple sphere target that will break into a bunch of pieces when destroyed. 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 nodes will handle damaging the sphere, which is why we are using a StaticBody node instead of something like an Area or RigidBody node. Outside of that, there isn’t really a lot to talk about, so let’s move straight into writing the code. Sphere_Target_Root node and make a new script called Sphere_Target.gd. Add the following code: GDScript

  1. extends Spatialvar destroyed = falsevar destroyed_timer = 0const DESTROY_WAIT_TIME = 80var health = 80const 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: A constant to hold the scene of the destroyed sphere target. Note RIGID_BODY_TARGET scene. It is just a bunch of RigidBody nodes and a broken sphere model. We’ll be instancing this scene so when the target is destroyed, it looks like it broke into a bunch of pieces.

    _ready function step-by-step explanation

    _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, to the destroyed_timer variable. It then checks to see if destroyed_timer is greater than or equal to DESTROY_WAIT_TIME. If destroyed_timer is greater than or equal to DESTROY_WAIT_TIME, then the sphere target frees/deletes itself by calling the queue_free function.

    damage function step-by-step explanation

    damage function will be called by the special RigidBody nodes, which will pass the amount of damage done to the target, which is a function argument variable called damage. The damage variable will hold the amount of damage the special RigidBody node did to the sphere target. destroyed variable is equal to true. If destroyed is equal to true, then the function calls return so none of the other code is called. This is just a safety check so that if two things damage the target at exactly the same time, the target cannot be destroyed twice. damage, from the target’s health, health. If then checks to see if health is equal to zero or less, meaning that the target has just been destroyed. CollisionShape by setting it’s disabled property to true. We then make the Sphere_Target MeshInstance invisible by setting the visible property to false. We do this so the target can no longer effect the physics world and so the non-broken target mesh is not visible. RIGID_BODY_TARGET scene and adds it as a child of the target. It then sets the global_transform of the newly instanced scene, called clone, to the global_transform of the non-broken target. This makes it where the broken target starts at the same position as the non-broken target with the same rotation and scale. destroyed variable to true so the target knows it has been destroyed and calls the set_physics_process function and passes true. This will start executing the code in _physics_process so that after DESTROY_WAIT_TIME seconds have passed, the sphere target will free/destroy itself. AudioStreamPlayer3D node and calls the play function so it plays its sound. remove_sphere function is called in Game.gd. To get Game.gd, the code uses the scene tree and works its way from the root of the scene tree to the root of the Game.tscn scene.

    remove_sphere function to Game.gd

    Game.gd, called remove_sphere, that we have not defined yet. Open up Game.gd and add the following additional class variables: GDScript
    1. var spheres_left = 10var sphere_ui = null
  • spheres_left: The amount of sphere targets left in the world. In the provided Game scene, there are 10 spheres, so that is the initial value.
  • sphere_ui: A reference to the sphere UI. We will use this later in the tutorial to display the amount of spheres left in the world. remove_sphere function. Add the following code to Game.gd: GDScript
    1. 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: 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. Note sphere_ui later in this tutorial! Sphere_Target is ready to be used, but we don’t have any way to destroy it. Let’s fix that by adding some special RigidBody-based nodes that can damage the targets.

    Adding a pistol

    RigidBody node. Open up Pistol.tscn, which you can find in the Scenes folder. Pistol.tscn real quick before we add the code. Pistol.tscn expect the root node are rotated. This is so the pistol is in the correct rotation relative to the VR controller when it is picked up. The root node is a RigidBody node, which we need because we’re going to use the VR_Interactable_Rigidbody class we created in the last part of this tutorial series. MeshInstance node called Pistol_Flash, which is a simple mesh that we will be using to simulate the muzzle flash on the end of the pistol’s barrel. A MeshInstance node called LaserSight is used to as a guide for aiming the pistol, and it follows the direction of the Raycast node, called Raycast, that the pistol uses to detect if its ‘bullet’ hit something. Finally, there is an AudioStreamPlayer3D node at the end of the pistol that we will use to play the sound of the pistol firing. RigidBody node called Pistol and make a new script called Pistol.gd. Add the following code: GDScript
    1. extends VR_Interactable_Rigidbodyvar flash_meshconst FLASH_TIME = 0.25var flash_timer = 0var laser_sight_meshvar pistol_fire_soundvar raycastconst BULLET_DAMAGE = 20const COLLISION_FORCE = 1.5func _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 = falsefunc 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.25func picked_up(): laser_sight_mesh.visible = truefunc dropped(): laser_sight_mesh.visible = false
    Let’s go over how this script works.

    Explaining the pistol code

    extends RigidBody, we instead have extends VR_Interactable_Rigidbody. This makes it where the pistol script extends the VR_Interactable_Rigidbody class so the VR controllers know this object can be interacted with and that the functions defined in VR_Interactable_Rigidbody can be called when this object is held by a VR controller. Next, let’s look at the class variables:
  • flash_mesh: A variable to hold the MeshInstance node that is used to simulate muzzle flash on the pistol.
  • FLASH_TIME: A constant to define how long the muzzle flash will be visible. This will also define how fast the pistol can fire.
  • 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: A constant that defines the amount of force that is applied to RigidBody nodes when the pistol’s bullet collides.

    _ready function step-by-step explanation

    flash_mesh and laser_sight_mesh nodes, both have their visible property set to false so they are not visible initially.

    _physics_process function step-by-step explanation

    _physics_process function first checks to see if the pistol’s muzzle flash is visible by checking if flash_timer is more than zero. If flash_timer is more than zero, then we remove time, delta from it. Next we check if the flash_timer variable is zero or less now that we removed delta from it. If it is, then the pistol muzzle flash timer just finished and so we need to make flash_mesh invisible by setting it’s visible property to false.

    interact function step-by-step explanation

    flash_timer is less than or equal to zero. We do this so we can limit the rate of fire of the pistol to the length of time the muzzle flash is visible, which is a simple solution for limiting how fast the player can fire. flash_timer is zero or less, we then set flash_timer to FLASH_TIME so there is a delay before the pistol can fire again. After that we set flash_mesh.visible to true so the muzzle flash at the end of the pistol is visible while flash_timer is more than zero. force_raycast_update function on the Raycast node in raycast so that it gets the latest collision info from the physics world. We then check if the raycast hit something by checking if the is_colliding function is equal to true.

raycast hit something, then we get the PhysicsBody it collided with through the get_collider function. We assign the hit PhysicsBody to a variable called body. Raycast by getting it’s positive Z directional axis from the Basis on the raycast node’s global_transform. This will give us the direction the raycast is pointing on the Z axis, which is the same direction as the blue arrow on the Spatial gizmo when Local space mode is enabled in the Godot editor. We store this direction in a variable called direction_vector. Raycast origin to the Raycast collision point by getting the distance from the global position, global_transform.origin of the raycast node to the collision point of the Raycast, raycast.get_collision_point, using the distance_to function. This will give us the distance the Raycast traveled before it collided, which we store in a variable called raycast_distance. PhysicsBody, body, has a function/method called damage using the has_method function. If the PhysicsBody has a function/method called damage, then we call the damage function and pass BULLET_DAMAGE so it takes damage from the bullet colliding into it. PhysicsBody has a damage function, we then check to see if body is a RigidBody-based node. If body is a RigidBody-based node, then we want to push it when the bullet collides. COLLISION_FORCE and divide it by raycast_distance, then we multiply the whole thing by body.mass. We store this calculation in a variable called collision_force. This will make collisions over a shorter distance apply move force than those over longer distances, giving a slightly more realistic collision response.

RigidBody using the apply_impulse function, where the position is a zero Vector3 so the force is applied from the center, and the collision force is the collision_force variable we calculated.

raycast variable hit something or not, we then play the pistol shot sound by calling the play function on the pistol_fire_sound variable. controller variable is not equal to null. If it is not equal to null, we then set the rumble property of the VR controller to 0.25, so there is a slight rumble when the pistol fires.

picked_up function step-by-step explanation

laser_sight_mesh MeshInstance visible by setting the visible property to true.

dropped function step-by-step explanation

laser_sight_mesh MeshInstance invisible by setting the visible property to false.

Pistol finished

That is all we need to do to have working pistols in the project! Go ahead and run the project. If you climb up the stairs and grab the pistols, you can fire them at the sphere targets in the scene using the trigger button on the VR controller! If you fire at the targets long enough, they will break into pieces.

Adding a shotgun

Next let’s add a shotgun to the VR project. RigidBody should be fairly straightforward, as almost everything with the shotgun is the same as the pistol. 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. RigidBody node called Shotgun and make a new script called Shotgun.gd. Add the following code: GDScript

  1. extends VR_Interactable_Rigidbodyvar flash_meshconst FLASH_TIME = 0.25var flash_timer = 0var laser_sight_meshvar shotgun_fire_soundvar raycastsconst BULLET_DAMAGE = 30const COLLISION_FORCE = 4func _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 = falsefunc 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.25func picked_up(): laser_sight_mesh.visible = truefunc dropped(): laser_sight_mesh.visible = false

minor changes that are primarily just different names. Due to how similar these scripts are, let’s just focus on the changes.

Explaining the shotgun code

VR_Interactable_Rigidbody so the VR controllers know that this object can be interacted with and what functions are available. There is only one new class variable:

  • raycasts: A variable to hold the node that has all of the Raycast nodes as its children. raycast variable from Pistol.gd, because with the shotgun we need to process multiple Raycast nodes instead of just one. All of the other class variables are the same as Pistol.gd and function the same way, some just are renamed to be non-pistol specific.

    interact function step-by-step explanation

    flash_timer is less than or equal to zero. We do this so we can limit the rate of fire of the shotgun to the length of time the muzzle flash is visible, which is a simple solution for limiting how fast the player can fire. flash_timer is zero or less, we then set flash_timer to FLASH_TIME so there is a delay before the shotgun can fire again. After that we set flash_mesh.visible to true so the muzzle flash at the end of the shotgun is visible while flash_timer is more than zero. force_raycast_update function on the Raycast node in raycast so that it gets the latest collision info from the physics world. We then check if the raycast hit something by checking if the is_colliding function is equal to true. raycasts variable using a for loop. This way the code will go through each of the Raycast nodes that are children of the raycasts variable.

raycast is not a Raycast node. If the node is not a Raycast node, we simply use continue to skip it. raycast node randomly around a small 10 degrees cone by settings the rotation_degrees variable of the raycast to a Vector3 where the X and Z axis are a random number from -10 to 10. This random number is selected using the rand_range function. force_raycast_update function on the Raycast node in raycast so that it gets the latest collision info from the physics world. We then check if the raycast hit something by checking if the is_colliding function is equal to true.

Raycast node that is a child of the raycasts variable.

raycast hit something, then we get the PhysicsBody it collided with through the get_collider function. We assign the hit PhysicsBody to a variable called body. Z directional axis from the Basis on the raycast node’s global_transform. This will give us the direction the raycast is pointing on the Z axis, which is the same direction as the blue arrow on the Spatial gizmo when Local space mode is enabled in the Godot editor. We store this direction in a variable called direction_vector. global_transform.origin of the raycast node to the collision point of the raycast, raycast.get_collision_point, using the distance_to function. This will give us the distance the Raycast traveled before it collided, which we store in a variable called raycast_distance. PhysicsBody, body, has a function/method called damage using the has_method function. If the PhysicsBody has a function/method called damage, then we call the damage function and pass BULLET_DAMAGE so it takes damage from the bullet colliding into it. PhysicsBody has a damage function, we then check to see if body is a RigidBody-based node. If body is a RigidBody-based node, then we want to push it when the bullet collides. COLLISION_FORCE and divide it by raycast_distance, then we multiply the whole thing by body.mass. We store this calculation in a variable called collision_force. This will make collisions over a shorter distance apply move force than those over longer distances, giving a slightly more realistic collision response.

RigidBody using the apply_impulse function, where the position is a zero Vector3 so the force is applied from the center, and the collision force is the collision_force variable we calculated.

Raycasts in the raycast variable have been iterated over, we then play the shotgun shot sound by calling the play function on the shotgun_fire_sound variable. controller variable is not equal to null. If it is not equal to null, we then set the rumble property of the VR controller to 0.25, so there is a slight rumble when the shotgun fires.

Shotgun finished

Everything else is exactly the same as the pistol, with at most just some simple name changes. 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!).

Adding a bomb

RigidBody. Instead of adding something that shoots, let’s add something we can throw - a bomb! Bomb.tscn, which is in the Scenes folder. RigidBody node that we’ll be extending to use VR_Interactable_Rigidbody, which has a CollisionShape like the other special RigidBody nodes we’ve made so far. Likewise, there is a MeshInstance called Bomb that is used to display the mesh for the bomb. Area node simply called Area that has a large CollisionShape as its child. We’ll use this Area node to effect anything within it when the bomb explodes. Essentially, this Area node will be the blast radius for the bomb. Particles nodes. One of the Particles nodes are for the smoke coming out of the bomb’s fuse, while another is for the explosion. You can take a look at the ParticlesMaterial resources, which define how the particles work, if you want. We will not be covering how the particles work in this tutorial due to it being outside of the scope of this tutorial. 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. Bomb RigidBody node and make a new script called Bomb.gd. Add the following code: GDScript

  1. extends VR_Interactable_Rigidbodyvar bomb_meshconst FUSE_TIME = 4var fuse_timer = 0var explosion_areaconst EXPLOSION_DAMAGE = 100const EXPLOSION_TIME = 0.75var explosion_timer = 0var exploded = falseconst COLLISION_FORCE = 8var fuse_particlesvar explosion_particlesvar explosion_soundfunc _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.

Explaining the bomb code

RigidBody nodes, the bomb extends VR_Interactable_Rigidbody so the VR controllers know this object can be interacted with and that the functions defined defined in VR_Interactable_Rigidbody can be called when this object is held by a VR controller. Next, let’s look at the class variables:

  • bomb_mesh: A variable to hold the MeshInstance node that is used for the non-exploded bomb.
  • FUSE_TIME: A constant to define how long the fuse will ‘burn’ before the bomb explodes
  • 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: A constant to define how long the bomb will last in the scene after it explodes. This value should be the same as the lifetime property of the explosion Particles node.
  • 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: A constant that defines the amount of force that is applied to RigidBody nodes when the bomb explodes.
  • 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 function step-by-step explanation

    _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 and pass false so _physics_process is not executed. We do this because the code in _physics_process will start burning the fuse and exploding the bomb, which we only want to do when the user interacts with the bomb. If we did not disable _physics_process, the bomb’s fuse would start before the user has a chance to get to the bomb.

    _physics_process function step-by-step explanation

    _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. delta, to the fuse_timer variable. We then check to see if fuse_timer is more than or equal to FUSE_TIME now that we have added delta to it. If fuse_timer is more than or equal to FUSE_TIME, then the fuse has just finished and we need to explode the bomb. emitting to false on fuse_particles. We then tell the explosion Particles node, explosion_particles, to emit all of its particle in a single shot by setting one_shot to true. After that, we set emitting to true on explosion_particles so it looks like the bomb has exploded. To help make it look like the bomb exploded, we hide the bomb MeshInstance node by setting bomb_mesh.visible to false. collision_layer and collision_mask properties of the bomb to 0. We also change the RigidBody mode to MODE_STATIC so the bomb RigidBody does not move. PhysicsBody nodes within the explosion_area node. To do this, we use the get_overlapping_bodies in a for loop. The get_overlapping_bodies function will return an array of PhysicsBody nodes within the Area node, which is exactly what we are looking for.

PhysicsBody node, which we store in a variable called body, we check to see if it is equal to self. We do this so the bomb does not accidentally explode itself, as the explosion_area could potentially detect the Bomb RigidBody as a PhysicsBody within the explosion area. PhysicsBody node, body, is not the bomb, then we first check to see if the PhysicsBody node has a function called damage. If the PhysicsBody node has a function called damage, we call it and pass EXPLOSION_DAMAGE to it so it takes damage from the explosion. PhysicsBody node is a RigidBody. If body is a RigidBody, we want to move it when the bomb explodes. RigidBody node when the bomb explodes, we first need to calculate the direction from the bomb to the RigidBody node. To do this we subtract the global position of the bomb, global_transform.origin from the global position of the RigidBody. This will give us a Vector3 that points from the bomb to the RigidBody node. We store this Vector3 in a variable called direction_vector. RigidBody is from the bomb by using the length function on direction_vector. We store the distance in a variable called bomb_distance. RigidBody node when the bomb explodes by dividing COLLISION_FORCE by bomb_distance, and multiplying that by collision_force. This will make it so if the RigidBody node is closer to the bomb, it will be pushed farther.

RigidBody node using the apply_impulse function, with a Vector3 position of zero and collision_force multiplied by direction_vector.normalized as the force. This will send the RigidBody node flying when the bomb explodes.

PhysicsBody nodes within the explosion_area, we set the exploded variable to true so the code knows the bomb exploded and call play on explosion_sound so the sound of an explosion is played.

exploded is equal to true. exploded is equal to true, then that means the bomb is waiting for the explosion particles to finish before it frees/destroys itself. We add time, delta, to explosion_timer so we can track how long it has been since the bomb has exploded. explosion_timer is greater than or equal to EXPLOSION_TIME after we added delta, then the explosion timer just finished. explosion_area.monitoring to false. The reason we do this is because there was a bug that would print an error when you freed/deleted an Area node when the monitoring property was true. To make sure this doesn’t happen, we simply set monitoring to false on explosion_area. controller variable is not equal to null. If the bomb is being held by a VR controller, we set the held_object property of the VR controller, controller, to null. Because the VR controller is no longer holding anything, we make the VR controller’s hand mesh visible by setting controller.hand_mesh.visible to true. Then we check to see if the VR controller grab mode is RAYCAST, and if it is we set controller.grab_raycast.visible to true so the ‘laser sight’ for the grab raycast is visible. queue_free so the bomb scene is freed/removed from the scene.

interact function step-by-step explanation

interact function calls set_physics_process and passes true so the code in _physics_process starts executing. This will start the bomb’s fuse and eventually lead to the bomb exploding. fuse_particles.visible to true.

Bomb finished

Now the bomb is ready to go! You can find the bombs in the orange building. 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.

Adding a sword

RigidBody-based node that can destroy targets. Let’s add a sword so we can slice through the targets! Sword.tscn, which you can find in the Scenes folder. Sword RigidBody node are rotated to they are positioned correctly when the VR controller picks them up, there is a MeshInstance node for displaying the sword, and there is an AudioStreamPlayer3D node that holds a sound for the sword colliding with something. KinematicBody node called Damage_Body. If you take a look at it, you’ll find that it is not on any collision layers, and is instead only on a single collision mask. This is so the KinematicBody will not effect other PhysicsBody nodes in the scene, but it will still be effected by PhysicsBody nodes. Damage_Body KinematicBody node to detect the collision point and normal when the sword collides with something in the scene. Tip KinematicBody this way means we can detect exactly where the sword collided with other PhysicsBody nodes. Sword RigidBody node and make a new script called Sword.gd. Add the following code: GDScript

  1. extends VR_Interactable_Rigidbodyconst SWORD_DAMAGE = 2const COLLISION_FORCE = 0.15var damage_body = nullfunc _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()

Let’s go over how this script works!

Explaining the sword code

RigidBody nodes, the sword extends VR_Interactable_Rigidbody so the VR controllers know this object can be interacted with and that the functions defined defined in VR_Interactable_Rigidbody can be called when this object is held by a VR controller. Next, let’s look at the class variables:

  • SWORD_DAMAGE: A constant to define the amount of damage the sword does. This damage is applied to every object in the sword on every _physics_process call
  • COLLISION_FORCE: A constant that defines the amount of force applied to RigidBody nodes when the sword collides with a PhysicsBody.
  • damage_body: A variable to hold the KinematicBody node used to detect whether the sword is stabbing a PhysicsBody node or not.
  • sword_noise: A variable to hold the AudioStreamPlayer3D node used to play a sound when the sword collides with something.

    _ready function step-by-step explanation

    _ready function is getting the Damage_Body KinematicBody node and assigning it to damage_body. Because we do not want the sword to detect a collision with the root RigidBody node of the sword, we call add_collision_exception_with on damage_body and pass self so the sword will not be detected. AudioStreamPlayer3D node for the sword collision sound and apply it to the sword_noise variable.

    _physics_process function step-by-step explanation

    move_and_collide function of the damage_body node. Unlike how move_and_collide is normally used, we are not passing a velocity and instead are passing an empty Vector3. Because we do not want the damage_body node to move, we set the test_only argument (the fourth argument) as true so the KinematicBody generates collision info without actually causing any collisions within the collision world. 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 is not equal to null. If collision_results is not equal to null, then we know that the sword has collided with something. PhysicsBody the sword collided with has a function/method called damage using the has_method function. If the PhysicsBody has a function called damage_body, we call it and pass the amount of damage the sword does, SWORD_DAMAGE, to it. PhysicsBody the sword collided with is a RigidBody. If what the sword collided with is a RigidBody node, we then check to see if the sword is being held by a VR controller or not by checking to see if controller is equal to null. controller is equal to null, then we move the RigidBody node the sword collided with using the apply_impulse function. For the position of the apply_impulse function, we use collision_position variable stored within the KinematicCollision class in collision_results. For the velocity of the apply_impulse function, we use the collision_normal multiplied by the linear_velocity of the sword’s RigidBody node multiplied by COLLISION_FORCE. controller is not equal to null, then we move the RigidBody node the sword collided with using the apply_impulse function. For the position of the apply_impulse function, we use collision_position variable stored within the KinematicCollision class in collision_results. For the velocity of the apply_impulse function, we use the collision_normal multiplied by the VR controller’s velocity multiplied by COLLISION_FORCE. PhysicsBody is a RigidBody or not, we play the sound of the sword colliding with something by calling play on sword_noise.

    Sword finished

    With that done, you can now slice through the targets! You can find the sword in the corner in between the shotgun and the pistol.

    Updating the target UI

    Let’s update the UI as the sphere targets are destroyed. Main_VR_GUI.tscn, which you can find in the Scenes folder. Feel free to look at how the scene is setup if you want, but in an effort to keep this tutorial from becoming too long, we will not be covering the scene setup in this tutorial. GUI Viewport node and then select the Base_Control node. Add a new script called Base_Control.gd, and add the following: GDScript
    1. extends Controlvar sphere_count_labelfunc _ready(): sphere_count_label = get_node("Label_Sphere_Count") get_tree().root.get_node("Game").sphere_ui = selffunc 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. _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. 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.

    Adding the final special RigidBody

    Finally, before we finish this tutorial, let’s add a way to reset the game while in VR. 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: GDScript
    1. extends VR_Interactable_Rigidbodyvar start_transformvar reset_timer = 0const RESET_TIME = 10const RESET_MIN_DISTANCE = 1func _ready(): start_transform = global_transformfunc _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 = 0func 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

    RigidBody-based objects we’ve created, the reset box extends VR_Interactable_Rigidbody. start_transform class variable will store the global transform of the reset box when the game starts, the reset_timer class variable will hold the length of time that has passed since the reset box’s position has moved, the RESET_TIME constant defines the length of time the reset box has to wait before being reset, and the RESET_MIN_DISTANCE constant defines how far the reset box has to be away from it’s initial position before the reset timer starts. _ready function all we are doing is storing the global_transform of the reset position when the scene starts. This is so we can reset the position, rotation, and scale of the reset box object to this initial transform when enough time has passed. _physics_process function, the code checks to see if the reset box’s initial position to the reset box’s current position is farther than RESET_MIN_DISTANCE. If it is farther, then it starts adding time, delta, to reset_timer. Once reset_timer is more than or equal to RESET_TIME, we reset the global_transform to the start_transform so the reset box is back in its initial position. We then set reset_timer to 0. interact function simply reloads the Game.tscn scene using get_tree().change_scene. This will reload the game scene, resetting everything. dropped function resets the global_transform to the initial transform in start_transform so the reset box has its initial position/rotation. Then reset_timer is set to 0 so the timer is reset.

    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! Note Resetting the scene abruptly without any sort of transition can lead to discomfort in VR.

    Final notes

    Whew! That was a lot of work. 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! Warning OpenVR GitHub repository, under the releases tab!