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/