Skip to content

Instantly share code, notes, and snippets.

@nasser
Created April 15, 2019 14:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nasser/02b792ee3dcf3678e723d4238508e79b to your computer and use it in GitHub Desktop.
Save nasser/02b792ee3dcf3678e723d4238508e79b to your computer and use it in GitHub Desktop.
rough cut of the ajeeb framework and ecs
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