feat: ecs
This commit is contained in:
parent
358509bcaa
commit
94addfb22a
116
packages/ecs/.gitignore
vendored
Normal file
116
packages/ecs/.gitignore
vendored
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Snowpack dependency directory (https://snowpack.dev/)
|
||||||
|
web_modules/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variables file
|
||||||
|
.env
|
||||||
|
.env.test
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
.cache/
|
||||||
|
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
# public
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
|
.vscode-test
|
||||||
|
|
||||||
|
# yarn v2
|
||||||
|
.yarn/cache
|
||||||
|
.yarn/unplugged
|
||||||
|
.yarn/build-state.yml
|
||||||
|
.yarn/install-state.gz
|
||||||
|
.pnp.*
|
24
packages/ecs/package.json
Normal file
24
packages/ecs/package.json
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"name": "@avocado/ecs",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"build": "flecks build",
|
||||||
|
"clean": "flecks clean",
|
||||||
|
"lint": "flecks lint",
|
||||||
|
"postversion": "cp package.json dist",
|
||||||
|
"test": "flecks test"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"build",
|
||||||
|
"index.js",
|
||||||
|
"index.js.map",
|
||||||
|
"src",
|
||||||
|
"test"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"@flecks/core": "^1.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@flecks/fleck": "^1.0.0"
|
||||||
|
}
|
||||||
|
}
|
331
packages/ecs/src/component.js
Normal file
331
packages/ecs/src/component.js
Normal file
|
@ -0,0 +1,331 @@
|
||||||
|
/* eslint-disable guard-for-in, max-classes-per-file, no-restricted-syntax */
|
||||||
|
import Schema from './schema';
|
||||||
|
|
||||||
|
class BaseComponent {
|
||||||
|
|
||||||
|
$$dirty = true;
|
||||||
|
|
||||||
|
map = [];
|
||||||
|
|
||||||
|
pool = [];
|
||||||
|
|
||||||
|
schema;
|
||||||
|
|
||||||
|
serializer;
|
||||||
|
|
||||||
|
constructor(schema) {
|
||||||
|
this.schema = schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
allocate() {
|
||||||
|
const [index] = this.allocateMany(1);
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
allocateMany(count) {
|
||||||
|
const results = [];
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
while (count-- > 0 && this.pool.length > 0) {
|
||||||
|
results.push(this.pool.pop());
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
create(entity, values) {
|
||||||
|
const [result] = this.createMany([entity, values]);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(entity) {
|
||||||
|
this.destroyMany([entity]);
|
||||||
|
}
|
||||||
|
|
||||||
|
destroyMany(entities) {
|
||||||
|
this.freeMany(entities.map((entity) => this.map[entity]).filter((index) => !!index));
|
||||||
|
for (let i = 0; i < entities.length; i++) {
|
||||||
|
this.map[entities[i]] = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get dirty() {
|
||||||
|
return this.$$dirty;
|
||||||
|
}
|
||||||
|
|
||||||
|
set dirty(dirty) {
|
||||||
|
this.$$dirty = dirty;
|
||||||
|
}
|
||||||
|
|
||||||
|
free(index) {
|
||||||
|
this.freeMany([index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
freeMany(indices) {
|
||||||
|
for (let i = 0; i < indices.length; ++i) {
|
||||||
|
this.pool.push(indices[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class FlatComponent extends BaseComponent {
|
||||||
|
|
||||||
|
chunkSize = 64;
|
||||||
|
|
||||||
|
caret = 0;
|
||||||
|
|
||||||
|
data = new ArrayBuffer(0);
|
||||||
|
|
||||||
|
Window;
|
||||||
|
|
||||||
|
window;
|
||||||
|
|
||||||
|
allocateMany(count) {
|
||||||
|
const results = super.allocateMany(count);
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
count -= results.length;
|
||||||
|
if (count > 0) {
|
||||||
|
const required = (this.caret + count) * this.schema.width;
|
||||||
|
if (required > this.data.byteLength) {
|
||||||
|
const chunkWidth = this.chunkSize * this.schema.width;
|
||||||
|
const remainder = required % chunkWidth;
|
||||||
|
const extra = 0 === remainder ? 0 : chunkWidth - remainder;
|
||||||
|
const size = required + extra;
|
||||||
|
const data = new ArrayBuffer(size);
|
||||||
|
(new Uint8Array(data)).set(this.data);
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < count; ++i) {
|
||||||
|
results.push(this.caret++);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
clean() {
|
||||||
|
if (!this.dirty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.Window) {
|
||||||
|
this.Window = this.makeWindowClass();
|
||||||
|
}
|
||||||
|
const window = new this.Window(this.data, this);
|
||||||
|
for (let i = 0; i < this.caret; ++i) {
|
||||||
|
window.dirty = false;
|
||||||
|
window.cursor += this.schema.width;
|
||||||
|
}
|
||||||
|
this.dirty = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
createMany(entries) {
|
||||||
|
if (entries.length > 0) {
|
||||||
|
const allocated = this.allocateMany(entries.length);
|
||||||
|
if (!this.Window) {
|
||||||
|
this.Window = this.makeWindowClass();
|
||||||
|
}
|
||||||
|
const window = new this.Window(this.data, this);
|
||||||
|
const {defaultValues} = this.schema;
|
||||||
|
for (let i = 0; i < entries.length; ++i) {
|
||||||
|
let entity;
|
||||||
|
let values = {};
|
||||||
|
if (Array.isArray(entries[i])) {
|
||||||
|
[entity, values] = entries[i];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
entity = entries[i];
|
||||||
|
}
|
||||||
|
this.map[entity] = allocated[i];
|
||||||
|
window.cursor = allocated[i] * this.schema.width;
|
||||||
|
for (const [i] of this.schema) {
|
||||||
|
if (i in values) {
|
||||||
|
window[i] = values[i];
|
||||||
|
}
|
||||||
|
else if (i in defaultValues) {
|
||||||
|
window[i] = defaultValues[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get(entity) {
|
||||||
|
if ('undefined' === typeof this.map[entity]) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (!this.Window) {
|
||||||
|
this.Window = this.makeWindowClass();
|
||||||
|
}
|
||||||
|
const window = new this.Window(this.data, this);
|
||||||
|
window.cursor = this.map[entity] * this.schema.width;
|
||||||
|
return window;
|
||||||
|
}
|
||||||
|
|
||||||
|
getUnsafe(entity) {
|
||||||
|
if ('undefined' === typeof this.map[entity]) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (!this.Window) {
|
||||||
|
this.Window = this.makeWindowClass();
|
||||||
|
}
|
||||||
|
if (!this.window) {
|
||||||
|
this.window = new this.Window(this.data, this);
|
||||||
|
}
|
||||||
|
this.window.cursor = this.map[entity] * this.schema.width;
|
||||||
|
return this.window;
|
||||||
|
}
|
||||||
|
|
||||||
|
makeWindowClass() {
|
||||||
|
class Window {
|
||||||
|
|
||||||
|
cursor = 0;
|
||||||
|
|
||||||
|
parent;
|
||||||
|
|
||||||
|
view;
|
||||||
|
|
||||||
|
constructor(data, parent) {
|
||||||
|
if (data) {
|
||||||
|
this.view = new DataView(data);
|
||||||
|
}
|
||||||
|
if (parent) {
|
||||||
|
this.parent = parent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
let offset = 0;
|
||||||
|
const properties = {};
|
||||||
|
const get = (type) => (
|
||||||
|
`return this.view.get${Schema.viewMethodFromType(type)}(this.cursor + ${offset}, true);`
|
||||||
|
);
|
||||||
|
const set = (type) => [
|
||||||
|
'this.parent.dirty = true;',
|
||||||
|
`this.view.set${Schema.viewMethodFromType(type)}(this.cursor + ${offset}, v, true);`,
|
||||||
|
`this.view.setUint8(this.cursor + ${this.schema.width - 1}, 1, true);`,
|
||||||
|
].join('');
|
||||||
|
/* eslint-disable no-new-func */
|
||||||
|
properties.dirty = {
|
||||||
|
get: new Function('', `return !!this.view.getUint8(this.cursor + ${this.schema.width - 1}, true);`),
|
||||||
|
set: new Function('v', `this.view.setUint8(this.cursor + ${this.schema.width - 1}, v ? 1 : 0, true);`),
|
||||||
|
};
|
||||||
|
for (const [i, spec] of this.schema) {
|
||||||
|
const {type} = spec;
|
||||||
|
properties[i] = {};
|
||||||
|
properties[i].get = new Function('', get(type));
|
||||||
|
properties[i].set = new Function('v', set(type));
|
||||||
|
offset += Schema.sizeOfType(type);
|
||||||
|
}
|
||||||
|
/* eslint-enable no-new-func */
|
||||||
|
Object.defineProperties(Window.prototype, properties);
|
||||||
|
return Window;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class ArbitraryComponent extends BaseComponent {
|
||||||
|
|
||||||
|
data = [];
|
||||||
|
|
||||||
|
Instance;
|
||||||
|
|
||||||
|
allocateMany(count) {
|
||||||
|
if (!this.Instance) {
|
||||||
|
this.Instance = this.constructor.instanceFromSchema(this.schema);
|
||||||
|
}
|
||||||
|
const results = super.allocateMany(count);
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
count -= results.length;
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
while (count--) {
|
||||||
|
results.push(this.data.push(new this.Instance()) - 1);
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
clean() {
|
||||||
|
if (!this.dirty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const i in this.map) {
|
||||||
|
this.data[this.map[i]].dirty = false;
|
||||||
|
}
|
||||||
|
this.dirty = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
createMany(entries) {
|
||||||
|
if (entries.length > 0) {
|
||||||
|
const allocated = this.allocateMany(entries.length);
|
||||||
|
for (let i = 0; i < entries.length; ++i) {
|
||||||
|
let entity;
|
||||||
|
let values = {};
|
||||||
|
if (Array.isArray(entries[i])) {
|
||||||
|
[entity, values] = entries[i];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
entity = entries[i];
|
||||||
|
}
|
||||||
|
this.map[entity] = allocated[i];
|
||||||
|
for (const j in values) {
|
||||||
|
if (this.schema.has(j)) {
|
||||||
|
this.data[allocated[i]][j] = values[j];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get(entity) {
|
||||||
|
return this.data[this.map[entity]];
|
||||||
|
}
|
||||||
|
|
||||||
|
getUnsafe(entity) {
|
||||||
|
return this.get(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
static instanceFromSchema(schema) {
|
||||||
|
const Instance = class {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.$$dirty = 1;
|
||||||
|
for (const [i, {defaultValue}] of schema) {
|
||||||
|
this[i] = defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
const properties = {};
|
||||||
|
properties.dirty = {
|
||||||
|
get: function get() {
|
||||||
|
return !!this.$$dirty;
|
||||||
|
},
|
||||||
|
set: function set(v) {
|
||||||
|
this.$$dirty = v;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
for (const [i] of schema) {
|
||||||
|
properties[i] = {
|
||||||
|
get: function get() {
|
||||||
|
return this[`$$${i}`];
|
||||||
|
},
|
||||||
|
set: function set(v) {
|
||||||
|
this[`$$${i}`] = v;
|
||||||
|
this.$$dirty = 1;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Object.defineProperties(Instance.prototype, properties);
|
||||||
|
return Instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ComponentRouter() {
|
||||||
|
const schema = new Schema(this.constructor.schema);
|
||||||
|
let RealClass;
|
||||||
|
if (schema.width > 0) {
|
||||||
|
RealClass = FlatComponent;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
RealClass = ArbitraryComponent;
|
||||||
|
}
|
||||||
|
return new RealClass(schema);
|
||||||
|
}
|
168
packages/ecs/src/ecs.js
Normal file
168
packages/ecs/src/ecs.js
Normal file
|
@ -0,0 +1,168 @@
|
||||||
|
/* eslint-disable guard-for-in, max-classes-per-file, no-restricted-syntax */
|
||||||
|
|
||||||
|
export default class Ecs {
|
||||||
|
|
||||||
|
$$actions = [];
|
||||||
|
|
||||||
|
$$caret = 1;
|
||||||
|
|
||||||
|
$$cleanEntities = new Set();
|
||||||
|
|
||||||
|
Components = {};
|
||||||
|
|
||||||
|
dirty = new Set();
|
||||||
|
|
||||||
|
$$entities = [];
|
||||||
|
|
||||||
|
$$pool = [];
|
||||||
|
|
||||||
|
$$systems = [];
|
||||||
|
|
||||||
|
constructor(Components) {
|
||||||
|
for (const i in Components) {
|
||||||
|
class Component extends Components[i] {
|
||||||
|
|
||||||
|
set dirty(dirty) {
|
||||||
|
super.dirty = dirty;
|
||||||
|
const it = this.$$cleanEntities.values();
|
||||||
|
let result = it.next();
|
||||||
|
while (!result.done) {
|
||||||
|
result = it.next();
|
||||||
|
}
|
||||||
|
for (let i = 0; i < this.$$entities.length; i++) {
|
||||||
|
if (this.get(this.$$entities[i])) {
|
||||||
|
this.dirty.add(this.$$entities[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
this.Components[i] = new Component();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addSystem(System) {
|
||||||
|
const system = new System(this.Components);
|
||||||
|
this.$$systems.push(system);
|
||||||
|
return system;
|
||||||
|
}
|
||||||
|
|
||||||
|
create(components) {
|
||||||
|
const [entity] = this.createMany(1, components);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
createMany(count, components) {
|
||||||
|
const componentKeys = Object.keys(components);
|
||||||
|
const entities = [];
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
while (this.$$pool.length > 0 && count--) {
|
||||||
|
const entity = this.$$pool.pop();
|
||||||
|
entities.push(entity);
|
||||||
|
this.$$entities[entity] = componentKeys.slice(0);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
while (count--) {
|
||||||
|
const entity = this.$$caret++;
|
||||||
|
entities.push(entity);
|
||||||
|
this.$$entities[entity] = componentKeys.slice(0);
|
||||||
|
}
|
||||||
|
for (const i in components) {
|
||||||
|
this.Components[i].createMany(entities.map((entity) => [entity, components[i](entity)]));
|
||||||
|
}
|
||||||
|
for (let i = 0; i < this.$$systems.length; i++) {
|
||||||
|
this.$$systems[i].reindex(entities);
|
||||||
|
}
|
||||||
|
return entities;
|
||||||
|
}
|
||||||
|
|
||||||
|
destroyMany(entities) {
|
||||||
|
const map = {};
|
||||||
|
for (let i = 0; i < entities.length; i++) {
|
||||||
|
for (let j = 0; j < this.$$entities[entities[i]].length; j++) {
|
||||||
|
const key = this.$$entities[entities[i]][j];
|
||||||
|
if (!map[key]) {
|
||||||
|
map[key] = [];
|
||||||
|
}
|
||||||
|
map[key].push(entities[i]);
|
||||||
|
}
|
||||||
|
this.$$pool.push(entities[i]);
|
||||||
|
}
|
||||||
|
for (const i in map) {
|
||||||
|
this.Components[i].destroyMany(map[i]);
|
||||||
|
}
|
||||||
|
for (let i = 0; i < this.$$systems.length; i++) {
|
||||||
|
this.$$systems[i].deindex(entities);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get(entity, Components = Object.keys(this.Components)) {
|
||||||
|
const result = {};
|
||||||
|
for (let i = 0; i < Components.length; i++) {
|
||||||
|
const component = this.Components[Components[i]].get(entity);
|
||||||
|
if ('undefined' !== typeof component) {
|
||||||
|
result[Components[i]] = component;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
insertMany(components) {
|
||||||
|
const unique = new Set();
|
||||||
|
for (const i in components) {
|
||||||
|
const entities = components[i];
|
||||||
|
for (let j = 0; j < entities.length; ++j) {
|
||||||
|
let entity;
|
||||||
|
if (Array.isArray(entities[j])) {
|
||||||
|
[entity] = entities[j];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
entity = entities[j];
|
||||||
|
}
|
||||||
|
if (!this.$$entities[entity].includes(i)) {
|
||||||
|
this.$$entities[entity].push(i);
|
||||||
|
}
|
||||||
|
unique.add(entity);
|
||||||
|
}
|
||||||
|
this.Components[i].createMany(entities);
|
||||||
|
}
|
||||||
|
const uniqueArray = Array.from(unique.values());
|
||||||
|
for (let i = 0; i < this.$$systems.length; i++) {
|
||||||
|
this.$$systems[i].reindex(uniqueArray);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tick(elapsed) {
|
||||||
|
for (let i = 0; i < this.$$systems.length; i++) {
|
||||||
|
this.$$systems[i].tick(elapsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tickClean() {
|
||||||
|
for (const i in this.Components) {
|
||||||
|
this.Components[i].clean();
|
||||||
|
}
|
||||||
|
this.dirty = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
tickFinalize() {
|
||||||
|
this.tickDestruction();
|
||||||
|
this.tickClean();
|
||||||
|
}
|
||||||
|
|
||||||
|
tickDestruction() {
|
||||||
|
const unique = new Set();
|
||||||
|
for (let i = 0; i < this.$$systems.length; i++) {
|
||||||
|
for (let j = 0; j < this.$$systems[i].destroying.length; j++) {
|
||||||
|
unique.add(this.$$systems[i].destroying[j]);
|
||||||
|
}
|
||||||
|
this.$$systems[i].tickDestruction();
|
||||||
|
}
|
||||||
|
const destroying = Array.from(unique.values());
|
||||||
|
if (destroying.length > 0) {
|
||||||
|
this.destroyMany(destroying);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
104
packages/ecs/src/index.js
Normal file
104
packages/ecs/src/index.js
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
import Component from './component';
|
||||||
|
import Ecs from './ecs';
|
||||||
|
import Schema from './schema';
|
||||||
|
import Serializer from './serializer';
|
||||||
|
import System from './system';
|
||||||
|
|
||||||
|
const N = 100;
|
||||||
|
const warm = 500;
|
||||||
|
|
||||||
|
class Position extends Component {
|
||||||
|
|
||||||
|
static schema = {
|
||||||
|
x: {type: 'int32', defaultValue: 32},
|
||||||
|
y: 'int32',
|
||||||
|
z: 'int32',
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class Direction extends Component {
|
||||||
|
|
||||||
|
static schema = {
|
||||||
|
direction: 'uint8',
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class ZSystem extends System {
|
||||||
|
|
||||||
|
static queries() {
|
||||||
|
return {
|
||||||
|
default: ['Position', 'Direction'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
tick() {
|
||||||
|
for (let [position, {direction}, entity] of this.select('default')) {
|
||||||
|
position.z = entity * direction * 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const ecs = new Ecs({Direction, Position});
|
||||||
|
ecs.addSystem(ZSystem);
|
||||||
|
|
||||||
|
const createMany = () => {
|
||||||
|
const entities = ecs.createMany(N, {Position: (entity) => ({y: entity})});
|
||||||
|
ecs.insertMany({Direction: entities.map((entity) => [entity, {direction: 1 + entity % 4}])});
|
||||||
|
return entities;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let i = 0; i < warm; ++i) {
|
||||||
|
ecs.destroyMany(createMany());
|
||||||
|
}
|
||||||
|
performance.mark('create0');
|
||||||
|
createMany();
|
||||||
|
performance.mark('create1');
|
||||||
|
|
||||||
|
for (let i = 0; i < warm; ++i) {
|
||||||
|
ecs.tick(0.01);
|
||||||
|
ecs.tickFinalize();
|
||||||
|
}
|
||||||
|
performance.mark('tick0');
|
||||||
|
ecs.tick(0.01);
|
||||||
|
ecs.tickFinalize();
|
||||||
|
performance.mark('tick1');
|
||||||
|
console.log(ecs.$$systems[0].queries.default.count);
|
||||||
|
|
||||||
|
performance.measure(`create ${N}`, 'create0', 'create1');
|
||||||
|
performance.measure(`tick ${N}`, 'tick0', 'tick1');
|
||||||
|
|
||||||
|
const {Position: p} = ecs.get(1);
|
||||||
|
console.log({
|
||||||
|
x: p.x,
|
||||||
|
y: p.y,
|
||||||
|
z: p.z,
|
||||||
|
dirty: p.dirty,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(performance.getEntriesByType('measure').map(({duration, name}) => ({duration, name})));
|
||||||
|
|
||||||
|
// const schema = new Schema({
|
||||||
|
// foo: 'uint8',
|
||||||
|
// bar: 'string',
|
||||||
|
// });
|
||||||
|
|
||||||
|
// const serializer = new Serializer(schema);
|
||||||
|
// // const source = {foo: 8, bar: 'hello'};
|
||||||
|
// // const size = schema.sizeOf(source);
|
||||||
|
|
||||||
|
// const buffer = new ArrayBuffer(10 + 8);
|
||||||
|
// const view = new DataView(buffer);
|
||||||
|
// serializer.encode({foo: 8, bar: 'hello'}, view);
|
||||||
|
// serializer.encode({foo: 8, bar: 'sup'}, view, 10);
|
||||||
|
// console.log(buffer);
|
||||||
|
|
||||||
|
// const thing = {};
|
||||||
|
// serializer.decode(view, thing);
|
||||||
|
// console.log(thing);
|
||||||
|
// serializer.decode(view, thing, 10);
|
||||||
|
// console.log(thing);
|
85
packages/ecs/src/query.js
Normal file
85
packages/ecs/src/query.js
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
export default class Query {
|
||||||
|
|
||||||
|
$$compiled = {with: [], without: []};
|
||||||
|
|
||||||
|
$$index = [];
|
||||||
|
|
||||||
|
constructor(parameters, Components) {
|
||||||
|
for (let i = 0; i < parameters.length; ++i) {
|
||||||
|
const parameter = parameters[i];
|
||||||
|
switch (parameter.charCodeAt(0)) {
|
||||||
|
case '!'.charCodeAt(0):
|
||||||
|
this.$$compiled.without.push(Components[parameter.slice(1)]);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.$$compiled.with.push(Components[parameter]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get count() {
|
||||||
|
return this.$$index.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
deindex(entities) {
|
||||||
|
for (let i = 0; i < entities.length; ++i) {
|
||||||
|
const index = this.$$index.indexOf(entities[i]);
|
||||||
|
if (-1 !== index) {
|
||||||
|
this.$$index.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reindex(entities) {
|
||||||
|
for (let i = 0; i < entities.length; ++i) {
|
||||||
|
const entity = entities[i];
|
||||||
|
const index = this.$$index.indexOf(entity);
|
||||||
|
let should = true;
|
||||||
|
for (let j = 0; j < this.$$compiled.with.length; ++j) {
|
||||||
|
const C = this.$$compiled.with[j];
|
||||||
|
if ('undefined' === typeof C.getUnsafe(entity)) {
|
||||||
|
should = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (should) {
|
||||||
|
for (let j = 0; j < this.$$compiled.without.length; ++j) {
|
||||||
|
const C = this.$$compiled.without[j];
|
||||||
|
if ('undefined' !== typeof C.getUnsafe(entity)) {
|
||||||
|
should = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (should && -1 === index) {
|
||||||
|
this.$$index.push(entity);
|
||||||
|
}
|
||||||
|
else if (!should && -1 !== index) {
|
||||||
|
this.$$index.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
select() {
|
||||||
|
const entities = this.$$index.slice(0);
|
||||||
|
return {
|
||||||
|
[Symbol.iterator]() {
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
next: () => {
|
||||||
|
if (0 === entities.length) {
|
||||||
|
return {done: true};
|
||||||
|
}
|
||||||
|
const entity = entities.pop();
|
||||||
|
const value = [];
|
||||||
|
for (let i = 0; i < this.$$compiled.with.length; ++i) {
|
||||||
|
value.push(this.$$compiled.with[i].getUnsafe(entity));
|
||||||
|
}
|
||||||
|
value.push(entity);
|
||||||
|
return {done: false, value};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
108
packages/ecs/src/schema.js
Normal file
108
packages/ecs/src/schema.js
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
/* eslint-disable guard-for-in, no-restricted-syntax */
|
||||||
|
|
||||||
|
export default class Schema {
|
||||||
|
|
||||||
|
defaultValues = {};
|
||||||
|
|
||||||
|
width = 1;
|
||||||
|
|
||||||
|
spec;
|
||||||
|
|
||||||
|
constructor(spec) {
|
||||||
|
this.spec = this.constructor.normalize(spec);
|
||||||
|
for (const i in this.spec) {
|
||||||
|
const {type} = spec[i];
|
||||||
|
const size = this.constructor.sizeOfType(type);
|
||||||
|
if (0 === size) {
|
||||||
|
this.width = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this.width += size;
|
||||||
|
}
|
||||||
|
for (const i in this.spec) {
|
||||||
|
this.defaultValues[i] = spec[i].defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Symbol.iterator]() {
|
||||||
|
const keys = Object.keys(this.spec);
|
||||||
|
return {
|
||||||
|
next: () => {
|
||||||
|
if (0 === keys.length) {
|
||||||
|
return {done: true};
|
||||||
|
}
|
||||||
|
const key = keys.shift();
|
||||||
|
return {
|
||||||
|
done: false,
|
||||||
|
value: [key, this.spec[key]],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
has(key) {
|
||||||
|
return key in this.spec;
|
||||||
|
}
|
||||||
|
|
||||||
|
static viewMethodFromType(type) {
|
||||||
|
const capitalizedType = `${type.slice(0, 1).toUpperCase()}${type.slice(1)}`;
|
||||||
|
switch (type) {
|
||||||
|
case 'uint8':
|
||||||
|
case 'int8':
|
||||||
|
case 'uint16':
|
||||||
|
case 'int16':
|
||||||
|
case 'uint32':
|
||||||
|
case 'int32':
|
||||||
|
case 'float32':
|
||||||
|
case 'float64': {
|
||||||
|
return capitalizedType;
|
||||||
|
}
|
||||||
|
case 'int64':
|
||||||
|
case 'uint64': {
|
||||||
|
return `Big${capitalizedType}`;
|
||||||
|
}
|
||||||
|
default: return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static normalize(spec) {
|
||||||
|
const normalized = {};
|
||||||
|
for (const i in spec) {
|
||||||
|
normalized[i] = 'string' === typeof spec[i] ? {type: spec[i]} : spec[i];
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
sizeOf(instance) {
|
||||||
|
let fullSize = 0;
|
||||||
|
for (const i in this.spec) {
|
||||||
|
const {type} = this.spec[i];
|
||||||
|
const size = this.constructor.sizeOfType(type);
|
||||||
|
if (0 === size) {
|
||||||
|
switch (type) {
|
||||||
|
case 'string':
|
||||||
|
fullSize += 4;
|
||||||
|
fullSize += instance[i].length;
|
||||||
|
break;
|
||||||
|
default: throw new TypeError(`can't measure size of ${type}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
fullSize += size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fullSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
static sizeOfType(type) {
|
||||||
|
switch (type) {
|
||||||
|
case 'uint8': case 'int8': return 1;
|
||||||
|
case 'uint16': case 'int16': return 2;
|
||||||
|
case 'uint32': case 'int32': return 4;
|
||||||
|
case 'float32': return 4;
|
||||||
|
case 'float64': case 'int64': case 'uint64': return 8;
|
||||||
|
default: return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
66
packages/ecs/src/serializer.js
Normal file
66
packages/ecs/src/serializer.js
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
/* eslint-disable guard-for-in, no-restricted-syntax */
|
||||||
|
import Schema from './schema';
|
||||||
|
|
||||||
|
export default class Serializer {
|
||||||
|
|
||||||
|
constructor(schema) {
|
||||||
|
this.schema = schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
decode(view, destination, offset = 0) {
|
||||||
|
let cursor = offset;
|
||||||
|
for (const [key, def] of this.schema) {
|
||||||
|
const {type} = def;
|
||||||
|
const size = Schema.sizeOfType(type);
|
||||||
|
const viewMethod = Schema.viewMethodFromType(type);
|
||||||
|
let value;
|
||||||
|
if (viewMethod) {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
value = view[`get${viewMethod}`](cursor, true);
|
||||||
|
cursor += size;
|
||||||
|
}
|
||||||
|
switch (type) {
|
||||||
|
case 'string': {
|
||||||
|
const length = view.getUint32(cursor, true);
|
||||||
|
cursor += 4;
|
||||||
|
const {buffer, byteOffset} = view;
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
value = decoder.decode(new DataView(buffer, byteOffset + cursor, length));
|
||||||
|
cursor += length;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
destination[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
encode(source, view, offset = 0) {
|
||||||
|
let cursor = offset;
|
||||||
|
for (const [key, def] of this.schema) {
|
||||||
|
const {type} = def;
|
||||||
|
const size = Schema.sizeOfType(type);
|
||||||
|
const viewMethod = Schema.viewMethodFromType(type);
|
||||||
|
if (viewMethod) {
|
||||||
|
view[`set${viewMethod}`](cursor, source[key], true);
|
||||||
|
cursor += size;
|
||||||
|
}
|
||||||
|
switch (type) {
|
||||||
|
case 'string': {
|
||||||
|
const {length} = source[key];
|
||||||
|
view.setUint32(cursor, length, true);
|
||||||
|
cursor += 4;
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const bytes = encoder.encode(source[key]);
|
||||||
|
for (let i = 0; i < bytes.length; ++i) {
|
||||||
|
view.setUint8(cursor++, bytes[i]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
49
packages/ecs/src/system.js
Normal file
49
packages/ecs/src/system.js
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
/* eslint-disable guard-for-in, no-restricted-syntax */
|
||||||
|
|
||||||
|
import Query from './query';
|
||||||
|
|
||||||
|
export default class System {
|
||||||
|
|
||||||
|
destroying = [];
|
||||||
|
|
||||||
|
queries = {};
|
||||||
|
|
||||||
|
constructor(Components) {
|
||||||
|
const queries = this.constructor.queries();
|
||||||
|
for (const i in queries) {
|
||||||
|
this.queries[i] = new Query(queries[i], Components);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deindex(entities) {
|
||||||
|
for (const i in this.queries) {
|
||||||
|
this.queries[i].deindex(entities);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destroyEntity(entity) {
|
||||||
|
this.destroyManyEntities([entity]);
|
||||||
|
}
|
||||||
|
|
||||||
|
destroyManyEntities(entities) {
|
||||||
|
for (let i = 0; i < entities.length; i++) {
|
||||||
|
this.destroying.push(entities[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reindex(entities) {
|
||||||
|
for (const i in this.queries) {
|
||||||
|
this.queries[i].reindex(entities);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
select(query) {
|
||||||
|
return this.queries[query].select();
|
||||||
|
}
|
||||||
|
|
||||||
|
tickDestruction() {
|
||||||
|
this.deindex(this.destroying);
|
||||||
|
this.destroying = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user