建立一个先进的场景

我们建立了一个基本的场景,但我们可以做更多吗?A-Frame只是一个抽象three.js之上,和A-Frame组件(不与Web组件混淆),我们可以做three.js可以做的任何事情,这是一个很多。让我们通过一个例子构建一个场景,工作流围绕写作组件。我们将构建一个交互式场景中我们向敌人发射激光周边。我们可以使用附带的尖顶的标准组件,或者使用组件a形构架的开发人员发布到生态系统。更好的是,我们可以编写自己的组件来做我们想做的事!

让我们开始通过增加敌人的目标:

<a-scene>
  <a-assets>
    <img id="enemy-sprite" crossorigin="" src="https://ucarecdn.com/f11bb3e6-ceb4-427c-bcaa-351cabac37d5/">
  </a-assets>

  <a-image look-at="#player" src="#enemy-sprite" transparent="true" position="0 1.8 -4"></a-image>

  <a-camera id="player" position="0 1.8 0"></a-camera>

  <a-sky color="#252243"></a-sky>
</a-scene>

这将创建一个基本的静态场景敌人盯着你,即使你移动。我们可以使用的尖顶组件从生态系统做一些对齐。

使用组件

The awesome-aframe repository是一个伟大的地方找到组件的社区创造了启用新特性,许多这些组件从[组件样板][样板和]应该提供构建在dist /文件夹的存储库。以布局组件 layout component为例。我们可以抓住构建,把它到我们的现场,立即可以使用3 d实体布局系统自动位置。而不是一个敌人,我们有十个敌人定位圈玩家:

<script src="https://rawgit.com/ngokevin/aframe-layout-component/master/dist/aframe-layout-component.min.js"></script>

<a-scene>
  <a-assets>
    <img id="enemy-sprite" crossorigin="" src="https://ucarecdn.com/f11bb3e6-ceb4-427c-bcaa-351cabac37d5/">
  </a-assets>

  <a-entity layout="type: circle; radius: 5" position="0 0.5 0">
    <a-image look-at="#player" src="#enemy-sprite" transparent="true"></a-image>
    <a-image look-at="#player" src="#enemy-sprite" transparent="true"></a-image>
    <a-image look-at="#player" src="#enemy-sprite" transparent="true"></a-image>
    <a-image look-at="#player" src="#enemy-sprite" transparent="true"></a-image>
    <a-image look-at="#player" src="#enemy-sprite" transparent="true"></a-image>
    <a-image look-at="#player" src="#enemy-sprite" transparent="true"></a-image>
    <a-image look-at="#player" src="#enemy-sprite" transparent="true"></a-image>
    <a-image look-at="#player" src="#enemy-sprite" transparent="true"></a-image>
    <a-image look-at="#player" src="#enemy-sprite" transparent="true"></a-image>
    <a-image look-at="#player" src="#enemy-sprite" transparent="true"></a-image>
  </a-entity>

  <a-camera id="player" position="0 1.8 0"></a-camera>

  <a-sky color="#252243"></a-sky>
</a-scene>

是混乱的在标记敌人实体重复十次。我们可以减少模板组件清洁。我们还可以使用动画系统animation system 有敌人在我们周围一圈:

<script src="https://rawgit.com/ngokevin/aframe-layout-component/master/dist/aframe-layout-component.min.js"></script>
<!-- Drop in another component and use it from markup. -->
<script src="https://rawgit.com/ngokevin/aframe-template-component/master/dist/aframe-template-component.min.js"></script>

