refactor: less magic

This commit is contained in:
cha0s 2024-06-26 21:08:09 -05:00
parent 9bae378ac7
commit 45cb158f2a
28 changed files with 489 additions and 511 deletions

View File

@ -1,3 +1,7 @@
export default { import Component from '@/ecs/component.js';
export default class Animation extends Component {
static properties = {
frame: {type: 'uint16'}, frame: {type: 'uint16'},
}; };
}

View File

@ -1,4 +1,8 @@
export default { import Component from '@/ecs/component.js';
export default class AreaSize extends Component {
static properties = {
x: {type: 'uint16'}, x: {type: 'uint16'},
y: {type: 'uint16'}, y: {type: 'uint16'},
};
} }

View File

@ -1,4 +1,8 @@
export default { import Component from '@/ecs/component.js';
export default class Camera extends Component {
static properties = {
x: {type: 'uint16'}, x: {type: 'uint16'},
y: {type: 'uint16'}, y: {type: 'uint16'},
};
} }

View File

@ -1,4 +1,7 @@
export default { import Component from '@/ecs/component.js';
export default class Controlled extends Component {
static properties = {
locked: {type: 'uint8'}, locked: {type: 'uint8'},
moveUp: {type: 'float32'}, moveUp: {type: 'float32'},
moveRight: {type: 'float32'}, moveRight: {type: 'float32'},
@ -6,3 +9,4 @@ export default {
moveLeft: {type: 'float32'}, moveLeft: {type: 'float32'},
changeSlot: {type: 'int8'}, changeSlot: {type: 'int8'},
}; };
}

View File

@ -1,3 +1,7 @@
export default { import Component from '@/ecs/component.js';
export default class Direction extends Component {
static properties = {
direction: {type: 'uint8'}, direction: {type: 'uint8'},
}; };
}

View File

@ -1,3 +1,7 @@
export default { import Component from '@/ecs/component.js';
export default class Ecs extends Component {
static properties = {
path: {type: 'string'}, path: {type: 'string'},
};
} }

View File

@ -1,7 +1,6 @@
import Schema from '@/ecs/schema.js'; import Component from '@/ecs/component.js';
export default function(Component) { export default class Emitter extends Component {
return class Emitter extends Component {
mergeDiff(original, update) { mergeDiff(original, update) {
const merged = {}; const merged = {};
if (update.emit) { if (update.emit) {
@ -23,9 +22,4 @@ export default function(Component) {
} }
}; };
} }
static schema = new Schema({
type: 'object',
properties: {},
});
}
} }

View File

@ -1 +1,3 @@
export default {}; import Component from '@/ecs/component.js';
export default class Engine extends Component {}

View File

@ -1,6 +1,10 @@
export default { import Component from '@/ecs/component.js';
export default class Forces extends Component {
static properties = {
forceX: {type: 'float32'}, forceX: {type: 'float32'},
forceY: {type: 'float32'}, forceY: {type: 'float32'},
impulseX: {type: 'float32'}, impulseX: {type: 'float32'},
impulseY: {type: 'float32'}, impulseY: {type: 'float32'},
};
} }

View File

@ -1,3 +1,7 @@
export default { import Component from '@/ecs/component.js';
export default class Health extends Component {
static properties = {
health: {type: 'uint32'}, health: {type: 'uint32'},
}; };
}

View File

@ -1,37 +1,14 @@
import Component from '@/ecs/component.js';
import Schema from '@/ecs/schema.js';
import gather from '@/util/gather.js'; import gather from '@/util/gather.js';
const specificationsAndOrDecorators = gather( const Gathered = gather(
import.meta.glob('./*.js', {eager: true, import: 'default'}), import.meta.glob('./*.js', {eager: true, import: 'default'}),
); );
const Components = {}; const Components = {};
for (const componentName in specificationsAndOrDecorators) { for (const componentName in Gathered) {
// TODO: byKey, byId, ... Components[componentName] = class Named extends Gathered[componentName] {
if (Number.isInteger(+componentName)) {
continue;
}
const specificationOrDecorator = specificationsAndOrDecorators[componentName];
if ('function' === typeof specificationOrDecorator) {
Components[componentName] = specificationOrDecorator(
class Decorated extends Component {
static componentName = componentName; static componentName = componentName;
} };
);
if (!Components[componentName]) {
throw new Error(`Component ${componentName} decorator returned nothing`);
}
}
else {
Components[componentName] = class WrappedComponent extends Component {
static componentName = componentName;
static schema = new Schema({
type: 'object',
properties: specificationOrDecorator,
});
}
}
} }
export default Components; export default Components;

