Lab 4: Escape Room
For today's lab, you will build a simple escape room to practice making VR interactions in Unity. It should be pretty simple, the user will need to dig through a pile of boxes to find a key, unlock a chest to find a code, then enter the code in a panel to open the escape door.
Part 1: Getting Started
First, download the starter project here: EscapeRoom_Start.zip
Unzip it and open it in Unity Hub. If asked to select a version of Unity, select a version that is 2021.3.29f1 or above. Follow the prompts to "migrate" the project to a newer version if needed.
Important: Go to Edit > Project Settings...
and then Player > Company Name
and replace this with your EID or a similar short name. This will allow multiple people to build on the same headset without clashing with each others' apk.
Open the EscapeRoom
scene (it should be in the Scenes
folder under Assets in your Project view). I've provided you with a simple room and objects to start with. However, they don't do anything yet! The user can't interact with anything. It's your goal to get the interactions in place.
Making the Controllers Look Like Hands
So one thing we want to set up before we get started is we want our controller models to look like hands. This is much more immersive and helps the user understand that they are supposed to grab and poke things to interact.
Unity XR doesn't come with hand models, so you would technically need to make your own as well as animate them. This is difficult and takes time to create the individual animations, so I've created1 a package with prefabs that you can use whenever you want hands in your VR apps.
Download the package here: ControllerHands.unitypackage
Import it into your project by going to your Project View, right-clicking Assets, then going to Import Package > Custom Package...
. Select the package you just downloaded.
This will import the "Oculus Hands" folder into your Assets. Inside, you should see a Prefabs folder. Open it. There are prefabs for the left and right hands. These have been pre-configured with animations and a (complex) script that animates them based on how much the grip and trigger buttons are pressed. Feel free to explore how the hand animations were done in your free time (also see Fist Full of Shrimp's Tutorial), if you're curious!
In the Hierarchy, navigate to the XR Interaction Setup
→ XR Origin (XR Rig)
→ Camera Offset
object. Let's replace the controller models with these hands. Drag the LeftHand
prefab onto the Left Controller
object in the Hierarchy, and the RightHand
prefab onto the Right Controller
object.
Click on the Left Controller
. In the Inspector, go to the bottom of the XR Controller (Action-based)
component. Click on the Model Prefab
and press delete on your keyboard to remove it.
You should see a property underneath Model Parent
called Model
. Now, expand the Left Controller
object in the Hierarchy. You should see a LeftHand
child object. Drag and drop this object onto the selector box next to the Model
property. This will set the controller's model to the left hand.
One more thing! The Poke Interactor
for the left controller needs to be set to the hand model's fingertip. Select the Poke Interactor
in the Hierarchy. In the XR Poke Interactor
component, click on the Attach Tranform
and delete it. Now, in your Hierarchy, expand the left hand until you find the TouchPoint
object, which is on the index finger:
Drag and drop this TouchPoint
object onto the selector box for Attach Transform
. Now when you poke things, it should correspond to the hand's index finger!
Repeat these steps for the right hand for both the controller model and the poke interactor.
Part 2: Make the Boxes Grabbable
Now that we have the scene set up, let's start by adding some simple grab interactions.
There is a key hidden underneath all the boxes. Let's make it so that the user can move the boxes out of the way. Click on one of the boxes. Add an XR Grab Interactable
component to it. This will make it so that the controllers are able to grab the box. Let's have the boxes interact with physics, so set the Movement Type
to Velocity Tracking
.
Also scroll down and check the Use Dynamic Attach
box. This makes it so that you can grab the boxes from the edges like in real life.
Adding a Color Affordance
We also want the user to be able to know if they can interact with the boxes (aka an affordance), so let's also add a simple color affordance to them.
You need to add two components to the box: a XR Interactable Affordance State Provider
and a Color Material Property Affordance Receiver
. Once you have both of these, go to the Event Constraints
and check all but the Ignore Hover Events
box. We only want the box to change colors when the user hovers over it.
Now, go down and check the Replace Idle State With Initial Value
box. Also click on the circle in the selector box for Affordance Theme Datum
. Pick the default InteractionColorAffordanceTheme
. If you want the box to take on a different color when you hover over it, you can change this theme (or create a new one).
Repeat this process for the other boxes. You can also select all the remaining boxes in the Hierarchy (use shift-click) and edit them simultaneously! Alternatively, you can try making that first box a prefab and then replace the other boxes by deleting them and adding the prefab instead (make sure to stack the boxes up in the corner).
Try it out in the simulator and make sure you can move boxes and that the affordances are working. Remember to use Tab to toggle between the controllers. You can use the G key to simulate the grip button.
Part 3: Socket Interactions with the Key
Remember, there's a key hidden in the corner underneath all the boxes. Find the key in the Hierarchy and select it. First, make the key grabbable by adding a XR Grab Interactable
component to it. In the component, set the Attach Transform
to the GrabPoint
child of the key. This makes it so that we hold the key by the handle.
Now, we need to make it so that we can insert the key into the chest's lock. We can do this by using a socket interactor, which lets us attach grabbable objects to the socket.
Find the Chest
object in the Hierarchy and expand it. Click on the Keyhole
object. This object has a box collider positioned exactly so that the key can fit in the lock. Also note how the collider is set to Is Trigger
. This is required for socket interactors.
Add an XR Socket Interactor
component to the keyhole. Now we're set up! This will let any interactable attach to the socket. But obviously, we don't want that! Only the key should be able to interact with this socket.
Thankfully, we have interaction layers, remember? Objects in a specific layer can only interact with other objects on that layer. In the XR Socket Interactor
, click on the drop-down for Interaction Layer Mask
. Create a new layer called "Keys". Now, make sure only the "Keys" layer is selected for the socket interactor.
Go to the key object and set its grab interactable to be on only the "Default" and "Keys" layers. This will enable it to be grabbed and inserted into the socket.
Now only the key (or technically any object on the "Keys" layer) can be put in the socket.
But how do we open the chest when the key is inserted? Take a look at the Lid
object. There is a Rotate Animation
script that will animate the lid of the chest opening. It also displays the panel's secret code on the lid of the chest.
Select the KeyHole
object again. Under the XR Socket Interactor
, you should see the Interactor Events. Here, under Select Entered
, click the plus to add a new action to occur (these are technically called "callbacks"). This callback will occur when an object is inserted into the lock. Drag and drop the Lid
object into the little selection box here. In the drop-down menu on the right, select RotateAnimation > PlayForward ()
. This will call the method and open the lid.
Now, when you put the key in the lock, the lid of the box should open! Inside are three numbers that represent the code for the door. Try it out in the simulator by grabbing the key and carefully walking over to the box.
Part 4: Pushing Buttons on the Panel
There is a panel on the wall next to the door. Let's take a look at it! On the Panel
GameObject, there is also a Panel Controller
component that basically lets you input text by calling a specific method (you can open the script and look at it, if desired).
There are four buttons, which are configured as children of the Panel
object. Look at the One_Button
object. I've set this one up completely to work with a poke interaction. In order to make a poke interaction work, you need a few components:
- An
XR Simple Interactable
that defines all the interactions and events for the object. Note: This is set to the "Pokeable" interaction layer. - An
XR Poke Filter
that determines if the user is poking the object from the correct direction. The Interactable and Collider are obtained automatically. - An
XR Poke Follow Affordance
that makes the object move when it is poked, like how a button should depress. Note: ThePoke Follow Transform
is set to theVisuals
child GameObject.
Also expand the XR Simple Interactable
's events and scroll down to the Select event. A select event is fired whenever the user pokes the button. Look at the callback specified underneath Select Entered
. It's calling the PanelController
's EnterText()
method and giving it an argument of 1, which types a "1" on the panel.
Use One_Button
as an example, and add these three components to the remaining three buttons. You can copy-and-paste components by clicking on the three dots to the right of a component. Make sure you set the Poke Follow Transform
to each button's child Visuals
object. Also configure each button's select event so that it enters the corresponding number to the panel.
Adding Sounds to the Buttons
After finishing setting up each button's interactions, let's make it so that pushing each button makes a click sound to give the user feedback. Select all four buttons in the Hierarchy.
Now, add two components, an XR Interactable Affordance State Provider
and a Audio Affordance Receiver
to the buttons. You should see an Audio Source
created automatically with the affordance. In the Audio Source
, under Spatial Blend
, drag the slider all the way to the right (towards "3D").
Now, in the Audio Affordance Receiver
, click on the three dots next to Affordance Theme Datum
. Select Use Value
. You should see a list with seven items appear. You can use these to configure what sounds to play for which interaction events occur.
Expand the selected
item in the list. Click the circle in the selection box for State Entered
. Select the click
audio clip. This will play the clip when the user presses each button.
Last, you need to configure the correct affordance state provider with each affordance receiver. Select the One_Button
object only. Now, scroll down in the inspector until you see the Audio Affordance Receiver
and the Affordance State Provider
. Drag-and-drop the One_Button
object from the Hierarchy into the selector box next to Affordance State Provider
.
Repeat this step for each of the buttons, dragging each corresponding button onto its Affordance State Provider
property.
Part 5: Opening the Door
Take a look at the door GameObject. You should see how it has a Rigidbody and a Hinge Joint set. These have been preconfigured for you so that the door swings open properly. Right now, the RigidBody has the Is Kinematic
setting enabled. This prevents it from being moved by other objects (consider the door locked).
With the door selected, go to the Inspector. Add an XR Grab Interactable
to the door. However, this will make the entire door grabbable. We want to specify that only the door's handle can used to grab and open the door.
In the grab interactable, expand Colliders
and click on the plus sign to add a new element to the list. Expand the door GameObject in the Hierarchy, and you should see a Handle
child. Drag this Handle
to the selector box next to "Element 0" under Colliders.
Also set Movement Type
to Velocity Tracking
. This makes the door swing open and obey physics. Also check Use Dynamic Attach
. This lets us interact with the door naturally, just like with the boxes we used earlier. After configuring these, the door is ready to go!
But we don't want it to be interactable just yet — this should only happen after the user enters the correct code on the panel. Go ahead and disable the XR Grab Interactable by clicking the checkbox at the top of the component.
In general, if you just want to make a door that swings open, add a Rigidbody, a Hinge Joint, and a Grab Interactable. You can follow this example for your own projects, if you wish!
Unlocking the Door
Now, let's configure the panel to unlock the door when the code is entered. Select the Panel
object. Add a new script component to the object called UnlockWithCode
. Once the component is added, double-click to open it in your code editor. The goal of this script is to check if the Panel Controller's text matches the unlock code, and if so, enable the door's interactions.
First, add a new using statement to import event support:
using UnityEngine.Events;
Then, add three public and one private instance variable in the class:
public PanelController panelController;
public string unlockCode = "";
public UnityEvent onUnlock;
private bool locked = true;
The panelController
is a reference to the Panel Controller component that has the user's input, the unlockCode
is the code we want to look for, and the onUnlock
contains all the actions we want to run when the user inputs the correct code.
In the Update()
method, enter the following lines of code:
if (locked && panelController.GetText().Equals(unlockCode))
{
onUnlock.Invoke();
locked = false; // Only invoke once
}
This will check if the text matches the code, and if so, it will invoke (aka call) all the actions assigned to the onUnlock
event.
Go back to the Unity editor. You should see the UnlockWithCode
component update to show the three new attributes. For Panel Controller, drag-and-drop the Panel Controller component into the selector box. For Unlock Code, enter 314
. You should also see that onUnlock is now a nifty On Unlock ()
event handler, just like with the other components you've used before!
For On Unlock ()
, click the plus two times. This will add two callbacks to be run when the event happens. Add the Door
GameObject to the first two items. In the first action, click the drop-down menu and select Rigidbody
→ bool isKinematic
. Make sure that the checkbox is unchecked. For the second action, click the drop-down and select XRGrabInteractable
→ bool enabled
. Make sure that this checkbox is checked.
Now, when the user types in the correct code, the door will unlock and the handle will become interactable.
Add Unlock Sound
One last thing: let's play a sound so that the user knows that the door has been unlocked. In the Project View, under Assets > Sounds
, you should see a "unlock" audio clip. Go ahead and drag-and-drop this clip onto the Panel
object. Uncheck Play on Awake
and drag the Spatial Blend
slider all the way to the right (to 3D).
Now, in the Unlock With Code
component, add a third callback. Drag the newly created Audio Source
to the callback, and select AudioSource > Play ()
in the drop-down on the right.
That's it! I recommend testing out the escape room on the headset; it's much better than using the simulator.
Submission
Submit your UnlockWithCode.cs
file to Canvas for lab credit.
-
Based on the tutorial by Fist Full of Shrimp -- extended to include more advanced animations and gesturing. Hand models by Oculus/Meta. ↩