Skip to content

Instantly share code, notes, and snippets.

@sketchpunk
Created April 2, 2019 20:38
Show Gist options
  • Save sketchpunk/3f48831e63516c255669ca8c39ea3b0b to your computer and use it in GitHub Desktop.
Save sketchpunk/3f48831e63516c255669ca8c39ea3b0b to your computer and use it in GitHub Desktop.
Entity - Component - System ( ECS )
//####################################################################################
function hashCode( str ){ // Create an hash Int based on string input.
let hash = 5381, i = str.length;
while(i) hash = (hash * 33) ^ str.charCodeAt(--i)
return hash >>> 0; // Force Negative bit to Positive;
}
function Components( com ){
switch( typeof com ){
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Register Function or Class to the Components List
case "function":
if(Components.list.has( com.name )){
console.log( "Component name already exist : %s", com.name );
return Components;
}
let typeID = hashCode( com.name ); // Generate Hash Integer
com.prototype.componentTypeID = typeID; // Add Type ID to Object's Prototype
com.prototype.componentIndex = null;
com.prototype.entity = null;
// Add object to list with extra meta data that may be needed.
let comData = {
object : com,
typeID : typeID,
instances : new Array(),
recycle : new Array(),
};
Components.list.set( com.name, comData );
//console.info( "Registered Component : %s, TypeID: %s", com.name, typeID );
return Components;
break;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Create a new component by using its Name
case "string":
let itm = Components.list.get( com ),
c = null;
//console.log("Requesting Component Instance", com);
if( itm.recycle.length != 0 ){
c = itm.recycle.pop();
//console.info("Using Recycled Instance -", com, c.componentIndex );
}else{
c = new itm.object();
c.componentIndex = itm.instances.length;
itm.instances.push( c );
//console.info("New Component Instance -", com, c.componentIndex );
}
return c;
break;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Create a new component by using its TypeID
case "number":
let v;
for([,v] of Components.list){
if( v.typeID == com ) return new v.object();
}
break;
}
return null;
}
Components.list = new Map();
Components.exists = function( name ){ return Components.list.has(name); }
Components.dispose = function( c ){
c.entity = null;
let itm = Components.list.get( c.constructor.name );
if( itm ) itm.recycle.push( c );
}
Components.getInstances = function( name ){
let itm = Components.list.get( name );
return ( itm )? itm.instances : null;
}
//####################################################################################
class Entity{
constructor( name = "", comList = null ){
this.info = {
id : 0, // Idenifier for Entity //randomID()
name : name, // Given name of entity
active : true, // Is entity Active
};
if( typeof comList == "string" ) Entity.addByName( this, comList );
else if( comList ) Entity.addByArray( this, comList );
}
///////////////////////////////////////////////
// COMPONENTS
///////////////////////////////////////////////
//Add component instance to Entity
static addCom( e, c ){
if( !c.componentTypeID ){ console.log("Entity.addCom : not an component instance"); return this; }
if( e[ c.constructor.name ] ){ console.log("Entity.addCom : component already exists", c.constructor.name ); return this; }
e[ c.constructor.name ] = c;
c.entity = e;
return this;
}
//Add Component by Component Name, Pass in Component Instance or Array of Component Names
static addByName( e, cName ){
let c;
if( !(c = Components( cName )) ){
console.log("Could not found component ", cName);
return null;
}
Entity.addCom( e, c );
return c;
}
static addByArray( e, ary ){
let i, c;
for( i of ary ){
if( c = Components(i) ) Entity.addCom( e, c );
else console.log("Could not found component ", i);
}
return Entity;
}
static removeCom( e, name ){
if( e[ name ] ){
Components.dispose( e[ name ] );
delete e[ name ];
}else console.error("Entity.removeCom name not found : ", name);
return Entity;
}
//////////////////////////////////////////////
// Object Handling
//////////////////////////////////////////////
static dispose( e ){
let c;
for( c in e ){
if( c != "info" ) Entity.removeCom( e, c );
}
}
}
//####################################################################################
class System{
constructor(){ this.active = true; }
}
//####################################################################################
class Ecs{
constructor(){
this.queryCache = new Map();
this.entities = new Array();
this.recycle = new Array(); // Cache Entities for reycling.
this.systems = new Array();
this.sysID = 0;
}
//////////////////////////////////////////////////////
// ENTITIES
//////////////////////////////////////////////////////
//Create a new Entity and add to the list automaticly
newEntity( eName="New_Entity", comList = null ){
let e = null;
if( this.recycle.length == 0 ){
e = new Entity( eName, comList );
if( e ) this.addEntity( e );
}else{
e = this.recycle.pop();
e.info.name = eName;
if( typeof comList == "string" ) Entity.addByName( e, comList );
else if( comList ) Entity.addByArray( e, comList );
}
return e;
}
//Add Entity Instance to the List
addEntity( e ){
e.info.id = this.entities.length;
this.entities.push( e );
return this;
}
endEntity( e ){
e.info.active = false;
e.info.name = "unnamed";
Entity.dispose( e ); // Remove Components and connection to em.
this.recycle.push( e ); // Save reference to recycle list.
return this;
}
getEntityByName( eName ){
let e;
for( e of this.Entities ) if( e.info.name == eName ) return e;
return null;
}
//////////////////////////////////////////////////////
// SYSTEMS
//////////////////////////////////////////////////////
addSystem( system, priority = 50, active=true ){
let order = this.sysID++,
itm = { system, priority, order, active, name:system.constructor.name },
saveIdx = -1,
i = -1,
s;
//...................................
//Find where on the area to put the system
for(s of this.systems){
i++;
if(s.priority < priority) continue; //Order by Priority First
if(s.priority == priority && s.order < order) continue; //Then by Order of Insertion
saveIdx = i;
break;
}
//...................................
//Insert system in the sorted location in the array.
if( saveIdx == -1 ) this.systems.push(itm);
else{
this.systems.push( null ); //Add blank space to the array
for(var x = this.systems.length-1; x > saveIdx; x--){ //Shift the array contents one index up
this.systems[ x ] = this.systems[ x-1 ];
}
this.systems[saveIdx] = itm; //Save new Item in its sorted location
}
//console.info("Adding System: %s with priority: %s and insert order of %d", system.constructor.name, priority, order );
return this;
}
updateSystems(){
let s;
for( s of this.systems ) if( s.active ) s.system.update( this );
return this;
}
removeSystem( sName ){
let i;
for(i=0; i < this.Systems.length; i++){
if(this.Systems[i].name == sName){
this.Systems.splice( i, 1 );
//console.log("Removing System :", sName);
break;
}
}
return this;
}
//////////////////////////////////////////////////////
// QUERIES
//////////////////////////////////////////////////////
clearQueries(){ this.queryCache.clear(); return this; }
queryEntities( comList, sortFunc=null ){
//.............................
//Check if query has already been cached
let queryName = "query~" + comList.join( "_" ),
out = this.queryCache.get( queryName );
if(out) return out;
//.............................
//Not in cache, Run search
let cLen = comList.length,
cFind = 0,
e,c;
out = new Array();
for( e of this.entities ){
cFind = 0;
for( c of comList ){
if(! e[c]) break; //Break Loop if Component Not Found
else cFind++; //Add to Find Count if Component Found.
}
if( cFind == cLen ) out.push( e );
}
//.............................
//Save Results for other systems
if(out.length > 0){
this.queryCache.set( queryName, out );
if(sortFunc) out.sort( sortFunc );
}
return out;
}
}
let ecs = new Ecs()
.addSystem( new TestSys(), 50, true );
let e = ecs.newEntity( "hero", ["Test"] );
ecs.updateSystems();
// COMPONENT
class Test{
constructor(){
this.string = "test";
}
} Components( Test );
// SYSTEM
class TestSys extends System{
update( ecs ){
// Example by searching entities that have a specific list of components
let list = ecs.queryEntities( ["Test"] );
for( let e of list ){
console.log("--", e.info.name );
}
// OR, just get the list of component instances, save time searching for entities.
list = Components.getInstances( "Test" );
for( let c of list ){
if( !c.entity ) continue;
console.log("++", c.entity.info.name );
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment