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