Unity VR Roller Coaster Shooter Game Tutorial Pt.2

Steps Part 2

Create Template Shooting Target 3D-Model
YouTube

Create Shooting Target 3D-Model
    Create Empty ShootingTarget GameObject

        Unity Menu Bar > GameObject > Create Empty
        Hierarchy view > rename empty ‘GameObject’ to ‘ShootingTarget-Prefab’
        Inspector view > ShootingTarget-Prefab.Transform.Position = (0, 0, 0)

    Create Target Cylinder GameObject
        Unity Menu Bar > GameObject > 3D Object > Cylinder
        Hierarchy view
            > rename ‘Cylinder’ to ‘Target’
            > parent Target to ShootingTarget-Prefab
            > select Target
        Inspector view > Target.Transform
            .Position = (0, 2, 0)
            .Rotation = (90, 0, 0)
            .Scale = (1, 0.05, 1)

    Remove Capsule Collider component and Add Mesh Collider component to Target
        Hierarchy view > ShootingTarget-Prefab > select Target
        Inspector view > Target
            .right click Capsule Collider > Remove Component
            .Add Component > Physics > Mesh Collider
            .Mesh Collider.Convex = true

    Create New Material for Target
        Project view > Assets/MyGame/Materials
            > right click > Create > Material
            > rename ‘New Material.mat’ to ‘ShootingTarget-Mat.mat’
            > select ShootingTarget-Mat
        Inspector view > ShootingTarget-Mat (Material).Main Maps
            .Albedo color = RGBA 0-1.0 (1,0,0)
            .Emission = true
                .Color = RGB 0-255 (255,0,0)
                    .Intensity = 1

    Assign New Material to Target Mesh Renderer Component
        Hierarchy view > ShootingTarget-Prefab > select Target
        Inspector view > Target.Mesh Renderer.Materials.Element 0 = ShootingTarget-Mat

    Create Particle System GameObject
        Unity Menu Bar > GameObject > Effects > Particle System
        Hierarchy view
            > parent Particle System to ShootingTarget-Prefab
            > ShootingTarget-Prefab > select Particle System
        Inspector view > ParticleSystem.Transform.Position = (0, 2, 0)

    Create Audio Source GameObject
        Unity Menu Bar > GameObject > Audio > Audio Source
        Hierarchy view
            > parent Audio Source to ShootingTarget-Prefab
            > ShootingTarget-Prefab > select Audio Source
        Inspector view > Audio Source.Transform.Position = (0, 2, 0)

Save changes made to the scene
    Unity > (Ctrl + S) or Unity Menu Bar > File > Save
    (Save your scene regularly to avoid losing your progress in case of a crash either by going to File > Save in the Unity menu bar or by pressing Control + S on your keyboard. You can see if there are any changes made to the scene that still need to be saved if there is a star * character displayed right after the name of your scene in the Hierarchy view.)

Notes

We create a basic shooting target template GameObject by creating an empty GameObject with a cylinder shaped target parented to it.

Giving the Target an empty parent makes it easier to place the targets on the floor or the ground by setting the position of the parent to the height of the floor or ground while the actual target is positioned at two meters above the floor height relative to its parent.

Because the default Cylinder uses a CapsuleCollider which doesn’t really fit the coin-like shape of the Target we swap it for a MeshCollider, which by default will use the Mesh from the Mesh Filter when it is added to a GameObject.
MeshColliders are a lot more expensive than CapsuleColliders so only use them for really important GameObjects and with really simple low-poly meshes.

To make the targets clearly visible to the player no matter the lighting circumstances we can make the target ’emit’ light by enabling the Emission property in the Material settings and setting the HDR Intensity value of the emission color to something like 1 or higher. This way even when the target is hidden in a very dark corner it can still be seen by the player because it will not receive any shadows if it is emitting light itself, and it gives the targets such a nice gamey look!

Finally we add a ParticleSystem and AudioSource to the shooting target so that it can emit some particles and play a sound clip when it is hit by a bullet.
In the next step we will setup the particle system, create a Target script and turn the shooting target into a prefab, so that we can duplicate it many times and still be able to modify all of them easily at once. 

Links

Unity Manual / Scripting / Important Classes / MonoBehaviour
<TODO: Add link>

Unity Scripting API/ MonoBehaviour class
https://docs.unity3d.com/ScriptReference/MonoBehaviour.html

Unity Scripting API / MonoBehaviour / Update function
docs.unity3d.com/ScriptReference/MonoBehaviour.Update.html

TODO: Figure out why pasting links here causes all the YouTube embeds to not work. Turns out that i can only add about 200 URL’s to a page before the YouTube embeds start to break so I’m now waiting on the WordPress team to come up with a solution…

Setup Shooting Target
YouTube

Setup Particle System
Inspector view > Particle System.Particle System
    .Particle System
        .Duration = 1
        .Looping = false
        .Start Lifetime = 2
        .Start Speed = 8
        .Start Size = 0.2
        .Gravity Modifier = 1
        .Simulation Space = World
        .Play on Awake = false
    .Emission
            .Rate over Time = 0
            .Bursts.+
    .Shape
        .Shape = Sphere
        .Radius = 0.5
    .Size over Lifetime = true
        .Size.
            .Key 1 = (0, 1)
            .Key 2 = (1, 0) 
    .Collision = true
        .Type = World
        .Dampen = 0.2
        .Lifetime Loss = 0.5
        .Collision Quality = low

Notes

To setup the Particle System’s Size over Lifetime behavior first click on the boolean checkmark to enable the Size over Lifetime module.
Then click on the AnimationCurve that you see below the Seperate Axes bool variable in the Size over Lifetime module, (think 2D spline/bezier curve but used for animations and things) and set the first Key to time/value (0,1) instead of (0,0) by right clicking on it and selecting Edit Key.
Set the second Key to (1, 0) and reset both keys ‘tangent mode’ to Auto instead of Free Smooth and Flat in the same right click menu to create a linear diagonal line that goes from high to low instead of low to high.
This will make the particles shrink in size over the duration of their lifetime instead of increase in size.

Setup Audio Source
    Inspector view > Audio Source.Audio Source
        .AudioClip = Button Pop.wav (XR Interaction Toolkit Starter Assets)H
        .PlayOnAwake = false

Create New Target.cs Script
    Project view > Assets/MyGame/Scripts/
        > right mouse > Create > C# Script > name ‘Target.cs’
        > open Target.cs

Create public ParticleSystem targetParticleSystem and AudioSource targetAudioSource variables/references
    Visual Studio Code > Target.cs > add Highlighted Code 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Target : MonoBehaviour
{
    public ParticleSystem targetParticleSystem;
    public AudioSource targetAudioSource;

    // Start is called before the first frame update
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

Create OnCollisionEnter(Collision col) Function and Logic in Target.cs Script
    Visual Studio Code > Target.cs > add Highlighted Code 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Target : MonoBehaviour
{
    public ParticleSystem targetParticleSystem;
    public AudioSource targetAudioSource;

    // Start is called before the first frame update
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        
    }
    
    // OnCollisionEnter is called when this collider/rigidbody 
    // has begun touching another rigidbody/collider.
    public IEnumerator OnCollisionEnter(Collision collider)
    {
        Debug.Log("Target was hit by: " + collider.gameObject.name);

        targetAudioSource.Play();
        targetParticleSystem.Play();

        // Disable the target's MeshCollider so it can not be hit multiple times.
        GetComponent<MeshCollider>().enabled = false;
        // Disable the target's MeshRenderer to turn it invisible
        GetComponent<MeshRenderer>().enabled = false;
    
        yield return null;
    }
}

Create public UnityEvent onTargetHitEvent variable
    Visual Studio Code > Target.cs > add highlighted code 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

public class Target : MonoBehaviour
{
    public ParticleSystem targetParticleSystem;
    public AudioSource targetAudioSource;
    public UnityEvent onTargetHitEvent;

    // Start is called before the first frame update
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        
    }
    
    // OnCollisionEnter is called when this collider/rigidbody 
    // has begun touching another rigidbody/collider.
    public IEnumerator OnCollisionEnter(Collision collider)
    {
        Debug.Log("Target was hit by: " + collider.gameObject.name);

        targetParticleSystem.Play();
        targetAudioSource.Play();

        // Disable the target's MeshCollider so it can not be hit multiple times.
        GetComponent<MeshCollider>().enabled = false;
        // Disable the target's MeshRenderer to turn it invisible
        GetComponent<MeshRenderer>().enabled = false;
    
        yield return null;
    }
}

Invoke functions subscribed to the onTargetHitEvent event in the Inspector from OnCollisionEnter()
    Visual Studio Code > Target.cs > add highlighted code 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

public class Target : MonoBehaviour
{
    public ParticleSystem targetParticleSystem;
    public AudioSource targetAudioSource;
    public UnityEvent onTargetHitEvent;

    // Start is called before the first frame update
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        
    }
    
    // OnCollisionEnter is called when this collider/rigidbody 
    // has begun touching another rigidbody/collider.
    public IEnumerator OnCollisionEnter(Collision collider)
    {
        Debug.Log("Target was hit by: " + collider.gameObject.name);

        targetParticleSystem.Play();
        targetAudioSource.Play();

        // Disable the target's MeshCollider so it can not be hit multiple times.
        GetComponent<MeshCollider>().enabled = false;
        // Disable the target's MeshRenderer to turn it invisible
        GetComponent<MeshRenderer>().enabled = false;
    
        if (onTargetHitEvent != null)
        {
            // If there are functions subscribed to the onTargetHitEvent invoke (call) them.
            onTargetHitEvent.Invoke();
        }
        
        yield return null;
    }
}

Save changes made to Target.cs
    Visual Studio Code > Target.cs > (Ctrl + S)

Add Target Script to Target GameObject
    Inspector view > Target
        .Add Component > Scripts > Target.cs
        .Target.TargetParticleSystem = Hierarchy view > ShootingTarget-Prefab > Particle System
        .Target.TargetAudioSource = Hierarchy view > ShootingTarget-Prefab  > Audio Source

Create ShootingTarget Prefab File
    Hierarchy view > drag ShootingTarget-Prefab to Project view > Assets/MyGame/Prefabs

Save changes made to the scene
    Unity > (Ctrl + S) or Unity Menu Bar > File > Save
    (Save your scene regularly to avoid losing your progress in case of a crash either by going to File > Save in the Unity menu bar or by pressing Control + S on your keyboard. You can see if there are any changes made to the scene that still need to be saved if there is a star * character displayed right after the name of your scene in the Hierarchy view.)

Notes

We setup the target particle system with behavior that sort of resembles flying fire sparks, caused by the ‘explosion’ of the target and we setup the audio source with a placeholder sound effect that we will later swap for a real sound effect. 

(UnityEvents are not as flexible as regular C# events but they’re shown in the inspector by Unity by default when public, so they’re really easy to use.) 

Links

Unity Scripting API / Events / UnityEvent class
https://docs.unity3d.com/ScriptReference/Events.UnityEvent.html
Unity Scripting API / MonoBehaviour / Invoke class
https://docs.unity3d.com/ScriptReference/MonoBehaviour.Invoke.html

YouTube / UnityEvents Explained
https://www.youtube.com/watch?v=TWxXD-UpvSg

Add multiple Shooting Targets to the scene
YouTube

Place Multiple Targets Around The Track
    Create Empty ShootingTargets Container GameObject
        Unity Menu Bar > Create > Empty GameObject
        Hierarchy view > rename Empty ‘GameObject’ to ‘ShootingTargets’
        Inspector view > ShootingTargets.Transform.Position = (0, 0, 0)
        Hierarchy view > parent ShootingTarget-Prefab to ShootingTargets

    Place the First ShootingTarget-Prefab in front of the rides’ start
        Inspector view > ShootingTarget-Prefab.Transform.Position = (0, <Your Terrain Height>, 5)

    Duplicate ShootingTarget-Prefab
        Hierarchy view > duplicate ShootingTarget-Prefab * ~50

Save changes made to the scene
    Unity > (Ctrl + S) or Unity Menu Bar > File > Save
    (Save your scene regularly to avoid losing your progress in case of a crash either by going to File > Save in the Unity menu bar or by pressing Control + S on your keyboard. You can see if there are any changes made to the scene that still need to be saved if there is a star * character displayed right after the name of your scene in the Hierarchy view.)

Notes

Create an empty GameObject named ‘ShootingTargets’ to hold all the shooting targets so they don’t clutter up the Hierarchy view and reset its position.
Parent the ShootingTarget-Prefab to the ShootingTargets empty GameObject and position it at a few meters distance in front of the RollerCoasterTrain.
This will be the target that starts the ride when the player fires at it.

Duplicate ShootingTarget-Prefab multiple times and place the targets around the track at positions where the player can shoot them from inside the moving roller coaster train.
For a level that has a relatively slow moving train like the one in this (template) level you probably need around 50 targets spread around the track to make sure the player doesn’t get bored!

Be creative with the placing of the targets, you can place small groups of targets in rows next to each other or above each other and you can place them right behind trees or high up in trees so the player can’t always spot them immediately. 

You can freely change the scale of the targets to make some targets smaller than others and you can swap the 3D-Model/Mesh inside of the MeshFilter component of any Target GameObject to whichever mesh you want.
Right now the Targets use a Cylinder Mesh with a perfectly fitting MeshCollider component but if you change a targets mesh to for instance a 3D-Model of a piggybank or a balloon then just use a SphereCollider component, instead of a MeshCollider, that fits around the shape of the 3D-Model.
For any 3D-Model that would fit better inside of a cube shape than a sphere shape just use a CubeCollider component that fits relatively well around the shape of the Mesh, to make sure the bullets can still hit the targets accurately.
It is important to use only box colliders, cube colliders and capsule colliders wherever you possibly can because they are the easiest primitive shapes for the Physics system to check against other colliders, because collisions are very expensive to calculate!
For very simple other shapes you can sometimes use a more complex MeshCollider but there is a very limited max amount of vertices that a MeshCollider may have built into Unity to stop you from using too complex shapes for collision detection.

In phase two we’ll do some experiments with different ways to create moving targets and different ways to make them explode etcetera.

Creating a GameManager
YouTube

Create a new Empty GameManager GameObject
    Unity Menu Bar > GameObject > Create Empty
    Hierarchy view > rename Empty ‘GameObject’ to ‘GameManager’

Create New GameManager.cs Script
    Project view > Assets/MyGame/Scripts
        > right click > Create > C# Script > name GameManager.cs
        > open GameManager.cs
        (Unity has a special cogwheel icon reserved for scripts that are called GameManager, so the script icon will look different from all the other scripts in your folder but it is still a regular .cs file!)

Check if GameManager class name matches GameManager.cs file name
    Visual Studio Code > GameManager.cs > edit Highlighted Code

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameManager : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {

    }
}

Call the Unity Debug.Log() function to print messages to the Unity Console view   
    Visual Studio Code > GameManager.cs > add Highlighted Code

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameManager : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        Debug.Log("The Game is Starting!!!");
    }

    // Update is called once per frame
    void Update()
    {
        Debug.Log("The Game is Running!!!");
    }
}

Save changes made to GameManager.cs
    Visual Studio Code > GameManager.cs > (Ctrl + S)

Add GameManager.cs script to GameManager GameObject
    Hierarchy view > select GameManager
    Inspector view > GameManager.Add Component > Scripts > GameManager.cs

Save changes made to the scene
    Unity > (Ctrl + S) or Unity Menu Bar > File > Save
    (Save your scene regularly to avoid losing your progress in case of a crash either by going to File > Save in the Unity menu bar or by pressing Control + S on your keyboard. You can see if there are any changes made to the scene that still need to be saved if there is a star * character displayed right after the name of your scene in the Hierarchy view.)

Notes

In most games it makes sense to have a single script that contains all or most of the logic responsible for ‘managing’ the different game states of a game (think of the game running normaly and being paused as two different states for example), so in this step we create a GameManager GameObject with a (rather empty for now) GameManager.cs script attached to it. The GameManager component will be responsible for controlling the states of our game, and also for keeping references to all of the other GameObjects and components of our game that we need references to, so they will be easy to access by other GameObjects and by us.
Calling the GameManager ‘GameManager’ is kind of a convention, so lets stick to it.

When you create a script with the name GameManager.cs then Unity will change the icon of the script in the Project view to a gear icon which is different from the regular script icons, but that is just a legacy bug that Unity decided not to fix.. Which is good, because maybe it makes you wonder if you could change the icons for other scripts as well, and yes, you can! 
By adding a Gizmos folder to the project containing a .PNG texture with the exact name of the script plus the word ‘Icon’ added you can change the icon of any script that you want (Assets/Gizmos/NewBehaviourScript Icon.png). 
A small example of ‘Editor coding’ which is something that Unity is made for.

For now we only print two messages to the console using the Debug.Log() function to tell us when the game is starting, and when the game is running, which we can do from Start() and from Update(). In the next step we will add a function to the GameManager class that will start the roller coaster ride when it is called, so that we can let the player control when exactly the ride starts.

Starting the roller coaster ride
YouTube

Add a public RollerCoasterTrain rollerCoasterTrain variable/reference to the GameManager class
    Visual Studio Code > GameManager.cs > add Highlighted Code

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameManager : MonoBehaviour
{
    public RollerCoasterTrain rollerCoasterTrain;

    // Start is called before the first frame update
    void Start()
    {
         Debug.Log("The Game is Starting!!!");
    }

    // Update is called once per frame
    void Update()
    {
        Debug.Log("The Game Is Running!!!");
    }
}

Add StartRollerCoasterRide() function and logic to GameManager class
    Visual Studio Code > GameManager.cs > add Highlighted Code
    (Don’t forget to add the using UnityEngine.Splines directive on line 4!)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Splines;

public class GameManager : MonoBehaviour
{
    public RollerCoasterTrain rollerCoasterTrain;

    // Start is called before the first frame update
    void Start()
    {
         Debug.Log("The Game is Starting!!!");
    }

    // Update is called once per frame
    void Update()
    {
        Debug.Log("The Game Is Running!!!");
    }

    public void StartRollerCoasterRide()
    {
        Debug.Log("The Ride is Starting!!!");

        rollerCoasterTrain.GetComponent<SplineAnimate>().Play();
    }
}

Save changes made to GameManager.cs
    Visual Studio Code > GameManager.cs > (Ctrl + S)

Setup the GameManager component on the GameManager GameObject
    Hierarchy view > select GameManager
    Inspector view > GameManager.GameManager.Roller Coaster Train = Hierarchy view > RollerCoasterTrain

Let the First ShootingTarget Start The Roller Coaster Ride by calling the GameManager.StartRollerCoasterRide() function from Target.OnTargetHitEvent() event
    Hierarchy view > ShootingTarget-Prefab (0) > select Target
    Inspector view > Target.Target.OnTargetHitEvent
            .+ (click Plus Button)
            .Object (None) = Hierarchy view > GameManager
            .No Function = GameManager.StartRollerCoasterRide ()

Playtest Your Game
    Unity Editor > press Play button
    Your Body > Head > wear Quest

Notes

To test your game in VR in the Unity Editor during Play Mode you first have to turn on your Quest headset and connect it to your computer with the USB link cable. Then you have to launch the Oculus App on your computer and make sure your headset shows up as connected in the Oculus App > Devices window. Then when you put on the Quest headset you should see a pop up window asking if you want to enable Oculus Link and a pop up window asking if you want to enable USB debugging, press yes on both pop ups.
When Oculus Link is enabled you will see the Oculus Dashboard instead of the regular Stand alone Quest dashboard environment.
(If you didn’t get the pop-up message then you can still enable Oculus link manually on the Quest, in the Quest’s Quick Settings window.)
When you are in the Oculus PC dashboard on your Quest then you can press the Play button in the Unity Editor on your computer. Your scene will load and you can playtest it in VR!

Save changes made to the scene
    Unity > (Ctrl + S) or Unity Menu Bar > File > Save
    (Save your scene regularly to avoid losing your progress in case of a crash either by going to File > Save in the Unity menu bar or by pressing Control + S on your keyboard. You can see if there are any changes made to the scene that still need to be saved if there is a star * character displayed right after the name of your scene in the Hierarchy view.)

Notes

We let the first ShootingTarget-Prefab start the roller coaster ride when it’s hit by ‘subscribing’ the GameManager.StartRollerCoasterRide() function to the Target.OnTargetHitEvent() event of the ShootingTarget’s Target component. This way we give the player time to look around and to get comfortable before starting the ride

Make sure that the Play On Awake property of the RollerCoasterTrain’s Spline Animate Component is set to false, so the ride doesn’t start automatically!

Links

Links
    Spline Animate Component – Unity Manual
    https://docs.unity3d.com/Packages/com.unity.splines@2.1/manual/animate-component.html
    Spline Animate Component – Unity Scripting API
    https://docs.unity3d.com/Packages/com.unity.splines@2.1/api/UnityEngine.Splines.SplineAnimate.html
        Spline Animate Component / Methods – Unity Scripting API
        https://docs.unity3d.com/Packages/com.unity.splines@2.1/api/UnityEngine.Splines.SplineAnimate.html#methods

Create a World Space Tutorial UI Canvas
YouTube

Create a new World Space Tutorial UI Canvas GameObject
    Unity Menu Bar > GameObject > UI > Canvas
    Hierarchy view > rename ‘Canvas’ to ‘TutorialCanvas’
    Inspector view > TutorialCanvas
        .Canvas.Render Mode = World Space
        .RectTransform
            .Position = (0, <YourTerrainHeight> + 2.3, 5)
            .Width = 2000
            .Height = 2000
            .Scale = (0.001, 0.001, 0.001)

Add a UI Panel GameObject to Canvas
    Hierarchy view > richt click on TutorialCanvas > UI > Panel
    (If you right click on the UI Canvas in the Hierarchy view to add a panel then Unity will automatically add it as a child of the Canvas)

    Inspector view > Panel
        .Rect Transform
            .Position = (1000, -1000, 0)
            .Width = 2000
            .Height = 2000
        .Image.Color = RGBA 0-1.0 (0, 0, 0, 0.95)

Add UI.Text GameObject to Panel
    Hierarchy view > richt click on Panel > UI > Text – Textmesh Pro
    Inspector view > Text (TMP)
        .Rect Transform
            .Position = (0, 600, 0)
            .Width = 1980
            .Heigth = 600
        .TextMeshPro -Text (UI).Text Input.Text = “Grab the Gun With the Grip Buttons and use the Trigger to fire at the Targets. 
Shoot This Target to Start the Ride!”
        .Main Settings
            .Font Asset = Bangers SDF (TMP_Font Asset)
            .Font Size = 120
            .Color Gradient = true
                .Color Preset = Dark to Light Green – Vertical (TMP_Color Gradient)
            .Alignment = Center, Middle

Save changes made to the scene
    Unity > (Ctrl + S) or Unity Menu Bar > File > Save
    (Save your scene regularly to avoid losing your progress in case of a crash either by going to File > Save in the Unity menu bar or by pressing Control + S on your keyboard. You can see if there are any changes made to the scene that still need to be saved if there is a star * character displayed right after the name of your scene in the Hierarchy view.)

Notes

The goal here is to create a ‘World Space’ Tutorial Canvas GameObject with text on it that tells the player to grab the gun and to shoot the first target (which is placed right in front of the canvas) to start the game/ride.

Think of the default UI Canvas as an empty glass plate, window, or layer in front of the camera, on which you can place other UI elements, like panels, text objects, buttons, images, sprites and scroll bars, to create all sorts of things with, like health bars, menu bars, main menu windows, score boards, enemy radar displays, ammo displays, score displays, timers, cross-hairs and all the other things that you see in game HUD’s and menu’s etcetera. Canvasses are very versatile and you could even create an entire 2D game on a UI Canvas.

The default/normal viewing mode or ‘Render Mode’ for canvasses is ‘Screen Space – Overlay‘ which means that it is rendered right in front of the camera like an actual flat camera layer or glass filter.
The second Render Mode is ‘Screen Space – Camera’ which means that it is still rendered in front of the camera and it will still move with the camera, like with screen space overlay, but it can also be placed on a certain distance away from the camera to create a UI/HUD that has more depth. This can be useful to create a HUD that looks like it is projected on the inside of a helmet’s visor for instance, so close to the camera but not right in front of it. Think of screen space camera as if the glass plate is parented to the camera so it’s rotation and position are relative to the camera.
The third Render Mode ‘World Space‘ turns the UI Canvas into a regular 3D GameObject with a position and a rotation relative to the game world, like all the other GameObjects, and is ideal for creating 2D menu’s and other displays or 2D stuff into a 3D game world.

In this tutorial we will use World Space canvasses to create a Main Menu screen, a Game Over screen, a player score display inside of the Roller Coaster Train and also a cool aiming reticle/cross-hair on the VRGun that will look a hologram display!

Links

Unity Manual / User Interface (UI) / Unity UI
    / Canvas

    https://docs.unity3d.com/2022.3/Documentation/Manual/UICanvas.html
    / Visual Components
    https://docs.unity3d.com/2022.3/Documentation/Manual/UIVisualComponents.html
    / UI Reference / Rect Transform
    https://docs.unity3d.com/2022.3/Documentation/Manual/class-RectTransform.html

Unity Scripting API / Canvas class
https://docs.unity3d.com/2022.3/Documentation/ScriptReference/Canvas.html

Setup TutorialCanvas
YouTube

Add GameObject tutorialCanvas variable/reference to GameManager class    
    Visual Studio Code > GameManager.cs > add Hightlighted Code  

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Splines;

public class GameManager : MonoBehaviour
{
    public RollerCoasterTrain rollerCoasterTrain;
    public GameObject tutorialCanvas;

    // Start is called before the first frame update
    void Start()
    {
        Debug.Log("The Game is Starting!!!");
    }

    // Update is called once per frame
    void Update()
    {
        Debug.Log("The Game is Running!!!");
    }

    public void StartRollerCoasterRide()
    {
        Debug.Log("The Ride is Starting!!!");

        // Play the spline animation to start the ride.
        rollerCoasterTrain.GetComponent<SplineAnimate>().Play();
    }
}

Disable TutorialCanvas GameObject from GameManager.StartRollerCoasterRide() function
    Visual Studio Code > GameManager.cs > add Hightlighted Code  

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Splines;

public class GameManager : MonoBehaviour
{
    public RollerCoasterTrain rollerCoasterTrain;
    public GameObject tutorialCanvas;

    // Start is called before the first frame update
    void Start()
    {
        Debug.Log("The Game is Starting!!!");
    }

    // Update is called once per frame
    void Update()
    {
        Debug.Log("The Game is Running!!!");
    }

    public void StartRollerCoasterRide()
    {
        Debug.Log("The Ride is Starting!!!");

        // Play the spline animation to start the ride.
        rollerCoasterTrain.GetComponent<SplineAnimate>().Play();

        // Deactivate the tutorial canvas when the player starts the ride.
        tutorialCanvas.SetActive(false);
    }
}

Save changes made to GameManager.cs
    Visual Studio Code > GameManager.cs > (Ctrl + S)

Save changes made to the scene
    Unity > (Ctrl + S) or Unity Menu Bar > File > Save
    (Save your scene regularly to avoid losing your progress in case of a crash either by going to File > Save in the Unity menu bar or by pressing Control + S on your keyboard. You can see if there are any changes made to the scene that still need to be saved if there is a star * character displayed right after the name of your scene in the Hierarchy view.)

Notes

Create a new Custom Event Trigger GameObject
YouTube

Create a new Cube GameObject named CustomEventTrigger-Prefab   
    Unity Menu Bar > GameObject > 3D Object > Cube
    Hierarchy view
        > rename ‘Cube’ to ‘CustomEventTrigger-Prefab’
        > select CustomEventTrigger-Prefab
    Inspector view > CustomEventTrigger
        .Transform
            .Position = (0, 0, 0)
            .Scale = (5, 5, 5)
        .MeshRenderer.Enabled = false
        .BoxCollider.Is Trigger = true

Create and add new CustomEventTrigger.cs script to CustomEventTrigger-Prefab GameObject
    Project view > Assets/MyGame/Scripts > right click > Create > C# Script > name ‘CustomEventTrigger.cs
    Hierarchy view > select CustomEventTrigger-Prefab
    Inspector view > CustomEventTrigger-Prefab.Add Component > Scripts > CustomEventTrigger.cs
    Project view > Assets/MyGame/Scripts > open CustomEventTrigger.cs

Create a new public string triggerTag variable 
    Visual Studio Code > CustomEventTrigger.cs > add Highlighted Code

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CustomEventTrigger : MonoBehaviour
{
    // The tag that should trigger the custom event.
    public string triggerTag = "Player";

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

Add OnTriggerEnter(), OnTriggerStay() and OnTriggerExit() functions to CustomEventTrigger class
    Visual Studio Code > CustomEventTrigger.cs > add highlighted code

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CustomEventTrigger : MonoBehaviour
{
    // The tag that should trigger the custom event.
    public string triggerTag = "Player";

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }

    // OnTriggerEnter is called when the Collider 'collider' has started touching the trigger.
    public IEnumerator OnTriggerEnter(Collider collider)
    {
        yield return null;
    }
    
    // OnTriggerStay is called while the Collider 'collider' is touching the trigger.
    public IEnumerator OnTriggerStay(Collider collider)
    {
        yield return null;
    }

    // OnTriggerExit is called when the Collider 'collider'has stopped touching the trigger.
    public IEnumerator OnTriggerExit(Collider collider)
    {
        yield return null;
    }
}

Add if statements with conditionals to check if the GameObject that enters the trigger has the right tag
    Visual Studio Code > CustomEventTrigger.cs > add Highlighted Code

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CustomEventTrigger : MonoBehaviour
{
    // The tag that should trigger the custom event.
    public string triggerTag = "Player";

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }

    // OnTriggerEnter is called when the Collider 'collider' has started touching the trigger.
    public IEnumerator OnTriggerEnter(Collider collider)
    {
        Debug.Log("A GameObject named " + collider.gameObject.name + " with the tag " + collider.gameObject.tag + " has Entered the trigger!!!");

        //Check if the tag on the collider is equal to the triggerTag.
        if(collider.gameObject.tag == triggerTag)
        {

        }

        yield return null;
    }

    // OnTriggerStay is called while the Collider 'collider' is touching the trigger.
    public IEnumerator OnTriggerStay(Collider collider)
    {
        //Check if the tag on the collider is equal to the triggerTag.
        if(collider.gameObject.tag == triggerTag)
        {

        }

        yield return null;
    }

    // OnTriggerExit is called when the Collider 'collider' has stopped touching the trigger.
    public IEnumerator OnTriggerExit(Collider collider)
    {
        //Check if the tag on the collider is equal to the triggerTag.
        if(collider.gameObject.tag == triggerTag)
        {

        }

        yield return null;
    }
}

Add a using UnityEngine.Events; directive and add three UnityEvent type variables named onTriggerEnterEvent, onTriggerStayEvent and onTriggerExitEvent to the CustomEventTrigger class
    Visual Studio Code > CustomEventTrigger.cs > add Highlighted Code

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

public class CustomEventTrigger : MonoBehaviour
{
    // The tag that should trigger the custom event.
    public string triggerTag = "Player";

    public UnityEvent onTriggerEnterEvent;
    public UnityEvent onTriggerStayEvent;
    public UnityEvent onTriggerExitEvent;

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }

    // OnTriggerEnter is called when the Collider 'collider' has started touching the trigger.
    public IEnumerator OnTriggerEnter(Collider collider)
    {
        Debug.Log("A GameObject named " + collider.gameObject.name + " with the tag " + collider.gameObject.tag + " has Entered the trigger!!!");

        //Check if the tag on the collider is equal to the triggerTag.
        if(collider.gameObject.tag == triggerTag)
        {
         
        }

        yield return null;
    }

    // OnTriggerStay is called while the Collider 'collider' is touching the trigger.
    public IEnumerator OnTriggerStay(Collider collider)
    {
        //Check if the tag on the collider is equal to the triggerTag.
        if(collider.gameObject.tag == triggerTag)
        {
            
        }

        yield return null;
    }

    // OnTriggerExit is called when the Collider 'collider' has stopped touching the trigger.
    public IEnumerator OnTriggerExit(Collider collider)
    {
        //Check if the tag on the collider is equal to the triggerTag.
        if(collider.gameObject.tag == triggerTag)
        {
            
        }

        yield return null;
    }
}

Call the Invoke() functions of the onTriggerEnter, Stay and Exit UnityEvents from the OnTriggerEnter, Stay and Exit MonoBehaviour functions
    Visual Studio Code > CustomEventTrigger.cs > add Highlighted Code

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

public class CustomEventTrigger : MonoBehaviour
{
    // The tag that should trigger the custom event.
    public string triggerTag = "Player";

    public UnityEvent onTriggerEnterEvent;
    public UnityEvent onTriggerStayEvent;
    public UnityEvent onTriggerExitEvent;

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }

    // OnTriggerEnter is called when the Collider 'collider' has started touching the trigger.
    public IEnumerator OnTriggerEnter(Collider collider)
    {
        Debug.Log("A GameObject named " + collider.gameObject.name + " with the tag " + collider.gameObject.tag + " has Entered the trigger!!!");

        //Check if the tag on the collider is equal to the triggerTag.
        if(collider.gameObject.tag == triggerTag)
        {
            // Invoke (call) the functions subscribed to the onTriggerEnterEvent.
            onTriggerEnterEvent.Invoke();
        }

        yield return null;
    }

    // OnTriggerStay is called while the Collider 'collider' is touching the trigger.
    public IEnumerator OnTriggerStay(Collider collider)
    {
        //Check if the tag on the collider is equal to the triggerTag.
        if(collider.gameObject.tag == triggerTag)
        {
            // Invoke (call) the functions subscribed to the onTriggerStayEvent.
            onTriggerStayEvent.Invoke();
        }

        yield return null;
    }

    // OnTriggerExit is called when the Collider 'collider' has stopped touching the trigger.
    public IEnumerator OnTriggerExit(Collider collider)
    {
        //Check if the tag on the collider is equal to the triggerTag.
        if(collider.gameObject.tag == triggerTag)
        {
            // Invoke (call) the functions subscribed to the onTriggerExitEvent.
            onTriggerExitEvent.Invoke();
        }

        yield return null;
    }
}

Add null checks to the if-statement conditionals so the UnityEvents aren’t invoked/called when there are no functions subscribed to them
    Visual Studio Code > CustomEventTrigger.cs > add Highlighted Code

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

public class CustomEventTrigger : MonoBehaviour
{
    // The tag that should trigger the custom event.
    public string triggerTag = "Player";

    public UnityEvent onTriggerEnterEvent;
    public UnityEvent onTriggerStayEvent;
    public UnityEvent onTriggerExitEvent;

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }

    // OnTriggerEnter is called when the Collider 'collider' has started touching the trigger.
    public IEnumerator OnTriggerEnter(Collider collider)
    {
        Debug.Log("A GameObject named " + collider.gameObject.name + " with the tag " + collider.gameObject.tag + " has Entered the trigger!!!");

        // Check if there are any functions subscribed to the onTriggerEnterEvent 
        // 'and' (&&) if the tag on the collider is equal to the triggerTag.
        if(onTriggerEnterEvent != null && collider.gameObject.tag == triggerTag)
        {
            // Invoke (call) the functions subscribed to the onTriggerEnterEvent.
            onTriggerEnterEvent.Invoke();
        }

        yield return null;
    }

    // OnTriggerStay is called while the Collider 'collider' is touching the trigger.
    public IEnumerator OnTriggerStay(Collider collider)
    {
        // Check if there are any functions subscribed to the onTriggerStayEvent 
        // 'and' (&&) if the tag on the collider is equal to the triggerTag.
        if(onTriggerStayEvent != null && collider.gameObject.tag == triggerTag)
        {
            // Invoke (call) the functions subscribed to the onTriggerStayEvent.
            onTriggerStayEvent.Invoke();
        }

        yield return null;
    }

    // OnTriggerExit is called when the Collider 'collider' has stopped touching the trigger.
    public IEnumerator OnTriggerExit(Collider collider)
    {
        // Check if there are any functions subscribed to the onTriggerExitEvent 
        // 'and' (&&) if the tag on the collider is equal to the triggerTag.        
        if(onTriggerExitEvent != null && collider.gameObject.tag == triggerTag)
        {
            // Invoke (call) the functions subscribed to the onTriggerExitEvent.
            onTriggerExitEvent.Invoke();
        }

        yield return null;
    }
}

Add a MonoBehavious.OnDrawGizmos() function with logic to draw a WireFrameCube in the Scene view 
    Visual Studio Code > CustomEventTrigger.cs > add Highlighted Code

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

public class CustomEventTrigger : MonoBehaviour
{
    // The tag that should trigger the custom event.
    public string triggerTag = "Player";

    public UnityEvent onTriggerEnterEvent;
    public UnityEvent onTriggerStayEvent;
    public UnityEvent onTriggerExitEvent;

    // Draw Gizmos and Handles into the Scene view for debugging, tools etcetera.
    void OnDrawGizmos()
    {
        // Set the color of all drawn Gizmos to a newly created Color (turquoise)
        Gizmos.color = new Color(0,1,1);
        
        // Draw a turqoise wire frame cube in the Scene view at this transform's position
        Gizmos.DrawWireCube(transform.position, transform.localScale);
    }

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }

    // OnTriggerEnter is called when the Collider 'collider' has started touching the trigger.
    public IEnumerator OnTriggerEnter(Collider collider)
    {
        Debug.Log("A GameObject named " + collider.gameObject.name + " with the tag " + collider.gameObject.tag + " has Entered the trigger!!!");

        // Check if there are any functions subscribed to the onTriggerEnterEvent 
        // 'and' (&&) if the tag on the collider is equal to the triggerTag.
        if(onTriggerEnterEvent != null && collider.gameObject.tag == triggerTag)
        {
            // Invoke (call) the functions subscribed to the onTriggerEnterEvent.
            onTriggerEnterEvent.Invoke();
        }

        yield return null;
    }

    // OnTriggerStay is called while the Collider 'collider' is touching the trigger.
    public IEnumerator OnTriggerStay(Collider collider)
    {
        // Check if there are any functions subscribed to the onTriggerStayEvent 
        // 'and' (&&) if the tag on the collider is equal to the triggerTag.
        if(onTriggerStayEvent != null && collider.gameObject.tag == triggerTag)
        {
            // Invoke (call) the functions subscribed to the onTriggerStayEvent.
            onTriggerStayEvent.Invoke();
        }

        yield return null;
    }

    // OnTriggerExit is called when the Collider 'collider' has stopped touching the trigger.
    public IEnumerator OnTriggerExit(Collider collider)
    {
        // Check if there are any functions subscribed to the onTriggerExitEvent 
        // 'and' (&&) if the tag on the collider is equal to the triggerTag.        
        if(onTriggerExitEvent != null && collider.gameObject.tag == triggerTag)
        {
            // Invoke (call) the functions subscribed to the onTriggerExitEvent.
            onTriggerExitEvent.Invoke();
        }

        yield return null;
    }
}

Save changes made to CustomEventTrigger.cs
    Visual Studio Code > CustomEventTrigger.cs > (Ctrl+S) 

Create CustomEventTrigger-Prefab prefab file
    Hierarchy view > drag CustomEventTrigger-Prefab GameObject to Project view > Assets/MyGame/Prefabs

Notes

The goal in this step is to create a cube, that is invisible to the player, with a BoxCollider component setup as a trigger that can be used to trigger other arbitrary ‘things’ to happen when a GameObject enters its trigger volume.
(Think of those traps in the jungle that yank you up a tree upside down by your legs with a rope when you step on them, or think of doors opening when you get close to them. Different things may happen!)

By enabling the ‘Is Trigger’ value on a Collider component the collider will not cause other objects to collide with it, so players and other objects can pass straight trough it, but we can get still get messages when other colliders enter, exit or stay within the trigger collider volume, by using the Unity MonoBehaviour OnTriggerEnter(), OnTriggerStay() and OnTriggerExit() functions to detect ‘Colliders‘ instead of the OnCollisionEnter(), OnCollisionStay() and OnCollisionExit() funcitons to detect ‘Collisions‘.

To only make things happen when a specific type of GameObject enters the trigger volume we can just check with an if-statement if the Collider.GameObject has a certain tag assigned to it. 

Any code could be written inside of the OnTrigger functions bodies to make anything happen directly at that moment by ‘hardcoding’ stuff, but by using public UnityEvents and calling their Invoke() functions instead, we can make a re-usable ‘custom’ event trigger prefab GameObject that we can easily subscribe other GameObjects functions to, by simply dragging and adding them to the CustomEventTrigger component of the CustomEventTrigger-Prefab GameObject in the Inspector, in the same way as is done with the OnButtonClick() events that you can see on Unity’s standard UI Button components…Neat!!!
(FYI: If you’ve ever read complaints in game reviews about a game having too many scripted gameplay events then you can be pretty sure it is because that games uses an overabundance of invisible trigger volumes spread throughout its game world to trigger those scripted events…They are a great tool indeed!)

Finally to make the bounds of the invisible trigger volume visible to us creators in the Scene view, add the super special Unity MonoBehaviour.OnDrawGizmos() function and the logic to draw a wireframe cube gizmo to the function.

In the next step we will use one CustomEventTrigger instance/clone to trigger the RollerCoasterTrain to stop when it is near the end of the ride and to also trigger a GameOver UI Canvas to pop up at the same time.
In later steps we can use copies of the CustomEventTrigger-Prefab to make random other things happen to the player at different lengths along the RollerCoasterSpline.

As an excercise,.. Can you make a trigger that turns on a Point Light in the scene when the player enters it and turns it off when the player exits it? Or can you make a huge Rigidbody boulder or stone ball fall from the sky behind the player when the player enters a small declining mountain corridor path, Indiana Jones style?

 

Links

    Unity Manual / Working in Unity / Editor Features / Project Settings / Tags and Layers
    https://docs.unity3d.com/Manual/class-TagManager.html
    Unity Scripting API / UnityEngine / GameObject class
    https://docs.unity3d.com/ScriptReference/GameObject.html
    Unity Scripting API / UnityEngine / GameObject / tag variable
    https://docs.unity3d.com/ScriptReference/GameObject-tag.html

    YouTube / Unity Official Tutorials / Colliders as Triggers
    
https://www.youtube.com/watch?v=m0fjrQkaES4

    Unity Manual / Scripting / Important Classes / MonoBehaviour
    https://docs.unity3d.com/Manual/class-MonoBehaviour.html
    Unity Scripting API / MonoBehaviour
    https://docs.unity3d.com/ScriptReference/MonoBehaviour.html
    Unity Scripting API / MonoBehaviour.OnDrawGizmos()
    https://docs.unity3d.com/ScriptReference/MonoBehaviour.OnDrawGizmos.html
    Unity Scripting API / MonoBehaviour.OnDrawGizmosSelected()
    https://docs.unity3d.com/ScriptReference/MonoBehaviour.OnDrawGizmosSelected.html

    Unity Manual / Scripting / Important Classes / Gizmos & Handles
    https://docs.unity3d.com/Manual/GizmosAndHandles.html
    Unity Scripting API / Gizmos
    https://docs.unity3d.com/ScriptReference/Gizmos.html
    Unity Scripting API / Gizmos.DrawWhireCube()
    https://docs.unity3d.com/ScriptReference/Gizmos.DrawWireCube.html

Ending the ride with the Custom Event Trigger Prefab
YouTube

Add StopRollerCoasterRide() function and logic to GameManager class
    Visual Studio Code > GameManager.cs > add highlighted code

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Splines;

public class GameManager : MonoBehaviour
{
    public RollerCoasterTrain rollerCoasterTrain;

    // Start is called before the first frame update
    void Start()
    {
         Debug.Log("The Game is Starting!!!");
    }

    // Update is called once per frame
    void Update()
    {
        Debug.Log("The Game Is Running!!!");
    }

    public void StartRollerCoasterRide()
    {
        Debug.Log("The Ride is Starting!!!");

        // Play the spline animation to start the ride.
        rollerCoasterTrain.GetComponent<SplineAnimate>().Play();

        // Deactivate the tutorial canvas when the player starts the ride.
        tutorialCanvas.SetActive(false);
    }

    public void StopRollerCoasterRide()
    {
        Debug.Log("The Ride is Stopping!!!");
        
        // Pause the spline animation to stop the ride.
        rollerCoasterTrain.GetComponent<SplineAnimate>().Pause();
    }
}

Save changes to GameManager.cs script
    Visual Studio Code > GameManager.cs > (Ctrl + S) or (Cmd + S)

Position the CustomEventTrigger-Prefab at the end of the ride
    Hierarchy view > select CustomEventTrigger-Prefab
    Inspector view > CustomEventTrigger-Prefab.Transform.Position = (0, <YourTerrainHeight> + 2.5, -10) 

Subscribe StopRollerCoasterRide() function to CustomEventTrigger-Prefab.CustomEventTrigger.OnTriggerEnterEvent event   
    Hierarchy view > select CustomEventTrigger-Prefab
    Inspector view > CustomEventTrigger-Prefab.CustomEventTrigger.OnTriggerEnterEvent ()
        .+ (press Plus Button)
        .None (Object) = Hierarchy view > GameManager
        .No Function = GameManager.StopRollerCoasterRide ()

Tag VRPlayer GameObject with tag “Player”
    Hierarchy view > select VRPlayer
    Inspector view > VRPlayer.Tag = Player

Save changes made to the scene
    Unity > (Ctrl + S) or Unity Menu Bar > File > Save
    (Save your scene regularly to avoid losing your progress in case of a crash either by going to File > Save in the Unity menu bar or by pressing Control + S on your keyboard. You can see if there are any changes made to the scene that still need to be saved if there is a star * character displayed right after the name of your scene in the Hierarchy view.)

Notes

In this step we place the CustomEventTrigger-Prefab at ten units before the end/start of the roller coaster ride and subscribe the StopRollerCoasterRide() function from the GameManager class to its OnTriggerEnterEvent, so that when the player enters the CustomEventTrigger-Prefab’s trigger volume the roller coaster ride will stop. 

In the next steps we will create a Game Over UI Canvas that will be deactivated at the start of the game and then activated also by the CustomEventTrigger-Prefab, when the player reaches the end of the ride, so that we can display a score to the player and the buttons to restart, quit or go on to the next ride. 

Links

   Unity Manual / Working in Unity
        / Create Gameplay / GameObjects / Tags
        https://docs.unity3d.com/2022.3/Documentation/Manual/Tags.html
        / Editor Features / Project Settings / Tags and Layers
        https://docs.unity3d.com/2022.3/Documentation/Manual/Tags.html

Create a new World Space Game Over UI Canvas GameObject
YouTube

Create New Canvas GameObject
    Unity Menu Bar > GameObject > UI > Canvas
    Hierarchy view
        > rename ‘Canvas’ to ‘GameOverCanvas’
        > select GameOverCanvas
    Inspector view > GameOverCanvas
        .Canvas.Render Mode = World Space
        .RectTransform
            .Position = (0, <YourTerrainHeight> + 4, -4.5)
            .Width = 4000
            .Height = 4000
            .Scale = (0.001, 0.001, 0.001)

Add UI.Panel GameObject to GameOverCanvas
    Hierarchy view > richt click GameOverCanvas > UI > Panel
    (If you right click on a Canvas to add a panel then Unity will automatically add it as a child of the Canvas)
     Inspector view > Panel
        .Rect Transform
            .Position = (2000, -2000, 0)
            .Width = 4000
            .Height = 4000
        .Image.Color = RGBA (0-1.0) 0, 0, 0, 0.85

Add Game Over and Player Score UI Text GameObjects to Panel
    Hierarchy view
        > right click Panel > UI > Text – Textmesh Pro * 3
        > rename ‘Text (TMP) (0)’ to ‘Text (TMP)-GameOver’
        > rename ‘Text (TMP) (1)’ to ‘Text (TMP)-PlayerScoreLabel’
        > rename ‘Text (TMP) (2)’ to ‘Text (TMP)-PlayerScoreValue’

    Hierarchy view > GameOverCanvas-Prefab > Panel > select Text (TMP)-GameOver
    Inspector view > Text (TMP)-GameOver
        .Rect Transform
            .Position = (0, 1000, 0)
            .Width = 3800
            .Heigth = 1000
        .TextMeshPro -Text (UI).Text Input.Text = “GAME OVER!”
        .Main Settings
            .Font Asset = Bangers SDF (TMP_Font Asset)
            .Font Size = 800
            .Color Gradient = true
                .Color Preset = Light to Dark Green – Vertical (TMP_Color Gradient)
            .Alignment = Center, Middle

    Hierarchy view > GameOverCanvas > Panel > select Text (TMP)-PlayerScoreLabel
    Inspector view > Text (TMP)-PlayerScoreLabel
        .Rect Transform
            .Position = (0, 350, 0)
            .Width = 3800
            .Heigth = 300
        .TextMeshPro -Text (UI).Text Input.Text = “YOUR SCORE:”
        .Main Settings
            .Font Asset = Bangers SDF (TMP_Font Asset)
            .Font Size = 200
            .Color Gradient = true
                .Color Preset = Yellow to Orange – Vertical (TMP_Color Gradient)
            .Alignment = Center, Middle

    Hierarchy view > GameOverCanvas > Panel > select Text (TMP)-PlayerScoreValue
    Inspector view > Text (TMP)-PlayerScoreValue
        .Rect Transform
            .Position = (0, 50, 0)
            .Width = 3800
            .Heigth = 300
        .TextMeshPro -Text (UI).Text Input.Text = “9999999999”
        .Main Settings
            .Font Asset = Bangers SDF (TMP_Font Asset)
            .Font Size = 200
            .Color Gradient = true
                .Color Preset = Blue to Purple – Vertical (TMP_Color Gradient)
            .Alignment = Center, Middle

Add Quit, Restart and Next Level UI Button GameObjects to Panel
    Hierarchy view
        > GameOverCanvas > right click on Panel > UI > Button * 3
        > rename ‘Button (0)’ to ‘Button-Quit’
        > rename ‘Button (1)’ to ‘Button-Restart’
        > rename ‘Button (2)’ to ‘Button-NextRide’

    Hierarchy view > GameOverCanvas > Panel > select Button-Quit
    Inspector view > Button-Quit
        .Rect Transform
            .Position = (-1250, -1500, 0)
            .Width = 1000
            .Height = 400
    Hierarchy view > Button-Quit > select Text (TMP)
    Inspector view > Text (TMP).TextMeshPro.
        .text = “QUIT’
        .Font Asset = Bangers SDF (TMP_Font Asset)
        .Font Size = 280

    Hierarchy view > select Button-Restart
    Inspector view > Button-Restart
        .Rect Transform
            .Position = (0, -1500, 0)
            .Width = 1000
            .Height = 400
    Hierarchy view > Button-Restart > select Text (TMP)
    Inspector view > Text (TMP).TextMeshPro.
        .text = “RESTART’
        .Font Asset = Bangers SDF (TMP_Font Asset)
        .Font Size = 280

    Hierarchy view > select Button-NextRide
    Inspector view > Button-NextRide
        .Rect Transform
            .Position = (1250, -1500, 0)
            .Width = 1000
            .Height = 400
    Hierarchy view > Button-NextRide > select Text (TMP)
    Inspector view > Text (TMP).TextMeshPro.
        .text = “NEXT RIDE’
        .Font Asset = Bangers SDF (TMP_Font Asset)
        .Font Size = 280

Add Tracked Device Graphic Raycaster component to Canvas to enable VR controller input from the player
    Hierarchy view > select GameOverCanvas
    Inspector view > GameOverCanvas.Add Component > search ‘Tracked Device Graphic Raycaster’

Notes

The TrackedDeviceGraphicRaycaster.cs script file is located all the way in the XR Interaction Toolkit package folder: Packages/com.unity.xr.interaction.toolkit/Runtime/UI/TrackedDeviceGraphicRaycaster.cs.
So in this case it is quicker to just search for it by name in the Add Component menu search field.

Create new GameOverCanvas.cs C# Script   
    Project view > Assets/MyGame/Scripts/ > right click > Create > C# Script > name ‘GameOverCanvas.cs

Add GameOverCanvas script component to GameOverCanvas GameObject
    Hierarchy view > select GameOverCanvas
    Inspector view > GameOverCanvas.Add Component > Scripts > GameOverCanvas.cs

Save changes made to the scene
    Unity > (Ctrl + S) or Unity Menu Bar > File > Save
    (Save your scene regularly to avoid losing your progress in case of a crash either by going to File > Save in the Unity menu bar or by pressing Control + S on your keyboard. You can see if there are any changes made to the scene that still need to be saved if there is a star * character displayed right after the name of your scene in the Hierarchy view.)

Notes

The goal here is to create a Game Over screen UI Canvas GameObject set to World Space that has one UI Panel GameObject with two UI Text GameObjects, one for the text “Game Over!” and another one as a placeholder for the text that shows the player his score in the end, as well as three UI Button GameObjects for going to the next level, restarting the current level and for going back to the game’s main menu scene.

Links

Setup GameOverCanvas
YouTube

Add public GameOverCanvas gameOverCanvas variable/reference to GameManager class    
    Visual Studio Code > GameManager.cs > add Hightlighted Code  

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Splines;

public class GameManager : MonoBehaviour
{
    public RollerCoasterTrain rollerCoasterTrain;
    public GameObject tutorialCanvas;
    public GameOverCanvas gameOverCanvas;

    // Start is called before the first frame update
    void Start()
    {
        Debug.Log("The Game is Starting!!!");
    }

    // Update is called once per frame
    void Update()
    {
        Debug.Log("The Game is Running!!!");
    }

    public void StartRollerCoasterRide()
    {
        Debug.Log("The Ride is Starting!!!");

        // Play the spline animation to start the ride.
        rollerCoasterTrain.GetComponent<SplineAnimate>().Play();

        // Deactivate the tutorial canvas when the player starts the ride.
        tutorialCanvas.SetActive(false);
    }

    public void StopRollerCoasterRide()
    {
        Debug.Log("The Ride is Stopping!!!");

        rollerCoasterTrain.GetComponent<SplineAnimate>().Pause();
    }
}

Add new GameOver() function to GameManager class
    Visual Studio Code > GameManager.cs > add Hightlighted Code  

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Splines;

public class GameManager : MonoBehaviour
{
    public RollerCoasterTrain rollerCoasterTrain;
    public GameObject tutorialCanvas;
    public GameOverCanvas gameOverCanvas;

    // Start is called before the first frame update
    void Start()
    {
        Debug.Log("The Game is Starting!!!");
    }

    // Update is called once per frame
    void Update()
    {
        Debug.Log("The Game is Running!!!");
    }

    public void StartRollerCoasterRide()
    {
        Debug.Log("The Ride is Starting!!!");

        // Play the spline animation to start the ride.
        rollerCoasterTrain.GetComponent<SplineAnimate>().Play();

        // Deactivate the tutorial canvas when the player starts the ride.
        tutorialCanvas.SetActive(false);
    }

    public void StopRollerCoasterRide()
    {
        Debug.Log("The Ride is Stopping!!!");

        rollerCoasterTrain.GetComponent<SplineAnimate>().Pause();
    }

    public void GameOver()
    {
        Debug.Log("Game Over!!!");
    }
}

Deactivate the GameOverCanvas-Prefab GameObject from Start() and Activate it from GameOver()
    Visual Studio Code > GameManager.cs > add Hightlighted Code  

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Splines;

public class GameManager : MonoBehaviour
{
    public RollerCoasterTrain rollerCoasterTrain;
    public GameObject tutorialCanvas;
    public GameOverCanvas gameOverCanvas;

    // Start is called before the first frame update
    void Start()
    {
        Debug.Log("The Game is Starting!!!");

        gameOverCanvas.gameObject.SetActive(false);
    }

    // Update is called once per frame
    void Update()
    {
        Debug.Log("The Game is Running!!!");
    }

    public void StartRollerCoasterRide()
    {
        Debug.Log("The Ride is Starting!!!");

        // Play the spline animation to start the ride.
        rollerCoasterTrain.GetComponent<SplineAnimate>().Play();

        // Deactivate the tutorial canvas when the player starts the ride.
        tutorialCanvas.SetActive(false);
    }

    public void StopRollerCoasterRide()
    {
        Debug.Log("The Ride is Stopping!!!");
        
        // Pause the spline animation to stop the ride.
        rollerCoasterTrain.GetComponent<SplineAnimate>().Pause();
    }

    public void GameOver()
    {
        Debug.Log("Game Over!!!");
        
        gameOverCanvas.gameObject.SetActive(true);
    }
}

Assign GameOverCanvas-Prefab.GameOverCanvas component to GameManager.GameManager.gameOverCanvas variable
    Hierarchy view > select GameManager
    Inspector view > GameManager.GameManager.Game Over Canvas = Hierarchy view > GameOverCanvas-Prefab

Notes

When you drag the GameOverCanvas-Prefab GameObject onto the GameManager.GameOverCanvas variable value slot, Unity will automatically look if there is a component with the type GameOverCanvas attached to the GameOverCanvas-Prefab GamObject and add it to the variable’s value slot.

If you want to drag a component attached to one GameObject to a variable reference slot on another GameObject’s component directly, you can open a second Inspector view and lock it to one of the GameObjects, so you can select the other GameObject and drag the component attached to it to the first GameObect from the second Inspector view.
You can lock the Inspector view to the currently selected GameObject with the little lock icon button in the top right corner of the Inspector view.

Subscribe GameManager.GameManager.GameOver function to CustomEventTrigger-Prefab.CustomEventTrigger.OnTriggerEnterEvent event   
    Hierarchy view > select CustomEventTrigger-Prefab
    Inspector view > CustomEventTrigger-Prefab.CustomEventTrigger.OnTriggerEnterEvent ()
        .+ (press plus button)
        .None (Object) = Hierarchy view > GameManager
        .No Function = GameManager.GameOver()

Save changes made to the scene
    Unity > (Ctrl + S) or Unity Menu Bar > File > Save
    (Save your scene regularly to avoid losing your progress in case of a crash either by going to File > Save in the Unity menu bar or by pressing Control + S on your keyboard. You can see if there are any changes made to the scene that still need to be saved if there is a star * character displayed right after the name of your scene in the Hierarchy view.)

Setup Quit, Restart and Next Level Buttons
YouTube

Add new LoadScene(), LoadNextScene() and RestartScene() functions to GameManager class
    Visual Studio Code > GameManager.cs > add Hightlighted Code

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Splines;

public class GameManager : MonoBehaviour
{
    public RollerCoasterTrain rollerCoasterTrain;
    public GameObject tutorialCanvas;
    public GameObject gameOverCanvas;

    // Start is called before the first frame update
    void Start()
    {
        Debug.Log("The Game is Starting!!!");

        gameOverCanvas.SetActive(false);
    }

    // Update is called once per frame
    void Update()
    {
        Debug.Log("The Game is Running!!!");
    }

    public void StartRollerCoasterRide()
    {
        Debug.Log("The Ride is Starting!!!");

        // Play the spline animation to start the ride.
        rollerCoasterTrain.GetComponent<SplineAnimate>().Play();

        // Deactivate the tutorial canvas when the player starts the ride.
        tutorialCanvas.SetActive(false);
    }

    public void StopRollerCoasterRide()
    {
        Debug.Log("The Ride is Stopping!!!");
        
        // Pause the spline animation to stop the ride.
        rollerCoasterTrain.GetComponent<SplineAnimate>().Pause();
    }

    public void GameOver()
    {
        Debug.Log("Game Over!!!");
        
        gameOverCanvas.SetActive(true);
    }

    public void LoadScene(string sceneName)
    {

    }

    public void LoadNextScene()
    {
    
    }

    public void RestartScene()
    {

    }
}

Call UnityEngine.SceneManagement.SceneManager.LoadScene() from GameManager.LoadScene()
    Visual Studio Code > GameManager.cs > add Hightlighted Code
    (Don’t forget to add the using UnityEngine.SceneManagement directive on line 5!)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Splines;
using UnityEngine.SceneManagement;

public class GameManager : MonoBehaviour
{
    public RollerCoasterTrain rollerCoasterTrain;
    public GameObject tutorialCanvas;
    public GameObject gameOverCanvas;

    // Start is called before the first frame update
    void Start()
    {
        Debug.Log("The Game is Starting!!!");

        gameOverCanvas.SetActive(false);
    }

    // Update is called once per frame
    void Update()
    {
        Debug.Log("The Game is Running!!!");
    }

    public void StartRollerCoasterRide()
    {
        Debug.Log("The Ride is Starting!!!");

        // Play the spline animation to start the ride.
        rollerCoasterTrain.GetComponent<SplineAnimate>().Play();

        // Deactivate the tutorial canvas when the player starts the ride.
        tutorialCanvas.SetActive(false);
    }

    public void StopRollerCoasterRide()
    {
        Debug.Log("The Ride is Stopping!!!");
        
        // Pause the spline animation to stop the ride.
        rollerCoasterTrain.GetComponent<SplineAnimate>().Pause();
    }

    public void GameOver()
    {
        Debug.Log("Game Over!!!");
        
        gameOverCanvas.SetActive(true);
    }

    public void LoadScene(string sceneName)
    {
        SceneManager.LoadScene(sceneName);
    }

    public void LoadNextScene()
    {

    }

    public void RestartScene()
    {

    }
}
Notes

Make sure the using directive to use the UnityEngine.SceneManagement class in our script is added to the other using directives on line 4.
Then call the Unity UnityEngine.SceneManagement.SceneManager.LoadScene() function from the body of our own GameManager.LoadScene() function passing in a string with the level name (the name of the .unity scene file) as an argument, to tell the Unity SceneManager which scene to load.

By creating a LoadScene() function with a string sceneName parameter we can pass in the name of the scene to load from outside the scope of the function and this way we can simply tell the buttons in the scene which scene to load when they’re clicked by passing a string in the Button.OnClick () event in the the Inspector.
We can use this simple ‘load scene by name’ function to let the Quit button on the GameOverCanvas load a Main Menu scene that we’ll create in later steps.

Call UnityEngine.SceneManagement.SceneManager.LoadScene() from GameManager.LoadNextScene()
    Visual Studio Code > GameManager.cs > add Hightlighted Code

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Splines;
using UnityEngine.SceneManagement;

public class GameManager : MonoBehaviour
{
    public RollerCoasterTrain rollerCoasterTrain;
    public GameObject tutorialCanvas;
    public GameOverCanvas gameOverCanvas;

    // Start is called before the first frame update
    void Start()
    {
        Debug.Log("The Game is Starting!!!");

        gameOverCanvas.gameObject.SetActive(false);
    }

    // Update is called once per frame
    void Update()
    {
        Debug.Log("The Game is Running!!!");
    }

    public void StartRollerCoasterRide()
    {
        Debug.Log("The Ride is Starting!!!");

        // Play the spline animation to start the ride.
        rollerCoasterTrain.GetComponent<SplineAnimate>().Play();

        // Deactivate the tutorial canvas when the player starts the ride.
        tutorialCanvas.SetActive(false);
    }

    public void StopRollerCoasterRide()
    {
        Debug.Log("The Ride is Stopping!!!");
        
        // Pause the spline animation to stop the ride.
        rollerCoasterTrain.GetComponent<SplineAnimate>().Pause();
    }

    public void GameOver()
    {
        Debug.Log("Game Over!!!");
        
        gameOverCanvas.gameObject.SetActive(true);
    }

    public void LoadScene(string sceneName)
    {
        SceneManager.LoadScene(sceneName);
    }

    public void LoadNextScene()
    {
        int nextSceneIndex = SceneManager.GetActiveScene().buildIndex + 1;
        if(SceneManager.sceneCountInBuildSettings > nextSceneIndex)
        {
            SceneManager.LoadScene(nextSceneIndex);
        }
        else
        {
            SceneManager.LoadScene(0);
        }
    }

    public void RestartScene()
    {

    }
}
Notes

Call the Unity UnityEngine.SceneManagement.SceneManager.LoadScene() function from the body of our own GameManager.LoadNextScene() function passing in an int with the next level’s build index number (the number of the next scene in the Scenes in Build list) as an argument, to tell the Unity SceneManager which scene to load.

First we get the active scene’s build index and store it in a int variable.
Then we check with an if-statement if a next scene exists by adding one to the activeSceneIndex and comparing if it is less than the number of scenes in the Scenes In Build list.
If there is a scene with a higher build index then load the next scene, else if there isn’t then we load the first scene in the Scenes In Build list, which will be our MainMenu scene later on.

Call UnityEngine.SceneManagement.SceneManager.LoadScene() from GameManager.RestartScene()
    Visual Studio Code > GameManager.cs > add Hightlighted Code

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Splines;
using UnityEngine.SceneManagement;

public class GameManager : MonoBehaviour
{
    public RollerCoasterTrain rollerCoasterTrain;
    public GameObject tutorialCanvas;
    public GameOverCanvas gameOverCanvas;

    // Start is called before the first frame update
    void Start()
    {
        Debug.Log("The Game is Starting!!!");

        gameOverCanvas.gameObject.SetActive(false);
    }