View File

@ -1,7 +1,6 @@
import Schema from '@/ecs/schema.js'; import Component from '@/ecs/component.js';
export default function(Component) { export default class Inventory extends Component {
return class Inventory extends Component {
insertMany(entities) { insertMany(entities) {
for (const [id, {slotChange}] of entities) { for (const [id, {slotChange}] of entities) {
if (slotChange) { if (slotChange) {
@ -95,9 +94,7 @@ export default function(Component) {
}; };
return Instance; return Instance;
} }
static schema = new Schema({ static properties = {
type: 'object',
properties: {
slots: { slots: {
type: 'map', type: 'map',
value: { value: {
@ -108,7 +105,5 @@ export default function(Component) {
}, },
}, },
}, },
}, };
});
}
} }

View File

@ -1 +1,3 @@
export default {}; import Component from '@/ecs/component.js';
export default class MainEntity extends Component {}

View File

@ -1,7 +1,6 @@
import Schema from '@/ecs/schema.js'; import Component from '@/ecs/component.js';
export default function(Component) { export default class Position extends Component {
return class Wielder extends Component {
instanceFromSchema() { instanceFromSchema() {
const Instance = super.instanceFromSchema(); const Instance = super.instanceFromSchema();
const Component = this; const Component = this;
@ -18,12 +17,8 @@ export default function(Component) {
}); });
return Instance; return Instance;
} }
static schema = new Schema({ static properties = {
type: 'object',
properties: {
x: {type: 'float32'}, x: {type: 'float32'},
y: {type: 'float32'}, y: {type: 'float32'},
}, };
});
}
} }

View File

@ -1 +0,0 @@
export default {};

View File

@ -1,7 +1,6 @@
import Schema from '@/ecs/schema.js'; import Component from '@/ecs/component.js';
export default function(Component) { export default class Sound extends Component {
return class Sound extends Component {
mergeDiff(original, update) { mergeDiff(original, update) {
const merged = {}; const merged = {};
if (update.play) { if (update.play) {
@ -21,9 +20,4 @@ export default function(Component) {
} }
}; };
} }
static schema = new Schema({
type: 'object',
properties: {},
});
}
} }

View File

@ -1,3 +1,7 @@
export default { import Component from '@/ecs/component.js';
export default class Speed extends Component {
static properties = {
speed: {type: 'float32'}, speed: {type: 'float32'},
}; };
}

View File

@ -1,5 +1,9 @@
import Component from '@/ecs/component.js';
import vector2d from "./helpers/vector-2d"; import vector2d from "./helpers/vector-2d";
export default {
export default class Sprite extends Component {
static properties = {
anchor: vector2d('float32', {x: 0.5, y: 0.5}), anchor: vector2d('float32', {x: 0.5, y: 0.5}),
animation: {type: 'string'}, animation: {type: 'string'},
elapsed: {type: 'float32'}, elapsed: {type: 'float32'},
@ -8,3 +12,4 @@ export default {
source: {type: 'string'}, source: {type: 'string'},
speed: {type: 'float32'}, speed: {type: 'float32'},
}; };
}

View File

@ -1,7 +1,6 @@
import Schema from '@/ecs/schema.js'; import Component from '@/ecs/component.js';
export default function(Component) { export default class Ticking extends Component {
return class Ticking extends Component {
instanceFromSchema() { instanceFromSchema() {
const Instance = super.instanceFromSchema(); const Instance = super.instanceFromSchema();
@ -34,11 +33,7 @@ export default function(Component) {
} }
return Instance; return Instance;
} }
static schema = new Schema({ static properties = {
type: 'object',
properties: {
isTicking: {defaultValue: 1, type: 'uint8'}, isTicking: {defaultValue: 1, type: 'uint8'},
},
});
}; };
} }