<a-scene>
  <a-assets>
    <img id="enemy-sprite" crossorigin="" src="https://ucarecdn.com/f11bb3e6-ceb4-427c-bcaa-351cabac37d5/">

    <!-- Template component lets us use Handlebars, Jade, Mustache, Nunjucks. -->
    <script id="enemies" type="text/x-nunjucks-template">
      <a-entity layout="type: circle; radius: 5" position="0 0.5 0">
        <!-- Use A-Frame's declarative animation system to have enemies march around us. -->
        <a-animation attribute="rotation" dur="30000" easing="linear" repeat="indefinite" to="0 360 0"></a-animation>

        {% for x in range(num) %}
        <a-image look-at="#player" src="#enemy-sprite" transparent="true"></a-image>
        {% endfor %}
      </a-entity>
    </script>
  </a-assets>

  <!-- Behold, the power of components. -->
  <a-entity template="src: #enemies" data-num="10"></a-entity>

  <a-camera id="player" position="0 1.8 0"></a-camera>

  <a-sky color="#252243"></a-sky>
</a-scene>

通过混合和匹配的布局和模板组件,我们现在有十个敌人我们周围围成一个圈。让我们使游戏通过编写自己的组件。

编写组件

开发人员与JavaScript和舒适的三人。js可以编写组件添加外观、行为和功能体验。正如我们所看到的这些组件可以被重用和共享的社区。虽然不是所有的组件都有共享;他们可以特别的或一次性的。自 A-Frame 是基于一种 entity-component-system pattern,大多数逻辑应该实现内部组件。 A-Frame 应该围绕内开发工作流组件。组件文档 component documentation进入更详细的组件是什么样子,以及如何编写一个。

spawner Component

让我们首先能够产生激光。我们希望能够产生激光的实体,从玩家的当前位置开始。我们将创建一个已成熟的雌鱼在实体组件,听一个事件,当发送事件,我们将生成一个实体与一个预定义的mixin的组件:

AFRAME.registerComponent('spawner', {
  schema: {
    on: { default: 'click' },
    mixin: { default: '' }
  },

  /**
   * Add event listener.
   */
  update: function (oldData) {
    this.el.addEventListener(this.data.on, this.spawn.bind(this));
  },

  /**
   * Spawn new entity at entity's current position.
   */
  spawn: function () {
    var el = this.el;
    var entity = document.createElement('a-entity');
    var matrixWorld = el.object3D.matrixWorld;
    var position = new THREE.Vector3();
    var rotation = el.getAttribute('rotation');
    var entityRotation;

    position.setFromMatrixPosition(matrixWorld);
    entity.setAttribute('position', position);

    // Have the spawned entity face the same direction as the entity.
    // Allow the entity to further modify the inherited rotation.
    position.setFromMatrixPosition(matrixWorld);
    entity.setAttribute('position', position);
    entity.setAttribute('mixin', this.data.mixin);
    entity.addEventListener('loaded', function () {
      entityRotation = entity.getComputedAttribute('rotation');
      entity.setAttribute('rotation', {
        x: entityRotation.x + rotation.x,
        y: entityRotation.y + rotation.y,
        z: entityRotation.z + rotation.z
      });
    });
    el.sceneEl.appendChild(entity);
  }
});

click-listener Component

现在我们需要一种方法来生成一个单击事件的球员实体为了产生激光。我们可以写一个香草JavaScript事件处理程序脚本的内容,但它更可重用组件编写,可以允许任何实体监听点击:

AFRAME.registerComponent('click-listener', {
  init: function () {
    var el = this.el;
    window.addEventListener('click', function () {
      el.emit('click', null, false);
    });
  }
});

从HTML,我们定义激光mixin和已成熟的雌鱼和鼠标点击组件附加到球员。当我们点击,已成熟的雌鱼组件将生成一个激光开始在镜头面前:

<script src="https://rawgit.com/ngokevin/aframe-layout-component/master/dist/aframe-layout-component.min.js"></script>
<script src="https://rawgit.com/ngokevin/aframe-template-component/master/dist/aframe-template-component.min.js"></script>