    // Update is called once per frame
    void Update()
    {
        Debug.Log("The Game is Running!!!");
    }

    public void StartRollerCoasterRide()
    {
        Debug.Log("The Ride is Starting!!!");

        // Play the spline animation to start the ride.
        rollerCoasterTrain.GetComponent<SplineAnimate>().Play();

        // Deactivate the tutorial canvas when the player starts the ride.
        tutorialCanvas.SetActive(false);
    }

    public void StopRollerCoasterRide()
    {
        Debug.Log("The Ride is Stopping!!!");
        
        // Pause the spline animation to stop the ride.
        rollerCoasterTrain.GetComponent<SplineAnimate>().Pause();
    }

    public void GameOver()
    {
        Debug.Log("Game Over!!!");
        
        gameOverCanvas.gameObject.SetActive(true);
    }

    public void LoadScene(string sceneName)
    {
        SceneManager.LoadScene(sceneName);
    }

    public void LoadNextScene()
    {
        int nextSceneIndex = SceneManager.GetActiveScene().buildIndex + 1;
        if(SceneManager.sceneCountInBuildSettings > nextSceneIndex)
        {
            SceneManager.LoadScene(nextSceneIndex);
        }
        else
        {
            SceneManager.LoadScene(0);
        }
    }

    public void RestartScene()
    {
        SceneManager.LoadScene(SceneManager.GetActiveScene().name);
    }
}
Notes

Call the Unity UnityEngine.SceneManagement.SceneManager.LoadScene() function from the body of the GameManager.RestartScene() function.
This time pass in a string with the currently loaded scene name as an argument by using the SceneManager.GetActiveScene() function.
The GetActiveScene() function returns the currently active scene. Each scene object has a variable named ‘name’ that returns a string with the name of the .unity scene file that we can use.
This way the restart button will always be able to restart the current scene without having to know which scene it is in.

Save changes made to GameManager.cs
Visual Studio Code > GameManager.cs > Ctrl + S or Cmd + s

Add Level 1.unity Scene to the project’s Scenes In Build list
Unity Menu Bar > File > Build Settings > Scenes In Build > click Add Open Scenes

Call GameManager.LoadScene() from the Quit button’s OnClick() event
    Hierarchy view > GameOverCanvas > select Button (0)
    Inspector view > Button (0).Button.On Click ()
        .+ (click plus button)
        .None (Object) = Hierarchy view > GameManager
        .No Function = GameManager.LoadScene (string)
        .sceneName= “MainMenu”

Call GameManager.RestartScene() from the Restart button’s OnClick() event
    Hierarchy view > GameOverCanvas > select Button (1)
    Inspector view > Button (1).Button.On Click ()
        .+ (click plus button)
        .None (Object) = Hierarchy view > GameManager
        .No Function = GameManager.RestartScene()

Call GameManager.LoadNextScene() from the Next Level button’s OnClick() event
    Hierarchy view > GameOverCanvas > select Button (2)
    Inspector view > Button (2).Button.On Click ()
        .+ (click plus button)
        .None (Object) = Hierarchy view > GameManager
        .No Function = GameManager.LoadNextScene()

Save changes made to the scene
    Unity > (Ctrl + S) or Unity Menu Bar > File > Save
    (Save your scene regularly to avoid losing your progress in case of a crash either by going to File > Save in the Unity menu bar or by pressing Control + S on your keyboard. You can see if there are any changes made to the scene that still need to be saved if there is a star * character displayed right after the name of your scene in the Hierarchy view.)

Create Player Score System
YouTube

Add a new public int playerScore variable to VRPlayer class
    > Visual Studio Code > VRPlayer.cs > add Highlighted Code

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class VRPlayer: MonoBehaviour
{
    public int playerScore = 0;

    // Start is called before the first frame update
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {

    }
}

Save changes made to VRPlayer.cs 
    Visual Studio > VRPlayer.cs > (Ctrl+S) 

Save changes made to the scene
    Unity > (Ctrl + S) or Unity Menu Bar > File > Save
    (Save your scene regularly to avoid losing your progress in case of a crash either by going to File > Save in the Unity menu bar or by pressing Control + S on your keyboard. You can see if there are any changes made to the scene that still need to be saved if there is a star * character displayed right after the name of your scene in the Hierarchy view.)

Increase Player Score On Target Hit
YouTube

Add VRPlayer variable/reference to GameManager class
    Visual Studio Code > GameManager.cs > add Highlighted Code

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Splines;
using UnityEngine.SceneManagement;

public class GameManager : MonoBehaviour
{
    public VRPlayer player;
    public RollerCoasterTrain rollerCoasterTrain;
    public GameObject tutorialCanvas;
    public GameOverCanvas gameOverCanvas;

    // Start is called before the first frame update
    void Start()
    {
        Debug.Log("The Game is Starting!!!");

        gameOverCanvas.gameObject.SetActive(false);
    }

    // Update is called once per frame
    void Update()
    {
        Debug.Log("The Game is Running!!!");
    }

    public void StartRollerCoasterRide()
    {
        Debug.Log("The Ride is Starting!!!");

        // Play the spline animation to start the ride.
        rollerCoasterTrain.GetComponent<SplineAnimate>().Play();

        // Deactivate the tutorial canvas when the player starts the ride.
        tutorialCanvas.SetActive(false);
    }

    public void StopRollerCoasterRide()
    {
        Debug.Log("The Ride is Stopping!!!");
        
        // Pause the spline animation to stop the ride.
        rollerCoasterTrain.GetComponent<SplineAnimate>().Pause();
    }

    public void GameOver()
    {
        Debug.Log("Game Over!!!");
        
        gameOverCanvas.gameObject.SetActive(true);
    }

    public void LoadScene(string sceneName)
    {
        SceneManager.LoadScene(sceneName);
    }

    public void LoadNextScene()
    {
        int nextSceneIndex = SceneManager.GetActiveScene().buildIndex + 1;
        if(SceneManager.sceneCountInBuildSettings > nextSceneIndex)
        {
            SceneManager.LoadScene(nextSceneIndex);
        }
        else
        {
            SceneManager.LoadScene(0);
        }
    }

    public void RestartScene()
    {
        SceneManager.LoadScene(SceneManager.GetActiveScene().name);
    }
}

Add IncreasePlayerSore() function to GameManager class
    Visual Studio Code > GameManager.cs > add Highlighted Code

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Splines;
using UnityEngine.SceneManagement;

public class GameManager : MonoBehaviour
{
    public VRPlayer player;
    public RollerCoasterTrain rollerCoasterTrain;
    public GameObject tutorialCanvas;
    public GameOverCanvas gameOverCanvas;

    // Start is called before the first frame update
    void Start()
    {
        Debug.Log("The Game is Starting!!!");

        gameOverCanvas.gameObject.SetActive(false);
    }

    // Update is called once per frame
    void Update()
    {
        Debug.Log("The Game is Running!!!");
    }

    public void StartRollerCoasterRide()
    {
        Debug.Log("The Ride is Starting!!!");

        // Play the spline animation to start the ride.
        rollerCoasterTrain.GetComponent<SplineAnimate>().Play();

        // Deactivate the tutorial canvas when the player starts the ride.
        tutorialCanvas.SetActive(false);
    }

    public void StopRollerCoasterRide()
    {
        Debug.Log("The Ride is Stopping!!!");
        
        // Pause the spline animation to stop the ride.
        rollerCoasterTrain.GetComponent<SplineAnimate>().Pause();
    }

    public void GameOver()
    {
        Debug.Log("Game Over!!!");
        
        gameOverCanvas.gameObject.SetActive(true);
    }

    public void LoadScene(string sceneName)
    {
        SceneManager.LoadScene(sceneName);
    }

    public void LoadNextScene()
    {
        int nextSceneIndex = SceneManager.GetActiveScene().buildIndex + 1;
        if(SceneManager.sceneCountInBuildSettings > nextSceneIndex)
        {
            SceneManager.LoadScene(nextSceneIndex);
        }
        else
        {
            SceneManager.LoadScene(0);
        }
    }

    public void RestartScene()
    {
        SceneManager.LoadScene(SceneManager.GetActiveScene().name);
    }
    
    public void IncreasePlayerScore(int amount)
    {
        Debug.Log("Adding " + amount + " points to Player Score!!!");

        // Increase the playerScore value on the player
        player.playerScore += amount;
    } 
}

Save Changes Made to GameManager.cs
    Visual Studio Code > GameManager.cs > Ctrl+S

Add int targetPointsValue variable to Target class 
    Visual Studio Code > Target.cs > add highlighted code 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

public class Target : MonoBehaviour
{
    public int targetPointsValue = 100;
    public ParticleSystem particles;
    public AudioSource audioSrc;
    public UnityEvent onTargetHitEvent;

    // Start is called before the first frame update
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        
    }
    
    // OnCollisionEnter is called when this collider/rigidbody 
    // has begun touching another rigidbody/collider.
    public IEnumerator OnCollisionEnter(Collision collider)
    {
        Debug.Log("Target was HIT!!!");
        Debug.Log("Hit by: " + collider.gameObject.name);

        audioSrc.Play();
        particles.Play();

        GetComponent<MeshCollider>().enabled = false;
        GetComponent<MeshRenderer>().enabled = false;

        if (onTargetHitEvent != null) 
        { 
            onTargetHitEvent.Invoke(); 
        }
        
        yield return null;
    }
}

Call GameManager.IncreasePlayerSore() function from Target.OnCollisionEnter() function
    Visual Studio Code > Target.cs > add highlighted code 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

public class Target : MonoBehaviour
{
    public int targetPointsValue = 100;
    public ParticleSystem particles;
    public AudioSource audioSrc;
    public UnityEvent onTargetHitEvent;

    // Start is called before the first frame update
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        
    }
    
    // OnCollisionEnter is called when this collider/rigidbody 
    // has begun touching another rigidbody/collider.
    public IEnumerator OnCollisionEnter(Collision collider)
    {
        Debug.Log("Target was HIT!!!");
        Debug.Log("Hit by: " + collider.gameObject.name);

        audioSrc.Play();
        particles.Play();

        GetComponent<MeshCollider>().enabled = false;
        GetComponent<MeshRenderer>().enabled = false;

        FindObjectOfType<GameManager>().IncreasePlayerScore(targetPointsValue);

        if (onTargetHitEvent != null) 
        { 
            onTargetHitEvent.Invoke(); 
        }
        
        yield return null;
    }
}

Add a null check if-statement before the function call to check if a GameManager exists in the scene
    Visual Studio Code > Target.cs > add highlighted code 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

public class Target : MonoBehaviour
{
    public int targetPointsValue = 100;
    public ParticleSystem particles;
    public AudioSource audioSrc;
    public UnityEvent onTargetHitEvent;

    private GameManager gameManager;

    // Start is called before the first frame update
    void Start()
    {
        gameManager = FindObjectOfType<GameManager>();
    }

    // Update is called once per frame
    void Update()
    {
        
    }
    
    // OnCollisionEnter is called when this collider/rigidbody 
    // has begun touching another rigidbody/collider.
    public IEnumerator OnCollisionEnter(Collision collider)
    {
        Debug.Log("Target was HIT!!!");
        Debug.Log("Hit by: " + collider.gameObject.name);

        audioSrc.Play();
        particles.Play();

        GetComponent<MeshCollider>().enabled = false;
        GetComponent<MeshRenderer>().enabled = false;

        if (gameManager != null)
        {
            gameManager.IncreasePlayerScore(targetPointsValue);
        }

        if (onTargetHitEvent != null) 
        { 
            onTargetHitEvent.Invoke(); 
        }
        
        yield return null;
    }
}

Save Changes Made to Target.cs
    Visual Studio > Target.cs > (Ctrl+S)

Notes

The objective here is to give each target a points variable to store how many points the target is worth so that when it is hit by the player’s gun we can add that amount to the player’s playerScore value.

The default targetPointsValue value is initialized at a hundred points, so if we don’t change the value in the inspector each target will be worth 100 points, but this way we can easily give individual types of targets different target points values. For instance a small target could be worth more points because it is harder to hit than a large target or maybe we can have ‘hidden’ targets which are harder to find so they could reward the player with more points.

When the target collider is hit by the bullet collider and the OnCollisionEnter() function of the target is called by Unity we search for the GameManager component in the scene with the GameObject.FindObjectOfType<>() function, which is ok to use in this case because we only have one GameManager in the scene, so we know which one the function will return.

To make sure that we don’t get any errors when there is no GameManager in the scene we can do a null check first so that we don’t try to call a function on the GameManager when there is no GameManager in the scene.
Instead of trying to find the GameManager at the moment that the target is hit from the OnCollisionEnter() function during the game, we can try to find it at the start of the game in the Start() function instead and store it in a variable/reference of the type GameManager written at the top of the script.

Links

    Object class – Unity Manual
    https://docs.unity3d.com/Manual/class-Object.html
    Object class – Unity Scripting API

    https://docs.unity3d.com/ScriptReference/Object.html
    Object.FindObjectOfType<>() – Unity Scripting API
    https://docs.unity3d.com/ScriptReference/Object.FindObjectOfType.html

Create new Player Score UI Canvas GameObject
YouTube

Create new UI Canvas GameObject
    Unity Menu Bar > GameObject > UI > Canvas
    Hierarchy view
        > rename GameObject ‘Canvas’ to ‘PlayerScoreCanvas’
        > select PlayerScoreCanvas

    Inspector view > PlayerScoreCanvas.Canvas.Render Mode = World Space

    Hierarchy view 
        > parent PlayerScoreCanvas to RollerCoasterTrain
        > select PlayerScoreCanvas

    Inspector view > PlayerScoreCanvas.Rect Transform
        .Position = (0, 1.6, 0.575)
        .Width = 200
        .Height = 50
        .Scale = (0.001, 0.001, 0.001)

Add new UI Panel GameObject to UI Canvas GameObject
    Hierarchy view
        > right click on PlayerScoreCanvas > UI > Panel
        > PlayerScoreCanvas > select Panel
    Inspector view > Panel.Image.Color = RGB 0-1.0 (0, 0, 0, 1)

Add new UI Text GameObject to UI Panel GameObject
    Hierarchy view
        > PlayerScoreCanvas
            > right click on Panel > UI > Text – TextMeshPro
            > Panel
                > rename ‘Text (TMP)’ to ‘Text (TMP)-PlayerScoreValue’
            > select Text (TMP)-PlayerScoreValue
    Inspector view > Text (TMP)-PlayerScoreValue.TextMeshPro – Text (UI)
        .Text Input = “0”
        .Main Settings
            .Font Asset = Electronic Highway Sign SDF (TMP_Font Asset)
            .Color Gradient = true
               .Color Preset = Yellow to Orange – Vertical (TMP_Color Gradient)
            Alignment = Right, Middle

Create a new PlayerScoreCanvas.cs Script   
    Project view > Assets/MyGame/Scripts > right click > Create > C# Script > name PlayerScoreCanvas.cs

Add PlayerScoreCanvas component to PlayerScoreCanvas GameObject
    Hierarchy view > RollerCoasterSpline > RollerCoasterTrain > select PlayerScoreCanvas
    Inspector view > PlayerScoreCanvas.Add Component > Scripts > Player Score Canvas

Notes

Links

Setup PlayerScoreCanvas
YouTube

Add  public TMP_Text playerScoreValueText variable to PlayerScoreCanvas class
    Visual Studio Code > PlayerScoreCanvas.cs > add Highlighted Code
    (Don’t forget to add the using TMPro directive on line 4)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro;

public class PlayerScoreCanvas : MonoBehaviour
{
    public TMP_Text playerScoreValueText;

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

Save Changes Made to PlayerScoreCanvas.cs
    Visual Studio Code > PlayerScoreCanvas.cs > (Ctrl+S)

Assign Text (TMP)-PlayerScoreValue.TextMeshPro – Text (UI) component to PlayerScoreCanvas.playerScoreValueText variable
    Hierarchy view > RollerCoasterSpline > RollerCoasterTrain > select PlayerScoreCanvas
    Inspector view > PlayerScoreCanvas.PlayerScoreCanvas.Player Score Value Text = Hierarchy view > RollerCoasterSpline > RollerCoasterTrain > PlayerScoreCanvas > Panel > Text (TMP)-PlayerScoreValue 

Add PlayerScoreCanvas playerScoreCanvas variable/reference to GameManager class
    Visual Studio Code > GameManager.cs > add Highlighted Code

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Splines;
using UnityEngine.SceneManagement;

public class GameManager : MonoBehaviour
{
    public VRPlayer player;
    public RollerCoasterTrain rollerCoasterTrain;
    public GameObject tutorialCanvas;
    public GameOverCanvas gameOverCanvas;
    public PlayerScoreCanvas playerScoreCanvas;

