-
Notifications
You must be signed in to change notification settings - Fork 4
ERA Engine Guide
NOTE: This guide is out of date as of Feb 2, 2020.
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.
The ERA Engine is mostly built on the concept of two things:
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.
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.
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:
- Audio
- Controls/Bindings
- Environments
- Light/Shaders
- Model Loading
- Network Calls
- Physics Engines
- Renderer Pool
- Renderer Stats
- Settings
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.