Making a Unity Game for Google Cardboard

Google have released a Unity plugin for Google Cardboard (Rocket Drop VR already supports it and is a featured app). The plugin has its own strengths and weaknesses, and I’ll do a blog post about how to use it. I’m leaving the original post below in case anyone finds it useful.

Get the Unity plugin here: https://developers.google.com/cardboard/unity/download

ORIGINAL POST:

Last week, a friend of mine brought a Google Cardboard kit home with him from Siggraph, and challenged me to make something cool with it. In response, I wrote a VR version of Rocket Drop. Apparently, both press and consumers are proclaiming it as Google Cardboard’s killer-app, but I digress!

Anyway, while I was writing Rocket Drop VR, I found that there is very little information online about how to actually support Google Cardboard, especially for Unity developers. Most of the forum advice so far is along the lines of “Durovis Dive, something about raw vectors, best of luck!”. So I’m writing everything I know here. As with the rest of this blog, everything presented here is anecdotal, and I reserve the right to revisit this topic if I learn anything new!

For those who don’t have a Google Cardboard kit, you can buy one here for $20, or follow this tutorial to build your own.

Beyond that, the only barrier to entry is an Android phone with a built-in gyroscope and compass. Any phone from the last three years should qualify. I’ve been told that iPhones also work with it, but I haven’t tested this myself.

Development notes:

Because the screen is cut in half to achieve the 3D effect, expect to author your game to a target resolution of 640×720. This can be surprisingly limiting in terms of scale and HUD elements.

The best thing about Google Cardboard is that it’s wireless, which opens the scope for some interesting gameplay. For example, one of the key features of Rocket Drop VR is that you can look around in any direction, necessitating the ability to physically turn on the spot in real life. Doing this on a Morpheus or an Oculus Rift would cause the player to be entangled in wires.

Unlike the Oculus Rift, GUI textures are actually readable from within Google Cardboard! But when designing your GUI elements, expect to lose the top third of your screen to peripheral vision. HUD elements seem to work best at either the centre or the bottom of the screen.

The only method of input that Google Cardboard apps can support is a button on the side of the device (unless you use a bluetooth controller or something). The button is simply a magnet, and the phone reads the button input by detecting the magnet using its compass. Not all phones have the compass in the same place. For example, the Google Cardboard kit advises that the phone should be placed with the camera to the left of the device. With the Samsung Galaxy S3, the phone needs to be placed with the camera to the right of the device. Basically, some phones need to be upside-down. I am unaware of any way of testing for this within the code. Your best bet is to support both left and right screen orientations in your app.

Non-interactive 3D – For menu screens etc:
Since Google Cardboard doesn’t require any distortion or image effects, it’s very easy to get the 3D effect without using any plug-ins. Here’s how:

Begin by creating two perspective cameras. One for the left eye, and one for the right.

Set both cameras with a “field of view” of 80.

Set the “viewport rect” of the left camera to (0, 0, 0.5, 1).

Set the “viewport rect” of the right camera to (0.5, 0, 0.5, 1).

Set the “X position” value of each camera so that they are equidistant from each other (e.g. -0.2 for the left camera, 0.2 for the right). In this case, it’s worth experimenting with the distances. Having the cameras further apart creates a more exaggerated sense of depth at the expense of pixel clarity. As a point of reference, Rocket Drop VR uses distance values of -1 and 1.

Head tracking for interactive 3D:
There are two ways of doing this. You could use the accelerometer, which would allow you to rotate the camera based on the rotation of the phone. This isn’t true VR, because it rotates the camera based on the speed of the acceleration, rather than on an absolute orientation of the player’s head. But it’s good if you want to make a game where the player can look behind themselves without physically having to turn their neck all the way around.

The other way is to use the gyroscope. This way is for true VR, in that the orientation of the camera in the game world is the exact orientation of the player’s head in real life. Unity’s in-built support for the gyro won’t be enough to support this, so you will need to download the Durovis Dive plugin for Unity and incorporate it into your project.
If you use the Dive plugin, and want to support variable screen orientations, you need to add the following to line 119 of “OpenDiveSensor.cs”:

The magnet button:
Getting the button on Google Cardboard to work in Unity is a scary, arcane pseudo-science. From a coder’s perspective, you can see the raw vector of your compass change in varying ways when you press the button, but your gyroscope can affect the vector in the same way, making it almost impossible to read reliably.

At the time of writing, it appears that no one has an elegant solution for this yet. A lot of people have techniques for detecting when the button has been “clicked”, but cannot detect when it is held down, plus any head tracking becomes an added variable. There is an ongoing thread in the Unity forum about supporting the button, and a few people have begun to write generic systems for reading the button input, but nothing concrete exists at the time of writing.

From my perspective, Rocket Drop VR was a perfect storm of really hard-to-support requirements! It is a game where the player must be able to hold down the button for an arbitrary length of time, while moving his/her head in any direction, with the expectation that the game can detect when the player is and is not holding the button. As a result, I needed my own method for supporting the button input. So I wrote an incredibly filthy technique that seems to be about 90% reliable. This technique supports both “clicking” the button, and holding the button down for prolonged periods.

I use the magnitude of the raw vector to determine when the button has been pressed. When the button is pressed, the magnitude will increase by roughly 2 to 5 times, depending on the strength/proximity of the magnet to the compass. Whereas fluctuations in the gyro will cause the magnitude to change by more than 23 times. So the way that I solve the problem is by having two scripts.

The first script remembers a baseline magnitude of the raw vector when the magnet is not pressed (in my code, it’s called the “initialrv”). If the magnitude changes massively, then the first script refreshes the initialrv to an up-to-date value. This prevents sudden changes in the magnitude readings from the gyro.

The second script reads the current magnitude of the raw vector, and tests it against the initialrv. If the current magnitude is more than double the initial magnitude, then the button is being held down.

// Catch if the initialrv is completely wrong // (sometimes the magnitude can jump from // 15 to 400, depending on how the player // rotates the device). //
if (prevrv > initialrv * 23)
{
initialrv = Mathf.RoundToInt(Input.compass.rawVector.magnitude);
}

// Refresh prevrv to test against at the start of the next update //
prevrv = Mathf.RoundToInt(Input.compass.rawVector.magnitude);
}
* “GameLogic” script *

// assuming the initialrv from the global script is roughly // consistent with the current magnitude before a button // is pressed, we can test against it. If the magnitude raises // two-fold, it indicates that the magnetic button has been // pressed. If the magnitude raises by 23 times or more, it // indicates a false-positive, at which point the initialrv is reset. //
var vectest = Mathf.RoundToInt(Input.compass.rawVector.magnitude);
if (vectest > Globals.initialrv * 2)
{
// Magnet button down
}