    // Start is called before the first frame update
    void Start()
    {
        Debug.Log("The Game is Starting!!!");

        gameOverCanvas.gameObject.SetActive(false);
    }

    // Update is called once per frame
    void Update()
    {
        Debug.Log("The Game is Running!!!");
    }

    public void StartRollerCoasterRide()
    {
        Debug.Log("The Ride is Starting!!!");

        // Play the spline animation to start the ride.
        rollerCoasterTrain.GetComponent<SplineAnimate>().Play();

        // Deactivate the tutorial canvas when the player starts the ride.
        tutorialCanvas.SetActive(false);
    }

    public void StopRollerCoasterRide()
    {
        Debug.Log("The Ride is Stopping!!!");
        
        // Pause the spline animation to stop the ride.
        rollerCoasterTrain.GetComponent<SplineAnimate>().Pause();
    }

    public void GameOver()
    {
        Debug.Log("Game Over!!!");
        
        gameOverCanvas.gameObject.SetActive(true);
    }

    public void LoadScene(string sceneName)
    {
        SceneManager.LoadScene(sceneName);
    }

    public void LoadNextScene()
    {
        int nextSceneIndex = SceneManager.GetActiveScene().buildIndex + 1;
        if(SceneManager.sceneCountInBuildSettings > nextSceneIndex)
        {
            SceneManager.LoadScene(nextSceneIndex);
        }
        else
        {
            SceneManager.LoadScene(0);
        }
    }

    public void RestartScene()
    {
        SceneManager.LoadScene(SceneManager.GetActiveScene().name);
    }
    
    public void IncreasePlayerScore(int amount)
    {
        Debug.Log("Adding " + amount + " points to Player Score!!!");

        // Increase the playerScore value on the player
        player.playerScore += amount;
    } 
}

Update PlayerScoreCanvas.playerScoreValueText.text variable from GameManager.IncreasePlayerScore() function
    Visual Studio Code > GameManager.cs > add Highlighted Code

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Splines;
using UnityEngine.SceneManagement;

public class GameManager : MonoBehaviour
{
    public VRPlayer player;
    public RollerCoasterTrain rollerCoasterTrain;
    public GameObject tutorialCanvas;
    public GameOverCanvas gameOverCanvas;

    // Start is called before the first frame update
    void Start()
    {
        Debug.Log("The Game is Starting!!!");

        gameOverCanvas.gameObject.SetActive(false);
    }

    // Update is called once per frame
    void Update()
    {
        Debug.Log("The Game is Running!!!");
    }

    public void StartRollerCoasterRide()
    {
        Debug.Log("The Ride is Starting!!!");

        // Play the spline animation to start the ride.
        rollerCoasterTrain.GetComponent<SplineAnimate>().Play();

        // Deactivate the tutorial canvas when the player starts the ride.
        tutorialCanvas.SetActive(false);
    }

    public void StopRollerCoasterRide()
    {
        Debug.Log("The Ride is Stopping!!!");
        
        // Pause the spline animation to stop the ride.
        rollerCoasterTrain.GetComponent<SplineAnimate>().Pause();
    }

    public void GameOver()
    {
        Debug.Log("Game Over!!!");
        
        gameOverCanvas.gameObject.SetActive(true);
    }

    public void LoadScene(string sceneName)
    {
        SceneManager.LoadScene(sceneName);
    }

    public void LoadNextScene()
    {
        int nextSceneIndex = SceneManager.GetActiveScene().buildIndex + 1;
        if(SceneManager.sceneCountInBuildSettings > nextSceneIndex)
        {
            SceneManager.LoadScene(nextSceneIndex);
        }
        else
        {
            SceneManager.LoadScene(0);
        }
    }

    public void RestartScene()
    {
        SceneManager.LoadScene(SceneManager.GetActiveScene().name);
    }
    
    public void IncreasePlayerScore(int amount)
    {
        Debug.Log("Adding " + amount + " points to Player Score!!!");

        // Increase the playerScore value on the player
        player.playerScore += amount;

        // Update the score displayed on the PlayerScoreCanvas
        playerScoreCanvas.playerScoreValueText.text = "" + player.playerScore;
    } 
}

Save Changes Made to GameManager.cs
    Visual Studio Code > GameManager.cs > (Ctrl+S)

Assing PlayerScoreCanvas.PlayerScoreCanvas component to GameManager.playerScoreCanvas variable
    Hierarchy view > select GameManager
    Inspector view > GameManager.GameManager.Player Score Canvas = Hierarchy view > RollerCoasterSpline > RollerCoasterTrain > PlayerScoreCanvas

Notes

Links

Create a new Main Menu Scene
YouTube

Create new MainMenu.unity scene file
    Project view > Assets/MyGame/Scenes
        > right click > Create > Scene
        > rename ‘New Scene.unity’ to ‘MainMenu.unity’

Create a new Primitive Floor GameObject
    Unity Menu Bar > GameObject > 3D Object > Cube
    Hierarchy view
        > rename ‘Cube’ to ‘Floor’
        > select Floor
    Inspector view > Floor.Transform
        .Position = (0, -0.5, 0)
        .Scale = (10, 1, 10)

Add VRPlayer GameObject to the MainMenu scene

Add MainMenu.unity Scene to the project’s Scenes In Build list
Unity Menu Bar > File > Build Settings > Scenes In Build > click Add Open Scenes

Reorder the project’s Scenes In Build list
    Unity Menu Bar > File > Build Settings > Scenes In Build >
        > 0 = MyGame/Scenes/MainMenu 
        > 1 = MyGame/Scenes/Level 1

Create a World Space Main Menu UI Canvas
YouTube

Create New Canvas GameObject
    Unity Menu Bar > GameObject > UI > Canvas
    Hierarchy view
        > rename ‘Canvas’ to ‘MainMenuCanvas’
        > select MainMenuCanvas
    Inspector view > MainMenuCanvas
        .Canvas.Render Mode = World Space
        .RectTransform
            .Position = (0, 3, 4)
            .Width = 4000
            .Height = 4000
            .Scale = (0.001, 0.001, 0.001)

Add UI.Panel GameObject to MainMenuCanvas
    Hierarchy view > richt click MainMenuCanvas > UI > Panel
    (If you right click on a Canvas to add a panel then Unity will automatically add it as a child of the Canvas)
     Inspector view > Panel.Rect Transform
        .Position = (2000, -2000, 0)
        .Width = 4000
        .Height = 4000

Add Game Title UI Text GameObject to Panel
    Hierarchy view
        > right click Panel > UI > Text – Textmesh Pro
        > rename ‘Text (TMP) (0)’ to ‘Text (TMP)-Title’
        > select Text (TMP)-Title
    Inspector view > Text (TMP)-Title
        .Rect Transform
            .Position = (0, 1000, 0)
            .Width = 3800
            .Heigth = 1000
        .TextMeshPro -Text (UI).Text Input.Text = “<Your Game Title>”
        .Main Settings
            .Font Asset = Bangers SDF (TMP_Font Asset)
            .Font Size = 800
            .Color Gradient = true
                .Color Preset = Light to Dark Green – Vertical (TMP_Color Gradient)
            .Alignment = Center, Middle

Add Quit, Play and Settings UI Button GameObjects to Panel
    Hierarchy view
        > GameOverCanvas > right click on Panel > UI > Button * 3
        > rename ‘Button (0)’ to ‘Button-Quit’
        > rename ‘Button (1)’ to ‘Button-Play’
        > rename ‘Button (2)’ to ‘Button-Settings’

    Hierarchy view > MainMenuCanvas> Panel > select Button-Quit
    Inspector view > Button-Quit
        .Rect Transform
            .Position = (-1250, -1500, 0)
            .Width = 1000
            .Height = 400
    Hierarchy view > Button-Quit > select Text (TMP)
    Inspector view > Text (TMP).TextMeshPro.
        .text = “QUIT’
        .Font Asset = Bangers SDF (TMP_Font Asset)
        .Font Size = 280

    Hierarchy view > select Button-Play
    Inspector view > Button-Play
        .Rect Transform
            .Position = (0, -1500, 0)
            .Width = 1000
            .Height = 400
    Hierarchy view > Button-Play > select Text (TMP)
    Inspector view > Text (TMP).TextMeshPro.
        .text = “PLAY’
        .Font Asset = Bangers SDF (TMP_Font Asset)
        .Font Size = 280

    Hierarchy view > select Button-Settings
    Inspector view > Button-Settings
        .Rect Transform
            .Position = (1250, -1500, 0)
            .Width = 1000
            .Height = 400
    Hierarchy view > Button-Settings> select Text (TMP)
    Inspector view > Text (TMP).TextMeshPro.
        .text = “SETTINGS’
        .Font Asset = Bangers SDF (TMP_Font Asset)
        .Font Size = 280

Add Tracked Device Graphic Raycaster component to Canvas to enable VR controller input from the player
    Hierarchy view > select MainMenuCanvas
    Inspector view > MainMenuCanvas.Add Component > search ‘Tracked Device Graphic Raycaster’

Setup Quit, Play and Settings UI Buttons
YouTube

Add new LoadScene() and QuitGame() functions to MainMenuCanvas class
    Visual Studio Code > MainMenuCanvas.cs > add Hightlighted Code

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MainMenuCanvas : MonoBehaviour
{
    public void LoadScene(string sceneName)
    {

    }

    public void QuitGame()
    {

    }
}

Call UnityEngine.SceneManagement.SceneManager.LoadScene() from MainMenuCanvas.LoadScene()
    Visual Studio Code > MainMenuCanvas.cs > add Hightlighted Code

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

public class MainMenuCanvas : MonoBehaviour
{
    public void LoadScene(string sceneName)
    {
        SceneManager.LoadScene(sceneName);
    }

    public void QuitGame()
    {

    }
}

Notes

Make sure the using directive to use the UnityEngine.SceneManagement class in our script is added to the other using directives on line 4.
Then call the Unity UnityEngine.SceneManagement.SceneManager.LoadScene() function from the body of our own MainMenu.LoadScene() function passing in a string with the level name (the name of the .unity scene file) as an argument, to tell the Unity SceneManager which scene to load.

By creating a LoadScene() function with a string sceneName parameter we can pass in the name of the scene to load from outside the scope of the function and this way we can simply tell the buttons in the scene which scene to load when they’re clicked by passing a string in the Button.OnClick () event in the Unity Inspector view.
We can use this simple ‘load scene by name’ function to let the Play button on the MainMenuCanvas load the Level 1 scene.

Call Application.Quit() from MainMenuCanvas.QuitGame()
    Visual Studio Code > MainMenuCanvas.cs > add Hightlighted Code

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

public class MainMenuCanvas : MonoBehaviour
{
    public void LoadScene(string sceneName)
    {
        SceneManager.LoadScene(sceneName);
    }

    public void QuitGame()
    {
        Application.Quit();
    }
}
Notes

Call the Unity Application.Quit() function from the body of our MainMenuCanvas.QuitGame() function to tell the running application to quit. In this case the game will quit and the player/user goes back to the Quest’s main menu environment.

Save changes made to MainMenuCanvas.cs
    Visual Studio Code > MainMenuCanvas.cs > (Ctrl + S)

Call MainMenu.QuitGame() from the Quit button’s OnClick() event
    Hierarchy view > MainMenuCanvas> select Button-Quit
    Inspector view > Button-Quit.Button.On Click ()
        .+ (click plus button)
        .None (Object) = Hierarchy view > MainMenuCanvas
        .No Function = MainMenuCanvas.QuitGame ()

Call MainMenu.LoadScene(“Level 1”) from the Play button’s OnClick() event
    Hierarchy view > MainMenuCanvas> select Button-Play
    Inspector view > Button-Play.Button.On Click ()
        .+ (click plus button)
        .None (Object) = Hierarchy view > MainMenuCanvas
        .No Function = MainMenuCanvas.LoadScene ()
        .sceneName= “Level 1”

Notes

Right now both the Quit and the Play buttons are connected to the QuitGame() and LoadScene() functions of the MainMenuCanvas script so when the Quit button is pressed the QuitGame() function will be called and when the Play button is pressed then the LoadScene() function will be called with a string with the level name as an argument to tell it which scene to load. (Make sure to write “Level 1” in the Play button’s OnClick() event in the Inspector, after selecting the MainMenuCanvas.LoadScene function.)
The Settings button doesn’t do anything at the moment yet, but we will make it activate a settings UI canvas panel with sliders to adjust sound effects and music volume and some other settings in a later step.

Add VRPlayer to the MainMenu scene
YouTube

Add ‘XR Interaction Setup’ prefab to Scene
    Project view > Assets/Samples/XR Interaction Toolkit/2.4.3/Starter Assets/Prefabs > drag ‘XR Interaction Setup.prefab’ to Hierarchy view

Reset ‘XR Interaction Setup’ Position

    Hierarchy view > select XR Interaction Setup
    Inspector view > XR Interaction Setup.Transform.Position = (0, 0, 0)

Unpack XR Interaction Setup Prefab
    Hierarchy view > right click XR Interaction Setup > Prefab > Unpack Completely

Rename Player GameObject
    Hierarchy view > rename ‘XR Origin (XR Rig)’ to ‘VRPlayer’

Place VRPlayer on Floor
    Hierarchy view > XR Interaction Setup > select VRPlayer
    Inspector view > VRPlayer.Transform.Position = (0, <Your Floor Height>, 0)

    (To make sure that the player doesn’t fall trough the map when the game starts, place the VRPlayer GameObject at the height of your floor on the y-position. The Character Controller (collider) component on the VRPlayer is what actually collides with the floor so make sure the bottom sits exactly on, or slightly above the floor.)

Remove Extra Camera from Scene
    Hierarchy view > delete Main Camera
    (Because the XR Origin player prefab comes with its own camera, and you only need one camera in your scene, you can delete the default Main Camera that came with your new scene from the hierarchy.)

 

Setup MainMenu VRPlayer
YouTube

Setup Tracking
    Set Tracking Origin Mode to Floor

        Hierarchy view > select ‘VR Player’
        Inspector view > VRPlayer.XR Origin.Tracking Origin Mode = Floor

Setup Walking
    Enable Smooth Turning Instead of Snap Turning
        Hierarchy view > VRPlayer > Camera Offset > select Right Controller
        Inspector View > Right Controller.Action Based Controller Manager.Locomotion Settings.Smooth Turn Enabled = True
    Adjust Smooth Turn Speed
        Hierarchy view > VRPlayer > Locomotion System > select Turn
        Inspector view > Turn.Continuous Turn Provider.Turn Speed = 90    

 

Build Game to Quest & Run
YouTube

