Created
April 2, 2019 20:38
-
-
Save sketchpunk/3f48831e63516c255669ca8c39ea3b0b to your computer and use it in GitHub Desktop.
Entity - Component - System ( ECS )
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//#################################################################################### | |
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; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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