<script>
// spawner component to generate an entity on an event.
AFRAME.registerComponent('spawner', {
  schema: {
    on: { default: 'click' },
    mixin: { default: '' }
  },

  /**
   * Add event listener.
   */
  update: function (oldData) {
    this.el.addEventListener(this.data.on, this.spawn.bind(this));
  },

  /**
   * Spawn new entity at entity's current position.
   */
  spawn: function () {
    var el = this.el;
    var entity = document.createElement('a-entity');
    var matrixWorld = el.object3D.matrixWorld;
    var position = new THREE.Vector3();
    var rotation = el.getAttribute('rotation');
    var entityRotation;

    position.setFromMatrixPosition(matrixWorld);
    entity.setAttribute('position', position);

    // Have the spawned entity face the same direction as the entity.
    // Allow the entity to further modify the inherited rotation.
    position.setFromMatrixPosition(matrixWorld);
    entity.setAttribute('position', position);
    entity.setAttribute('mixin', this.data.mixin);
    entity.addEventListener('loaded', function () {
      entityRotation = entity.getComputedAttribute('rotation');
      entity.setAttribute('rotation', {
        x: entityRotation.x + rotation.x,
        y: entityRotation.y + rotation.y,
        z: entityRotation.z + rotation.z
      });
    });
    el.sceneEl.appendChild(entity);
  }
});

// click-listener component to pass window clicks to an entity.
AFRAME.registerComponent('click-listener', {
  init: function () {
    var el = this.el;
    window.addEventListener('click', function () {
      el.emit('click', null, false);
    });
  }
});
</script>

<a-scene>
  <a-assets>
    <img id="enemy-sprite" crossorigin="" src="https://ucarecdn.com/f11bb3e6-ceb4-427c-bcaa-351cabac37d5/">

    <script id="enemies" type="text/x-nunjucks-template">
      <a-entity layout="type: circle; radius: 5" position="0 0.5 0">
        <a-animation attribute="rotation" dur="30000" easing="linear" repeat="indefinite" to="0 360 0"></a-animation>

        {% for x in range(num) %}
        <a-image look-at="#player" src="#enemy-sprite" transparent="true"></a-image>
        {% endfor %}
      </a-entity>
    </script>

    <!-- Laser. -->
    <a-mixin id="laser"
             geometry="primitive: cylinder; radius: 0.05; translate: 0 -2 0"
             material="color: green; metalness: 0.2; opacity: 0.4; roughness: 0.3"
             rotation="90 0 0"></a-mixin>
  </a-assets>

  <a-entity template="src: #enemies" data-num="10"></a-entity>

  <!-- Add spawner and click-listener to player. -->
  <a-camera id="player" position="0 1.8 0" spawner="mixin: laser; on: click" click-listener></a-camera>

  <a-sky color="#252243"></a-sky>
</a-scene>

projectile Component

现在激光可以产生在我们面前当我们点击,但我们需要他们火和旅行。已成熟的雌鱼组件,我们有激光点的旋转摄像头,我们绕x轴旋转90度使它正确。我们可以添加一个弹丸组件连续激光旅行的方向已经面临(当地轴在这种情况下):

AFRAME.registerComponent('projectile', {
  schema: {
    speed: { default: -0.4 }
  },

  tick: function () {
    this.el.object3D.translateY(this.data.speed);
  }
});

然后把弹丸组件激光混合:

<a-assets>
  <!-- Attach projectile behavior. -->
  <a-mixin id="laser" geometry="primitive: cylinder; radius: 0.05; translate: 0 -2 0"
                      material="color: green; metalness: 0.2; opacity: 0.4; roughness: 0.3"
                      projectile="speed: -0.5"></a-mixin>
</a-assets>

激光将火像一个弹丸点击:

<script src="https://rawgit.com/ngokevin/aframe-layout-component/master/dist/aframe-layout-component.min.js"></script>
<script src="https://rawgit.com/ngokevin/aframe-template-component/master/dist/aframe-template-component.min.js"></script>

<script>
// projectile component to have an entity travel straight.
AFRAME.registerComponent('projectile', {
  schema: {
    speed: { default: -0.4 }
  },

  tick: function () {
    this.el.object3D.translateY(this.data.speed);
  }
});  

