Skip to content

ERA Engine Guide

Greg Rogers edited this page Feb 2, 2020 · 12 revisions

Getting Started

NOTE: This guide is out of date as of Feb 2, 2020.

What's the point?

The ERA Engine is meant to provide cleaner and modular code structure when using Three.js and other peripherals when creating games and visuals. The ERA Engine is not an abstraction above Three.js, as one still needs to have a good understanding of how it works and make calls to the library directly.

What does it do/How does it work?

The ERA Engine is mostly built on the concept of two things:

Entity

The Entity class inherits from THREE.Object3D, and is available to extend by your own objects in-game. It allows you to override functions like generateMesh, generatePhysicsBody, and update, making custom objects easy to create and read.

Plugin

The Plugin class is also available to the developer, allowing them to build their own logic that should be updated every engine tick. When a Plugin is created, it is installed to the engine directly. Similar to the Entity class, it has an update function that the developer can override.

Core Features

There are a few core components that are built into the ERA Engine (some of which inherit from Plugin) These are meant to reduce boilerplate and handling needed to build a game or visual. To list them briefly:

Example

Let's jump into a direct example of how ERA is used. When creating an object in your scene, say, a laser bolt shot out of a cannon, without ERA, you'd need to do the following in your script:

Vanilla Three.js

// index.js
// THREE Scene init code
const camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight, 2, 2000);
const scene = new THREE.Scene();
const renderer = new THREE.WebGLRenderer();
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// Resize handling code
window.addEventListener('resize', onWindowResize, false);
function onWindowResize() {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
}

// Debug stats code
stats = new Stats();
document.body.appendChild( stats.dom );

// Load models. We only have one, though you can see how this would
// get complicated with more.
let cannon = null;
const loader = new THREE.GLTFLoader();
loader.load(cannon.gtlf, (gltf) => {
  cannon = gltf.scene;
  // Fire the "ready" function, starting rendering.
  onReady();
});

function onReady() {
  // Attach keydown listener.
  document.addEventListener('keydown', handleKeydown);
  requestAnimationFrame(render);
}

// Keep references to lasers in order to update them.
const lasers = new Map();

function render() {
  renderer.render(scene, camera);
  // Update laser. Becomes more complex if take direction into consideration.
  lasers.forEach((laser) => laser.position.z += 5);
  requestAnimationFrame(render);
}

function handleKeydown(e) {
  switch (e.keyCode) {
    case 32:
      fireLaser();
      break;
}

function fireLaser() {
  const geometry = new THREE.CubeGeometry(.3, .3, 8);
  const material = new THREE.MeshLambertMaterial({color: 0xff2222});
  const laser = new THREE.Mesh(geometry, material);
  laser.position.copy(cannon.position);
  lasers.set(laser.uuid, laser);
  scene.add(laser);
  // Remove the laser after a given time.
  setTimeout(() => {
    scene.remove(laser);
    lasers.delete(laser.uuid);
  }, 5000);
}

The above works just fine, but if we add any more complexity, it will become unreadable. What if we want to add a safety switch to the cannon? What if we have multiple cannons? How bloated will our render loop become?

With ERA, we break out of needing those references and other boilerplate, simplifying the code immensely. We still have our index.js file, but we've added cannon.js and laser.js:

ERA Engine

// index.js
async function start() {
  // Load model.
  await Models.get().loadFromFile('/', 'cannon', 'gtlf');

  // Create engine and load models.
  const engine = Engine.get().setCamera(Camera.get().buildPerspectiveCamera());
  engine.start();
  const scene = engine.getScene();

  // Enable debug stats.
  new RendererStats(engine.getRenderer());

  // Create cannon.
  const cannon = new Cannon().build();
  scene.add(cannon);

  // Attach controls to the cannon.
  Controls.get().registerEntity(cannon);
}

document.addEventListener('DOMContentLoaded', start);

We isolate controls logic to the Cannon class, which creates a new laser when the FIRE button is hit.

// cannon.js
const CANNON_BINDINGS = {
  FIRE: {
    keys: {
      keyboard: 32,
      controller: 'button5',
    }
  },
};
class Cannon extends Entity {
  /** @override */
  static GetBindings() {
    return new Bindings(CONTROLS_ID).load(CANNON_BINDINGS);
  }

  constructor() {
    super();
    // Uses the model that we loaded above as the mesh.
    this.modelName = 'cannon';
  }

  /** @override */
  getControlsId() {
    return 'cannon';
  }

  /** @override */
  update() {
    if (this.getActionValue(this.bindings.FIRE)) {
      this.fire();
    }
  }

  fire() {
    const laser = new Laser().build();
    laser.position.copy(this.position);
    Engine.get().getScene().add(laser);
  }

The Laser class simply updates its own position every tick, destroying itself when its lifetime is reached.

// laser.js
const GEOMETRY = new THREE.CubeGeometry(.3, .3, 8);
const MATERIAL = new THREE.MeshLambertMaterial({color: 0xff2222});
const LIFETIME = 2000;
const VELOCITY = 10;

class Laser extends Entity {
  constructor() {
    super();
    this.fireTime = Date.now();
  }

  /** @override */
  generateMesh() {
    return new THREE.Mesh(GEOMETRY, MATERIAL);
  }

  /** @override */
  update() {
    this.position.z += VELOCITY;
    if (Date.now() - this.fireTime > LIFETIME) {
      this.destroy();
    }
  }
}

There are 20 more lines in the ERA example than in the vanilla example, but we now have more structure and room to grow into a more complex system. We also have a bit more functionality than before, as controls have not only been registered to the cannon only (not globally), they also support controllers.

Clone this wiki locally