-
Notifications
You must be signed in to change notification settings - Fork 52
Extending markup with custom elements
One of the most powerful features of JanusWeb is that any room can extend the markup with its own custom elements. Janus comes with a number of basic building blocks for you to build your experience with - <object>
, <image>
, <particle>
, etc, and using room scripts you can add logic which uses instances of these built-in elements to do all kinds of things. But beyond a certain point, it becomes difficult to manage too many of these objects with room scripts alone.
Custom elements are the solution to this problem. Instead of all of our logic existing at the room level, we can define custom elements which have self-contained logic, where each instance of the object is responsible for controlling itself, and we can create any number of instances of these objects in our room's markup or scripts. This is similar in concept to Unity's prefabs, or Blueprint classes in Unreal Engine.
To define a custom element, we can write some code like this, and put it in a file, let's call it things.js
:
room.defineElement('my-custom-thing', {
create() {
// initialize object here
},
update() {
// run any per-frame code here
}
});
We can now create our objects in markup:
<janus-viewer>
<assets>
<assetscript src="things.js" />
</assets>
<room use_local_asset="room1">
<my-custom-thing pos="0 0 4" col="purple" />
</room>
</janus-viewer>
or in another room script:
let mything = room.createObject('my-custom-thing');
Of course, this basic object doesn't do anything, it doesn't even have any visible components. Let's extend it a bit, we'll make a cube that jumps in the air and explodes when clicked on.
Pay special attention to the fact that we use this.createObject()
and not room.createObject()
- this creates objects as children of our custom element, which means they'll follow that object around wherever we place them in the room. Custom elements can of course spawn objects at the room level, eg, a gun would spawn bullets in the room rather than as a child of the gun - but when defining our "prefab" objects, we generally want to be creating children, not orphans.
room.registerElement('my-custom-thing', {
cubesize: .5,
jumpspeed: 10,
explodedelay: 1500,
explosionsize: 2,
create() {
// Create objects as children of our custom element
this.cube = this.createObject('object', {
id: 'cube',
col: this.col, // pass through color from our base object's "col" attribute
scale: V(this.cubesize),
collision_id: 'cube'
});
this.cube.addEventListener('click', ev => this.jump());
},
update() {
},
jump() {
this.vel.y = this.jumpspeed;
// Add some gravity to the situation
this.addForce('gravity', V(0, -9.8, 0));
// Set our timer
setTimeout(() => this.explode(), this.explodedelay);
},
explode() {
// hide our cube...
this.cube.visible = false;
// and spawn a particle system to show some sparks
this.sparks = this.createObject('particle', {
vel: V(-this.explosionsize / 2),
rand_vel: V(this.explosionsize),
col: this.col,
count: 100,
rate: 100,
duration: 3
});
}
});
Great, it works - but the real power of building this way is how easy it is to reuse this. Let's add a bunch more!
<janus-viewer>
<assets>
<assetscript src="things.js" />
</assets>
<room use_local_asset="room1">
<my-custom-thing pos="0 0 4" col="purple" />
+ <my-custom-thing pos="-1 0 6" col="yellow" />
+ <my-custom-thing pos="-1 0 3" col="orange" cubesize=".4" />
+ <my-custom-thing pos="2 0 5" col="purple" cubesize="1.2" jumpspeed="15" explodedelay="500" />
+ <my-custom-thing pos="-2 0 4" col="red" />
+ <my-custom-thing pos=".5 0 6" col="green" />
+ <my-custom-thing pos="-.5 0 8" col="blue" />
+ <my-custom-thing pos="0 0 7" col="black" />
</room>
</janus-viewer>
As you can see, we can now create any number of our custom things, with different parameters, and they all work independently of each other without having to write any additional code to manage them. Each one is responsible for maintaining its own state - this behavior can be as complex as we need it to be for whatever it is we're building.
Another thing we can do with our custom elements is to make them fire custom events when certain things happen. This allows us to build more advanced interactive behavior, where other objects in our room can react when things happen with our custom elements. To do this, we define some new oneventname
properties on our object, and then call this.dispatchEvent({type: 'eventname'})
in our code where we want the event to be fired. So extending our existing object, that would look like this:
room.registerElement('my-custom-thing', {
cubesize: .5,
jumpspeed: 10,
explodedelay: 1000,
explosionsize: 20,
+ onjump: new CustomEvent('jump'),
+ onexplode: new CustomEvent('explode'),
create() {
this.mass = 1; // our object needs some mass if gravity is going to affect it
this.cube = this.createObject('object', {
id: 'cube',
col: this.col, // pass through color from our base object's "col" attribute
scale: V(this.cubesize),
pos: V(0, this.cubesize / 2, 0), // so we're not stuck in the ground
collision_id: 'cube'
});
this.cube.addEventListener('click', ev => this.jump());
},
update() {
},
jump() {
console.log('boing', this);
// Shoot up into the air
this.vel = V(0, +this.jumpspeed, 0);
// Add some gravity to the situation
this.addForce('gravity', V(0, -9.8, 0));
+ // Fire our "jump" event
+ this.dispatchEvent({type: 'jump'});
+
// Count down to explosion
setTimeout(() => this.explode(), this.explodedelay);
},
explode() {
console.log('boom', this);
// hide our cube...
this.cube.visible = false;
// and spawn a particle system to show some sparks
this.sparks = this.createObject('particle', {
vel: V(-this.explosionsize / 2).add(this.vel),
rand_vel: V(this.explosionsize),
//accel: V(0, -9.8, 0),
col: this.col,
scale: V(.1),
count: 500,
rate: 5000,
duration: 10,
loop: false,
});
+
+ // Fire our "explode" event
+ this.dispatchEvent({type: 'explode'});
}
});
We've added two new properties, onjump
and onexplode
, and two calls to this.dispatchEvent()
in the jump()
and explode()
functions. We can now respond to these events in our markup. We have access to two special variables inside of our event handlers when specified in the markup: this
refers to the object which fires the event, and event
refers to the event object that was fired.
<janus-viewer>
<assets>
<assetscript src="things.js" />
</assets>
<room use_local_asset="room1">
<my-custom-thing pos="0 0 4" col="purple" />
+ <my-custom-thing pos="-1 0 6" col="yellow" onjump="this.angular.y = 10" onexplode="room.createObject('light', { pos: this.pos.clone() })"/>
<my-custom-thing pos="-1 0 3" col="orange" cubesize=".4" />
<my-custom-thing pos="2 0 5" col="purple" cubesize="1.2" jumpspeed="15" explodedelay="500" />
<my-custom-thing pos="-2 0 4" col="red" />
<my-custom-thing pos=".5 0 6" col="green" />
<my-custom-thing pos="-.5 0 8" col="blue" />
<my-custom-thing pos="0 0 7" col="black" />
</room>
</janus-viewer>
Note that this new behavior only triggers when we click on the yellow object - it's the only one we added event handlers for. The others behave as they did before, while our yellow one starts to spin when it jumps, and spawns a new light in the room when it explodes.
We can also add these events in scripts, to achieve the same result:
let myobject = room.createObject('my-custom-thing', { col: 'lime'});
myobject.addEventListener('jump', ev => myobject.angular.y = 20);
myobject.addEventListener('explode', ev => room.createObject('light', { pos: myobject.pos.clone() }));