Search code examples
javascriptvirtual-realityaframe

Aframe: prevent two objects from overlapping.


Following the Minecraft demo. How can I make the blocks not to spawn in the same place? This is the script which spawns the boxes.

There are two approaches which I can think of:

  1. Create a tracking list of the coordinates which I've set up a box, and prevent creating one if the intersection point matches (or is close to a threshold).
  2. Check if the reticule matches a bounding box (Using Threejs' Box3, which I don't know how to use).

Any ideas on the best way to approach this?


Solution

  • This is my answer to #2: I created an array. This array will contain the points of evt.detail.intersection.point. Before inserting a new object, I apply Pythagoras (x, and z points), and compare it with a threshold. Only if it's above the threshold I allow it to continue, and store the new points in it.

    The whole code is below, with comments:

    I used some TypeScript in here, I will not include it so it reaches a broader audience.

    I wrapped the intersection-spawn component with an ES2015 class just for the sake of separating the code. I don't know of a native way of creating A-Frame through ES2015.

    This is the main class "intersection-spawn.js"

    export default class IntersectionSpawn {
        constructor(lamp) {
            //The array that will track the position. 
            this.positionHistory = new Array();
            //The spacing which it will allow to span another light.
            this.minSpacing = 2;
            //Captures the class' *this* so it can be used in the 
            //Event Listener.
            const _this = this;
            //Dependency Injection. Injects the lamp class that manages
            //the lamp creation. 
            this.lamp = lamp;
    
            AFRAME.registerComponent('intersection-spawn', {
                schema: {
                    default: '',
                    parse: AFRAME.utils.styleParser.parse
                },
    
                init: function () {
                    //This data comes from the HTML's <a-entity> attribute
                    const data = this.data;
                    //References the current element. This comes from A-Frame.
                    const el = this.el;
                    //Reducing the code a little bit.
                    //This will create an event listener and pass it to the
                    //intersection method.
                    el.addEventListener(data.event, evt => {
                        _this.intersection(evt, el, data)
                    });
                }
            });
        }
    
        //This takes care of create the element and insert it. 
        intersection(evt, el, data) {
    
            //Just a safeguard. If the event data doesn't contain 
            //the intersection property, then I can't do anything. 
            if (evt.detail.hasOwnProperty("intersection") === false)
                return;
            //Define a position object to keep hold of everything.
            //Note that in here I'm just selecting points x and z
            //because in my app, those are the only ones which interests
            //me. "y" is also available by using vt.detail.intersection.point.y
            let pos = {
                x: evt.detail.intersection.point.x,
                z: evt.detail.intersection.point.z
            };
    
            //If true then it continues, and adds the element. 
            //Otherwise exit. 
            if (!this.canAddToGrid(pos))
                return;
    
            //Creates a new lamp to be inserted. 
            const elem = this.lamp.generate(data, pos);
            el.sceneEl.appendChild(elem);
            this.appendToHistory(pos);
        }
        //Adds to the current history to be tracked. 
        appendToHistory(pos) {
            this.positionHistory.push(pos);
        }
    
        /**
         * Checks whether it's posisble to add to the grid or not.
         * This will check if the distance of the current insertion point
         * is equal or smaller to the distance to any of the cylinders. 
         * If that's the case, it will return false. Otherwise it will return
         * true.
         * The position of the current object to be inserted.
         * @param pos 
         */
        canAddToGrid(pos) {
            for (let position of this.positionHistory) {
                if (this.calcDistance(pos.x, pos.z, position.x, position.z) <= this.minSpacing) {
                    return false;
                }
            }
            return true;
        }
    
        /**
         * Calculates the distance from the center of the lamp to the center 
         * of the insertion points.
         * 
         * @param x1 Position x of the object to be inserted
         * @param z1 Position z of the object to be inserted
         * @param x2 Position x of the object inside the array
         * @param z2 Position z of the object inside the array
         */
        calcDistance(x1, z1, x2, z2) {
            return Math.sqrt(Math.pow((x2 - x1), 2) + Math.pow((z2 - z1), 2));
        }
    
    }
    

    This is lamp.js (The one which creates the lamp object)and is injected to the intersectionSpawn class:

    export default class Lamp {
        constructor() {
    
        }
        /**
         * Creates the Lamp. Right now it's a cylinder. 
         * @param pos The positions you want the lamp to be in.
         */
        create(pos) {
            let elem = (document.createElement("a-cylinder"));
            elem.setAttribute('width', "1");
            elem.setAttribute('height', "4");
            elem.setAttribute('depth', "1");
            elem.setAttribute('position', `${pos.x} 0 ${pos.z}`);
            return elem;
        }
    
        /**
         * This works like a decorator. this was originaly in the intersection-spawn.
         * I do not know a lot what it does, but it's necessary for the element to work.
         * @param elem The generated element from the create(pos) method.
         * @param data Comes from A-Frame's data. 
         */
        AddAframeUtils(elem, data) {
            Object.keys(data).forEach(name => {
                if (name === 'event') { return; }
                AFRAME.utils.entity.setComponentProperty(elem, name, data[name]);
            });
    
            return elem;
    
        }
    
        /**
         * The public method which generates a fully functional element. 
         * @param data This comes from A-Frame's data.
         * @param position The position in which I want to create the element.
         */
        generate(data, position) {
            return this.AddAframeUtils(this.create(position), data);
        }
    }
    

    script.js which includes both classes:

    import Lamp from './js/lamp/lamp';
    import IntersectionSpawn from './js/components/intersection-spawn';
    
    new IntersectionSpawn(new Lamp());
    

    And now, the index.html:

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>Document</title>
        <script src="https://aframe.io/releases/0.5.0/aframe.min.js"></script>
        <script src="https://rawgit.com/mayognaise/aframe-mouse-cursor-component/master/dist/aframe-mouse-cursor-component.min.js"></script>
        <script src="script.js"></script>
    </head>
    
    <body>
        <a-scene>
            <a-sky color="#ECECEC"></a-sky>
            <a-camera>
                <!-- We include the intersection-spawn in here:-->
                <a-cursor intersection-spawn="event: click;"></a-cursor>
            </a-camera>
        </a-scene>
    </body>
    
    </html>