// spawner component to generate an entity on an event.
AFRAME.registerComponent('spawner', {
  schema: {
    on: { default: 'click' },
    mixin: { default: '' }
  },

  /**
   * Add event listener.
   */
  update: function (oldData) {
    this.el.addEventListener(this.data.on, this.spawn.bind(this));
  },

  /**
   * Spawn new entity at entity's current position.
   */
  spawn: function () {
    var el = this.el;
    var entity = document.createElement('a-entity');
    var matrixWorld = el.object3D.matrixWorld;
    var position = new THREE.Vector3();
    var rotation = el.getAttribute('rotation');
    var entityRotation;

    position.setFromMatrixPosition(matrixWorld);
    entity.setAttribute('position', position);

    // Have the spawned entity face the same direction as the entity.
    // Allow the entity to further modify the inherited rotation.
    position.setFromMatrixPosition(matrixWorld);
    entity.setAttribute('position', position);
    entity.setAttribute('mixin', this.data.mixin);
    entity.addEventListener('loaded', function () {
      entityRotation = entity.getComputedAttribute('rotation');
      entity.setAttribute('rotation', {
        x: entityRotation.x + rotation.x,
        y: entityRotation.y + rotation.y,
        z: entityRotation.z + rotation.z
      });
    });
    el.sceneEl.appendChild(entity);
  }
});

// click-listener component to pass window clicks to an entity.
AFRAME.registerComponent('click-listener', {
  init: function () {
    var el = this.el;
    window.addEventListener('click', function () {
      el.emit('click', null, false);
    });
  }
});
</script>

<a-scene>
  <a-assets>
    <img id="enemy-sprite" crossorigin="" src="https://ucarecdn.com/f11bb3e6-ceb4-427c-bcaa-351cabac37d5/">

    <script id="enemies" type="text/x-nunjucks-template">
      <a-entity layout="type: circle; radius: 5" position="0 0.5 0">
        <a-animation attribute="rotation" dur="30000" easing="linear" repeat="indefinite" to="0 360 0"></a-animation>

        {% for x in range(num) %}
        <a-image look-at="#player" src="#enemy-sprite" transparent="true"></a-image>
        {% endfor %}
      </a-entity>
    </script>

    <!-- Laser. -->
    <a-mixin id="laser"
             geometry="primitive: cylinder; radius: 0.05; translate: 0 -2 0"
             material="color: green; metalness: 0.2; opacity: 0.4; roughness: 0.3"
             rotation="90 0 0" projectile></a-mixin>
  </a-assets>

  <a-entity template="src: #enemies" data-num="10"></a-entity>

  <!-- Add spawner and click-listener to player. -->
  <a-camera id="player" position="0 1.8 0" spawner="mixin: laser; on: click" click-listener></a-camera>

  <a-sky color="#252243"></a-sky>
</a-scene>

collider Component

最后一步是添加一个对撞机组件我们可以发现当激光冲击一个实体。我们可以使用three.js Raycaster画一线(线)从激光的一端到另一端,然后不断检查如果一个敌人是射线相交。如果敌人是我们射线相交,那么这影响激光,我们使用一个事件来告诉它被击中的敌人:

AFRAME.registerComponent('collider', {
  schema: {
    target: { default: '' }
  },

  /**
   * Calculate targets.
   */
  init: function () {
    var targetEls = this.el.sceneEl.querySelectorAll(this.data.target);
    this.targets = [];
    for (var i = 0; i < targetEls.length; i++) {
      this.targets.push(targetEls[i].object3D);
    }
    this.el.object3D.updateMatrixWorld();
  },

  /**
   * Check for collisions (for cylinder).
   */
  tick: function (t) {
    var collisionResults;
    var directionVector;
    var el = this.el;
    var mesh = el.getObject3D('mesh');
    var object3D = el.object3D;
    var raycaster;
    var vertices = mesh.geometry.vertices;
    var bottomVertex = vertices[0].clone();
    var topVertex = vertices[vertices.length - 1].clone();

    // Calculate absolute positions of start and end of entity.
    bottomVertex.applyMatrix4(object3D.matrixWorld);
    topVertex.applyMatrix4(object3D.matrixWorld);

    // Direction vector from start to end of entity.
    directionVector = topVertex.clone().sub(bottomVertex).normalize();

    // Raycast for collision.
    raycaster = new THREE.Raycaster(bottomVertex, directionVector, 1);
    collisionResults = raycaster.intersectObjects(this.targets, true);
    collisionResults.forEach(function (target) {
      // Tell collided entity about the collision.
      target.object.el.emit('collider-hit', {target: el});
    });
  }
});

然后将一个类附加到敌人指定为目标,把动画侦听碰撞让他们消失,并附上对撞机组件的激光目标敌人。另外,让它一个挑战,你周围的敌人

然后我们将一个类来指定它们的敌人为目标,将动画,引发碰撞让他们消失,最后把对撞机组件激光目标敌人:

<a-assets>
  <img id="enemy-sprite" src="img/enemy.png">

  <script id="enemies" type="text/x-nunjucks-template">
    <a-entity layout="type: circle; radius: 5">
      <a-animation attribute="rotation" dur="8000" easing="linear" repeat="indefinite" to="0 360 0"></a-animation>

      {% for x in range(num) %}
        <!-- Attach enemy class. -->
        <a-image class="enemy" look-at="#player" src="#enemy-sprite" transparent="true">
          <!-- Attach collision handler animations. -->
          <a-animation attribute="opacity" begin="collider-hit" dur="400" ease="linear"
                       from="1" to="0"></a-animation>
          <a-animation attribute="scale" begin="collider-hit" dur="400" ease="linear"
                       to="0 0 0"></a-animation>
        </a-image>
      {% endfor %}
    </a-entity>
  </script>

  <!-- Attach collider that targets enemies. -->
  <a-mixin id="laser" geometry="primitive: cylinder; radius: 0.05; translate: 0 -2 0"
                      material="color: green; metalness: 0.2; opacity: 0.4; roughness: 0.3"
                      projectile="speed: -0.5" collider="target: .enemy"></a-mixin>
</a-assets>

我们有一个完整的基本交互可以在虚拟现实场景的 A-Frame。我们包成组件,允许我们以声明的方式构建场景没有失去控制和灵活性。结果是一个基本的FPS游戏,支持虚拟现实在最终仅30行HTML:

<script src="https://rawgit.com/ngokevin/aframe-layout-component/master/dist/aframe-layout-component.min.js"></script>
<script src="https://rawgit.com/ngokevin/aframe-template-component/master/dist/aframe-template-component.min.js"></script>

<script>
// collider component to check for collisions.
AFRAME.registerComponent('collider', {
  schema: {
    target: { default: '' }
  },

  /**
   * Calculate targets.
   */
  init: function () {
    var targetEls = this.el.sceneEl.querySelectorAll(this.data.target);
    this.targets = [];
    for (var i = 0; i < targetEls.length; i++) {
      this.targets.push(targetEls[i].object3D);
    }
    this.el.object3D.updateMatrixWorld();
  },

  /**
   * Check for collisions (for cylinder).
   */
  tick: function (t) {
    var collisionResults;
    var directionVector;
    var el = this.el;
    var mesh = el.getObject3D('mesh');
    var object3D = el.object3D;
    var raycaster;
    var vertices = mesh.geometry.vertices;
    var bottomVertex = vertices[0].clone();
    var topVertex = vertices[vertices.length - 1].clone();

    // Calculate absolute positions of start and end of entity.
    bottomVertex.applyMatrix4(object3D.matrixWorld);
    topVertex.applyMatrix4(object3D.matrixWorld);

    // Direction vector from start to end of entity.
    directionVector = topVertex.clone().sub(bottomVertex).normalize();

    // Raycast for collision.
    raycaster = new THREE.Raycaster(bottomVertex, directionVector, 1);
    collisionResults = raycaster.intersectObjects(this.targets, true);
    collisionResults.forEach(function (target) {
      // Tell collided entity about the collision.
      target.object.el.emit('collider-hit', {target: el});
    });
  }
});  

