Created
April 15, 2019 14:41
-
-
Save nasser/02b792ee3dcf3678e723d4238508e79b to your computer and use it in GitHub Desktop.
rough cut of the ajeeb framework and ecs
This file contains 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
type Entity = number | |
export class ComponentStore { | |
name: string // for debugging | |
entities: Array<Entity>; | |
indices: Array<number>; | |
components: Array<any>; | |
size: number; | |
constructor(name:string) { | |
this.entities = [] | |
this.indices = [] | |
this.components = [] | |
this.size = 0; | |
this.name = name; | |
} | |
clear() { | |
this.entities.length = 0 | |
this.indices.length = 0 | |
this.components.length = 0 | |
this.size = 0 | |
} | |
insert(entity: Entity, component: any) { | |
this.entities[this.size] = entity; | |
this.components[this.size] = component; | |
this.indices[entity] = this.size; | |
this.size++; | |
} | |
replace(entity: Entity, component: any) { | |
this.components[this.indices[entity]] = component; | |
} | |
assign(entity: Entity, component: any) { | |
if (this.contains(entity)) | |
this.replace(entity, component); | |
else | |
this.insert(entity, component); | |
} | |
remove(entity: Entity) { | |
// TODO this is buggy | |
if (!this.contains(entity)) | |
return; | |
let lastComponent = this.components[this.size - 1]; | |
let lastEntity = this.entities[this.size - 1]; | |
this.components[this.indices[entity]] = lastComponent; | |
this.entities[this.indices[entity]] = lastEntity; | |
this.indices[lastEntity] = this.indices[entity]; | |
this.size--; | |
} | |
get(entity: Entity) { | |
return this.components[this.indices[entity]]; | |
} | |
contains(entity: Entity) { | |
return this.indices[entity] < this.size && this.entities[this.indices[entity]] == entity; | |
} | |
eachEntity(f: (e: Entity) => any) { | |
for (var i = this.size - 1; i >= 0; i--) { | |
f(this.entities[i]); | |
} | |
} | |
eachComponent(f: (c: any) => any) { | |
for (var i = this.size - 1; i >= 0; i--) { | |
f(this.components[i]); | |
} | |
} | |
each(f: (e: Entity, c: any) => any) { | |
for (var i = this.size - 1; i >= 0; i--) { | |
f(this.entities[i], this.components[i]); | |
} | |
} | |
} | |
export class CachedView { | |
componentStores:any = []; | |
packedComponents:ComponentStore = new ComponentStore("#packed "); | |
injest(componentStore) { | |
let __this = this; | |
let oldClear = componentStore.clear.bind(componentStore); | |
componentStore.clear = function() { | |
oldClear(); | |
__this.packedComponents.clear(); | |
} | |
let oldRemove = componentStore.remove.bind(componentStore); | |
componentStore.remove = function(entity) { | |
oldRemove(entity); | |
__this.packedComponents.remove(entity); | |
} | |
let oldInsert = componentStore.insert.bind(componentStore); | |
componentStore.insert = function(entity, component) { | |
oldInsert(entity, component); | |
// if entity has all the components then add it to packedComponents | |
for(let i=0; i<__this.componentStores.length; i++) { | |
if(!__this.componentStores[i].contains(entity)) { | |
return; | |
} | |
} | |
let packed = new Array(__this.componentStores.length); | |
for(let i=0; i<__this.componentStores.length; i++) { | |
packed[i] = (__this.componentStores[i].get(entity)) | |
} | |
__this.packedComponents.insert(entity, packed); | |
} | |
let oldReplace = componentStore.replace.bind(componentStore); | |
componentStore.replace = function(entity, component) { | |
oldReplace(entity, component); | |
// if entity has all the components then replace it in packedComponents | |
for(let i=0; i<__this.componentStores.length; i++) { | |
if(!__this.componentStores[i].contains(entity)) { | |
return; | |
} | |
} | |
let idx = __this.componentStores.indexOf(componentStore); | |
let packed = __this.packedComponents.get(entity) | |
packed[idx] = component; | |
} | |
} | |
constructor(componentStores) { | |
this.componentStores = componentStores; | |
this.packedComponents.name += this.componentStores.map(cs => cs.name).join(",") | |
for(let i=0; i<componentStores.length; i++) { | |
this.injest(componentStores[i]) | |
} | |
} | |
eachEntity(f) { this.packedComponents.eachEntity(f); } | |
eachComponent(f) { this.packedComponents.eachComponent(f); } | |
each(f) { this.packedComponents.each(f); } | |
} | |
export class View { | |
componentStores: Array<ComponentStore>; | |
constructor(componentStores) { | |
this.componentStores = componentStores; | |
} | |
each(f) { | |
if (this.componentStores.length == 0) | |
return; | |
if (this.componentStores.length == 1) | |
return this.componentStores[0].each(f); | |
let smallestSet = this.componentStores[0]; | |
for (var i = this.componentStores.length - 1; i >= 0; i--) | |
if (this.componentStores[i].size < smallestSet.size) | |
smallestSet = this.componentStores[i]; | |
var componentCount = this.componentStores.length; | |
var args = new Array(componentCount + 1); | |
for (var i = smallestSet.size - 1; i >= 0; i--) { | |
let e = smallestSet.entities[i]; | |
args[0] = e; | |
var j = 0; | |
for (; j < componentCount; j++) { | |
if (!this.componentStores[j].contains(e)) | |
break; | |
args[j + 1] = this.componentStores[j].get(e); | |
} | |
if (j < componentCount) | |
continue; | |
f.apply(null, args); | |
} | |
} | |
} | |
export class Registry { | |
entities: Array<Entity> = [] | |
available: number = 0 | |
next: number = 0 | |
create() { | |
if(this.available > 0) { | |
let entity = this.next | |
this.next = this.entities[entity] | |
this.entities[entity] = entity | |
this.available-- | |
return entity | |
} else { | |
let entity = this.entities.length | |
this.entities.push(entity) | |
return entity | |
} | |
} | |
destroy(entity:number) { | |
let node = (this.available > 0 ? this.next : -1) | |
this.entities[entity] = node | |
this.next = entity | |
this.available++ | |
} | |
// TODO might be wrong | |
valid(entity:number) { | |
return entity < this.entities.length && this.entities[entity] === entity | |
} | |
get count():number { | |
return this.entities.length - this.available; | |
} | |
} | |
export class ECS { | |
registry: Registry = new Registry() | |
inputs :any = {} | |
components: {[name:string]:ComponentStore} = {} | |
systems: any = [] | |
createSystems: {[name:string]:(e:Entity, component)=>any} = {} | |
destroySystems: {[name:string]:(e:Entity, component)=>any} = {} | |
coroutines: any = [] | |
input: any = null | |
// ****** | |
// ** input | |
// ****** | |
addInput(name, f) { | |
this.inputs[name] = f; | |
} | |
computeInput():any { | |
let input = {} | |
for(let name in this.inputs) { | |
input[name] = this.inputs[name](); | |
} | |
Object.freeze(input); | |
return input; | |
} | |
// ****** | |
// ** components | |
// ****** | |
component(name) { | |
if(this.components[name] === undefined) { | |
this.components[name] = new ComponentStore(name) | |
} | |
return this.components[name]; | |
} | |
getComponent(e, name) { | |
return this.component(name).get(e) | |
} | |
addComponent(e, name, cmpt) { | |
return this.component(name).assign(e, cmpt) | |
} | |
removeComponent(e, name) { | |
return this.component(name).remove(e) | |
} | |
hasComponent(e, name) { | |
return this.component(name).contains(e) | |
} | |
// ****** | |
// ** views | |
// ****** | |
view(names) { | |
let stores = [] | |
for(let i=0; i<names.length; i++) { | |
if(typeof names[i] === "string") | |
stores.push(this.component(names[i])) | |
else stores.push(names[i]) | |
} | |
return new View(stores); | |
} | |
cachedView(names) { | |
let stores = [] | |
for(let i=0; i<names.length; i++) { | |
if(typeof names[i] === "string") | |
stores.push(this.component(names[i])) | |
else stores.push(names[i]) | |
} | |
return new CachedView(stores); | |
} | |
// ****** | |
// ** systems | |
// ****** | |
addSystem(f) { | |
this.systems.push(f) | |
} | |
_getParamNames(func) { | |
const STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg; | |
const ARGUMENT_NAMES = /([^\s,]+)/g; | |
var fnStr = func.toString().replace(STRIP_COMMENTS, ''); | |
var result = fnStr.slice(fnStr.indexOf('[')+1, fnStr.indexOf(']')).match(ARGUMENT_NAMES); | |
if(result === null) | |
result = []; | |
return result; | |
} | |
onCreate(name, f) { | |
this.createSystems[name] = f | |
} | |
onDestroy(name, f) { | |
this.destroySystems[name] = f | |
} | |
// very sugary | |
system(f) { | |
let components = this._getParamNames(f) | |
let cached = this.cachedView(components); | |
this.addSystem(() => cached.each(f)); | |
} | |
// ****** | |
// ** entities | |
// ****** | |
create(spec) { | |
let newEntity = this.registry.create() | |
if(spec) | |
for(const prop in spec) { | |
let store = this.component(prop) | |
store.assign(newEntity, spec[prop]) | |
if(this.createSystems[prop]) | |
this.createSystems[prop](newEntity, spec[prop]) | |
} | |
return newEntity; | |
} | |
destroy(entity) { | |
for (const name in this.components) { | |
if(this.destroySystems[name]) | |
this.destroySystems[name](entity, this.components[name].get(entity)) | |
this.components[name].remove(entity); | |
} | |
this.registry.destroy(entity); | |
} | |
// ****** | |
// ** coroutines | |
// ****** | |
coro(gen) { | |
let g = gen.next ? gen : gen(); | |
this.coroutines.push(g) | |
} | |
// ****** | |
// ** set up sugar | |
// ****** | |
init(fns) { | |
if(typeof fns === "function") { | |
return fns(this); | |
} else { | |
for(const k in fns) { | |
this.init(fns[k]) | |
} | |
} | |
} | |
// ****** | |
// ** main loop | |
// ****** | |
update() { | |
// TODO not sure if passing input in as argument or globalish thing is better | |
// globalish is nice because it cleans up signatures and is more unity like | |
// argument is nice because its more local, maybe? | |
try { | |
this.input = this.computeInput() | |
let toRemove = [] | |
for(let i=0; i<this.coroutines.length; i++) { | |
let ret = this.coroutines[i].next() | |
if(ret.done) | |
toRemove.push(this.coroutines[i]) | |
} | |
for(let i=0; i<toRemove.length; i++) { | |
this.coroutines.splice(this.coroutines.indexOf(toRemove[i]),1) | |
} | |
for(let i=0; i<this.systems.length; i++) { | |
this.systems[i]() | |
} | |
} catch (e) { | |
console.error(e) | |
} | |
} | |
} | |
export function main(fn) { | |
let mainfn = function() { | |
fn() | |
requestAnimationFrame(mainfn); | |
} | |
mainfn() | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment