SteamVR Overlay with Unity: Create Overlay
Tags: unity3d, steamvr, openvr, vr
Published: 2024-06-16 18:45:43

## Key and Name

Overlay needs two strings `key` and `name`.
`key` is a unique string among all overlays.
`name` is just an overlay name sometimes shown to users.
We use `"WatchOverlayKey"` and `"WatchOverlay"`.

```diff
void Start()
{
    InitOpenVR();

+   var key = "WatchOverlayKey";
+   var name = "WatchOverlay";
}
```

## Overlay handle

Create a variable to save an overlay handle.
Similar to controlling a file with a file handle, overlays are controlled with an overlay handle.

```diff
void Start()
{
    InitOpenVR();

    var key = "WatchOverlayKey";
    var name = "WatchOverlay";
+   var overlayHandle = OpenVR.k_ulOverlayHandleInvalid;
}
```

[OpenVR.k_ulOverlayHandleInvalid](https://valvesoftware.github.io/steamvr_unity_plugin/api/Valve.VR.OpenVR.html?q=k_ulOverlayHandleInvalid#Valve_VR_OpenVR_k_ulOverlayHandleInvalid) means the overlay is not created. Overlay handle is `ulong` type.

## Create an overlay and get a handle

Use [CreateOverlay()](https://valvesoftware.github.io/steamvr_unity_plugin/api/Valve.VR.CVROverlay.html#Valve_VR_CVROverlay_CreateOverlay_System_String_System_String_System_UInt64__) to create an overlay. (read the [wiki](https://github.com/ValveSoftware/openvr/wiki/IVROverlay::CreateOverlay) for details)
Pass the `key` and `name` and reference of `overlayHandle` to `CreateOverlay()`.

```diff
void Start()
{
    InitOpenVR();

    var key = "WatchOverlayKey";
    var name = "WatchOverlay";
    var overlayHandle = OpenVR.k_ulOverlayHandleInvalid;
+   var error = OpenVR.Overlay.CreateOverlay(key, name, ref overlayHandle);
}
```

`CreateOverlay()` sets overlay handle to the variable `overlayHandle`.
The return value is an overlay creation error. All overlay errors are defined in [EVROverlayError](https://valvesoftware.github.io/steamvr_unity_plugin/api/Valve.VR.EVROverlayError.html).

## Error handling

Add error handling for overlay creation.

```diff
void Start()
{
    InitOpenVR();

    var key = "WatchOverlayKey";
    var name = "WatchOverlay";
    var overlayHandle = OpenVR.k_ulOverlayHandleInvalid;
    var error = OpenVR.Overlay.CreateOverlay(key, name, ref overlayHandle);
+   if (error != EVROverlayError.None)
+   {
+       throw new Exception("Failed to create overlay: " + error);
+   }
}
```

If there is no error, `EVROverlayError.None` is back.

## Cleanup overlay

Add code to dispose of the overlay at the end of the application.

### Move overlay handle

Move `overlayHandle` from `Start()` to the class member variable.

```diff
public class WatchOverlay : MonoBehaviour
{
+   private ulong overlayHandle = OpenVR.k_ulOverlayHandleInvalid;

    private void Start()
    {
        InitOpenVR();

        var key = "WatchOverlayKey";
        var name = "WatchOverlay";
-       var overlayHandle = OpenVR.k_ulOverlayHandleInvalid;
        var error = OpenVR.Overlay.CreateOverlay(key, name, ref overlayHandle);
        if (error != EVROverlayError.None)
        {
            throw new Exception("Failed to create overlay: " + error);
        }
    }

    ...
}
```

### Dispose of overlay

The function for overlay cleanup is [DestroyOverlay()](https://valvesoftware.github.io/steamvr_unity_plugin/api/Valve.VR.CVROverlay.html#Valve_VR_CVROverlay_DestroyOverlay_System_UInt64_). (read the [wiki](https://github.com/ValveSoftware/openvr/wiki/IVROverlay::DestroyOverlay) for details)
Add `OnApplicationQut()` with overlay cleanup code.

```diff
public class WatchOverlay : MonoBehaviour
{
    private ulong overlayHandle = OpenVR.k_ulOverlayHandleInvalid;

    private void Start()
    {
        InitOpenVR();

        var key = "WatchOverlayKey";
        var name = "WatchOverlay";
        var error = OpenVR.Overlay.CreateOverlay(key, name, ref overlayHandle);
        if (error != EVROverlayError.None)
        {
            throw new Exception("Failed to create overlay: " + error);
        }
    }

+   private void OnApplicationQuit()
+   {
+       if (overlayHandle != OpenVR.k_ulOverlayHandleInvalid)
+       {
+           var error = OpenVR.Overlay.DestroyOverlay(overlayHandle);
+           if (error != EVROverlayError.None)
+           {
+               throw new Exception("Failed to dispose overlay: " + error);
+           }
+       }
+   }

    private void OnDestroy()
    {
        ShutdownOpenVR();
    }

    ...
}
```

`DestroyOverlay()` must be called before `Shutdown()` so we put the overlay cleanup code inside `OnApplicationQuit()` that is called before `OnDestroy()`.

### Check created overlay

Check whether the overlay is created.
**Overlay Viewer** included in the SteamVR is useful to check overlays.

Select **Developer > Overlay Viewer** in the SteamVR menu.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k2io4pakb2p8xv20rz3v.png)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2wgo2cw0qu4w3og9svsd.png)

Overlay Viewer displays a list of created overlays. You can see many overlays already created by SteamVR system.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zlex2o6ksvegf7wnrcsk.png)

In this state, run the program from Unity.

Then the overlay key `WatchOverlayKey` is added to the list. You can see the overlay details by clicking the key.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/358mupcnh97frl3wtwgm.png)

