Web-based Virtual Reality Environments

21. Custom Components

A-Frame is internally structured around the Entity-Component-System architecture. In this architecture, entities are containers to which we can add components that give entities specific characteristics (appearance, functionality, behaviour).

Entities are represented by the HTML elements <a-*. Components are represented by the attributes in those elements.

For example, if I write:

<a-entity position="1 2 3"></a-entity>

I'm creating an entity, and giving it the position component so that the entity can be placed in 3D space.

Components are written in JavaScript and depending on what we want the component to do, we may need to know about other libraries such as Three.js. Three.js is the 3D rendering library used internally by A-Frame to render the 3D scene.

The purpose of this chapter is not talk about Three.js or even JavaScript in detail, but simply to show the high-level structure of a custom component in A-Frame.

Structure of a custom component

Let's say we wish to create a custom component, that triggers toggle_on and toggle_off events when users click on the entity:

To use this component, which we will call toggle, in our scene we will write something like:

<a-box
    toggle

    event-set__toggle_on="color: red"
    event-set__toggle_off="color: blue"

    color="blue"
    position="0 1.6 -2"
></a-box>

Notice how we add the toggle attribute/component but also make use of the event-set. Our toggle component simply generates new events, the event-set than uses those events to create something useful (toggle the color of the box).

Example 1800-customcomponents-01-toggle [new tab ] [source ]
Click to load.
Use keys 'w', 'a', 's', 'd' to move around.
Mouse to look around.

So how do we write our custom component? We need to add a <script> and provide our own JavaScript code inside of it. The skeleton of a component is something like:

<script>
AFRAME.registerComponent('toggle', {
  schema: {},
  init: function () {},
  update: function () {},
  tick: function () {},
  remove: function () {},
  pause: function () {},
  play: function () {}
});
</script>

With this, we call AFRAME.registerComponent() function, which will cause A-Frame to know about our new component. We also define the name of the component -- toggle -- and provide the implementation for the component. The functions init, update, etc., are part of A-Frame's component execution lifecycle.

In our toggle component, we will make use of the init function only. This function is called by A-Frame when it detects that the component has been attached to an entity. This function is called only once, to initialize the component.

<script>
      AFRAME.registerComponent("toggle", {
        schema: {},
        init: function () {          
          this._clickCount = 0;

          this.el.addEventListener("click", this.onClick.bind(this));
        },
        update: function () {},
        tick: function () {},
        remove: function () {},
        pause: function () {},
        play: function () {},

        onClick: function () {
          this._clickCount = this._clickCount + 1;

          if (this._clickCount % 2 == 0) {
            this.el.emit('toggle_off');
          } else {
            this.el.emit('toggle_on');
          }
        },
      });
    </script>

In the init function, we simply initialize a variable this._clickCount = 0; which will keep track of how many clicks the user has done over the entity. We then add a click event listener to the entity.

The entity to which the component is attached is accessible through the this.el object. So with this we call tha JavaScript function addEventListener which needs two parameters: the name of th event, and the function that will handle the event. The function that handles the event is the onClick, which we define at the bottom. (The onClick.bind(this) is needed to make sure that the event handler can access the varible that counts the clicks.)

So, every time the user clicks on the entity which has our toggle component, the onClick function will run. This function simply increments the click counter and checks whether the click is an odd or even click. Depending on this, it will emit a toggle_off or toggle_on events.

We can, of course, use the same toggle component on multiple entities:

<a-box
    toggle
    event-set__toggle_on="color: red"
    event-set__toggle_off="color: blue"
    color="blue"
    position="-1 1.6 -2"
></a-box>

<a-box
    toggle
    event-set__toggle_on="color: green"
    event-set__toggle_off="color: yellow"
    color="yellow"
    position="1 1.6 -2"
></a-box>
Example 1800-customcomponents-02-toggle-multiple [new tab ] [source ]
Click to load.
Use keys 'w', 'a', 's', 'd' to move around.
Mouse to look around.

Or change multiple characteristics of the entity by applying multiple event-sets:

<a-box
    toggle
    event-set__color_on="_event: toggle_on; color: red"
    event-set__color_off="_event: toggle_off; color: blue"
    event-set__size_on="_event: toggle_on; height: 0.5"
    event-set__size_off="_event: toggle_off; height: 1"

    color="blue"
    position="-1 1.6 -2"
></a-box>
Example 1800-customcomponents-03-toggle-multiple-size [new tab ] [source ]
Click to load.
Use keys 'w', 'a', 's', 'd' to move around.
Mouse to look around.

Counting clicks on different entities

To demonstrate a few more details about the use of components, let's now implement a component that is able to count clicks on two different entities and trigger and event when both entities have been clicked twice by the user.

We will use this custom component, which I called my-count, in our scene, like this:

<a-box
    my-count="entity1: #box1; entity2: #box2"

    animation="property: rotation; to: 0 360 0; startEvents: myevent; loop:true"

    position="0 1.6 -3"
    color="yellow"
></a-box>

<a-box
   id="box1"
   color="red"
   position="-1 1.6 -1"
   width="0.1"
   height="0.1"
   depth="0.1"
></a-box>

<a-box
   id="box2"
   color="blue"
   position="1 1.6 -1"
   width="0.1"
   height="0.1"
   depth="0.1"
></a-box>

