src/core/game.js
import { Entity } from './entity.js';
import { System } from './system.js';
import { Signal } from './signal.js';
import * as PIXI from 'pixi.js';
export class Game {
/**
* The Game class takes in dimensions of the game screen
* @param {number} width - width of game screen
* @param {number} height - height of game screen
* @param {Object} extra settings for the renderer
*/
constructor(width, height, rendererSettings) {
/**
* @type {boolean}
*/
this.running = false;
/**
* @type {boolean}
*/
this.hasSetup = false;
/**
* @type {number}
*/
this.width = width;
/**
* @type {number}
*/
this.height = height;
/**
* @type {Object}
*/
this.entities = {};
/**
* @type {Object}
*/
this.components = {};
/**
* @type {Object}
*/
this.systems = {};
/**
* @type {Object}
*/
this.callbacks = {};
/**
* @type {Array}
*/
this.callbacks.onStart = [];
/**
* @type {Array}
*/
this.callbacks.onUpdate = [];
/**
* @type {Object}
*/
this.rendererSettings = rendererSettings;
/**
* @type {Signal}
*/
this.signal = new Signal();
}
/**
* The game start function. Use onStart() callback to hook in here
*/
start() {
this.running = true;
this._rendererSetup(this.rendererSettings);
this.onStart();
// Run start callbacks
this.callbacks.onStart.forEach((systemObj) => {
systemObj.callback(systemObj.system);
});
// Run component callbacks
for(let i=0; i < Object.keys(this.entities).length; i++) {
let entity = this.entities[Object.keys(this.entities)[i]];
this.addEntityToStage(entity);
for(let j=0; j < Object.keys(entity.components).length; j++) {
entity.componentCallback(entity.components[Object.keys(entity.components)[j]]);
}
};
// Start game loop
global.requestAnimationFrame(this.update.bind(this));
}
stop() {
this.running = false;
}
/**
* The game update function. Use onUpdate() callback to hook into here
*/
update(time) {
if(!this.running) return;
this.onUpdate(time);
this.callbacks.onUpdate.forEach((systemObj) => {
systemObj.callback(systemObj.system, time);
});
global.requestAnimationFrame(this.update.bind(this));
}
/**
* The game update that happens every game frame/tick.
*/
onUpdate(time) {
// Override
}
/**
* The intialization function for the game.
*/
onStart() {
// Override
}
/**
* Create an array of entities & add them to the game
* @param {Entity[]} entities An array of entities
*/
createEntities(entities) {
for(let i=0; i<entities.length; i++) {
if(entities[i] instanceof Entity) {
this.addEntity(entities[i]);
} else {
throw new TypeError(`Trying to add an entity which is not of type 'Entity'`);
}
}
}
/**
* Create & add a single entity
* @param {Entity} entity
*/
addEntity(entity) {
if(entity instanceof Entity) {
this.entities[entity.id] = entity;
entity.game = this;
entity.attachComponents();
this.addEntityToStage(entity);
} else {
throw new TypeError(`Trying to add an entity which is not of type 'Entity'`);
}
}
/**
* Create & add multiple entities
* @param {Entity[]} array of entities
*/
addEntities(entities) {
entities.forEach((entity) => {
if(entity instanceof Entity) {
this.entities[entity.id] = entity;
entity.game = this;
entity.attachComponents();
this.addEntityToStage(entity);
} else {
throw new TypeError(`Trying to add an entity which is not of type 'Entity'`);
}
});
}
/**
* Remove entity from game
* @param {string} ID of entity
*/
removeEntity(id) {
if(this.entities[id] !== undefined) {
// Remove Pixi child for this entity
this.stage.children.forEach((child) => {
if(child.gameId == id) {
this.stage.removeChild(child);
}
});
// Remove components in the game global list
let entity = this.entities[id];
let compIds = Object.keys(entity.components);
compIds.forEach((compId) => {
this.components[compId].forEach((comp) => {
if(comp.entity.id == id) {
let index = this.components[comp.id].indexOf(comp);
if(index > -1) {
this.components[comp.id].splice(index, 1);
}
}
});
});
delete this.entities[id];
}
}
/**
* Register a system in the game instance
* @param {System} system
* @return {System}
*/
addSystem(system) {
if(this.systems[system.id] === undefined && system instanceof System) {
this._registerSystem(system);
return system;
} else {
throw(new TypeError(`Trying to add system which is not type of 'System'`));
}
}
/**
* Register multiple systems in the game instance
* @param {System[]} systems
*/
addSystems(systems) {
systems.forEach((system) => {
this.addSystem(system)
});
}
_registerSystem(system) {
system.game = this;
this.systems[system.id] = system;
if(system.onStart) {
this.callbacks.onStart.push({system: system, callback: system.onStart});
}
if(system.onUpdate) {
this.callbacks.onUpdate.push({system: system, callback: system.onUpdate});
}
}
/**
* Retrieve an entity
* @param {string} id - The ID of the entity
* @return {Entity} Returns the matching entity
*/
getEntity(id) {
if(this.entities[id] !== undefined) {
return this.entities[id];
}
}
/**
* Retrieves all entities that have all provided components.
* @param {string[]} components - A list of component ID's to match against
* @return {Entity[]}
*/
getEntitiesWith(components) {
let entities = [];
for(let i=0; i<Object.keys(this.entities).length; i++) {
let entity = this.entities[Object.keys(this.entities)[i]];
let count = 0;
let target = components.length;
for(let j=0; j<Object.keys(entity.components).length; j++) {
let key = Object.keys(entity.components)[j];
let result = components.find(c => c == entity.components[key].id);
if(result) count++
}
if(count == target) {
entities.push(entity);
}
}
return entities;
}
/**
* Retrive a system in the game
* @param {string} id - The ID of the system
*/
getSystem(id) {
return this.systems[id];
}
/**
* Retrieves a component from a specific entity
* @param {string} entityId - The entity to retrieve the component from
* @param {string} componentId - The component ID
* @return {Component}
*/
getComponentFromEntity(entityId, componentId) {
return this.getEntity(entityId).find(componentId);
}
_rendererSetup(settings) {
if(!this.hasSetup) {
this.PIXI = PIXI;
PIXI.utils.skipHello(); // Disables console log stuff;
this.application = new PIXI.Application({
width: this.width,
height: this.height,
antialias: settings.antialias ? settings.antialias : false,
transparent: settings.transparent ? settings.transparent : false,
resolution: settings.resolution ? settings.resolution : 1
});
this.stage = this.application.stage;
this.renderer = this.application.renderer;
this.renderer.autoResize = true;
document.body.appendChild(this.application.view);
this.hasSetup = true;
};
}
addEntityToStage(entity) {
if(this.stage) {
this.stage.addChild(entity.container);
}
}
}