 Add Game Scenes to Build Settings
    File > Build Settings > Scenes in Build > click Add Open Scenes
    (Make sure that both the MainMenu.unity scene and the Level 1.unity scene are added to the Project’s Build Settings. The first scene in the list gets loaded when the player launches the game application so that should be the MainMenu scene. You can add scenes to the list with the Add Open Scenes button or by dragging the .unity scene files from the Project view > Assets/MyGame/Scenes folder to the Build Settings list.)

Reorder the project’s Scenes In Build list
    Unity Menu Bar > File > Build Settings > Scenes In Build >
        > 0 = MyGame/Scenes/MainMenu 
        > 1 = MyGame/Scenes/Level 1

Setup Run Device
    Connect the Quest Headset to your computer with the USB-C cable
    File > Build Settings > Platform > Android > Run Device = Oculus Quest 2
    (If your Quest headset doesn’t show up in the list with Run Devices then first make sure it is connected to your computer, powered on and that you’ve confirmed USB debugging in the pop-up notification that appears in Quest headset. If everything is connected and it still doesn’t show then try restarting Unity.)

Build the game to the Quest HMD
    File > Build Settings > click ‘Build & Run’
    (After Unity is done building the game will be started on the Quest HMD automatically. You can quit the game by pressing the Oculus button on the Quest controller and if you want to restart the game on the Quest HMD then you can find it under ‘Unknown Sources’ in the App library window.)

Completed Scene Hierarchies Phase 1
Level 1.unity

Hierarchy
    ▼Level 1
            GameManager
        ▼XR Interaction Setup
                Input Action Manager
                XR Interaction Manager
                Event System
            Sun
            Terrain
        ▼Water
                WaterPlane (0)
                WaterPlane (1)
                WaterPlane (2)
                WaterPlane (3)
                WaterPlane (4)
                WaterPlane (5)
                WaterPlane (6)
                WaterPlane (7)
                WaterPlane (8)
            WindZone
        ▼RollerCoasterSpline
            ▼RollerCoasterTrain
                ▼Mesh
                        Cube (0)
                        Cube (1)
                        Cube (2)
                        Cube (3)
                        Cube (4)
                        Cube (5)
                        Cube (6)
                        Cylinder (0)
                        Cylinder (1)
                        Cylinder (2)
                        Cylinder (3)
                        Cylinder (4)
                        Cylinder (5)
                    Audio Source
                ▼PlayerScoreCanvas
                    ▼Panel
                            Text (TMP)-PlayerScoreValue
                ▼VRGunHolder
                    ▼Mesh
                            Cylinder
                    ▼GunSnapTransform
                        ▼VRGun
                            ▼Mesh
                                    Sphere (0)
                                    Sphere (1)
                                    Sphere (2)
                                    Sphere (3)
                                    Cylinder (0)
                                    Cylinder (1)
                                    Cylinder (2)
                                    Cylinder (3)
                                GrabAttachTransform
                                BulletSpawnTransform
                                Audio Source
                                Particle System
                ▼VRPlayer
                    ▼Camera Offset
                            Main Camera
                            Gaze Interactor
                        ▼Gaze Stabilized
                                Gaze Stabilized Attach
                        ▼Left Controller
                            ▼Poke Interactor
                                ▼Poke Point
                                        Cylinder
                                Direct Interactor
                                Ray Interactor
                        ▼Left Controller Stabilized
                                Left Controller Stabilized Attach
                        ▼Right Controller
                            ▼Poke Interactor
                                ▼Poke Point
                                        Cylinder
                                Direct Interactor
                                Ray Interactor
                        ▼Right Controller Stabilized
                                Right Controller Stabilized Attach
                    ▼Locomotion System
                            Turn
                            Move
                            Grab Move
                            Teleportation
                            Climb
        ▼Targets
            ▼ShootingTarget-Prefab (0)
                    Target
                    Particle System
                    Audio Source
             ▶ShootingTarget-Prefab (1)
             ▶ShootingTarget-Prefab (2)
                 …
        ▼TutorialCanvas
            ▼Panel
                ▼Text (TMP)-TutorialText   
        ▼GameOverCanvas
            ▼Panel
                    Text (TMP)-GameOver
                    Text (TMP)-PlayerScoreLabel
                    Text (TMP)-PlayerScoreValue
                ▼Button-Quit
                        Text (TMP)
                ▼Button-Restart
                        Text (TMP)
                ▼Button-NextRide
                        Text (TMP)
            CustomEventTrigger-Prefab

MainMenu.unity

Hierarchy
    ▼MainMenu
            Directional Light
        ▼XR Interaction Setup
                Input Action Manager
                XR Interaction Manager
                Event System
            Floor
        ▼VRPlayer
            ▼Camera Offset
                    Main Camera
                    Gaze Interactor
                ▼Gaze Stabilized
                        Gaze Stabilized Attach
                ▼Left Controller
                    ▼Poke Interactor
                        ▼Poke Point
                                Cylinder
                        Direct Interactor
                        Ray Interactor
                ▼Left Controller Stabilized
                        Left Controller Stabilized Attach
                ▼Right Controller
                    ▼Poke Interactor
                        ▼Poke Point
                                Cylinder
                        Direct Interactor
                        Ray Interactor
                ▼Right Controller Stabilized
                        Right Controller Stabilized Attach
            ▼Locomotion System
                    Turn
                    Move
                    Grab Move
                    Teleportation
                    Climb
        ▼MainMenuCanvas
            ▼Panel
                    Text (TMP)-GameTitle
                ▼Button-Quit
                        Text (TMP)
                ▼Button-Play
                        Text (TMP)
                ▼Button-Settings
                        Text (TMP)

Completed Scripts Phase 1

Bullet.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Bullet : MonoBehaviour
{
    public ParticleSystem explosionParticleSystem;
    public AudioSource explosionAudioSource;

    // OnCollisionEnter is called when this collider/rigidbody 
    // has begun touching another rigidbody/collider.
    public IEnumerator OnCollisionEnter(Collision collider)
    {
        Debug.Log("Bullet HIT!!!");

        explosionParticleSystem.Play();
        explosionAudioSource.Play();

        GetComponent<SphereCollider>().enabled = false;
        GetComponent<MeshRenderer>().enabled = false;

        yield return null;
    }
}

GameManager.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Splines;
using UnityEngine.SceneManagement;

public class GameManager : MonoBehaviour
{
    public VRPlayer player;
    public RollerCoasterTrain rollerCoasterTrain;
    public GameObject tutorialCanvas;
    public GameOverCanvas gameOverCanvas;

    // Start is called before the first frame update
    void Start()
    {
        Debug.Log("The Game is Starting!!!");

        gameOverCanvas.gameObject.SetActive(false);
    }

    // Update is called once per frame
    void Update()
    {
        Debug.Log("The Game is Running!!!");
    }

    public void StartRollerCoasterRide()
    {
        Debug.Log("The Ride is Starting!!!");

        // Play the spline animation to start the ride.
        rollerCoasterTrain.GetComponent<SplineAnimate>().Play();

        // Deactivate the tutorial canvas when the player starts the ride.
        tutorialCanvas.SetActive(false);
    }

    public void StopRollerCoasterRide()
    {
        Debug.Log("The Ride is Stopping!!!");

        rollerCoasterTrain.GetComponent<SplineAnimate>().Pause();
    }

    public void GameOver()
    {
        Debug.Log("Game Over!!!");
        
        gameOverCanvas.gameObject.SetActive(true);
    }

    public void LoadScene(string sceneName)
    {
        SceneManager.LoadScene(sceneName);
    }

    public void LoadNextScene()
    {
        int nextSceneIndex = SceneManager.GetActiveScene().buildIndex + 1;
        if(SceneManager.sceneCountInBuildSettings > nextSceneIndex)
        {
            SceneManager.LoadScene(nextSceneIndex);
        }
        else
        {
            SceneManager.LoadScene(0);
        }
    }

    public void RestartScene()
    {
        SceneManager.LoadScene(SceneManager.GetActiveScene().name);
    }
    
    public void IncreasePlayerScore(int amount)
    {
        Debug.Log("Adding " + amount + " points to Player Score!!!");

        // Increase the playerScore value on the player
        player.playerScore += amount;
    } 
}

MainMenuCanvas.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

public class MainMenuCanvas : MonoBehaviour
{
    public void LoadScene(string sceneName)
    {
        SceneManager.LoadScene(sceneName);
    }

    public void QuitGame()
    {
        Application.Quit();
    }
}

PlayerScoreCanvas.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro;

public class PlayerScoreCanvas : MonoBehaviour
{
    public TMP_Text playerScoreValueText;
}

Target.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

public class Target : MonoBehaviour
{
    public ParticleSystem targetParticleSystem;
    public AudioSource targetAudioSource;
    public UnityEvent onTargetHitEvent;

    // Start is called before the first frame update
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        
    }
    
    // OnCollisionEnter is called when this collider/rigidbody 
    // has begun touching another rigidbody/collider.
    public IEnumerator OnCollisionEnter(Collision collider)
    {
        Debug.Log("Target was hit by: " + collider.gameObject.name);

        targetParticleSystem.Play();
        targetAudioSource.Play();

        // Disable the target's MeshCollider so it can not be hit multiple times.
        GetComponent<MeshCollider>().enabled = false;
        // Disable the target's MeshRenderer to turn it invisible
        GetComponent<MeshRenderer>().enabled = false;
    
        if (onTargetHitEvent != null)
        {
            // If there are functions subscribed to the onTargetHitEvent invoke (call) them.
            onTargetHitEvent.Invoke();
        }
        
        yield return null;
    }
}

VRPlayer.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class VRPlayer: MonoBehaviour
{
    public int playerScore = 0;
}

CustomEventTrigger.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

public class CustomEventTrigger : MonoBehaviour
{
    // The tag that should trigger the custom event.
    public string triggerTag = "Player";

    public UnityEvent onTriggerEnterEvent;
    public UnityEvent onTriggerStayEvent;
    public UnityEvent onTriggerExitEvent;

    // Draw Gizmos and Handles into the Scene view for debugging, tools etcetera.
    void OnDrawGizmos()
    {
        // Set the color of all drawn Gizmos to a newly created Color (turquoise)
        Gizmos.color = new Color(0,1,1);
        
        // Draw a turqoise wire frame cube in the Scene view at this transform's position
        Gizmos.DrawWireCube(transform.position, transform.localScale);
    }

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }

    // OnTriggerEnter is called when the Collider 'collider' has started touching the trigger.
    public IEnumerator OnTriggerEnter(Collider collider)
    {
        Debug.Log("A GameObject named " + collider.gameObject.name + " with the tag " + collider.gameObject.tag + " has Entered the trigger!!!");

        // Check if there are any functions subscribed to the onTriggerEnterEvent 
        // 'and' (&&) if the tag on the collider is equal to the triggerTag.
        if(onTriggerEnterEvent != null && collider.gameObject.tag == triggerTag)
        {
            // Invoke (call) the functions subscribed to the onTriggerEnterEvent.
            onTriggerEnterEvent.Invoke();
        }

        yield return null;
    }

    // OnTriggerStay is called while the Collider 'collider' is touching the trigger.
    public IEnumerator OnTriggerStay(Collider collider)
    {
        // Check if there are any functions subscribed to the onTriggerStayEvent 
        // 'and' (&&) if the tag on the collider is equal to the triggerTag.
        if(onTriggerStayEvent != null && collider.gameObject.tag == triggerTag)
        {
            // Invoke (call) the functions subscribed to the onTriggerStayEvent.
            onTriggerStayEvent.Invoke();
        }

        yield return null;
    }

    // OnTriggerExit is called when the Collider 'collider' has stopped touching the trigger.
    public IEnumerator OnTriggerExit(Collider collider)
    {
        // Check if there are any functions subscribed to the onTriggerExitEvent 
        // 'and' (&&) if the tag on the collider is equal to the triggerTag.        
        if(onTriggerExitEvent != null && collider.gameObject.tag == triggerTag)
        {
            // Invoke (call) the functions subscribed to the onTriggerExitEvent.
            onTriggerExitEvent.Invoke();
        }

        yield return null;
    }
}

VRGun.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class VRGun: MonoBehaviour
{
    public Transform gunSnapTransform;
    public Transform bulletSpawnTransform;
    public GameObject bulletPrefab;
    public ParticleSystem gunFireParticleSystem;
    public AudioSource gunFireAudioSource;
    public Collider[] gunColliders;
    
    public float fireSpeed = 125.0f;

    private Vector3 gunVelocity;
    private Vector3 previousPosition;
    private bool fireFlag = false;

    // Start is called before the first frame update
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    { 
        // Velocity is distance between current position and previous position divided by time.
        gunVelocity = (bulletSpawnTransform.position - previousPosition) / Time.deltaTime;

        // Store the position at the end of the Update() function for the next frame.
        previousPosition = bulletSpawnTransform.position;
    }

    // FixedUpdate is called every fixed frame-rate frame
    void FixedUpdate()
    {
        // If the fireFlag boolean was set to true during Update()
        // call the Fire() function and set the fireFlag back to false.
        if(fireFlag == true)
        {
            fireFlag = false;
            Fire();
        }
    }

    // Called by Gun.XRGrabInteractable.InteractableEvents.Activated, when the gun is fired.
    public void FireBullet()
    {
        // Set the boolean fireFlag variable to true so that in the next
        // FixedUpdate loop iteration the Fire() function can be called. 
        fireFlag = true;
    }

    public void Fire()
    {
        Debug.Log("Gun is Fired!!!");
        
        // Play the gunFireParticleSystem and the gunFireAudioSource
        gunFireParticleSystem.Play();
        gunFireAudioSource.Play();

        // Spawn/Instantiate a clone of the bulletPrefab and store 
        // a reference to it in the GameObject spawnedBullet variable.
        GameObject spawnedBullet = Instantiate(bulletPrefab);

        // Position the spawnedBullet at the tip of the gun at the bulletSpawnTransform.position.
        spawnedBullet.transform.position = bulletSpawnTransform.position;
        // Rotate the spawnedBullet in the direction of the bulletSpawnTransform
        spawnedBullet.transform.rotation = bulletSpawnTransform.rotation;

        // Get the spawnedBullet's Rigidbody component and set its velocity
        // to the velocity of the gun plus the forward direction 
        // of the bullet multiplied by the firespeed.
        spawnedBullet.GetComponent<Rigidbody>().velocity = gunVelocity + (spawnedBullet.transform.forward * fireSpeed);
        
        // Destroy the spawned bullet after five seconds.
        Destroy(spawnedBullet,5);

        // Get the SphereCollider component on the spawnedBullet and 
        // store a reference to it in the SphereCollider spawnedBulletCollider variable.
        SphereCollider spawnedBulletCollider = spawnedBullet.GetComponent<SphereCollider>();

        // Loop trough the gunColliders array and set IgnoreCollision 
        // between the gunColliders and the spawnedBulletCollider to true.
        for(int i=0; i<gunColliders.Length; i++)
        {
            Physics.IgnoreCollision(spawnedBulletCollider, gunColliders[i], true);
        }
    }

    // Called by Gun.XRGrabInteractable.InteractableEvents.FirstSelectEntered, when the gun is grabbed.
    public void OnGrab()
    {
        Debug.Log("Gun is Grabbed!!!");
    }

    // Called by Gun.XRGrabInteractable.Interactable Events.LastSelectExited, when the gun is released.
    public void OnRelease()
    {
        Debug.Log("Gun is Dropped!!!");

        transform.position = gunSnapTransform.position;
        transform.rotation = gunSnapTransform.rotation;

        GetComponent<Rigidbody>().isKinematic = true;
    }
}

Steps Phase 2

Better Sun

Add a Lens Flare to the Sun
Hierarchy view > select Sun
Inspector view > Sun
    .Add Component > Rendering > Lens Flare
    .Lens Flare.Flare  = Sun+
    .Fade Speed = 0.5f
    .Directional = true

Add a Flare Layer to the Main Camera
Hierarchy view > RollerCoasterSpline > RollerCoasterTrain > PlayerVR > Camera Offset > select Main Camera
Inspector view > Main Camera.Add Component > Rendering > Flare Layer

Position the Sun correctly

Better RollerCoaster

….

Better Trees

>Add capsule colliders to tree prefabs

Better RollerCoasterTrain

….

Variable RollerCoasterTrain Speed

TEMP

RollerCoasterTrain.cs (Attach to RollerCoasterTrain GameObject)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class RollerCoasterTrain : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

AnimateRollerCoasterTrain.cs (Attach to RollerCoasterTrain GameObject)

using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Splines;

public class AnimateRollerCoasterTrain : MonoBehaviour
{
    public SplineContainer splineContainer;
    public RollerCoasterTrain rollerCoasterTrain;
    public float MaxSpeed = 40f;

    [SerializeField]
    SplineData<float> speed = new SplineData<float>();

    private float currentOffset;
    private float currentSpeed;
    private float splineLength;
    private Spline spline;

    void Start()
    {
        spline        = splineContainer.Spline;
        splineLength  = spline.GetLength();
        currentOffset = 0f;
    }

    void OnValidate()
    {
        if (speed != null)
        {
            for (int index = 0; index < speed.Count; index++)
            {
                var data = speed[index];

                //We don't want to have a value that is negative or null as it might block the simulation
                if (data.Value <= 0)
                {
                    data.Value = Mathf.Max(0f, speed.DefaultValue);
                    speed[index] = data;
                }
            }
        }
    }