View File

@ -1,9 +1,8 @@
import Component from '@/ecs/component.js';
import vector2d from './helpers/vector-2d'; import vector2d from './helpers/vector-2d';
import Schema from '@/ecs/schema.js'; export default class TileLayers extends Component {
export default function(Component) {
return class TileLayers extends Component {
insertMany(entities) { insertMany(entities) {
for (const [id, {layerChange}] of entities) { for (const [id, {layerChange}] of entities) {
if (layerChange) { if (layerChange) {
@ -86,9 +85,7 @@ export default function(Component) {
}; };
return Instance; return Instance;
} }
static schema = new Schema({ static properties = {
type: 'object',
properties: {
layers: { layers: {
type: 'array', type: 'array',
subtype: { subtype: {
@ -106,7 +103,5 @@ export default function(Component) {
}, },
}, },
}, },
}, };
});
}
} }

View File

@ -1 +0,0 @@
export default {};

View File

@ -1,6 +1,10 @@
export default { import Component from '@/ecs/component.js';
export default class VisibleAabb extends Component {
static properties = {
x0: {type: 'float32'}, x0: {type: 'float32'},
x1: {type: 'float32'}, x1: {type: 'float32'},
y0: {type: 'float32'}, y0: {type: 'float32'},
y1: {type: 'float32'}, y1: {type: 'float32'},
};
} }

View File

@ -1 +0,0 @@
export default {};

View File

@ -1,7 +1,6 @@
import Schema from '@/ecs/schema.js'; import Component from '@/ecs/component.js';
export default function(Component) { export default class Wielder extends Component {
return class Wielder extends Component {
instanceFromSchema() { instanceFromSchema() {
const Instance = super.instanceFromSchema(); const Instance = super.instanceFromSchema();
const Component = this; const Component = this;
@ -66,11 +65,7 @@ export default function(Component) {
} }
return Instance; return Instance;
} }
static schema = new Schema({ static properties = {
type: 'object',
properties: {
activeSlot: {type: 'uint16'}, activeSlot: {type: 'uint16'},
}, };
});
}
} }

View File

@ -7,12 +7,8 @@ export default class Component {
Instance; Instance;
map = {}; map = {};
pool = []; pool = [];
serializer; static properties = {};
static $$schema;
static schema = new Schema({
type: 'object',
properties: {},
});
constructor(ecs) { constructor(ecs) {
this.ecs = ecs; this.ecs = ecs;
@ -41,7 +37,8 @@ export default class Component {
createMany(entries) { createMany(entries) {
if (entries.length > 0) { if (entries.length > 0) {
const allocated = this.allocateMany(entries.length); const allocated = this.allocateMany(entries.length);
const keys = Object.keys(this.constructor.properties); const {properties} = this.constructor.schema.specification;
const keys = Object.keys(properties);
for (let i = 0; i < entries.length; ++i) { for (let i = 0; i < entries.length; ++i) {
const [entityId, values = {}] = entries[i]; const [entityId, values = {}] = entries[i];
this.map[entityId] = allocated[i]; this.map[entityId] = allocated[i];
@ -51,7 +48,7 @@ export default class Component {
} }
for (let k = 0; k < keys.length; ++k) { for (let k = 0; k < keys.length; ++k) {
const j = keys[k]; const j = keys[k];
const {defaultValue} = this.constructor.properties[j]; const {defaultValue} = properties[j];
if (j in values) { if (j in values) {
this.data[allocated[i]][j] = values[j]; this.data[allocated[i]][j] = values[j];
} }
@ -64,7 +61,7 @@ export default class Component {
} }
deserialize(entityId, view, offset) { deserialize(entityId, view, offset) {
const {properties} = this.constructor; const {properties} = this.constructor.schema.specification;
const instance = this.get(entityId); const instance = this.get(entityId);
const deserialized = this.constructor.schema.deserialize(view, offset); const deserialized = this.constructor.schema.deserialize(view, offset);
for (const key in properties) { for (const key in properties) {
@ -92,9 +89,10 @@ export default class Component {
} }
static filterDefaults(instance) { static filterDefaults(instance) {
const {properties} = this.schema.specification;
const json = {}; const json = {};
for (const key in this.properties) { for (const key in properties) {
const {defaultValue} = this.properties[key]; const {defaultValue} = properties[key];
if (key in instance && instance[key] !== defaultValue) { if (key in instance && instance[key] !== defaultValue) {
json[key] = instance[key]; json[key] = instance[key];
} }
@ -131,15 +129,15 @@ export default class Component {
instanceFromSchema() { instanceFromSchema() {
const Component = this; const Component = this;
const {specification} = Component.constructor.schema;
const Instance = class { const Instance = class {
$$entity = 0; $$entity = 0;
constructor() { constructor() {
this.$$reset(); this.$$reset();
} }
$$reset() { $$reset() {
const {properties} = Component.constructor; for (const key in specification.properties) {
for (const key in properties) { const {defaultValue} = specification.properties[key];
const {defaultValue} = properties[key];
this[`$$${key}`] = defaultValue; this[`$$${key}`] = defaultValue;
} }
} }
@ -157,7 +155,7 @@ export default class Component {
this.$$reset(); this.$$reset();
}, },
}; };
for (const key in Component.constructor.properties) { for (const key in specification.properties) {
properties[key] = { properties[key] = {
get: function get() { get: function get() {
return this[`$$${key}`]; return this[`$$${key}`];
@ -182,8 +180,14 @@ export default class Component {
return {...original, ...update}; return {...original, ...update};
} }
static get properties() { static get schema() {
return this.schema.specification.properties; if (!this.$$schema) {
this.$$schema = new Schema({
type: 'object',
properties: this.properties,
});
}
return this.$$schema;
} }
serialize(entityId, view, offset) { serialize(entityId, view, offset) {

View File

@ -1,14 +1,12 @@
import {expect, test} from 'vitest'; import {expect, test} from 'vitest';
import Schema from './schema.js';
import Component from './component.js'; import Component from './component.js';
test('creates instances', () => { test('creates instances', () => {
class CreatingComponent extends Component { class CreatingComponent extends Component {
static schema = new Schema({ static properties = {
type: 'object', foo: {defaultValue: 'bar', type: 'string'},
properties: {foo: {defaultValue: 'bar', type: 'string'}}, };
});
} }
const ComponentInstance = new CreatingComponent(); const ComponentInstance = new CreatingComponent();
ComponentInstance.create(1); ComponentInstance.create(1);
@ -18,10 +16,9 @@ test('creates instances', () => {
test('does not serialize default values', () => { test('does not serialize default values', () => {
class CreatingComponent extends Component { class CreatingComponent extends Component {
static schema = new Schema({ static properties = {
type: 'object', foo: {defaultValue: 'bar', type: 'string'}, bar: {type: 'uint8'},
properties: {foo: {defaultValue: 'bar', type: 'string'}, bar: {type: 'uint8'}}, };
});
} }
const fakeEcs = {markChange() {}}; const fakeEcs = {markChange() {}};
const ComponentInstance = new CreatingComponent(fakeEcs); const ComponentInstance = new CreatingComponent(fakeEcs);
@ -35,10 +32,9 @@ test('does not serialize default values', () => {
test('reuses instances', () => { test('reuses instances', () => {
class ReusingComponent extends Component { class ReusingComponent extends Component {
static schema = new Schema({ static properties = {
type: 'object', foo: {type: 'string'},
properties: {foo: {type: 'string'}}, };
});
} }
const ComponentInstance = new ReusingComponent(); const ComponentInstance = new ReusingComponent();
ComponentInstance.create(1); ComponentInstance.create(1);

View File

@ -2,26 +2,22 @@ import {expect, test} from 'vitest';
import Component from './component.js'; import Component from './component.js';
import Ecs from './ecs.js'; import Ecs from './ecs.js';
import Schema from './schema.js';
import System from './system.js'; import System from './system.js';
function wrapSpecification(name, specification) { function wrapProperties(name, properties) {
return class WrappedComponent extends Component { return class WrappedComponent extends Component {
static componentName = name; static componentName = name;
static schema = new Schema({ static properties = properties;
type: 'object',
properties: specification,
});
}; };
} }
const Empty = wrapSpecification('Empty', {}); const Empty = wrapProperties('Empty', {});
const Name = wrapSpecification('Name', { const Name = wrapProperties('Name', {
name: {type: 'string'}, name: {type: 'string'},
}); });
const Position = wrapSpecification('Position', { const Position = wrapProperties('Position', {
x: {type: 'int32', defaultValue: 32}, x: {type: 'int32', defaultValue: 32},
y: {type: 'int32'}, y: {type: 'int32'},
z: {type: 'int32'}, z: {type: 'int32'},
@ -117,7 +113,7 @@ test('inserts components into entities', () => {
}); });
test('ticks systems', () => { test('ticks systems', () => {
const Momentum = wrapSpecification('Momentum', { const Momentum = wrapProperties('Momentum', {
x: {type: 'int32'}, x: {type: 'int32'},
y: {type: 'int32'}, y: {type: 'int32'},
z: {type: 'int32'}, z: {type: 'int32'},
@ -220,7 +216,7 @@ test('schedules entities to be deleted when ticking systems', () => {
test('adds components to and remove components from entities when ticking systems', () => { test('adds components to and remove components from entities when ticking systems', () => {
let addLength, removeLength; let addLength, removeLength;
const ecs = new Ecs({ const ecs = new Ecs({
Components: {Foo: wrapSpecification('Foo', {bar: {type: 'uint8'}})}, Components: {Foo: wrapProperties('Foo', {bar: {type: 'uint8'}})},
Systems: { Systems: {
AddComponent: class extends System { AddComponent: class extends System {
static queries() { static queries() {

View File

@ -2,21 +2,17 @@ import {expect, test} from 'vitest';
import Component from './component.js'; import Component from './component.js';
import Query from './query.js'; import Query from './query.js';
import Schema from './schema.js';
function wrapSpecification(name, specification) { function wrapProperties(name, properties) {
return class WrappedComponent extends Component { return class WrappedComponent extends Component {
static name = name; static name = name;
static schema = new Schema({ static properties = properties;
type: 'object',
properties: specification,
});
}; };
} }
const A = new (wrapSpecification('A', {a: {type: 'int32', defaultValue: 420}})); const A = new (wrapProperties('A', {a: {type: 'int32', defaultValue: 420}}));
const B = new (wrapSpecification('B', {b: {type: 'int32', defaultValue: 69}})); const B = new (wrapProperties('B', {b: {type: 'int32', defaultValue: 69}}));
const C = new (wrapSpecification('C', {c: {type: 'int32'}})); const C = new (wrapProperties('C', {c: {type: 'int32'}}));
const Components = {A, B, C}; const Components = {A, B, C};
Components.A.createMany([[2], [3]]); Components.A.createMany([[2], [3]]);
@ -70,7 +66,7 @@ test('can deindex', () => {
}); });
test('can reindex', () => { test('can reindex', () => {
const Test = new (wrapSpecification('Test', {a: {type: 'int32', defaultValue: 420}})); const Test = new (wrapProperties('Test', {a: {type: 'int32', defaultValue: 420}}));
Test.createMany([[2], [3]]); Test.createMany([[2], [3]]);
const query = new Query(['Test'], fakeEcs({Test})); const query = new Query(['Test'], fakeEcs({Test}));
query.reindex([2, 3]); query.reindex([2, 3]);