Documentazione

Message bus (DiscoVR.bus)

La documentazione tecnica è in inglese.

When several parts of an experience need to talk to each other - a button that opens a door, a trigger that starts a video, a score shown on a sign - plain DOM events get awkward: you need a reference to the other object's element, you have to wait for it to exist, and a message sent before the listener is ready is lost. DiscoVR.bus is a small page-wide message bus built for exactly that.

DiscoVR.bus exists as soon as discovr-core.js has run, before the scene does, so a script can subscribe at load time. It works without creating a DiscoVR instance.

DiscoVR.bus.on("door:open", function (payload) {
    console.log("opening at speed", payload.speed);
});

DiscoVR.bus.emit("door:open", { speed: 2 });

Topics

A topic is a string without spaces, conventionally area:action (door:open, score, quiz:answer:correct).

In a subscription, * is a wildcard:

Pattern Matches
door:open exactly door:open
door:* everything below door: - door:open, door:open:slow
*:open one segment, then open - door:open, lamp:open
door:*:done door:open:done (one segment in the middle)
* every message (handy for logging)

Emitted topics are always literal.

API

Call What it does
bus.on(pattern, handler, options?) Subscribe. handler(payload, message). Returns a function that unsubscribes. Retained messages (below) are delivered immediately unless { replay: false }.
bus.once(pattern, handler) Like on, for the first message only.
bus.off(pattern, handler) Unsubscribe by handler.
bus.emit(topic, payload?, options?) Publish. Returns how many handlers received it. { retain: true } keeps it as the topic's current value.
bus.set(key, value) / bus.get(key, fallback?) / bus.has(key) Shared state: set is emit(key, value, { retain: true }).
bus.watch(pattern, handler) Subscribe including the current value - "keep this in sync".
bus.clear(topic?) Forget the retained value of a topic (or of all).
bus.forward(element, eventName, topic, options?) Publish a DOM event of an element as a bus message.
bus.scope(component?) A handle that removes all its subscriptions together (see below).
bus.getHistory() / bus.topics() The last 200 messages / every topic seen so far.

message is { topic, payload, time, retained, replayed }. Handlers run synchronously, in subscription order. A handler that throws is logged and does not stop the others. A chain of more than 32 nested emits is cut and logged (it is almost always a message loop).

Retained messages: never "too early"

DiscoVR.bus.set("score", 10);                       // somewhere, early

// ...much later, from another script or component - still gets 10 right away:
DiscoVR.bus.watch("score", function (value) { sign.setAttribute("value", "Score: " + value); });

Cleaning up automatically

Inside an A-Frame component, subscribe through a scope: everything is removed when the component - or the whole entity - is removed, so remove() has nothing to undo.

AFRAME.registerComponent("door", {
    init: function () {
        this.bus = DiscoVR.bus.scope(this);
        this.bus.on("door:open", this.open.bind(this));
    },
    open: function () { this.el.setAttribute("visible", false); }
});

Outside components, var scope = DiscoVR.bus.scope(); ... scope.destroy();.

Wiring without code: the i-bus component

<!-- click -> publishes "door:open" -->
<a-box class="raycastable" i-bus="on: click; emit: door:open"></a-box>

<!-- hears "door:open" -> hides itself -->
<a-entity id="door" i-bus="listen: door:open; do: hide"></a-entity>

A sound entity already publishes the DOM event sound-ended when it finishes by itself, so on: sound-ended; emit: ... chains sounds or actions.

An entity can carry several rules: i-bus__open="...", i-bus__close="...".

Property Meaning
on / emit Publish emit when this entity gets the DOM event on (click, mouseenter, animation-finished, ...).
payload Constant payload (JSON is parsed: 5, true, {"speed":2}; anything else is a string). Without it the payload is { source: <entity id>, detail: <event detail> }.
retain Keep the published message as the topic's current value.
listen / do React to a topic or pattern with an action (below).
target CSS selector of the entity the action applies to (default: this entity). Looked up when the message arrives, so it may be created later.
attribute, value For do: set. attribute may be component.property (text.value); value: $payload uses the message's payload.
volume, rate For do: sound-play, sound-once: volume (0-1) and playback speed for that play (negative volume / 0 rate = leave as configured).
value, duration, easing, relative For do: move, rotate, scale: value is "x y z" (or $payload: an "x y z" string or {x, y, z}), reached over duration ms (default 1000; 0 = jump) with easing (default easeInOutQuad). relative: true adds to the current position/rotation (and multiplies the current scale). A new move on the same property restarts an unfinished one.