Notice how this component can be parametrized: my-count="entity1: #box1; entity2: #box2". Our are saying to the component that entity one will be the red box (#box1) and entity two will be the blue box (#box2) -- these are the entities on which our component will count the clicks. Our component will then emit a myevent event that is used to start the animation. The end result is that when a user clicks twice on the red box and twice on the blue box, the yellow box will start rotating.

Example 1800-customcomponents-04-differententity [new tab ] [source ]
Click to load.
Use keys 'w', 'a', 's', 'd' to move around.
Mouse to look around.

How do we implement this component? The first part is defining the parameters of the component. This is done in the schema:

AFRAME.registerComponent("my-count", {
    schema: {
        entity1: { type: "selector" },
        entity2: { type: "selector" },
        event: { type: "string", default: "myevent" },
    },

    . . .

In the schema, we specify the parameters that the component knows about. We specify an entity1 and entity2 parameters of type selector. A selector type makes A-Frame fetch the entity from our scene automatically. When we pass the string #box1 as the value for entity1, A-Frame will search for the entity the the id box1 and assign it to the parameter.
We also specify an event parameter. Notice how this parameter has a default value of myevent so that, even if we don't pass a value for this, the component will automatically use myevent as the value. The values of the parameters can be accessed within the component by using this.data.<parametername>. For example:

this.data.entity1

So now, in the init function, we can simply attach the event listeners to the specified entities:

init: function () {
    this.clicksOnEntity1 = 0;
    this.clicksOnEntity2 = 0;

    this.data.entity1.addEventListener("click", this.onClickEntity1.bind(this) );
    this.data.entity2.addEventListener("click", this.onClickEntity2.bind(this) );
},

onClickEntity1: function () {
    this.clicksOnEntity1++;
    this.check();
},
onClickEntity2: function () {
    this.clicksOnEntity2++;
    this.check();
},
check: function () {
    console.log(this.clicksOnEntity1, this.clicksOnEntity2);
    if (this.clicksOnEntity1 > 1 && this.clicksOnEntity2 > 1) {
        console.log("emitting event myevent");
        this.el.emit(this.data.event);
    }
},

Time-based media events

The next example custom component shows how we can create components to deal with time-based media, namely videos.

Example 1800-customcomponents-05-time-based-media [new tab ] [source ]
Click to load.
Use keys 'w', 'a', 's', 'd' to move around.
Mouse to look around.

In this example (1800-customcomponents-05-time-based-media), we want to be able to trigger the play of a video by clicking on an entity in our scene (a green box).
To do this, we create a custom component video-play that we can parametrize with the video element that will be played (the video element in the <a-assets>) and the event that we want to use to trigger it (click):

AFRAME.registerComponent("video-play", {
    schema: {
        event: { type: "string" },
        video: { type: "selector" }
    },
    init: function () {
        this.el.addEventListener(this.data.event, this.playVideo.bind(this));
    },
    playVideo: function () {
        console.log("Playing video")
        this.data.video.play();
    }
});

The above component simply adds an event listener to the entity that has the component and, when the event is triggered, calls the play() function on the video element.

To use it, we attach the video-play component to the green box.

<a-box 
    video-play="event: click; video: #countdown" 

    position="0 1 -1.5" 
    width="0.5" 
    height="0.5" 
    depth="0.5" 
    color="green">

        <a-text value="Click to \nStart" 
                width="2" 
                position="0 0 0.25" 
                align="center"></a-text>
</a-box>

In example 1800-customcomponents-05-time-based-media, we also want to be able to start a second video when the first one ends. To do this, we create another custom component -- video-events. This component listens for standard HTML video events and triggers them on the A-Frame entities:

AFRAME.registerComponent("video-events", {
    schema: {
        video:  { type: "selector" }
    },
    init: function () {
        this.data.video.addEventListener("play", this.onVideoPlay.bind(this));
        this.data.video.addEventListener("pause", this.onVideoPause.bind(this));            
        this.data.video.addEventListener("ended", this.onVideoEnded.bind(this));
    },
    onVideoPlay: function () {
        console.log("Video play");
        this.el.emit("video_play");
    },
    onVideoPause: function () {
        console.log("Video pause");
        this.el.emit("video_pause");
    },      
    onVideoEnded: function () {
        console.log("Video ended");
        this.el.emit("video_ended");
    },
});

This component has a single parameter video which identifies the video that we want to monitor for events. It then adds event listeners for the standard play, pause, and ended events and emits those events to the entity (their names are changed to video_play, video_pause, and video_ended simply to make it easier to know these are our custom events).

We these two custom components, we can then listen for the video events from the first video and use those events to start the second video:

<a-plane 
    video-events="video: #countdown"
    video-play="event: video_ended; video: #closeup" 

    src="#countdown" 
    position="-1 1.6 -2"                      
></a-plane>

Constantly updating entity's position

The last component is taken from the A-Frame documentation. The follow component causes an entity in the scene to follow another entity.

In example 1800-customcomponents-06-follow, a sphere is made to follow the camera (the user).

Example 1800-customcomponents-06-follow [new tab ] [source ]
Click to load.
Use keys 'w', 'a', 's', 'd' to move around.
Mouse to look around.

This is accomplished by using the tick function of the component, which runs on every frame. Inside this function, you will see a lot of Three.js methods which are outside of the scope of this course.

Exercises

CustomComponents-01

Create a custom component that is able to track a sequence of clicks on three coloured boxes. If the sequence of clicks (for example 3 clicks) is the same as a pre-defined sequence, than the component should trigger a securitypass event that causes an animation:

(Make a click sequence blue, gree, red, to see the animation in the following example)

Click to open in new tab

References

Comments

Notice anything wrong? Have a doubt?


Copyright © 2024 Jorge C. S. Cardoso jorgecardoso@ieee.org