    void Update()
    {
        if (splineContainer == null || rollerCoasterTrain == null)
        {
            return;
        }
        
        currentOffset = (currentOffset + currentSpeed * Time.deltaTime / splineLength) % 1f;

        if (speed.Count > 0)
        {
            currentSpeed = speed.Evaluate(spline, currentOffset, PathIndexUnit.Normalized, new UnityEngine.Splines.Interpolators.LerpFloat());
        } 
        else
        {
            currentSpeed = speed.DefaultValue;
        }

        Vector3 posOnSplineLocal  = SplineUtility.EvaluatePosition(spline, currentOffset);
        Vector3 direction         = SplineUtility.EvaluateTangent(spline, currentOffset);
        Vector3 upSplineDirection = SplineUtility.EvaluateUpVector(spline, currentOffset);

        rollerCoasterTrain.transform.position = splineContainer.transform.TransformPoint(posOnSplineLocal);
        rollerCoasterTrain.transform.rotation = Quaternion.LookRotation(direction, upSplineDirection);
    }
}
Adjust RollerCoasterTrain Sound Pitch Based on Speed

….

Longer RollerCoasterTrain with extra wagons

….

Better Bullets

Add Trail Renderer

Better Gun Firing

 Better Gun Firing Audio

Better Gun Firing Particles

Prevent bullets from colliding with the gun

Add player velocity to bullet velocity
Fire from FixedUpdate instead of Update to stay in sync with the physics engine

Better Targets

Bonus Points based on hit distance
Moving Targets

    Download Target Bullseye Texture to Assets/MyGame/Textures folder
        Right click on ShootingTargetBullseye-Tex.png texture below > Save Image as.. > ../Documents/Unity/Projects/VRRollerCoasterShooter/VRRollerCoasterShooter-Project/Assets/MyGame/Textures/ShootingTargetBullseye-Tex.png

Better Custom Event Trigger

Fix transform matrix for Gizmos rotation

Player Health

….

Target Health

….

Particle Systems

….

Saving HighScores

….

Sound FX

….

Music
    Download & Install Sonic Pi App (Code music with Python) 

Environment Props

….

Game Optimization

….

Bake Occlusion Data

Bake Lighting Data

Completed Scene Hierarchies Phase 2
Level 1.unity

Hierarchy
    ▼Level 1
            GameManager
        ▼XR Interaction Setup
                Input Action Manager
                XR Interaction Manager
                Event System
            Sun
            Terrain
        ▼Water
                WaterPlane (0)
                WaterPlane (1)
                WaterPlane (2)
                WaterPlane (3)
                WaterPlane (4)
                WaterPlane (5)
                WaterPlane (6)
                WaterPlane (7)
                WaterPlane (8)
            WindZone
        ▼RollerCoasterSpline
            ▼RollerCoasterTrain
                ▼Mesh
                        Cube (0)
                        Cube (1)
                        Cube (2)
                        Cube (3)
                        Cube (4)
                        Cube (5)
                        Cube (6)
                        Cylinder (0)
                        Cylinder (1)
                        Cylinder (2)
                        Cylinder (3)
                        Cylinder (4)
                        Cylinder (5)
                    Audio Source
                ▼PlayerScoreCanvas
                    ▼Panel
                            Text (TMP)-PlayerScoreValue
                ▼VRGunHolder
                    ▼Mesh
                            Cylinder
                    ▼GunSnapTransform
                        ▼VRGun
                            ▼Mesh
                                    Sphere (0)
                                    Sphere (1)
                                    Sphere (2)
                                    Sphere (3)
                                    Cylinder (0)
                                    Cylinder (1)
                                    Cylinder (2)
                                    Cylinder (3)
                                GrabAttachTransform
                                BulletSpawnTransform
                                Audio Source
                                Particle System
                ▼VRPlayer
                    ▼Camera Offset
                            Main Camera
                            Gaze Interactor
                        ▼Gaze Stabilized
                                Gaze Stabilized Attach
                        ▼Left Controller
                            ▼Poke Interactor
                                ▼Poke Point
                                        Cylinder
                                Direct Interactor
                                Ray Interactor
                        ▼Left Controller Stabilized
                                Left Controller Stabilized Attach
                        ▼Right Controller
                            ▼Poke Interactor
                                ▼Poke Point
                                        Cylinder
                                Direct Interactor
                                Ray Interactor
                        ▼Right Controller Stabilized
                                Right Controller Stabilized Attach
                    ▼Locomotion System
                            Turn
                            Move
                            Grab Move
                            Teleportation
                            Climb
        ▼Targets
            ▼ShootingTarget-Prefab (0)
                    Target
                    Particle System
                    Audio Source
             ▶ShootingTarget-Prefab (1)
             ▶ShootingTarget-Prefab (2)
                 …
        ▼TutorialCanvas
            ▼Panel
                ▼Text (TMP)-TutorialText   
        ▼GameOverCanvas
            ▼Panel
                    Text (TMP)-GameOver
                    Text (TMP)-PlayerScoreLabel
                    Text (TMP)-PlayerScoreValue
                ▼Button-Quit
                        Text (TMP)
                ▼Button-Restart
                        Text (TMP)
                ▼Button-NextRide
                        Text (TMP)
            CustomEventTrigger-Prefab

MainMenu.unity

Hierarchy
    ▼MainMenu
            Directional Light
        ▼XR Interaction Setup
                Input Action Manager
                XR Interaction Manager
                Event System
            Floor
        ▼VRPlayer
            ▼Camera Offset
                    Main Camera
                    Gaze Interactor
                ▼Gaze Stabilized
                        Gaze Stabilized Attach
                ▼Left Controller
                    ▼Poke Interactor
                        ▼Poke Point
                                Cylinder
                        Direct Interactor
                        Ray Interactor
                ▼Left Controller Stabilized
                        Left Controller Stabilized Attach
                ▼Right Controller
                    ▼Poke Interactor
                        ▼Poke Point
                                Cylinder
                        Direct Interactor
                        Ray Interactor
                ▼Right Controller Stabilized
                        Right Controller Stabilized Attach
            ▼Locomotion System
                    Turn
                    Move
                    Grab Move
                    Teleportation
                    Climb
        ▼MainMenuCanvas
            ▼Panel
                    Text (TMP)-GameTitle
                ▼Button-Quit
                        Text (TMP)
                ▼Button-Play
                        Text (TMP)
                ▼Button-Settings
                        Text (TMP)

Completed Scripts Phase 2

Bullet.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Bullet : MonoBehaviour
{
    public ParticleSystem explosionParticleSystem;
    public AudioSource explosionAudioSource;

    // OnCollisionEnter is called when this collider/rigidbody 
    // has begun touching another rigidbody/collider.
    public IEnumerator OnCollisionEnter(Collision collider)
    {
        Debug.Log("Bullet HIT!!!");

        explosionParticles.Play();
        explosionAudio.Play();

        GetComponent<SphereCollider>().enabled = false;
        GetComponent<MeshRenderer>().enabled = false;

        yield return null;
    }
}

GameManager.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Splines;
using UnityEngine.SceneManagement;

public class GameManager : MonoBehaviour
{
    public VRPlayer player;
    public RollerCoasterTrain rollerCoasterTrain;
    public GameObject tutorialCanvas;
    public GameOverCanvas gameOverCanvas;

    // Start is called before the first frame update
    void Start()
    {
        Debug.Log("The Game is Starting!!!");

        gameOverCanvas.gameObject.SetActive(false);
    }

    // Update is called once per frame
    void Update()
    {
        Debug.Log("The Game is Running!!!");
    }

    public void StartRollerCoasterRide()
    {
        Debug.Log("The Ride is Starting!!!");

        rollerCoasterTrain.GetComponent<SplineAnimate>().Play();
        tutorialCanvas.SetActive(false);
    }

    public void StopRollerCoasterRide()
    {
        Debug.Log("The Ride is Stopping!!!");

        rollerCoasterTrain.GetComponent<SplineAnimate>().Pause();
    }

    public void GameOver()
    {
        Debug.Log("Game Over!!!");
        
        gameOverCanvas.gameObject.SetActive(true);
    }

    public void LoadScene(string sceneName)
    {
        SceneManager.LoadScene(sceneName);
    }

    public void LoadNextScene()
    {
        int nextSceneIndex = SceneManager.GetActiveScene().buildIndex + 1;
        if(SceneManager.sceneCountInBuildSettings > nextSceneIndex)
        {
            SceneManager.LoadScene(nextSceneIndex);
        }
        else
        {
            SceneManager.LoadScene(0);
        }
    }

    public void RestartScene()
    {
        SceneManager.LoadScene(SceneManager.GetActiveScene().name);
    }
    
    public void IncreasePlayerScore(int amount)
    {
        Debug.Log("Adding " + amount + " points to Player Score!!!");

        // Increase the playerScore value on the player
        player.playerScore += amount;
    } 
}

MainMenuCanvas.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

public class MainMenuCanvas : MonoBehaviour
{
    public void LoadScene(string sceneName)
    {
        SceneManager.LoadScene(sceneName);
    }

    public void QuitGame()
    {
        Application.Quit();
    }
}

PlayerScoreCanvas.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro;

public class PlayerScoreCanvas : MonoBehaviour
{
    public TMP_Text playerScoreValueText;
}

Target.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

public class Target : MonoBehaviour
{
    public int targetPointsValue = 100;
    public ParticleSystem particles;
    public AudioSource audioSrc;
    public UnityEvent onTargetHitEvent;

    // OnCollisionEnter is called when this collider/rigidbody 
    // has begun touching another rigidbody/collider.
    public IEnumerator OnCollisionEnter(Collision collider)
    {
        Debug.Log("Target was HIT!!!");
        Debug.Log("Hit by: " + collider.gameObject.name);

        audioSrc.Play();
        particles.Play();

        GetComponent<MeshCollider>().enabled = false;
        GetComponent<MeshRenderer>().enabled = false;

        if (onTargetHitEvent != null) 
        { 
            onTargetHitEvent.Invoke(); 
        }
        
        yield return null;
    }
}

VRPlayer.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class VRPlayer: MonoBehaviour
{
    public int playerScore = 0;
}

CustomEventTrigger.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

public class CustomEventTrigger : MonoBehaviour
{
    public string triggerTag = "Player";
    public UnityEvent onTriggerEnterEvent;
    public UnityEvent onTriggerStayEvent;
    public UnityEvent onTriggerExitEvent;

    void OnDrawGizmos()
    {
        // Set the color of all drawn Gizmos to a newly created Color (turquoise)
        Gizmos.color = new Color(0,1,1);
        
        // Draw a turqoise wire frame cube in the Scene view at this transform's position
        Gizmos.DrawWireCube(transform.position, transform.localScale);
    }

    public IEnumerator OnTriggerEnter(Collider collider)
    {
        Debug.Log("A GameObject named " + collider.gameObject.name + " with the tag " + collider.gameObject.tag + " has Entered the trigger!!!");

        if(onTriggerEnterEvent != null && collider.gameObject.tag == triggerTag)
        {
            onTriggerEnterEvent.Invoke();
        }
        yield return null;
    }

    public IEnumerator OnTriggerStay(Collider collider)
    {
        if(onTriggerStayEvent != null && collider.gameObject.tag == triggerTag)
        {
            onTriggerStayEvent.Invoke();
        }
        yield return null;
    }

    public IEnumerator OnTriggerExit(Collider collider)
    {
        if(onTriggerExitEvent != null && collider.gameObject.tag == triggerTag)
        {
            onTriggerExitEvent.Invoke();
        }
        yield return null;
    }
}

VRGun.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class VRGun: MonoBehaviour
{
    public Transform gunSnapTransform;
    public Transform bulletSpawnTransform;
    public GameObject bulletPrefab;
    public ParticleSystem gunFireParticleSystem;
    public AudioSource gunFireAudioSource;
    public Collider[] gunColliders;
    
    public float fireSpeed = 125.0f;

    private Vector3 gunVelocity;
    private Vector3 previousPosition;
    private bool fireFlag = false;

    // Start is called before the first frame update
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    { 
        // Velocity is distance between current position and previous position divided by time.
        gunVelocity = (bulletSpawnTransform.position - previousPosition) / Time.deltaTime;

        // Store the position at the end of the Update() function for the next frame.
        previousPosition = bulletSpawnTransform.position;
    }

    // FixedUpdate is called every fixed frame-rate frame
    void FixedUpdate()
    {
        if(fireFlag == true)
        {
            fireFlag = false;
            Fire();
        }
    }

    // Called by Gun.XRGrabInteractable.InteractableEvents.Activated, when the gun is fired.
    public void FireBullet()
    {
        fireFlag = true;
    }

    public void Fire()
    {
        Debug.Log("Gun is Fired!!!");
        
        // Play the gunFireParticleSystem and the gunFireAudioSource
        gunFireParticleSystem.Play();
        gunFireAudioSource.Play();

        // Spawn/Instantiate a clone of the bulletPrefab and store 
        // a reference to it in the GameObject spawnedBullet variable.
        GameObject spawnedBullet = Instantiate(bulletPrefab);

        // Position the spawnedBullet at the tip of the gun at the bulletSpawnTransform.position.
        spawnedBullet.transform.position = bulletSpawnTransform.position;
        // Rotate the spawnedBullet in the direction of the bulletSpawnTransform
        spawnedBullet.transform.rotation = bulletSpawnTransform.rotation;

        // Get the spawnedBullet's Rigidbody component and set its velocity
        // to the velocity of the gun plus the forward direction 
        // of the bullet multiplied by the firespeed.
        spawnedBullet.GetComponent<Rigidbody>().velocity = gunVelocity + (spawnedBullet.transform.forward * fireSpeed);

        // Get the SphereCollider component on the spawnedBullet and 
        // store a reference to it in the SphereCollider spawnedBulletCollider variable.
        SphereCollider spawnedBulletCollider = spawnedBullet.GetComponent<SphereCollider>();

        // Loop trough the gunColliders array and set IgnoreCollision 
        // between the gunColliders and the spawnedBulletCollider to true.
        for(int i=0; i<gunColliders.Length; i++)
        {
            Physics.IgnoreCollision(spawnedBulletCollider, gunColliders[i], true);
        }
        
        // Destroy the spawned bullet after five seconds.
        Destroy(spawnedBullet, 5);
    }

    // Called by Gun.XRGrabInteractable.InteractableEvents.FirstSelectEntered, when the gun is grabbed.
    public void OnGrab()
    {
        Debug.Log("Gun is Grabbed!!!");
    }

    // Called by Gun.XRGrabInteractable.Interactable Events.LastSelectExited, when the gun is released.
    public void OnRelease()
    {
        Debug.Log("Gun is Dropped!!!");

        transform.position = gunSnapTransform.position;
        transform.rotation = gunSnapTransform.rotation;

        GetComponent<Rigidbody>().isKinematic = true;
    }
}
Extra

Before downloading/installing these packages into your working project it is always better to test packages in an empty new 3D Core Template (or 3D Core URP if needed) project first.
Some packages may change your project settings and some packages add unnecessary files/dependencies to your project!

Interesting Unity Registry Packages:
    -Terrain Tools (extra terrain textures etcetera) (Check samples)
    -XR Hands (VR hands)
    -Recorder (Record gameplay videos and GIFs)
    -ProBuilder (Extra mesh editing and level prototyping/building tools)
    -Post Processing (Camera filters and effects)
    -Shader Graph (check samples) (for URP!)
    -Visual Effect Graph (check samples) (for URP!)
    -Visual Scripting (Node based Programming like Shader Graph, Visual Effect Graph and Unreal engines’ blueprints, but for scripting)
    -Cinemachine (Create professional cinematic camera movements)

Interesting AssetStore Packages:
    Official Unity AssetStore Packages
    https://assetstore.unity.com/publishers/1
    Third Person Character Controller
    https://assetstore.unity.com/packages/essentials/starter-assets-third-person-character-controller-urp
    First Person Character Controller
    https://assetstore.unity.com/packages/essentials/starter-assets-first-person-character-controller-urp
    Unity Terrain – URP Demo Scene
    https://assetstore.unity.com/packages/3d/environments/unity-terrain-urp-demo-scene
    Unity Terrain – HDRP Demo Scene
    https://assetstore.unity.com/packages/3d/environments/unity-terrain-hdrp-demo-scene

Interesting GitHub Packages:
    XR Interaction Toolkit Examples (Project)
    https://github.com/Unity-Technologies/XR-Interaction-Toolkit-Examples/tree/main

Interesting YouTube Tutorials:
    Intro to XR Interaction Toolkit (Playlist)
    https://www.youtube.com/playlist?list=PLWcLPdrF6kOmwlF8mOx8bY6HfYnV3NPqq
    How to Make a VR Game in Unity (Playlist)
    https://www.youtube.com/playlist?list=PLpEoiloH-4eP-OKItF8XNJ8y8e1asOJud

Resources
    Oculus Developer Documentation
    https://developer.oculus.com/documentation/unity/unity-gs-overview/