Search code examples
javascriptaframe

Why is an event not being recognized by another element?


I am having a problem with an A-Frame logic gate type component. I'm working on this for a 7th grade school project (I may have chosen something quite advanced, but I have managed to almost complete it), however I am unable to find the error here.

The issue I am having is the connected component (in this case a door with the gate as its child) is not responding to signals from the gate when it seems like it should. Currently, the door is stuck in the "closed" position no matter the states of the buttons, however what should be happening is that when the buttons that are active (red) meet the logic condition specified in the gate's attributes, the gate should emit the specified event. The door that the gate controls I have tested before and has worked with buttons, which makes me think the problem is with the logic gate itself. I've checked the syntax on everything, and it seems correct, and the debug console gives no errors at all, which makes debugging much harder.

<script>
        AFRAME.registerComponent('logic', {
            schema: {
                type: {
                    type: 'string',
                    default: 'and'
                },

                input1: {
                    type: 'string'
                },

                input2: {
                    type: 'string'
                },

                event: {
                    type: 'string'
                }
            },

            init: function () {
                var entity = this.el;
                var scene = entity.sceneEl;
                var event = this.data.event;
                this.activeOut = false;
                this.active1 = false;
                this.active2 = false;
                var type = this.data.type;
                var input1 = this.data.input1;
                var input2 = this.data.input2;
                // Toggle each input
                entity.addEventListener(input1, function (event) {
                    if(!this.active1) {
                        this.active1 = true;
                    }
                    else {
                        this.active1 = false;
                    }
                });
                entity.addEventListener(input2, function (event) {
                    if(!this.active2) {
                        this.active2 = true;
                    }
                    else {
                        this.active2 = false;
                    }
                });
            },

            tick: function (time, timeD) {
                var entity = this.el;
                var scene = entity.sceneEl;
                var event = this.data.event;
                var type = this.data.type;
                var input1 = this.data.input1;
                var input2 = this.data.input2;
                // Detect the gate type and check for the corresponding condition
                switch (type) {
                    case 'and' :
                        if((this.active1 && this.active2) && !this.activeOut) {
                            entity.emit(event, {}, true);
                            this.activeOut = true;
                        }
                        else if(this.activeOut && !(this.active1 && this.active2)) {
                            entity.emit(event, {}, true);
                            this.activeOut = false;
                        }
                        break;

                    case 'or' :
                        if((this.active1 || this.active2) && !this.activeOut) {
                            entity.emit(event, {}, true);
                            this.activeOut = true;
                        }
                        else if(this.activeOut && !(this.active1 || this.active2)) {
                            entity.emit(event, {}, true);
                            this.activeOut = false;
                        }
                        break;

                    case 'xor' :
                        if(((this.active1 || this.active2) && !(this.active1 && this.active2)) && !this.activeOut) {
                            entity.emit(event, {}, true);
                            this.activeOut = true;
                        }
                        else if(this.activeOut && !((this.active1 || this.active2) && !(this.active1 && this.active2))) {
                            entity.emit(event, {}, true);
                            this.activeOut = false;
                        }
                        break;

                    case 'nand' :
                        if(!(this.active1 && this.active2) && !this.activeOut) {
                            entity.emit(event, {}, true);
                            this.activeOut = true;
                        }
                        else if(this.activeOut && (this.active1 && this.active2)) {
                            entity.emit(event, {}, true);
                            this.activeOut = false;
                        }
                        break;

                    case 'nor' :
                        if(!(this.active1 || this.active2) && !this.activeOut) {
                            entity.emit(event, {}, true);
                            this.activeOut = true;
                        }
                        else if(this.activeOut && (this.active1 || this.active2)) {
                            entity.emit(event, {}, true);
                            this.activeOut = false;
                        }
                        break;

                    case 'xnor' :
                        if(!((this.active1 || this.active2) && !(this.active1 && this.active2)) && !this.activeOut) {
                            entity.emit(event, {}, true);
                            this.activeOut = true;
                        }
                        else if(this.activeOut && ((this.active1 || this.active2) && !(this.active1 && this.active2))) {
                            entity.emit(event, {}, true);
                            this.activeOut = false;
                        }
                        break;
                }
            }
        });
    </script>

And here's the gate in HTML (including the door and buttons incase it matters):

<a-box
            color="blue"
            door="to: 5 3 -5; toggleEvent: open;"
            position="0 3 -5"
            width="5"
            height="6"
            depth="0.06"
        >
            <a-box
                logic="type: and; input1: button; input2: button2; event: open;"
                position="0 0 0"
                width="0.0001"
                height="0.0001"
                depth="0.0001"
            >
                <a-box
                    color="blue"
                    position="4 1 -2"
                    button="eventOn: button; eventOff: button;"
                ></a-box>

                <a-box
                    color="blue"
                    button="eventOn: button2; eventOff: button2;"
                    position="4 3 -2"
                ></a-box>
            </a-box>
        </a-box>

