Skip to content
Snippets Groups Projects
personal-space-bubble.js 5.90 KiB
const invaderPos = new AFRAME.THREE.Vector3();
const bubblePos = new AFRAME.THREE.Vector3();

/**
 * Iterates through bubbles and invaders on every tick and sets invader state accordingly.
 * testing multiline things
 * @namespace avatar/personal-space-bubble
 * @system personal-space-bubble
 */
AFRAME.registerSystem("personal-space-bubble", {
  schema: {
    debug: { default: false },
    enabled: { default: true }
  },

  init() {
    this.invaders = [];
    this.bubbles = [];

    this.el.addEventListener("action_space_bubble", () => {
      this.el.setAttribute("personal-space-bubble", { enabled: !this.data.enabled });
    });
  },

  registerBubble(bubble) {
    this.bubbles.push(bubble);
  },

  unregisterBubble(bubble) {
    const index = this.bubbles.indexOf(bubble);

    if (index !== -1) {
      this.bubbles.splice(index, 1);
    }
  },

  registerInvader(invader) {
    NAF.utils.getNetworkedEntity(invader.el).then(networkedEl => {
      const owner = NAF.utils.getNetworkOwner(networkedEl);

      if (owner !== NAF.clientId) {
        this.invaders.push(invader);
      }
    });
  },

  unregisterInvader(invader) {
    const index = this.invaders.indexOf(invader);

    if (index !== -1) {
      this.invaders.splice(index, 1);
    }
  },

  update() {
    for (let i = 0; i < this.bubbles.length; i++) {
      this.bubbles[i].updateDebug();
    }

    for (let i = 0; i < this.invaders.length; i++) {
      this.invaders[i].updateDebug();
      if (!this.data.enabled) {
        this.invaders[i].setInvading(false);
      }
    }

    if (this.data.enabled) {
      this.el.addState("spacebubble");
    } else {
      this.el.removeState("spacebubble");
    }
  },

  tick() {
    if (!this.data.enabled) return;

    // precondition for this stuff -- the bubbles and invaders need updated world matrices.
    // right now this is satisfied because we update the world matrices in the character controller

    for (let i = 0; i < this.invaders.length; i++) {
      this.invaders[i].setInvading(false);
    }

    // Loop through all of the space bubbles (usually one)
    for (let i = 0; i < this.bubbles.length; i++) {
      const bubble = this.bubbles[i];

      bubblePos.setFromMatrixPosition(bubble.el.object3D.matrixWorld);

      // Hide the invader if inside the bubble
      for (let j = 0; j < this.invaders.length; j++) {
        const invader = this.invaders[j];

        invaderPos.setFromMatrixPosition(invader.el.object3D.matrixWorld);

        const distanceSquared = bubblePos.distanceToSquared(invaderPos);
        const radiusSum = bubble.data.radius + invader.data.radius;
        if (distanceSquared < radiusSum * radiusSum) {
          invader.setInvading(true);
        }
      }
    }
  }
});

function createSphereGizmo(radius) {
  const geometry = new THREE.SphereBufferGeometry(radius, 10, 10);
  const wireframe = new THREE.WireframeGeometry(geometry);
  const line = new THREE.LineSegments(wireframe);
  line.material.opacity = 0.5;
  line.material.transparent = true;
  return line;
}

// TODO: we need to come up with a more generic way of doing this as this is very specific to our avatars.
/**
 * Specifies a mesh associated with an invader.
 * @namespace avatar/personal-space-bubble
 * @component space-invader-mesh
 */
AFRAME.registerComponent("space-invader-mesh", {
  schema: {
    meshName: { type: "string" }
  },
  update() {
    this.targetMesh = this.el.object3D.getObjectByName(this.data.meshName);
  }
});

function findInvaderMesh(entity) {
  while (entity && !(entity.components && entity.components["space-invader-mesh"])) {
    entity = entity.parentNode;
  }
  return entity && entity.components["space-invader-mesh"].targetMesh;
}

const DEBUG_OBJ = "psb-debug";

/**
 * Represents an entity that can invade a personal space bubble
 * @namespace avatar/personal-space-bubble
 * @component personal-space-invader
 */
AFRAME.registerComponent("personal-space-invader", {
  schema: {
    radius: { type: "number", default: 0.1 },
    useMaterial: { default: false },
    debug: { default: false },
    invadingOpacity: { default: 0.3 }
  },

  init() {
    const system = this.el.sceneEl.systems["personal-space-bubble"];
    system.registerInvader(this);
    if (this.data.useMaterial) {
      const mesh = findInvaderMesh(this.el);
      if (mesh) {
        this.targetMaterial = mesh.material;
      }
    }
    this.invading = false;
  },

  update() {
    this.radiusSquared = this.data.radius * this.data.radius;
    this.updateDebug();
  },

  updateDebug() {
    const system = this.el.sceneEl.systems["personal-space-bubble"];
    if (system.data.debug || this.data.debug) {
      !this.el.object3DMap[DEBUG_OBJ] && this.el.setObject3D(DEBUG_OBJ, createSphereGizmo(this.data.radius));
    } else if (this.el.object3DMap[DEBUG_OBJ]) {
      this.el.removeObject3D(DEBUG_OBJ);
    }
  },

  remove() {
    this.el.sceneEl.systems["personal-space-bubble"].unregisterInvader(this);
  },

  setInvading(invading) {
    if (this.targetMaterial) {
      this.targetMaterial.opacity = invading ? this.data.invadingOpacity : 1;
      this.targetMaterial.transparent = invading;
    } else {
      this.el.object3D.visible = !invading;
    }
    this.invading = invading;
  }
});

/**
 * Represents a personal space bubble on an entity.
 * @namespace avatar/personal-space-bubble
 * @component personal-space-bubble
 */
AFRAME.registerComponent("personal-space-bubble", {
  schema: {
    radius: { type: "number", default: 0.8 },
    debug: { default: false }
  },
  init() {
    this.system.registerBubble(this);
  },

  update() {
    this.radiusSquared = this.data.radius * this.data.radius;
    this.updateDebug();
  },

  updateDebug() {
    if (this.system.data.debug || this.data.debug) {
      !this.el.object3DMap[DEBUG_OBJ] && this.el.setObject3D(DEBUG_OBJ, createSphereGizmo(this.data.radius));
    } else if (this.el.object3DMap[DEBUG_OBJ]) {
      this.el.removeObject3D(DEBUG_OBJ);
    }
  },

  remove() {
    this.system.unregisterBubble(this);
  }
});