Oren Shalev
Oren Shalev

Reputation: 962

How to make A-Frame components talk to each other?

I want some components to respond to the user's position and orientation in the scene. I have little experience with interactive a-frame scenes and haven't written a component myself.

Generally, I'd want components to be able to provide callbacks for other components to call, or if that's not possible then some kind of inter-component data handoff. The "receiving" component would change its contents (children), appearance and/or behavior.

If we were to take a really simple example, let's say that I want the scene to include either a box if the user is at x>0, or a sphere if they're at x<=0.

Breaking this down, I'll be happy to understand how to...:

  1. Read user position and make it available for others. I found how to read the position; I guess I could just take the <a-scene> element and set some attribute, such as user-position="1 2 3".
  2. Write some code, somewhere, that runs a function when this position changes (I'll debounce it, I imagine) and makes changes to a scene. I think that if I wrote my own component to include the whole scene, I'd need to...:
    • Set the user position as an attribute on that element;
    • Define an update method;
    • In the update method, compare current vs previous user location.

...but I'm wondering if maybe this is overkill and I can just hook somehow into a-scene, or something else entirely.

If I take the approach I mentioned above, I guess the missing piece is how to "declare" what to render? For example, using ReactJS I'd just do return x > 0 ? <a-box/> : <a-sphere/>;. Is there an equivalent, or would I need to reach into the DOM and manually add/remove <a-box> child and such?

Thank you!

EDIT: I sort of got my box/sphere working (glitch), but it feels quite strange, would love to improve this.

Upvotes: 0

Views: 1119

Answers (1)

Piotr Adam Milewski
Piotr Adam Milewski

Reputation: 14645

How to make A-Frame components talk to each other?

0. setAttribute

You can change any property in any component with

element.setAttribute("component_name", "value");

but I assume you want more than reacting to update calls. Something more flexible than the component schema and a bit more performant when used 60 times per second/

1. events

  • component 1 emits an event
  • components 2 - x listen for an event, and react accordingly.

Not dependant on hard-coded component names, you can easily have multiple recipients, and a possibly stable API:

<script src="https://aframe.io/releases/1.2.0/aframe.min.js"></script>
<script>
  AFRAME.registerComponent("position-reader", {
    tick: function() {
      // read the position and broadcast it around
      const pos = this.el.object3D.position;
      const positionString = "x: " + pos.x.toFixed(2) +
        ", z: " + pos.z.toFixed(2)
      this.el.emit("position-update", {text: positionString})
    }
  })
  AFRAME.registerComponent("position-renderer", {
    init: function() {
      const textEl = document.querySelector("a-text");
      this.el.addEventListener("position-update", (evt) => {
        textEl.setAttribute("value", evt.detail.text);
      })
    }
  })
</script>
<a-scene>
  <a-box position="-1 0.5 -3" rotation="0 45 0" color="#4CC3D9"></a-box>
  <a-sphere position="0 1.25 -5" radius="1.25" color="#EF2D5E"></a-sphere>
  <a-cylinder position="1 0.75 -3" radius="0.5" height="1.5" color="#FFC65D"></a-cylinder>
  <a-plane position="0 0 -4" rotation="-90 0 0" width="4" height="4" color="#7BC8A4"></a-plane>
  <a-camera position-renderer position-reader>
    <a-text position="-0.5 0 -0.75" color="black" value="test"></a-text>
  </a-camera>
</a-scene>

2. Directly

Taking this literally, you can grab the component "object" reference with

entity.components["componentName"]

and call its functions:

entity.components["componentName"].function();

For example - one component grabs the current position, and tells the other one to print it:

<script src="https://aframe.io/releases/1.2.0/aframe.min.js"></script>
<script>
  AFRAME.registerComponent("position-reader", {
    init: function() {
      // wait until the entity is loaded and grab the other component reference
      this.el.addEventListener("loaded", evt => {
        this.rendererComp = this.el.components["position-renderer"];
      })
    },
    tick: function() {
      if (!this.rendererComp) return;
      // read the position and call 'updateText' in the 'position-renderer'
      const pos = this.el.object3D.position;
      const positionString = "x: " + pos.x.toFixed(2) +
        ", z: " + pos.z.toFixed(2)
      this.rendererComp.updateText(positionString)
    }
  })
  AFRAME.registerComponent("position-renderer", {
    init: function() {
      this.textEl = document.querySelector("a-text");
    },
    updateText: function(string) {
      this.textEl.setAttribute("value", string);
    }
  })
</script>
<a-scene>
  <a-box position="-1 0.5 -3" rotation="0 45 0" color="#4CC3D9"></a-box>
  <a-sphere position="0 1.25 -5" radius="1.25" color="#EF2D5E"></a-sphere>
  <a-cylinder position="1 0.75 -3" radius="0.5" height="1.5" color="#FFC65D"></a-cylinder>
  <a-plane position="0 0 -4" rotation="-90 0 0" width="4" height="4" color="#7BC8A4"></a-plane>
  <a-camera position-renderer position-reader>
    <a-text position="-0.5 0 -0.75" color="black" value="test"></a-text>
  </a-camera>
</a-scene>


In Your case I'd check the position, and manage the elements in one component. Or use one to determine if the position.x > 0 || < 0, and the other one for visibility changes.

<script src="https://aframe.io/releases/1.2.0/aframe.min.js"></script>
<script>
  AFRAME.registerComponent("position-check", {
    schema: {
      z: {default: 0}
    },
    tick: function() {
      const pos = this.el.object3D.position;
      // check if we're 'inside', or outside
      if (pos.z >= this.data.z) {
        // emit an event only once per occurence
        if (!this.inside) this.el.emit("got-inside");
        this.inside = true
      } else {
        // emit an event only once per occurence
        if (this.inside) this.el.emit("got-outside");
        this.inside = false
      }
    }
  })
  AFRAME.registerComponent("manager", {
    init: function() {
      const box = this.el.querySelector("a-box");
      const sphere = this.el.querySelector("a-sphere")
      //react to the changes
      this.el.sceneEl.camera.el.addEventListener("got-inside", e => {
        box.setAttribute("visible", true);
        sphere.setAttribute("visible", false);
      })
      this.el.sceneEl.camera.el.addEventListener("got-outside", e => {
        box.setAttribute("visible", false);
        sphere.setAttribute("visible", true);
      })
    }
  })
</script>
<a-scene>
  <a-box position="-1 0.5 -3" rotation="0 45 0" color="#4CC3D9"></a-box>
  <a-sphere position="0 1.25 -5" radius="1.25" color="#EF2D5E"></a-sphere>
  <a-cylinder position="1 0.75 -3" radius="0.5" height="1.5" color="#FFC65D"></a-cylinder>
  <a-plane position="0 0 -4" rotation="-90 0 0" width="4" height="4" color="#7BC8A4"></a-plane>

  <a-entity manager>
    <a-box position="0 1 -3" visible="false"></a-box>
    <a-sphere position="0 1 -3" visible="false"></a-sphere>
  </a-entity>
  <a-camera position-check="z: 0"></a-camera>
</a-scene>

Upvotes: 3

Related Questions