Observables
How To Use Observables
For many of you creating Babylon.js projects, only GUI and scene Observables will be needed, particularly scene.onPointerObservable.
Introduction
Babylon.js provides a lot of events (like scene.beforeRender), and before v2.4 there was no unified way to handle them. Starting with v2.4, we introduced (without breaking backward compatibility of course) a new pattern: the Observables.
There are two parts: the Observable and the Observer. The Observable is a property of an object that represents a given event (like beforeRender, for instance). Users who want to run their own code in response to such an event will register an Observer with the appropriate Observable. Then it's the Observable's job to execute the Observers when appropriate.
The implementer uses an Observable to create a property that will trigger all registered observers. The generic type T is used to communicate a given data type from the Observable to the Observer.
Most users will simply add their own Observers to the Observables that Babylon.js provides. But it is also possible to create your own Observables (a simple example of which is below). For those who want to delve deeper, please see the API.
-
Simple Custom Observable Example
An Observable,
onXChange, is added to the master sphere. The two minion spheres and the actions they perform form the two Observers, which react when a change in the master's x position is observed.
Observable Methods and Properties
The following are available:
- add(): to add an Observer
- addOnce(): to add an Observer which will be executed once and then removed
- remove(): to remove a previously registered Observer
- removeCallback(): same as above, but by giving the callback instead of the Observer instance
- notifyObservers(): used to notify all registered Observers
- notifyObserversWithPromise(): calling this will execute each callback, expecting it to be a promise or return a value. If at any point in the chain one function fails, the promise will fail and execution will not continue.
- hasObservers: a property that returns true if at least one Observer is registered
- hasSpecificMask(mask): a function that returns true if at least one Observer is registered with this mask
- clear() to remove all Observers
- clone() to simply clone the object but not the registered Observers.
The following static methods are available:
- FromPromise(): to create an Observable from a Promise.
Many Babylon.js objects have a range of available Observables. Here is an unordered list from the Documentation search facility, with links to the API.
Creating An Observable
It is not necessary to create a new Observable in order to add an Observer to an Observable that Babylon.js provides, but you may wish to create your own Observables. In particular, Observables can be very useful for connecting external libraries to Babylon.js:
import { TextBlock } from "@babylonjs/gui/2D/controls/textBlock";import { io } from "socket.io-client";const socket = io("/admin");const onConnectObservable = new Observable();const text1 = new TextBlock();socket.on("connect", () => { onConnectObservable.notifyObservers();});onConnectObservable.add(() => { text1.text = "Connected";});There is also a utility method Observable.FromPromise to create an Observable from a Promise:
const onStatusObservable = Observable.FromPromise(axios("/ping").then((response) => response.statusText));
onStatusObservable.add((statusText) => { text1.text = "Server status: " + statusText;});Creating your own Observables can help reduce coupling between different components. Instead of a hierarchy of components depending on each other, you can create multiple independent components and then simply connect them using a single parent component.
Add An Observer
An Observer consists of an object set to watch the Observable and the object's reaction to the observation.
In the following example, the sphere and its scale change create an Observer through the Observable.add() method.
Use the Observable that notifies its Observers before the scene starts rendering each frame.
const alpha = 0;scene.onBeforeRenderObservable.add(function () { sphere.scaling.y = Math.cos(alpha);
alpha += 0.01;});To remove an Observer, you need to store it during its creation so you can refer to it with remove. The following example removes the Observer before it is notified for even the first rendered frame.
const alpha = 0;const observer = scene.onBeforeRenderObservable.add(function () { sphere.scaling.y = Math.cos(alpha);
alpha += 0.01;});
scene.onBeforeRenderObservable.remove(observer);The following example removes the Observer during the rendering cycle. Since it is not possible to remove an Observer that does not exist, you need to check whether the Observable still has that Observer.
const alpha = 0;const observer = scene.onBeforeRenderObservable.add(function () { sphere.scaling.y = Math.cos(alpha);
alpha += 0.01;
if (scene.onBeforeRenderObservable.hasObservers && alpha > 3) { scene.onBeforeRenderObservable.remove(observer); }});Scene Observables
The Babylon.js Scene object has over 20 observables that 'fire' under various conditions. Most of them are checked on each frame/render in a deterministic order. Below is a list of Scene observables checked during each render loop, in the order they are checked:
- onBeforeAnimationsObservable
- onAfterAnimationsObservable
- onBeforePhysicsObservable
- onAfterPhysicsObservable
- onBeforeRenderObservable
- onBeforeRenderTargetsRenderObservable
- onAfterRenderTargetsRenderObservable
- onBeforeCameraRenderObservable
- onBeforeActiveMeshesEvaluationObservable
- onAfterActiveMeshesEvaluationObservable
- onBeforeParticlesRenderingObservable
- onAfterParticlesRenderingObservable
- onBeforeDrawPhaseObservable
- onAfterDrawPhaseObservable
- onAfterCameraRenderObservable
- onAfterRenderObservable
The Scene object also has observables: onReady, onDataLoaded, and onDispose, but they are not triggered during rendering.
Also, onBeforeStepObservable and onAfterStepObservable are available when using deterministic lock step.
However, possibly the most useful Observable is the one that checks what is happening to the screen pointer, whether by mouse, finger, or controller: scene.onPointerObservable. For more details, have a look at the Interactions HowTo.
Observable-based countdown function
Starting with Babylon 4.2, a new way to set a delayed function call was added. This method calculates the delay using observables. Think of it as a setTimeout function, but inside the Babylon context. The best way to explain it is with an example:
import { setAndStartTimer } from "@babylonjs/core/Misc/timer";// classic set timeout:setTimeout(() => { // code running here is not guaranteed to be called inside the render loop // Actually, it is most likely that it will be called OUTSIDE the render loop}, 3000);// the new and simple waysetAndStartTimer({ timeout: 3000, contextObservable: scene.onBeforeRenderObservable, onEnded: () => { // code running here is guaranteed to run inside the beforeRender loop },});setAndStartTimer
As you can see in the example, the Babylon countdown timer takes an observable as context. This observable is the most important part of the code: it is the context in which the remaining time functions will be called, and it is responsible for calculating the delta time until the timer finishes. To explain it simply, these are the steps for the function we implemented before:
- set time = 0, add an observer to scene.onBeforeRenderObservable
- wait for the observer to be called.
- in the observer - check if time-passed-since-start > timeout
- If not, go to 2
- If yes, call onEnded, remove the observer
- We are done
A full example of the function's API:
import { setAndStartTimer } from "@babylonjs/core/Misc/timer";setAndStartTimer({ timeout: 3000, contextObservable: scene.onBeforeRenderObservable, breakCondition: () => { // this will check if we need to break before the timeout has reached return scene.isDisposed(); }, onEnded: (data) => { // this will run when the timeout has passed }, onTick: (data) => { // this will run }, onAborted: (data) => { // this function will run when the break condition has met (premature ending) },});In theory, any observable can be used here, but some do not really make sense. If, for example, we use the pointer down observable, it might take a long time until the observer is called again, making it impractical. But if you need to process pointer-down input for the next 2 minutes, you can use it this way:
import { PointerEventTypes } from "@babylonjs/core/Events/pointerEvents";import { setAndStartTimer } from "@babylonjs/core/Misc/timer";let gameIsOn = true;setAndStartTimer({ timeout: 2 * 60 * 1000, contextObservable: scene.onPointerObservable, observableParameters: { mask: PointerEventTypes.POINTERDOWN }, breakCondition: () => { // break if the game ended prior to this timeout return !gameIsOn; }, onEnded: () { console.log('time is over'); }, onTick: (data) => { // data.payload is the pointerInfo object from the onPointerObservable doSomethingWithTheData(data); }});A more practical example is making the user touch something for 3 seconds while indicating that the 3 seconds are being counted:
import { setAndStartTimer } from "@babylonjs/core/Misc/timer";const guiButton = // created a GUI buttonconst guiButtonMaterial = ... // get the materiallet pressed = false;scene.onPointerDown = () => { pressed = true; setAndStartTimer({ timeout: 2 * 60 * 1000, contextObservable: scene.onBeforeRenderObservable, breakCondition: () => { // break if no longer pressed return !pressed; }, onEnded: () { console.log('Button pressed!'); // back to a black button guiButtonMaterial.diffuseColor.set(0,0,0); }, onTick: (data) => { // turn it slowly green on each call to the registered observer guiButtonMaterial.diffuseColor.set(0,data.completeRate,0); }, onAborted: () => { // Aborted, back to a black button guiButtonMaterial.diffuseColor.set(0,0,0); } });}
scene.onPointerUp = () => { pressed = false;}Advanced Timer
Apart from this quick function, you can use the AdvancedTimer class, which offers a bit more flexibility but is also much more verbose. The object itself is reusable, so it can eventually save a few unnecessary calls and object creations. The last example using AdvancedTimer looks like this:
import { AdvancedTimer } from "@babylonjs/core/Misc/timer";const guiButton = // created a GUI buttonconst guiButtonMaterial = ... // get the materialconst advancedTimer : AdvancedTimer<Scene> = new AdvancedTimer({ timeout: 3000, contextObservable: scene.onBeforeRenderObservable});advancedTimer.onEachCountObservable.add(() => { // turn it slowly green on each call to the registered observer guiButtonMaterial.diffuseColor.set(0,data.completeRate,0);});advancedTimer.onTimerAbortedObservable.add(() => { // Aborted, back to a black button guiButtonMaterial.diffuseColor.set(0,0,0);});advancedTimer.onTimerEndedObservable.add(() => { // back to a black button guiButtonMaterial.diffuseColor.set(0,0,0); console.log('Button pressed!');});
scene.onPointerDown = () => { advancedTimer.start();}
scene.onPointerUp = () => { advancedTimer.stop();}Usage with RxJS
RxJS is a common library for handling Observables which are compliant with the current ECMAScript Observable proposal. It provides a wide range of operators, allowing for advanced execution patterns.
The following (TypeScript) code can be used to convert a Babylon Observable into its RxJS equivalent:
/** * Wraps a Babylon Observable into an rxjs Observable * * @param bjsObservable The Babylon Observable you want to observe * @example * ``` * import { Engine, Scene, AbstractMesh } from '@babylonjs/core' * * const canvas = document.getElementById('canvas') as HTMLCanvasElement * const engine = new Engine(canvas) * const scene = new Scene(engine) * * const render$: Observable<Scene> = fromBabylonObservable(scene.onAfterRenderObservable) * const onMeshAdded$: Observable<AbstractMesh> = fromBabylonObservable(scene.onNewMeshAddedObservable) * ``` */export function fromBabylonObservable<T>(bjsObservable: BJSObservable<T>): Observable<T> { return new Observable<T>((subscriber) => { if (!(bjsObservable instanceof BJSObservable)) { throw new TypeError("the object passed in must be a Babylon Observable"); }
const handler = bjsObservable.add((v) => subscriber.next(v));
return () => bjsObservable.remove(handler); });}