Search code examples
aframe

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.


Solution

  • 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>