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.
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.
{
"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" } }
]
}{
"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
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.