Skip to content

For developers

Write the prefab. The tool writes the rest.

GVR does not want your team building courseware. It wants two things from you: interactive prefabs that behave correctly, and an honest description of what they can do. Everything a training team can build is generated from that description.

In Unity

Attributes, not documentation

Two attributes are the whole authoring API. One marks something the prefab can announce; the other marks something the prefab can be asked to do.

The export pass reflects over them and writes the manifest, so the dropdowns an author sees can never drift from the code. If you delete an event, the next export removes it — and the portal flags every scenario that was binding to it rather than silently publishing a build that does nothing.

FireDoor.cscsharp
using GVR.Interaction;

[GvrPrefab("fire-door-type-b")]
public class FireDoor : GvrInteractable
{
  // Becomes an entry in "events" — an author can hang logic off it.
  [GvrEvent("onDoorOpen", "Door opened")]
  public GvrSignal DoorOpened;

  // Becomes an entry in "actions" — a binding step can call it.
  [GvrAction("OpenDoor", "Open door")]
  public void OpenDoor()
  {
    if (IsOpen) return; // actions must be idempotent
    Swing(1f);
    Raise(DoorOpened);
  }
}

The prefab manifest

One file, two audiences

The portal reads it to populate the Actions tab. The headset runtime reads it to know which action keys it may be asked to invoke. Nothing else in the system needs to know how your prefab is built.

bundleKey
The Unity AssetBundle the headset downloads. Built per target platform.
glbKey
The web preview mesh the browser editor renders. Same pivot, same scale, fewer triangles.
events[]
Key, human label, and an optional payload schema so a condition can read what came with the event.
actions[]
Key, human label, and an optional params schema that drives which fields the step editor renders.
fire-door-type-b/manifest.jsonjsonc
{
  "manifestVersion": 1,
  "name": "Fire Door, Type B",
  "slug": "fire-door-type-b",
  "version": 4,
  "unityVersion": "2022.3.44f1",
  "targetPlatforms": ["android-quest", "windows-openxr"],
  "bundleKey": "prefabs/fire-door-type-b/4/bundle.unity3d",
  "glbKey": "prefabs/fire-door-type-b/4/preview.glb",

  // What the prefab can TELL the scenario.
  "events": [
    { "key": "onDoorOpen", "label": "Door opened" },
    { "key": "onDoorClosed", "label": "Door closed" },
    { "key": "onHandleGrab", "label": "Handle grabbed",
      "payloadSchema": { "hand": "left | right" } }
  ],

  // What the scenario can TELL the prefab.
  "actions": [
    { "key": "OpenDoor", "label": "Open door" },
    { "key": "LockDoor", "label": "Lock door",
      "paramsSchema": { "durationMs": "number" } }
  ]
}
builds/pres_8f21/3.jsonjson
{
  "manifestVersion": 1, "version": 3,
  "title": "Warehouse fire response",
  "settings": { "locomotion": "teleport", "passthrough": false },
  "scenes": [
    {
      "index": 0, "name": "Loading bay",
      "environment": { "type": "SKYBOX_360", "assetId": "ast_4c9" },
      "bindings": [
        {
          "sourceObjectId": "obj_door", "eventKey": "onDoorOpen",
          "steps": [
            { "type": "ADD_SCORE", "params": { "amount": 100 } },
            { "type": "SHOW_OBJECT", "delayMs": 400, "targetObjectId": "obj_ext" }
          ]
        }
      ]
    }
  ]
}

The build manifest

What the headset actually receives

Publishing a scenario freezes it into one JSON document. The headset gets that document and the assets it names, and nothing else — no database access, no live queries, no partial state.

Both the browser preview player and the Unity runtime consume the same file, which is the point: if a scenario behaves differently in the two, one of them has a bug, and the manifest is the arbiter.

Transforms are stored as discrete numbers rather than an opaque blob — position, a quaternion rotation, and scale — because the runtime diffs them between builds and the analytics heatmap queries them.

Neither side adds a field without the other. The shape is versioned by manifestVersion.

Before you publish

Six things that save a support ticket

  1. 01

    Inherit from GvrInteractable

    It gives your prefab the grab, gaze and pointer plumbing the runtime expects, and the Raise() call that turns a Unity event into a GVR event.

  2. 02

    Name your events for the author, not the code

    The key is the contract (onDoorOpen); the label is what a non-developer reads in a dropdown ("Door opened"). Both are required.

  3. 03

    Keep actions idempotent

    An action can be invoked from a binding, a condition or a retry. OpenDoor() on an already-open door should be a no-op, not a second animation.

  4. 04

    Ship a preview mesh worth placing

    The GLB is what an author drags around in the browser. It does not need your shaders, but it does need the right silhouette and pivot.

  5. 05

    Bump the version, never the contents

    Published builds pin the prefab version they were authored against. Overwriting a version changes scenarios that are already live.

  6. 06

    Declare your target platforms

    An AssetBundle built for android-quest will not load on a desktop OpenXR runtime. The manifest is where the portal learns which headsets a scenario can run on.

Export one prefab and see it in a scene

Create an organization, upload a bundle and its manifest, and open the library. If the events and actions are listed, your prefab is authorable.

Email and password. No card, no sales call.