The overlay preview is shown on the right side gray area but now we draw nothing to the overlay yet.

Here, we confirmed the overlay is created. Stop the program and close Overlay Viewer.

---

### Optional: Overlay Viewer location

Overlay Viewer is located in the SteamVR install directory.
`C:/ProgramFiles(x86)/Steam/steamapps/common/SteamVR/bin/win32/overlay_viewer.exe`
We use this tool frequently during development so I recommend creating short cut.

---

## Organize code

### Create overlay
Move the overlay creation code `intoCreateOverlay()`. It gets `key` and `name` as arguments, and return created overlay handle. Please note that some variable names are changed in code organization.

```diff
public class WatchOverlay : MonoBehaviour
{
    private ulong overlayHandle = OpenVR.k_ulOverlayHandleInvalid;

    private void Start()
    {
        InitOpenVR();

-       var key = "WatchOverlayKey";
-       var name = "WatchOverlay";
-       var error = OpenVR.Overlay.CreateOverlay(key, name, ref overlayHandle);
-       if (error != EVROverlayError.None)
-       {
-           throw new Exception("Failed to create overlay: " + error);
-       }
+       overlayHandle = CreateOverlay("WatchOverlayKey", "WatchOverlay");
    }

    ...

+   private ulong CreateOverlay(string key, string name) {
+       // Some code changed here.
+       var handle = OpenVR.k_ulOverlayHandleInvalid;
+       var error = OpenVR.Overlay.CreateOverlay(key, name, ref handle);
+       if (error != EVROverlayError.None)
+       {
+           throw new Exception("Failed to create overlay: " + error);
+       }
+       return handle;
+   }
}
```

### Cleanup overlay

Similarly, move the overlay cleanup code into `DestroyOverlay()`.

```diff
public class WatchOverlay : MonoBehaviour
{
    ...

    private void OnApplicationQuit()
    {
-       if (overlayHandle != OpenVR.k_ulOverlayHandleInvalid)
-       {
-           var error = OpenVR.Overlay.DestroyOverlay(overlayHandle);
-           if (error != EVROverlayError.None)
-           {
-               throw new Exception("Failed to dispose overlay: " + error);
-           }
-       }
+       DestroyOverlay(overlayHandle);
    }

    ...

+   // overlayHandle -> handle variable name changed.
+   private void DestroyOverlay(ulong handle)
+   {
+       if (handle != OpenVR.k_ulOverlayHandleInvalid)
+       {
+           var error = OpenVR.Overlay.DestroyOverlay(handle);
+           if (error != EVROverlayError.None)
+           {
+               throw new Exception("Failed to dispose overlay: " + error);
+           }
+       }
+   }
}
```

## Final code

```cs
using UnityEngine;
using Valve.VR;
using System;

public class WatchOverlay : MonoBehaviour
{
    private ulong overlayHandle = OpenVR.k_ulOverlayHandleInvalid;

    private void Start()
    {
        InitOpenVR();
        overlayHandle = CreateOverlay("WatchOverlayKey", "WatchOverlay");
    }
    
    private void OnApplicationQuit()
    {
        DestroyOverlay(overlayHandle);
    }

    private void OnDestroy()
    {
        ShutdownOpenVR();
    }

    private void InitOpenVR()
    {
        if (OpenVR.System != null) return;

        var error = EVRInitError.None;
        OpenVR.Init(ref error, EVRApplicationType.VRApplication_Overlay);
        if (error != EVRInitError.None)
        {
            throw new Exception("Failed to initialize OpenVR: " + error);
        }
    }

    private void ShutdownOpenVR()
    {
        if (OpenVR.System != null)
        {
            OpenVR.Shutdown();
        }
    }

    private ulong CreateOverlay(string key, string name)
    {
        var handle = OpenVR.k_ulOverlayHandleInvalid;
        var error = OpenVR.Overlay.CreateOverlay(key, name, ref handle);
        if (error != EVROverlayError.None)
        {
            throw new Exception("Failed to create overlay: " + error);
        }
        return handle;
    }

    private void DestroyOverlay(ulong handle)
    {
        if (handle != OpenVR.k_ulOverlayHandleInvalid)
        {
            var error = OpenVR.Overlay.DestroyOverlay(handle);
            if (error != EVROverlayError.None)
            {
                throw new Exception("Failed to dispose overlay.: " + error);
            }
        }
    }
}
```

With that, we have created and cleaned up an overlay. Next part, we will draw an image file to the overlay.