Virtual reality allows us to immersive ourselves in 3D worlds. This tutorial will show you how to create a VR tour of multiple worlds. In the process, we will be importing Blender models, creating interactive UI components, and switching scenes.

Source Code Files

You can download the complete project here . I created this project in Unity 5.6.1.

Creating the Project

To create the project, open Unity and choose “New Project.” Choose a name and directory for your project.

You will need an Oculus signature file that is unique to your phone in order to run the game on a headset. Instructions to create a new signature file, if you don’t have one already, are at this link. Once you have created a file, copy it into the Assets/Plugins/Android/assets folder in your project’s directory.

Importing Models

We’ll be using low-poly models created in Blender for our landscapes and our flying platform. The models are located in the Assets/Models folder of the included Unity project. Copy this folder into your own project.

You can also create and import your own models from Blender. In Blender, delete the model’s camera and export the model as a .fbx file. Save this file in the Assets/Models folder of your Unity project.

Don't miss out! Offer ends in Access all 200+ courses

Access all 200+ courses New courses added monthly

New courses added monthly Cancel anytime

Cancel anytime Certificates of completion ACCESS NOW

Setting the Scene

Our first scene will be a tour of a mountainous landscape. Load the Assets/Models folder in the Project pane to see your models. If you’ve copied the Assets/Models folder from the included project, drag the Mountains model into the Scene pane. Select Mountains in the hierarchy pane, and adjust the scale values in the Inspector pane until the scene looks pleasing.

Creating the Player

Next, we’re going to create our Player object. The Player will be traversing the scene on a flying platform, stopping at various waypoints along the way to view the scenery and descriptive text.

If you were building a non-VR first person experience, you could move the player just by moving the camera. However, in virtual reality, the headset is the camera. When you move your head, the headset sends the camera coordinates to the game. As a result, the game should not be changing the camera’s coordinates. If it does, your camera view will not match your actual head movement.

How do we move the player if the game should not change camera coordinates directly? The solution is to encapsulate the camera inside another object that represents the player. When the player moves within the game world, the game changes the coordinates of the player object, but the camera’s coordinates are still controlled by the player’s physical head movements.

In our game, create a new empty object named Player by right clicking and selecting “Create Empty” in the hierarchy pane. Drag the Main Camera onto the Player object in the Hierarchy pane to nest Main Camera under Player. In the Inspector pane, select MainCamera as the tag of the Main Camera object. This allows us to access the Main Camera in other scripts using main.Camera. We will be using this later.

Since the player will be standing on a platform, we will need to add a platform asset to the Player object as well. From Assets/Models, drag the Platform model into the Player object to nest it. In the Inspector pane, reset the coordinates of the platform and then rotate it until it is right side up in the scene. We want the player to stand on the platform with railings around him.

Use the transform widget to move the Player object to where you want to start the tour. For best results, position the Player above the terrain. If the terrain is looking a little too small relative to your platform, or the platform too large, scale up the terrain, or scale down the platform.

To make the player’s platform move, we will need to add a script that updates the player’s position each frame. Create a script named PlayerController.cs in Assets/Scripts, and add this script to Player.

This is the code:

