Introduction
Usually, when we make a menu in Unity, we would just have it show up on our screen. However, in VR, there’s no “screen”, we see everything in the first person and it would be uncomfortable to have the menu stay in front of our face until we close it.
We could leave the menu at where we created it, but let’s say it’s an important menu that we want the user to focus on until they close it.
What are we going to do to solve this problem?
We’ll create a menu that appears in front of the player like normal, however, to make this more “VR friendly” is that we’ll allow the player to look around the screen, however, the moment the menu leaves the screen, we’ll start moving the screen to be in front of the player.
How are we going to implement this?
A lot of the required code is something we already know, like moving a game
object to be in front of our player.
The only part we haven’t learned how to do is to detect if a UI element is visible. We could easily check if a game
object is visible to a camera with a renderer component, unfortunately for a UI element, we don’t have a renderer component.
Instead, I found a great solution provided for us by KGC in this thread: Test if UI element is visible on the screen. Thanks for the awesome solution!
Now that we know what we want to build and have some idea on how to build it, let’s get started!
Creating the Following Menu
Now at this point, we have an idea of what we want to make. Let’s go make it. To do that, we’re going to make a new script that will help us deal with the state.
We’re going to call this class ViewHelper
.
In MenuContainer game
object, we’re going to create a new script. We’re going to call it ViewHelper
.
In ViewHelper
, we’re going to do some very similar state management that we did with MenuController
, but it’s not going to be that complicated.
Here’s what ViewHelper
looks like:
using System.Collections;
using UnityEngine;
public class ViewHelper : MonoBehaviour
{
private RectTransform _canvasRectTransform;
private Camera _mainCamera;
private float _yGap;
enum State { InScreen, NotInScreen, Moving };
private State _currentState;
void Start ()
{
_canvasRectTransform = GetComponentInChildren<RectTransform>();
_mainCamera = Camera.main;
_currentState = State.InScreen;
_yGap = 2f;
}
void Update ()
{
switch (_currentState)
{
case State.InScreen:
if (!IsFullyVisibleFrom(_canvasRectTransform, _mainCamera))
{
_currentState = State.NotInScreen;
}
break;
case State.NotInScreen:
_currentState = State.Moving;
StartCoroutine(MoveToFrontOfPlayer());
break;
}
}
private Vector3 GetCameraFoward()
{
Vector3 forward = _mainCamera.transform.forward * 3;
forward.y += _yGap;
return forward;
}
private IEnumerator MoveToFrontOfPlayer()
{
while (transform.position != GetCameraFoward())
{
transform.eulerAngles = _mainCamera.transform.eulerAngles;
float speed = 4f * Time.deltaTime;
transform.position = Vector3.MoveTowards
(transform.position, GetCameraFoward(), speed);
yield return null;
}
_currentState = State.InScreen;
}
private int CountCornersVisibleFrom(RectTransform rectTransform, Camera camera)
{
Rect screenBounds = new Rect(0f, 0f, Screen.width, Screen.height);
Vector3[] objectCorners = new Vector3[4];
rectTransform.GetWorldCorners(objectCorners);
int visibleCorners = 0;
for (var i = 0; i < objectCorners.Length; i++)
{
Vector3 tempScreenSpaceCorner =
camera.WorldToScreenPoint(objectCorners[i]);
if (screenBounds.Contains(tempScreenSpaceCorner))
{
visibleCorners++;
}
else
{
return -1;
}
}
return visibleCorners;
}
private bool IsFullyVisibleFrom(RectTransform rectTransform, Camera camera)
{
return CountCornersVisibleFrom(rectTransform, camera) == 4;
}
}
Looking at the Variables
Here’s what we’re going to be working with:
private RectTransform _canvasRectTransform
– The RectTransform
of our menu which we will use to see if it’s on the screen or not. private Camera _mainCamera
– Shortcut to refer to our main camera. private float _yGap
– A value that I used to keep the center of the menu to be at the front of our gaze. Specifically, wherever we’re looking at + the y value the menu was at when we first started. private State _currentState
– The state that we are in. There are 3 states our menu can be in: on the screen, not in the screen, and then moving back to the center of the screen.
Walking Through the Code
Now that we have a sneak peek of what we’re doing with our fields, let’s look and see how the code works.
- Once again, we initialize everything in
Start()
. - In
Update()
, we’re once again running different lines of code based off of what state we’re currently in. If we’re currently in the InScreen
state, we’ll have to constantly check if we’re still on the screen by calling IsFullyVisibleFrom()
which will count the corners via CountCornersVisibleFrom()
. The moment the screen isn’t fully visible, we would switch to the NotInScreen
state. - You can read more about these 2 functions from where it was shared, but essentially, all we’re doing is checking if the corners of our menu are on the player’s screen. If they are, then we return
true
, if not, we return false
. - In
Update()
, when we’re in the NetScreen
state, we switch to the Moving
state and start a coroutine MoveToFrontOfPlayer()
to move our menu to the front of the player. What’s interesting to note here is that I chose to use a coroutine, but we could have just as easily moved everything into our switch
statement in Update()
and we could have done almost the same thing. - In
MoveToFrontOfPlayer()
, we want to move our menu to be in front of the player, specifically our camera’s forward. To achieve this, we check its current position relative to our goal location which we create from GetCameraFoward()
. If we’re not at the position, we would always set the rotation of the menu to be the same as the camera
so it doesn’t look weird and use Vector3.MoveTowards
to give us a position between our current position and our goal position. Then we yield and wait until the next frame. Once the menu goes to the front of the player, we change back to the InScreen
state and go back to step 2. - In
GetCameraFoward()
, we set the location of where we want the menu to go to. Specifically, right in front of us. Luckily, this time we don’t have to do any trigonometry to get that specific location, we just need to get our camera
’s forward
and move it further out. We also need to adjust the y
position to make sure that our menu also maintains the same y
position we started at.
And that’s it for getting our camera to follow our head in VR!
Surprisingly, it’s not as difficult as we might have initially thought.
Here’s what it looks like:
We could have gradually rotated our menu if we wanted to, but I’ll leave that as something you can implement if you’re curious about how to do it.
Conclusion
With this finished, we have pretty much-implemented everything we wanted to regarding the menu.
However, before we finish, there are 2 important things that were left out:
- How to create a menu and…
- How to get rid of the menu!
In the next post, let’s look at how we can instantiate the menu and get rid of it programmatically!