// projectile component to have an entity travel straight.
AFRAME.registerComponent('projectile', {
  schema: {
    speed: { default: -0.4 }
  },

  tick: function () {
    this.el.object3D.translateY(this.data.speed);
  }
});  

// spawner component to generate an entity on an event.
AFRAME.registerComponent('spawner', {
  schema: {
    on: { default: 'click' },
    mixin: { default: '' }
  },

  /**
   * Add event listener.
   */
  update: function (oldData) {
    this.el.addEventListener(this.data.on, this.spawn.bind(this));
  },

  /**
   * Spawn new entity at entity's current position.
   */
  spawn: function () {
    var el = this.el;
    var entity = document.createElement('a-entity');
    var matrixWorld = el.object3D.matrixWorld;
    var position = new THREE.Vector3();
    var rotation = el.getAttribute('rotation');
    var entityRotation;

    position.setFromMatrixPosition(matrixWorld);
    entity.setAttribute('position', position);

    // Have the spawned entity face the same direction as the entity.
    // Allow the entity to further modify the inherited rotation.
    position.setFromMatrixPosition(matrixWorld);
    entity.setAttribute('position', position);
    entity.setAttribute('mixin', this.data.mixin);
    entity.addEventListener('loaded', function () {
      entityRotation = entity.getComputedAttribute('rotation');
      entity.setAttribute('rotation', {
        x: entityRotation.x + rotation.x,
        y: entityRotation.y + rotation.y,
        z: entityRotation.z + rotation.z
      });
    });
    el.sceneEl.appendChild(entity);
  }
});

// click-listener component to pass window clicks to an entity.
AFRAME.registerComponent('click-listener', {
  init: function () {
    var el = this.el;
    window.addEventListener('click', function () {
      el.emit('click', null, false);
    });
  }
});
</script>

<a-scene>
  <a-assets>
    <img id="enemy-sprite" crossorigin="" src="https://ucarecdn.com/f11bb3e6-ceb4-427c-bcaa-351cabac37d5/">

    <script id="enemies" type="text/x-nunjucks-template">
      <a-entity layout="type: circle; radius: 5" position="0 0.5 0">
        <a-animation attribute="rotation" dur="30000" easing="linear" repeat="indefinite" to="0 360 0"></a-animation>

        {% for x in range(num) %}
          <a-image class="enemy" look-at="#player" src="#enemy-sprite" transparent="true">
            <!-- Attach collision handler animations. -->
            <a-animation attribute="opacity" begin="collider-hit" dur="400"  
                         ease="linear" from="1" to="0"></a-animation>
            <a-animation attribute="scale" begin="collider-hit" dur="400"
                         ease="linear" to="0 0 0"></a-animation>
          </a-image>
        {% endfor %}
      </a-entity>
    </script>

    <!-- Attach collider to laser. -->
    <a-mixin id="laser"
             geometry="primitive: cylinder; radius: 0.05; translate: 0 -2 0"
             material="color: green; metalness: 0.2; opacity: 0.4; roughness: 0.3"
             rotation="90 0 0" projectile collider="target: .enemy"></a-mixin>
  </a-assets>

  <a-entity template="src: #enemies" data-num="10"></a-entity>

  <a-camera id="player" position="0 1.8 0" spawner="mixin: laser; on: click" click-listener></a-camera>

  <a-sky src="https://ucarecdn.com/49f8cfbc-ee57-444b-b954-3beb1709080c/"></a-sky>
</a-scene>