Actions: show, hide, toggle (visibility); set; xray-on, xray-off, xray-toggle (the target's i-xray); animation-play, animation-pause, animation-toggle (the target's animation-mixer); mixer-set, mixer-toggle (the audio mixer, below); move, rotate (degrees), scale (animated transforms); sound-play (restarts), sound-stop, sound-toggle (the target's sound component); sound-once (plays the audio file in value - #assetId or a URL - once, flat, without needing a sound entity); emit (publish the topic in value with the same payload - chain topics).

Sound zones: i-sound-zone

Makes an entity's sound play only while the visitor is inside an area, fading in on entry and out on exit - room ambience, or narration that starts when someone comes close:

<a-entity sound="src: #rain; loop: true; positional: false" i-sound-zone="shape: box; size: 10 4 10; fade: 2"></a-entity>
Property Meaning
shape, radius, size sphere (radius in meters) or box (full size x y z), measured in the entity's own space - moving, rotating or scaling the entity moves, turns or scales the zone.
fade Seconds to fade in / out (0 = instant).
volume Volume while fully inside.
restart true: every entry starts the sound again from the beginning.
target CSS selector of the entity that counts as the visitor (default: the active camera).
topic Bus topic prefix, default zone:<entity id>: publishes <topic>:enter and <topic>:exit.

The entity also gets the DOM events zone-enter / zone-exit (usable as on: in an i-bus rule). The zone decides when the sound plays - its autoplay is overridden.

The audio mixer: i-audio-mixer and i-sound-channel

Every sound belongs to a channel - effects, ambience, music or narration - and is heard at its own volume x master x its channel. The scene's i-audio-mixer (created automatically; set it yourself to choose the starting mix) holds the gains and changes them while the page runs:

<a-scene i-audio-mixer="master: 0.8; music: 0.4"> ...
<a-entity sound="src: #theme; loop: true" i-sound-channel="channel: music"></a-entity>
DiscoVR.bus.emit("mixer:music", 0.2);                           // fade to 20%
DiscoVR.bus.emit("mixer:master", { value: 0, duration: 1500 }); // mute over 1.5 s (duration in ms, 0 = at once)
DiscoVR.bus.emit("mixer:music", { toggle: true });              // mute / restore the previous level

The background music (audio:bgMusic) is on the music channel. In i-bus rules use do: mixer-set; attribute: music; value: 0.2; duration: 500 and do: mixer-toggle; attribute: master. A command sent before the scene has finished loading is not delivered. An entity with i-sound-zone has the mixer applied to its zone volume.

Messages from outside: i-net-feed

An outside system (a sensor gateway, a PLC bridge, a customer's backend) can drive a scene through DiscoVR NET. The scene listens with i-net-feed:

<a-scene i-net-feed="server: wss://net.discovr-engine.eu; room: plant-3; channels: telemetry, alarm">

Every message the outside system broadcasts on one of those channels becomes a bus message whose topic is the channel and whose payload is its data, retained by default (retain: false to turn that off). From there, i-bus rules or scripts react to it: i-bus="listen: alarm; do: show" on a warning sign, or do: set with value: $payload to use the data itself as a value (send a string or a number for that; an object is for scripts, DiscoVR.bus.on("telemetry", (data) => data.temp)).

The outside system is a plain WebSocket client, in any language:

const ws = new WebSocket("wss://net.discovr-engine.eu");
ws.onopen = () => {
  ws.send(JSON.stringify({ event: "joinRoom", data: { room: "plant-3", app: "default" } }));
  setInterval(() => ws.send(JSON.stringify({
    event: "broadcast",
    data: { type: "telemetry", data: { temp: 23.5 } },
  })), 1000);
};
  • The page joins as a NET listener: it doesn't need multiplayer or a player rig, isn't a person in the room, and receives only the listed channels. It never sends anything.
  • Messages reach the pages open at that moment; a page opened later gets the next one (send at regular intervals, or the last value is missing until then).
  • net-feed:status (retained) says connected / disconnected; the feed reconnects on its own.
  • Anyone who knows the server and the room can send. Don't drive anything sensitive with it until NET has client keys.

Showing a value: i-gauge

A round gauge (an arc that fills from min to max, a needle, the number with its unit, and a label) that follows a topic:

<a-entity position="0 1.5 -2" i-gauge="topic: bio:heart-rate; label: Heart rate; min: 40; max: 160; decimals: 0; unit: bpm"></a-entity>

It shows the topic's retained value at once, then every new message. The payload can be a number, a numeric string, or an object with a value field ({ value: 0.7 }). Until the first message it shows --; while <topic>:signal is lost it turns grey and shows -- too, so a source can say it has lost its sensor with DiscoVR.bus.set("bio:heart-rate:signal", "lost") (and "ok" afterwards).

Without a topic it shows a fixed value instead (i-gauge="value: 72; max: 100; unit: %; decimals: 0"), which a rule can still change: i-bus="listen: load; do: set; attribute: i-gauge.value; value: $payload".

Other properties: radius 0.3 (meters), thickness 0.05, sweep 240 (degrees of arc), needle true, smoothing 0.4 (seconds to glide to a new value, 0 = jump), and the colors colorLow / colorHigh (the fill fades from one to the other along the range), trackColor, lostColor, textColor. The unit follows the number after a space (72 bpm), except a unit starting with % or ° (72%, 21.5°C). Example: public/examples/gauge/.

Debugging

With debug="true" on the <discovr> tag, emitting a topic nobody listens to prints a warning - with a "did you mean ...?" hint when a similar topic exists. (A lone * monitor does not count as listening, so a logger can't hide a typo.) To watch all traffic:

DiscoVR.bus.on("*", function (payload, message) {
    if (!message.replayed) console.log(message.topic, payload);
}, { replay: false });

A complete example is in examples/bus/.