The formatting in the code is due to how it was pasted in here.


Solution

  • event listener functions have this set to the element on which the listener is placed:

    this.active1 = false; // "this" refers to the component
    entity.addEventListener(input1, function (event) {
      if(!this.active1) {
        this.active1 = true; // "this" refers to "entity"
      } else {
        this.active1 = false; // "this" refers to entity"
      }
    });
    

    so You're thinking it's the same 'variable' when actually you're setting a new property of the entity.

    Two common ways of solving this are :

    // assing `this` to another variable, which is used in the listener
    const self = this;
    entity.addEventListener(input1, function (event) {
      self.active1 = false; // this is the components active1 property
    })
    
    // use an arrow function, which does not have it's own scope
    entity.addEventListener(input1, event => {
      this.active1 = false; // this is the components active1 property
    })
    

    Some remarks:

    • no need to check the logic 60 times per second if you know for sure the states change only upon button clicks. The listeners could trigger a function which checks the current gate state.
    • simplify the code as much as possible when trying to find what's wrong. It's easier to put logs in code having one/two execution paths.
    • log, log, and again log anything you think is relevant. The conditions look good? console.log("and") inside. Not triggering? console.log(this.active1, this.active2) before the condition. Always false? console.log(this.active1) inside the event listeners.

    <script src="https://aframe.io/releases/1.3.0/aframe.min.js"></script>
    <script src="https://unpkg.com/aframe-event-set-component@4.2.1/dist/aframe-event-set-component.min.js"></script>
    <script>
        AFRAME.registerComponent('logic', {
            schema: {
                type: { type: 'string', default: 'and' },
                input1: { type: 'string' },
                input2: { type: 'string' },
                event: { type: 'string' }
            },
            init: function () {
                var entity = this.el;
                var scene = entity.sceneEl;
                var event = this.data.event;
                this.activeOut = false;
                this.active1 = false;
                this.active2 = false;
                var type = this.data.type;
                var input1 = this.data.input1;
                var input2 = this.data.input2;
                // Toggle each input
                entity.addEventListener(input1, (event) => {
                    // toggle betweeen true / false
                    this.active1 = !this.active1 ? true : false
                    console.log(input1, "triggered. active1: ", this.active1)
                    this.checkState() // check if the gate is open or closed
                });
                entity.addEventListener(input2, (event) => {
                    // toggle betweeen true / false
                    this.active2 = !this.active2 ? true : false
                    console.log(input2, "triggered. active1: ", this.active2)
                    this.checkState() // check if the gate is open or closed
                });
            },
            checkState: function () {
                var entity = this.el;
                var scene = entity.sceneEl;
                var event = this.data.event;
                var type = this.data.type;
                var input1 = this.data.input1;
                var input2 = this.data.input2;
                // Detect the gate type and check for the corresponding condition
                console.log("checkState: active1/2", this.active1, this.active2)
                switch (type) {
                    case 'and':
                        if ((this.active1 && this.active2) && !this.activeOut) {
                            console.log("and fullfiled")
                            entity.emit(event, {}, true);
                            this.activeOut = true;
                        } else if (this.activeOut && !(this.active1 && this.active2)) {
                            entity.emit(event, {}, true);
                            this.activeOut = false;
                        }
                        break;
                }
            }
        });
        // set text open/closed depending on the 'open' events from the entity with the logic
        AFRAME.registerComponent("door", {
            init: function () {
                const gate = document.querySelector("[logic]")
                var toggle = false;
                gate.addEventListener("open", evt => {
                    toggle = !toggle
                    this.el.setAttribute("text", "value", toggle ? "open" : "closed")
                })
            }
        })
        // not sure where this came from, simple toggle between to events on click
        AFRAME.registerComponent("button", {
            schema: {
                eventOn: { type: "string" },
                eventOff: { type: "string" }
            },
            init: function () {
                var toggle = false;
                this.el.addEventListener("click", evt => {
                    if (!toggle) {
                        this.el.emit(this.data.eventOn)
                        this.el.setAttribute("color", "red")
                    } else {
                        this.el.emit(this.data.eventOff)
                        this.el.setAttribute("color", "blue")
                    }
                    toggle = !toggle
                })
            }
        })
    </script>
    <a-scene cursor="rayOrigin: mouse" raycaster="objects: a-box">
        <a-box logic="type: and; input1: button; input2: button2; event: open;" width="0.0001"
            height="0.0001" depth="0.0001">
            <a-box color="blue" position="1 2 -2" button="eventOn: button; eventOff: button;"></a-box>
    
            <a-box color="blue" button="eventOn: button2; eventOff: button2;" position="-1 2 -2"></a-box>
        </a-box>
        <a-entity door position="0.45 1.8 -0.275" text="color:black; value: closed"></a-entity>
        <a-sky color="#ECECEC"></a-sky>
        <a-entity position="0 1.6 0" camera>
    </a-scene>