- VR starter tutorial part 2
- Adding a shotgun
- Raycast node that is a child of the
raycastsvariable. - RigidBody using the
apply_impulsefunction, where the position is a zero Vector3 so the force is applied from the center, and the collision force is thecollision_forcevariable we calculated. - Adding a bomb
- RigidBody node using the
apply_impulsefunction, with a Vector3 position of zero andcollision_forcemultiplied bydirection_vector.normalizedas the force. This will send the RigidBody node flying when the bomb explodes. - PhysicsBody nodes within the
explosion_area, we set theexplodedvariable totrueso the code knows the bomb exploded and callplayonexplosion_soundso the sound of an explosion is played. - Bomb finished
- Adding a sword
- Updating the target UI
- Adding the final special RigidBody
- Final notes
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
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. NoteRIGID_BODY_TARGETscene. 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._readyfunction step-by-step explanation_readyfunction does is that it stops the_physics_processfrom being called by callingset_physics_processand passingfalse. The reason we do this is because all the code in_physics_processis for destroying this node when enough time has passed, which we only want to do when the target has been destroyed._physics_processfunction step-by-step explanationdelta, to thedestroyed_timervariable. It then checks to see ifdestroyed_timeris greater than or equal toDESTROY_WAIT_TIME. Ifdestroyed_timeris greater than or equal toDESTROY_WAIT_TIME, then the sphere target frees/deletes itself by calling thequeue_freefunction.damagefunction step-by-step explanationdamagefunction 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 calleddamage. Thedamagevariable will hold the amount of damage the special RigidBody node did to the sphere target.destroyedvariable is equal totrue. Ifdestroyedis equal totrue, then the function callsreturnso 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 ifhealthis equal to zero or less, meaning that the target has just been destroyed. CollisionShape by setting it’sdisabledproperty totrue. We then make theSphere_TargetMeshInstance invisible by setting thevisibleproperty tofalse. 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_TARGETscene and adds it as a child of the target. It then sets theglobal_transformof the newly instanced scene, calledclone, to theglobal_transformof 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.destroyedvariable totrueso the target knows it has been destroyed and calls theset_physics_processfunction and passestrue. This will start executing the code in_physics_processso that afterDESTROY_WAIT_TIMEseconds have passed, the sphere target will free/destroy itself. AudioStreamPlayer3D node and calls theplayfunction so it plays its sound.remove_spherefunction is called inGame.gd. To getGame.gd, the code uses the scene tree and works its way from the root of the scene tree to the root of theGame.tscnscene.remove_spherefunction toGame.gdGame.gd, calledremove_sphere, that we have not defined yet. Open upGame.gdand add the following additional class variables: GDScriptvar spheres_left = 10var sphere_ui = null
spheres_left: The amount of sphere targets left in the world. In the providedGamescene, there are10spheres, 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_spherefunction. Add the following code toGame.gd: GDScript
Let’s go through what this function does real quick:func remove_sphere(): spheres_left -= 1 if sphere_ui != null: sphere_ui.update_ui(spheres_left)
spheres_leftvariable. It then checks to see if thesphere_uivariable is not equal tonull, and if it is not equal tonullit calls theupdate_uifunction onsphere_ui, passing in the number of spheres as an argument to the function. Notesphere_uilater in this tutorial!Sphere_Targetis 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 upPistol.tscn, which you can find in theScenesfolder.Pistol.tscnreal quick before we add the code.Pistol.tscnexpect 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 theVR_Interactable_Rigidbodyclass we created in the last part of this tutorial series. MeshInstance node calledPistol_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 calledLaserSightis used to as a guide for aiming the pistol, and it follows the direction of the Raycast node, calledRaycast, 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 calledPistoland make a new script calledPistol.gd. Add the following code: GDScript
Let’s go over how this script works.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
Explaining the pistol code
extends RigidBody, we instead haveextends VR_Interactable_Rigidbody. This makes it where the pistol script extends theVR_Interactable_Rigidbodyclass so the VR controllers know this object can be interacted with and that the functions defined inVR_Interactable_Rigidbodycan 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._readyfunction step-by-step explanationflash_meshandlaser_sight_meshnodes, both have theirvisibleproperty set tofalseso they are not visible initially._physics_processfunction step-by-step explanation_physics_processfunction first checks to see if the pistol’s muzzle flash is visible by checking ifflash_timeris more than zero. Ifflash_timeris more than zero, then we remove time,deltafrom it. Next we check if theflash_timervariable is zero or less now that we removeddeltafrom it. If it is, then the pistol muzzle flash timer just finished and so we need to makeflash_meshinvisible by setting it’svisibleproperty tofalse.interactfunction step-by-step explanationflash_timeris 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_timeris zero or less, we then setflash_timertoFLASH_TIMEso there is a delay before the pistol can fire again. After that we setflash_mesh.visibletotrueso the muzzle flash at the end of the pistol is visible whileflash_timeris more than zero.force_raycast_updatefunction on the Raycast node inraycastso that it gets the latest collision info from the physics world. We then check if theraycasthit something by checking if theis_collidingfunction is equal totrue.
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
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.raycastvariable fromPistol.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 asPistol.gdand function the same way, some just are renamed to be non-pistol specific.interactfunction step-by-step explanationflash_timeris 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_timeris zero or less, we then setflash_timertoFLASH_TIMEso there is a delay before the shotgun can fire again. After that we setflash_mesh.visibletotrueso the muzzle flash at the end of the shotgun is visible whileflash_timeris more than zero.force_raycast_updatefunction on the Raycast node inraycastso that it gets the latest collision info from the physics world. We then check if theraycasthit something by checking if theis_collidingfunction is equal totrue.raycastsvariable using a for loop. This way the code will go through each of the Raycast nodes that are children of theraycastsvariable.
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
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 explodesfuse_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 thelifetimeproperty of the explosion Particles node.explosion_timerA 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._readyfunction step-by-step explanation_readyfunction first gets all of the nodes from the bomb scene and assigns them to their respective class variables for later use.set_physics_processand passfalseso_physics_processis not executed. We do this because the code in_physics_processwill 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_processfunction step-by-step explanation_physics_processfunction first checks to see iffuse_timeris less thanFUSE_TIME. If it is, then the bomb’s fuse is still burning.delta, to thefuse_timervariable. We then check to see iffuse_timeris more than or equal toFUSE_TIMEnow that we have addeddeltato it. Iffuse_timeris more than or equal toFUSE_TIME, then the fuse has just finished and we need to explode the bomb.emittingtofalseonfuse_particles. We then tell the explosion Particles node,explosion_particles, to emit all of its particle in a single shot by settingone_shottotrue. After that, we setemittingtotrueonexplosion_particlesso it looks like the bomb has exploded. To help make it look like the bomb exploded, we hide the bomb MeshInstance node by settingbomb_mesh.visibletofalse.collision_layerandcollision_maskproperties of the bomb to0. We also change the RigidBody mode toMODE_STATICso the bomb RigidBody does not move. PhysicsBody nodes within theexplosion_areanode. To do this, we use theget_overlapping_bodiesin a for loop. Theget_overlapping_bodiesfunction 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
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_processcallCOLLISION_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._readyfunction step-by-step explanation_readyfunction is getting theDamage_BodyKinematicBody node and assigning it todamage_body. Because we do not want the sword to detect a collision with the root RigidBody node of the sword, we calladd_collision_exception_withondamage_bodyand passselfso the sword will not be detected. AudioStreamPlayer3D node for the sword collision sound and apply it to thesword_noisevariable._physics_processfunction step-by-step explanationmove_and_collidefunction of thedamage_bodynode. Unlike howmove_and_collideis normally used, we are not passing a velocity and instead are passing an empty Vector3. Because we do not want thedamage_bodynode to move, we set thetest_onlyargument (the fourth argument) astrueso the KinematicBody generates collision info without actually causing any collisions within the collision world.move_and_collidefunction will return a KinematicCollision class that has all of the information we need for detecting collisions on the sword. We assign the return value ofmove_and_collideto a variable calledcollision_results.collision_resultsis not equal tonull. Ifcollision_resultsis not equal tonull, then we know that the sword has collided with something. PhysicsBody the sword collided with has a function/method calleddamageusing thehas_methodfunction. If the PhysicsBody has a function calleddamage_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 ifcontrolleris equal tonull.controlleris equal tonull, then we move the RigidBody node the sword collided with using theapply_impulsefunction. For thepositionof theapply_impulsefunction, we usecollision_positionvariable stored within the KinematicCollision class incollision_results. For thevelocityof theapply_impulsefunction, we use thecollision_normalmultiplied by thelinear_velocityof the sword’s RigidBody node multiplied byCOLLISION_FORCE.controlleris not equal tonull, then we move the RigidBody node the sword collided with using theapply_impulsefunction. For thepositionof theapply_impulsefunction, we usecollision_positionvariable stored within the KinematicCollision class incollision_results. For thevelocityof theapply_impulsefunction, we use thecollision_normalmultiplied by the VR controller’s velocity multiplied byCOLLISION_FORCE. PhysicsBody is a RigidBody or not, we play the sound of the sword colliding with something by callingplayonsword_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 theScenesfolder. 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.GUIViewport node and then select theBase_Controlnode. Add a new script calledBase_Control.gd, and add the following: GDScript
Let’s go over how this script works real quick.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!"
_ready, we get the Label that shows how many spheres are left and assign it to thesphere_count_labelclass variable. Next, we getGame.gdby usingget_tree().rootand assignsphere_uito 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 inScenes. Select theReset_BoxRigidBody node and make a new script calledReset_Box.gd. Add the following code: GDScript
Let’s quickly go over how this script works.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
Explaining the reset box code
RigidBody-based objects we’ve created, the reset box extendsVR_Interactable_Rigidbody.start_transformclass variable will store the global transform of the reset box when the game starts, thereset_timerclass variable will hold the length of time that has passed since the reset box’s position has moved, theRESET_TIMEconstant defines the length of time the reset box has to wait before being reset, and theRESET_MIN_DISTANCEconstant defines how far the reset box has to be away from it’s initial position before the reset timer starts._readyfunction all we are doing is storing theglobal_transformof 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_processfunction, the code checks to see if the reset box’s initial position to the reset box’s current position is farther thanRESET_MIN_DISTANCE. If it is farther, then it starts adding time,delta, toreset_timer. Oncereset_timeris more than or equal toRESET_TIME, we reset theglobal_transformto thestart_transformso the reset box is back in its initial position. We then setreset_timerto0.interactfunction simply reloads theGame.tscnscene usingget_tree().change_scene. This will reload the game scene, resetting everything.droppedfunction resets theglobal_transformto the initial transform instart_transformso the reset box has its initial position/rotation. Thenreset_timeris set to0so 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!