using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.EventSystems; /** * This class manages the movement of the player. **/ public class PlayerController : MonoBehaviour { // Array of waypoints that the player will visit. // Set in the Inspector. public WaypointController[] waypoints; public float speed = 2f; // The current waypoint that the player is traveling to. int currentWaypoint = 0; // Flag that indicates whether the player is currently in motion. bool isMoving = true; // Checks if any of the Waypoints are null. void CheckWaypoints() { Debug.Log("Checking " + waypoints.Length + " waypoints"); if (waypoints == null) { Debug.Log ("waypoints array is null"); } for (int i = 0; i < waypoints.Length; i++) { if (waypoints [i] == null) { Debug.Log ("Waypoint " + i + " is null."); } } } // Checks if the player has reached the current waypoint. bool AtWaypoint() { float distance; if (waypoints [currentWaypoint] != null) { distance = Vector3.Distance (transform.position, waypoints [currentWaypoint].transform.position); } else { Debug.Log ("Cannot determine distance because waypoint is null."); distance = 0; } return (distance == 0); } // Update is called once per frame. // If isMoving is true, and the player has not arrived at the current // Waypoint, move the player towards the Waypoint. void Update () { if (isMoving) { if (!AtWaypoint ()) { transform.position = Vector3.MoveTowards (transform.position, waypoints [currentWaypoint].transform.position, speed * Time.deltaTime); } else { Debug.Log ("Arrived at waypoint " + currentWaypoint + ". Showing description."); isMoving = false; waypoints [currentWaypoint].ShowDescription (); } } } // Called after the player clicks on the button to close a waypoint's description. public void ContinueTour() { Debug.Log("ContinueTour: currentWaypoint=" + currentWaypoint); if (AtWaypoint() && waypoints [currentWaypoint] != null) { // Hide the description. Debug.Log ("Hiding waypoint " + currentWaypoint); waypoints [currentWaypoint].HideDescription (); // If this is the last waypoint in the circuit, // load the next scene. if (currentWaypoint == waypoints.Length - 1) { Debug.Log ("Reached end of tour."); Debug.Log("Loading next scene"); LoadNextScene (); } // If there are more waypoints, continue to the next waypoint. else { currentWaypoint++; Debug.Log ("Next waypoint: " + currentWaypoint); isMoving = true; } } else { Debug.Log ("ERROR: Something's wrong... Either we have not arrived at the waypoint, or the waypoint is null"); } } // Loads the next scene. If we're already at the last scene, // loads the first scene. void LoadNextScene() { Debug.Log ("Loading next scene"); int currentSceneIndex = SceneManager.GetActiveScene ().buildIndex; if (currentSceneIndex < SceneManager.sceneCountInBuildSettings - 1) { SceneManager.LoadScene (currentSceneIndex + 1); } else { SceneManager.LoadScene (0); } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 using System . Collections ; using System . Collections . Generic ; using UnityEngine ; using UnityEngine . SceneManagement ; using UnityEngine . EventSystems ; /** * This class manages the movement of the player. **/ public class PlayerController : MonoBehaviour { // Array of waypoints that the player will visit. // Set in the Inspector. public WaypointController [ ] waypoints ; public float speed = 2f ; // The current waypoint that the player is traveling to. int currentWaypoint = 0 ; // Flag that indicates whether the player is currently in motion. bool isMoving = true ; // Checks if any of the Waypoints are null. void CheckWaypoints ( ) { Debug . Log ( "Checking " + waypoints . Length + " waypoints" ) ; if ( waypoints == null ) { Debug . Log ( "waypoints array is null" ) ; } for ( int i = 0 ; i < waypoints . Length ; i ++ ) { if ( waypoints [ i ] == null ) { Debug . Log ( "Waypoint " + i + " is null." ) ; } } } // Checks if the player has reached the current waypoint. bool AtWaypoint ( ) { float distance ; if ( waypoints [ currentWaypoint ] ! = null ) { distance = Vector3 . Distance ( transform . position , waypoints [ currentWaypoint ] . transform . position ) ; } else { Debug . Log ( "Cannot determine distance because waypoint is null." ) ; distance = 0 ; } return ( distance == 0 ) ; } // Update is called once per frame. // If isMoving is true, and the player has not arrived at the current // Waypoint, move the player towards the Waypoint. void Update ( ) { if ( isMoving ) { if ( ! AtWaypoint ( ) ) { transform . position = Vector3 . MoveTowards ( transform . position , waypoints [ currentWaypoint ] . transform . position , speed * Time . deltaTime ) ; } else { Debug . Log ( "Arrived at waypoint " + currentWaypoint + ". Showing description." ) ; isMoving = false ; waypoints [ currentWaypoint ] . ShowDescription ( ) ; } } } // Called after the player clicks on the button to close a waypoint's description. public void ContinueTour ( ) { Debug . Log ( "ContinueTour: currentWaypoint=" + currentWaypoint ) ; if ( AtWaypoint ( ) && waypoints [currentWaypoint] != null) { // Hide the description. Debug.Log ("Hiding waypoint " + currentWaypoint); waypoints [ currentWaypoint ] . HideDescription ( ) ; // If this is the last waypoint in the circuit, // load the next scene. if ( currentWaypoint == waypoints . Length - 1 ) { Debug . Log ( "Reached end of tour." ) ; Debug . Log ( "Loading next scene" ) ; LoadNextScene ( ) ; } // If there are more waypoints, continue to the next waypoint. else { currentWaypoint ++ ; Debug . Log ( "Next waypoint: " + currentWaypoint ) ; isMoving = true ; } } else { Debug . Log ( "ERROR: Something's wrong... Either we have not arrived at the waypoint, or the waypoint is null" ) ; } } // Loads the next scene. If we're already at the last scene, // loads the first scene. void LoadNextScene ( ) { Debug . Log ( "Loading next scene" ) ; int currentSceneIndex = SceneManager . GetActiveScene ( ) . buildIndex ; if ( currentSceneIndex < SceneManager . sceneCountInBuildSettings - 1 ) { SceneManager . LoadScene ( currentSceneIndex + 1 ) ; } else { SceneManager . LoadScene ( 0 ) ; } } }

PlayerController contains a field for speed and a flag that indicates whether the player should be moving. Waypoints the player will visit are stored in the waypoints array. currentWaypoint keeps track of which waypoint is the current destination. In Update() , if the player has arrived at a waypoint (distance is 0), the player stops ( isMoving is set to false), and the waypoint’s description is shown. When the player is done with a waypoint, ContinueTour() is called, and the player moves on to the next waypoint. If the player is already at the last waypoint, LoadNextScene() is called to switch to the next scene.

We will discuss the implementation of waypoints later. PlayerController’s waypoints array is populated by dragging Waypoint instances into the inspector.

Adding a Reticle

The player will need a reticle, or targeting crosshair, to interact with objects like the buttons we will be creating. The VR standard assets, which are included in the free VRSamples package from the Unity store, contain the assets needed to create a reticle. You can copy VRStandardAssets from the Assets folder in the project included with this tutorial and paste it into your project’s Assets folder.

Add the following scripts from VRStandardAssets/Scripts to the Main Camera: VREyeRayCaster.cs and VRInput.cs. Also add the following scripts from VRStandardAssets/Utils/Scripts: VRCameraUI.cs and Reticle.cs. You can attach scripts by dragging them from the project pane directly onto the Main Camera in the hierarch pane.

Now create an image for the reticle. Right click on the Main Camera and select Create->UI->Canvas. Right click on the new canvas and select Create->UI->Image. Rename the image to Reticle and the canvas to Reticle Canvas.

In the Reticle’s inspector, set GUIReticle as the sprite. Select a color that is easy to see. Set width and height to 1 and scale to 0.1. This sets the reticle image to a reasonable size.

In the Reticle Canvas’s inspector, set render mode to World and drag the Main Camera into the camera field.

In the Main Camera’s inspector, make sure the scripts’ fields match the screenshot by dragging the appropriate objects (usually Main Camera, Reticle, or Reticle Canvas) into the fields.

Adding Waypoints

The Player will be visiting Waypoints that are game objects containing a canvas with text and a clickable button.

To create a waypoint, create a new empty object in the hierarchy pane and name it “Waypoint.” Use the transform widget to move the Waypoint to a place in the scene where you want the player to stop. When the player reaches the Waypoint, text is supposed to appear, along with a button that continues the tour when clicked. Text and buttons in Unity are UI (user interface) elements that must be rendered on a canvas. In fact, if you try to create a UI element by itself, Unity will automatically create a canvas.

We are going to nest a canvas under Waypoint and nest our UI elements under the canvas. Right click Waypoint in the hierarchy pane and select UI->Canvas. In the inspector for the canvas, select World Space as the render mode. This makes the canvas exist as an object in the game world instead of as a fixed overlay over the camera. Under Rect Transform, set width and height to 2.5 and 1, respectively. With reference pixels per unit set to 100 in the Canvas Scaler, this equates to dimensions of 0.025 meter by 0.01 meter. Set Pos Y to 1. Set Pos Z to -3 for the canvas to appear about 3m past the Waypoint.

Click Add Component and choose Canvas Group. A Canvas Group lets us set the visibility of the canvas and all its UI components.

Create a panel nested under the canvas by right clicking the canvas and selecting UI->Panel. In the Image section in the Inspector pane, choose “None” as the source image, and set a color you like. In the hierarchy pane, right click the panel and select UI->Text to create a text element nested under the panel.

Text will appear sharper if you set the width, height, and font to large values and then scale down. To match the canvas, set width and height to 2500 by 100. Set the X, Y, and Z scales to 0.001 to scale down to the size of the canvas. In the Text section of the Inspector, set font size to 100. In the text box, type a description of the Waypoint’s location.

We want the canvas to be visible until after we arrive at a Waypoint, so we will need to create another script to set the visibility. In the project pane, select the Assets/Scripts folder and create a script named WaypointController.cs with the following code:

using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; /** * This class manages Waypoints' canvases. **/ public class WaypointController : MonoBehaviour { Canvas waypointDescriptionCanvas; CanvasGroup canvasGroup; // Called when this object is created. void Awake() { waypointDescriptionCanvas = GetComponentInChildren<Canvas> (); if (waypointDescriptionCanvas == null) { Debug.Log ("Could not get canvas."); } else { // Get the canvas's CanvasGroup, and hide it. canvasGroup = waypointDescriptionCanvas.GetComponent<CanvasGroup>(); HideDescription(); } } // Make the CanvasGroup visible. public void ShowDescription() { waypointDescriptionCanvas.transform.position = Camera.main.transform.position + new Vector3 (0f, -0.5f, 3f); //waypointDescriptionCanvas.transform.rotation = new Quaternion( 0.0f, Camera.main.transform.rotation.y, 0.0f, Camera.main.transform.rotation.w ); // Set the canvas's forward vector to face the camera, // so that the text is right-side up to the camera. Vector3 direction = waypointDescriptionCanvas.transform.position - Camera.main.transform.position; waypointDescriptionCanvas.transform.forward = direction; // Make the CanvasGroup visible, and allow interactions // with its UI components. canvasGroup.alpha = 1; canvasGroup.interactable = true; } // Hide the CanvasGroup. public void HideDescription() { if (canvasGroup != null) { // Make the CanvasGroup invisible, // and do not allow interactions. canvasGroup.alpha = 0; canvasGroup.interactable = false; } else { Debug.Log ("canvasGroup is null"); } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 using System . Collections ; using System . Collections . Generic ; using UnityEngine ; using UnityEngine . UI ; /** * This class manages Waypoints' canvases. **/ public class WaypointController : MonoBehaviour { Canvas waypointDescriptionCanvas ; CanvasGroup canvasGroup ; // Called when this object is created. void Awake ( ) { waypointDescriptionCanvas = GetComponentInChildren < Canvas > ( ) ; if ( waypointDescriptionCanvas == null ) { Debug . Log ( "Could not get canvas." ) ; } else { // Get the canvas's CanvasGroup, and hide it. canvasGroup = waypointDescriptionCanvas . GetComponent < CanvasGroup > ( ) ; HideDescription ( ) ; } } // Make the CanvasGroup visible. public void ShowDescription ( ) { waypointDescriptionCanvas . transform . position = Camera . main . transform . position + new Vector3 ( 0f , - 0.5f , 3f ) ; //waypointDescriptionCanvas.transform.rotation = new Quaternion( 0.0f, Camera.main.transform.rotation.y, 0.0f, Camera.main.transform.rotation.w ); // Set the canvas's forward vector to face the camera, // so that the text is right-side up to the camera. Vector3 direction = waypointDescriptionCanvas . transform . position - Camera . main . transform . position ; waypointDescriptionCanvas . transform . forward = direction ; // Make the CanvasGroup visible, and allow interactions // with its UI components. canvasGroup . alpha = 1 ; canvasGroup . interactable = true ; } // Hide the CanvasGroup. public void HideDescription ( ) { if ( canvasGroup ! = null ) { // Make the CanvasGroup invisible, // and do not allow interactions. canvasGroup . alpha = 0 ; canvasGroup . interactable = false ; } else { Debug . Log ( "canvasGroup is null" ) ; } } }

In Awake() , which is called when the script is loaded, we assign the Canvas and CanvasGroup. The methods ShowDescription() and HideDescription() toggle the CanvasGroup’s visibility by changing its alpha value, or transparency. They also set the CanvasGroup’s interactable field, which determines whether the user can interact with the UI components in the Canvas Group. We would not want the player to be able to click an invisible button.

Add WaypointController.cs to the Waypoint object by dragging it from the project pane onto Waypoint in the hierarchy view.

Now set the text of the Waypoint in the scene to something more descriptive. Also change the name of the Waypoint object to something more identifiable.

Clickable Buttons

When the Player arrives at a Waypoint, PlayerController.isMoving is set to false to stop the Player, and the Waypoint’s canvas becomes visible. The Player needs to click a button to close the canvas and continue the tour. We’re going to add this button to the Waypoint’s canvas and make it an interactive VR item. We’re also going to add a collider to the button.

Right click Canvas in the hierarchy pane, and add an empty object named VRButton. This object will be the parent of a standard Unity button. Set VRButton’s with and height to 160 x 30.

Right click VRButton and select UI->Button to nest a button. Set the button’s width and height to 160×30, but set the scale factors to 0.01 to make it fit on the canvas.

In the Button’s inspector, scroll down until you see the “On Click()” section. This is where you define a function to be called when the button is clicked. Select “Runtime or Editor” from the first dropdown. In the dropdown below it, click the circle, and, in the popup window, select “Scene”. Find “Player”, and double click to select it. In the remaining dropdown, select PlayerController->ContinueTour() .

To make the button interactive, drag VRInteractiveItem.cs from Assets/VRStandardAssets/Scripts onto VRButton. In the inspector, click “Add Component”, and add a box collider. Click “Edit Collider” and resize the collider so that it encloses the button in the Scene view. We are adding these components to VRButton, not Button.

Finally, we will attach a script to VRButton that will connect VRInteractiveItem.OnClick with the button. Create a script named VRButtonController.cs, and drag it to the VRButton object.

public class VRButtonController : MonoBehaviour { VRInteractiveItem vrItem; Button button; void Awake () { // Get the VRInteractiveItem and the Button. vrItem = GetComponent<VRInteractiveItem> (); button = GetComponentInChildren<Button> (); } void OnEnable() { // Subscribe the button's onClick function to // vrItem's. vrItem.OnClick += button.onClick.Invoke; } void OnDisable() { vrItem.OnClick -= button.onClick.Invoke; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class VRButtonController : MonoBehaviour { VRInteractiveItem vrItem ; Button button ; void Awake ( ) { // Get the VRInteractiveItem and the Button. vrItem = GetComponent < VRInteractiveItem > ( ) ; button = GetComponentInChildren < Button > ( ) ; } void OnEnable ( ) { // Subscribe the button's onClick function to // vrItem's. vrItem . OnClick += button . onClick . Invoke ; } void OnDisable ( ) { vrItem . OnClick -= button . onClick . Invoke ; } }

Now, when the button is targeted by the reticle, and the Player clicks, the button’s onClick function ( PlayerController.ContinueTour() ) will be invoked.

Prefabs

We’ve created one Waypoint, but we need more Waypoints that have different locations and descriptions with the same functionality. We might also want want to reuse these Waypoints in other scenes. To accomplish this, we can turn Waypoint into a prefab.

In the project pane, create a new folder, Assets/Prefabs. Drag Waypoint from the hierarchy pane to Assets/Prefabs.

Now add more Waypoints to the scene by dragging Waypoint from Assets/Prefabs to the hierarchy pane. Use the scene view and the transform widget to position Waypoints. Remember to set the text description of each Waypoint. You can also change the names of your Waypoint instances to something more identifiable.

You will need to assign the onClick function of each Waypoint’s button separately. This information is not saved in the prefab.

As you add Waypoints to the scene, you will also need to update PlayerController’s waypoints array. With Player highlighted, set the size of the array in the inspector, and drag Waypoints into the fields in the order they should be visited.

While we’re on the subject of prefabs, let’s think about the other complex object in this scene: Player. When we create a new scene, we should not have to re-create the Player object all over again. In programming, don’t repeat yourself. Instead, drag the Player object into Assets/Prefabs to turn Player into a prefab.

But remember: You will need to repopulate the Waypoints array when you add Player to a new scene. Also, be careful when you set the button’s onClick function to select the Player game object, not the Player prefab.

Adding Scenes

Now that we have created prefabs, we can create additional scenes that will be connected by PlayerController.LoadNextScene() . When the Player has reached the last Waypoint in a scene and clicks the “Onwards!” button, PlayerController::LoadNextScene() is called to automatically load the next scene. If we are already at the last scene, the first scene is loaded.

void LoadNextScene() { Debug.Log ("Loading next scene"); // Get the current scene's index in the build order. int currentSceneIndex = SceneManager.GetActiveScene ().buildIndex; // If there are more scenes, load the next scene. if (currentSceneIndex < SceneManager.sceneCountInBuildSettings - 1) { SceneManager.LoadScene (currentSceneIndex + 1); } else { // Otherwise, load the first scene. SceneManager.LoadScene (0); } } 1 2 3 4 5 6 7 8 9 10 11 void LoadNextScene ( ) { Debug . Log ( "Loading next scene" ) ; // Get the current scene's index in the build order. int currentSceneIndex = SceneManager . GetActiveScene ( ) . buildIndex ; // If there are more scenes, load the next scene. if ( currentSceneIndex < SceneManager . sceneCountInBuildSettings - 1 ) { SceneManager . LoadScene ( currentSceneIndex + 1 ) ; } else { // Otherwise, load the first scene. SceneManager . LoadScene ( 0 ) ; } }

SceneManager is a class in the Unity.SceneManagement namespace. The current scene is returned by SceneManager.getActiveScene() . buildIndex identifies the scene by the order in which the scenes were built. SceneManager.sceneCountInBuildSettings is the total number of scenes. SceneManager.LoadScene() loads a scene by name or by index.

After saving the current scene, create a new scene by clicking on File->New Scene. Drag Arctic from Assets/Models to the scene, or import your own model. Drag Player from Assets/Prefabs to the scene, and position it where you want the Player to start the tour. Drag Waypoint from Assets/Prefabs as many times as you want, and customize each Waypoint’s name and description. Then drag the Waypoint objects into the Player’s waypoints array in the inspector in the order you want them to be visited.

Building the Project

Once you’re ready to try out the game, select File->Build Settings from the menu. Drag all the scenes you want to include from the project pane to the “Scenes in Build” window in Build Settings. Select Android as the platform.

Click the “Player Settings” button to load settings in the Inspector pane. Enter a company name and product name. Scroll down to the Rendering section, and make sure “Virtual Reality Supported” is checked. Click “+” under “Virtual Reality SDKs” and add Oculus.

In the “Identification” section, give the game a package name. Typically, package names are reversed domain names, such as org.gamedevacademy.gearvr.vrtour. You are not restricted to existing domain names. Chose a minimum API level of Android 6.0 “Marshmallow”, which is the lowest API level that supports Gear VR.

When you’re ready to test your game, click the “Build and Run” button in the File->”Build and Run” window to build an APK and run it on a connected phone.

Enjoy the tour!