src/core/Engine.ts
// Aurora is distributed under the MIT license.
import present from "present";
import Entity from "./Entity";
import System from "./System";
import getItem from "../utils/getItem";
import hasItem from "../utils/hasItem";
/**
* @module core
* @classdesc Core singleton representing an instance of the Aurora engine. The engine is
* responsible for the creation and registration of entities, as well as initialization and running
* of systems containing game logic.
*/
export default class Engine {
private _assemblies: Entity[];
private _entities: Entity[];
private _lastTickTime: number;
private _onTickComplete: Function; // Hook called when update is complete
private _onTickStart: Function; // Hook called when update starts
private _running: boolean; // Whether or not the engine is running
private _systems: System[];
/**
* @description Create an instance of the Aurora engine.
*/
constructor() {
// TODO: Build from JSON in the case of loading a save
console.log( "Aurora: Initializing a new engine." );
// These are the things which are actually saved per game
this._assemblies = [];
this._entities = [];
this._systems = [];
// The heart of the engine
this._running = false;
this._lastTickTime = null;
this._onTickStart = undefined;
this._onTickComplete = undefined;
return this;
}
/**
* @description Get all of the engine's assemblies.
* @readonly
* @returns {Entity[]} - Array of assembly (entity) instances
*/
get assemblies(): Entity[] {
return this._assemblies;
}
/**
* @description Get all of the engine's entities.
* @readonly
* @returns {Entity[]} - Array of entity instances
*/
get entities(): Entity[] {
return this._entities;
}
/**
* @description Get the function currently set to execute after every tick.
* @readonly
* @returns {Function} - Function currently set to execute
*/
get onTickComplete(): Function {
return this._onTickComplete;
}
/**
* @description Get the function currently set to execute before every tick.
* @readonly
* @returns {Function} - Function currently set to execute
*/
get onTickStart(): Function {
return this._onTickStart;
}
/**
* @description Get whether or not the engine is currently running.
* @readonly
* @returns {boolean} - True if the engine is running
*/
get running(): boolean {
return this._running;
}
/**
* @description Get all of the engine's systems.
* @readonly
* @returns {System[]} - Array of system instances
*/
get systems(): System[] {
return this._systems;
}
/**
* @description Get the timestamp of the engine's last tick.
* @readonly
* @returns {number} - Timestamp in milliseconds
*/
get lastTickTime() {
return this._lastTickTime;
}
/**
* @description Set a function to execute after every update tick.
* @param {Function} fn - Function to execute
*/
set onTickComplete( fn: Function ) {
this._onTickComplete = fn;
}
/**
* @description Set a function to execute before every update tick.
* @param {Function} fn - Function to execute
*/
set onTickStart( fn: Function ) {
this._onTickStart = fn;
}
/**
* @description Add an assembly (entity) instance to the engine.
* @param {Entity} assembly - Assembly instance
* @returns {Entity[]} - Array of assembly (entity) instances
*/
addAssembly( assembly: Entity ): Entity[] {
// Validate
if ( this.hasAssembly( assembly.type ) ) {
throw Error( "Assembly of that type has already been added!" );
}
// Freeze entity's structure
Object.seal( assembly );
this._assemblies.push( assembly );
return this._assemblies;
}
/**
* @description Add an entity instance to the engine. This will check which systems should watch
* it, and add it to those systems (running the entity through each system's onAdd hook. After
* being added and initialized, entities are immutable (although their component data is not).
* @param {Entity} entity - Entity instance
* @returns {Entity[]} - Array of entity instances
*/
addEntity( entity: Entity ): Entity[] {
// Validate
if ( this.hasEntity( entity.uuid ) ) {
throw Error( "Entity with that UUID has already been added!" );
}
// Freeze entity's structure
Object.seal( entity );
this._entities.push( entity );
// Check all systems to see if they should be watching this entity
this._systems.forEach( ( system ) => {
if ( entity.isWatchableBy( system ) ) {
system.watchEntity( entity );
}
});
return this._entities;
}
/**
* @description Add a system instance to the engine.
*
* This will run the system's onInit hook. After being added and initialized, systems are
* immutable and are updated every game tick.
* @param {System} system - System instance
* @returns {System[]} - Array of system instances
*/
addSystem( system: System ): System[] {
// Validate
if ( this.hasSystem( system.name ) ) {
throw Error( "System with that name has already been added!" );
}
// Add it and start it
this._systems.push( system );
system.init( this );
// Freeze entity's structure
Object.freeze( system );
return this._systems;
}
/**
* @description Get an assembly (entity) instance by type from the engine.
* @readonly
* @param {string} type - Assembly type
* @returns {Entity} - Requested assembly (entity) instance
*/
getAssembly( type: string ): Entity {
if ( !this.hasAssembly( type ) ) {
throw Error( "No assembly of that type found!" );
}
return getItem( type, this._assemblies, "type" );
}
/**
* @description Get an entity instance by UUID from the engine.
* @readonly
* @param {string} uuid - Entity UUID
* @returns {Entity} - Requested entity instance
*/
getEntity( uuid: string ): Entity {
if ( !this.hasEntity( uuid ) ) {
throw Error( "No enitity with that UUID found!" );
}
return getItem( uuid, this._entities, "uuid" );
}
/**
* @description Get a system instance by name from the engine.
* @readonly
* @param {string} name - System name
* @returns {System} - Requested system instance
*/
getSystem( name: string ): System {
if ( !this.hasSystem( name ) ) {
throw Error( "No system with that name found!" );
}
return getItem( name, this._systems, "name" );
}
/**
* @description Check if an assembly is present within the engine.
* @readonly
* @param {string} name - Assembly name
* @returns {boolean} - True if the assembly is present
*/
hasAssembly( type: string ): boolean {
return hasItem( type, this._assemblies, "type" );
}
/**
* @description Check if a system is present within the engine.
* @readonly
* @param {string} name - System name
* @returns {boolean} - True if the entity is present
*/
hasEntity( uuid: string ): boolean {
return hasItem( uuid, this._entities, "uuid" );
}
/**
* @description Check if a system is present within the engine.
* @readonly
* @param {string} name - System name
* @returns {boolean} - True if the system is present
*/
hasSystem( name: string ): boolean {
return hasItem( name, this._systems, "name" );
}
/**
* @description Start the execution of the update loop.
*/
start(): void {
// Always reset in case engine was stopped and restarted
this._lastTickTime = present();
// Start ticking!
this._running = true;
this.tick();
}
/**
* @description Stop the execution of the update loop.
*/
stop(): void {
this._running = false;
}
/**
* @description Perform one tick and update all systems.
*
* It is up to the user to decide how often to call this function. Typically on the client-side
* this would be called in requestAnimationFrame() and on the server-side this would simply be
* tied to a very fast setInterval() function (perhaps 10 ms).
*/
tick(): void {
if ( this._running ) {
const now = present();
const delta = now - this._lastTickTime;
this._lastTickTime = now;
// Run any pre-update behavior
if ( this._onTickStart ) {
this._onTickStart();
}
// Perform the update on every system
this._systems.forEach( ( system ) => {
system.update( delta );
});
// Run any post-update behavior
if ( this._onTickComplete ) {
this._onTickComplete();
}
}
}
/**
* @description Set all entities to "clean".
*
* When this function is invoked is up to the developer. In some cases it makes sense to invoke it
* at the beginning of each tick, in other cases after all entities are sent from a server to the
* clients (for example).
*/
cleanEntities() {
this._entities.forEach( ( entity ) => {
entity.dirty = false;
});
}
}