refactor: component

This commit is contained in:
cha0s 2024-06-26 10:33:31 -05:00
parent 6d6904bd22
commit ee96c69a30
7 changed files with 160 additions and 173 deletions

View File

@ -1,4 +1,4 @@
import Arbitrary from '@/ecs/arbitrary.js';
import Component from '@/ecs/component.js';
import Schema from '@/ecs/schema.js';
import gather from '@/util/gather.js';
@ -15,7 +15,7 @@ for (const componentName in specificationsAndOrDecorators) {
const specificationOrDecorator = specificationsAndOrDecorators[componentName];
if ('function' === typeof specificationOrDecorator) {
Components[componentName] = specificationOrDecorator(
class Decorated extends Arbitrary {
class Decorated extends Component {
static componentName = componentName;
}
);
@ -24,7 +24,7 @@ for (const componentName in specificationsAndOrDecorators) {
}
}
else {
Components[componentName] = class Component extends Arbitrary {
Components[componentName] = class WrappedComponent extends Component {
static componentName = componentName;
static schema = new Schema({
type: 'object',

View File

@ -1,56 +0,0 @@
import {expect, test} from 'vitest';
import Schema from './schema.js';
import Arbitrary from './arbitrary.js';
test('creates instances', () => {
class CreatingArbitrary extends Arbitrary {
static schema = new Schema({
type: 'object',
properties: {foo: {defaultValue: 'bar', type: 'string'}},
});
}
const Component = new CreatingArbitrary();
Component.create(1);
expect(Component.get(1).entity)
.to.equal(1);
});
test('does not serialize default values', () => {
class CreatingArbitrary extends Arbitrary {
static schema = new Schema({
type: 'object',
properties: {foo: {defaultValue: 'bar', type: 'string'}, bar: {type: 'uint8'}},
});
}
const fakeEcs = {markChange() {}};
const Component = new CreatingArbitrary(fakeEcs);
Component.create(1)
expect(Component.get(1).toJSON())
.to.deep.equal({});
Component.get(1).bar = 1;
expect(Component.get(1).toJSON())
.to.deep.equal({bar: 1});
});
test('reuses instances', () => {
class ReusingArbitrary extends Arbitrary {
static schema = new Schema({
type: 'object',
properties: {foo: {type: 'string'}},
});
}
const Component = new ReusingArbitrary();
Component.create(1);
const instance = Component.get(1);
Component.destroy(1);
expect(Component.get(1))
.to.be.undefined;
expect(() => {
Component.destroy(1);
})
.to.throw();
Component.create(1);
expect(Component.get(1))
.to.equal(instance);
});

View File

@ -1,101 +0,0 @@
import Schema from './schema.js';
export default class Base {
ecs;
map = {};
pool = [];
static schema = new Schema({
type: 'object',
properties: {},
});
constructor(ecs) {
this.ecs = ecs;
}
allocateMany(count) {
const results = [];
while (count-- > 0 && this.pool.length > 0) {
results.push(this.pool.pop());
}
return results;
}
create(entityId, values) {
this.createMany([[entityId, values]]);
}
destroy(entityId) {
this.destroyMany([entityId]);
}
destroyMany(entities) {
this.freeMany(
entities
.map((entityId) => {
if ('undefined' !== typeof this.map[entityId]) {
return this.map[entityId];
}
throw new Error(`can't free for non-existent id ${entityId}`);
}),
);
for (let i = 0; i < entities.length; i++) {
this.map[entities[i]] = undefined;
}
}
static filterDefaults(instance) {
const json = {};
for (const key in this.properties) {
const {defaultValue} = this.properties[key];
if (key in instance && instance[key] !== defaultValue) {
json[key] = instance[key];
}
}
return json;
}
freeMany(indices) {
for (let i = 0; i < indices.length; ++i) {
this.pool.push(indices[i]);
}
}
insertMany(entities) {
const creating = [];
for (let i = 0; i < entities.length; i++) {
const [entityId, values] = entities[i];
if (!this.get(entityId)) {
creating.push([entityId, values]);
}
else {
const instance = this.get(entityId);
for (const i in values) {
instance[i] = values[i];
}
}
}
this.createMany(creating);
}
markChange(entityId, key, value) {
this.ecs.markChange(entityId, {[this.constructor.componentName]: {[key]: value}})
}
mergeDiff(original, update) {
return {...original, ...update};
}
static get properties() {
return this.schema.specification.properties;
}
sizeOf(entityId) {
return this.constructor.schema.sizeOf(this.get(entityId));
}
}

View File

@ -1,24 +1,43 @@
import Base from './base.js';
import Schema from './schema.js';
export default class Arbitrary extends Base {
export default class Component {
data = [];
ecs;
Instance;
map = {};
pool = [];
serializer;
Instance;
static schema = new Schema({
type: 'object',
properties: {},
});
constructor(ecs) {
this.ecs = ecs;
}
allocateMany(count) {
if (!this.Instance) {
this.Instance = this.instanceFromSchema();
}
const results = super.allocateMany(count);
count -= results.length; while (count--) {
results.push(this.data.push(new this.Instance()) - 1);
const results = [];
while (count > 0) {
results.push(
this.pool.length > 0
? this.pool.pop()
: this.data.push(new this.Instance()) - 1,
)
count -= 1;
}
return results;
}
create(entityId, values) {
this.createMany([[entityId, values]]);
}
createMany(entries) {
if (entries.length > 0) {
const allocated = this.allocateMany(entries.length);
@ -53,14 +72,63 @@ export default class Arbitrary extends Base {
}
}
serialize(entityId, view, offset) {
this.constructor.schema.serialize(this.get(entityId), view, offset);
destroy(entityId) {
this.destroyMany([entityId]);
}
destroyMany(entities) {
this.freeMany(
entities
.map((entityId) => {
if ('undefined' !== typeof this.map[entityId]) {
return this.map[entityId];
}
throw new Error(`can't free for non-existent id ${entityId}`);
}),
);
for (let i = 0; i < entities.length; i++) {
this.map[entities[i]] = undefined;
}
}
static filterDefaults(instance) {
const json = {};
for (const key in this.properties) {
const {defaultValue} = this.properties[key];
if (key in instance && instance[key] !== defaultValue) {
json[key] = instance[key];
}
}
return json;
}
freeMany(indices) {
for (let i = 0; i < indices.length; ++i) {
this.pool.push(indices[i]);
}
}
get(entityId) {
return this.data[this.map[entityId]];
}
insertMany(entities) {
const creating = [];
for (let i = 0; i < entities.length; i++) {
const [entityId, values] = entities[i];
if (!this.get(entityId)) {
creating.push([entityId, values]);
}
else {
const instance = this.get(entityId);
for (const i in values) {
instance[i] = values[i];
}
}
}
this.createMany(creating);
}
instanceFromSchema() {
const Component = this;
const Instance = class {
@ -106,4 +174,24 @@ export default class Arbitrary extends Base {
return Instance;
}
markChange(entityId, key, value) {
this.ecs.markChange(entityId, {[this.constructor.componentName]: {[key]: value}})
}
mergeDiff(original, update) {
return {...original, ...update};
}
static get properties() {
return this.schema.specification.properties;
}
serialize(entityId, view, offset) {
this.constructor.schema.serialize(this.get(entityId), view, offset);
}
sizeOf(entityId) {
return this.constructor.schema.sizeOf(this.get(entityId));
}
}

56
app/ecs/component.test.js Normal file
View File

@ -0,0 +1,56 @@
import {expect, test} from 'vitest';
import Schema from './schema.js';
import Component from './component.js';
test('creates instances', () => {
class CreatingComponent extends Component {
static schema = new Schema({
type: 'object',
properties: {foo: {defaultValue: 'bar', type: 'string'}},
});
}
const ComponentInstance = new CreatingComponent();
ComponentInstance.create(1);
expect(ComponentInstance.get(1).entity)
.to.equal(1);
});
test('does not serialize default values', () => {
class CreatingComponent extends Component {
static schema = new Schema({
type: 'object',
properties: {foo: {defaultValue: 'bar', type: 'string'}, bar: {type: 'uint8'}},
});
}
const fakeEcs = {markChange() {}};
const ComponentInstance = new CreatingComponent(fakeEcs);
ComponentInstance.create(1)
expect(ComponentInstance.get(1).toJSON())
.to.deep.equal({});
ComponentInstance.get(1).bar = 1;
expect(ComponentInstance.get(1).toJSON())
.to.deep.equal({bar: 1});
});
test('reuses instances', () => {
class ReusingComponent extends Component {
static schema = new Schema({
type: 'object',
properties: {foo: {type: 'string'}},
});
}
const ComponentInstance = new ReusingComponent();
ComponentInstance.create(1);
const instance = ComponentInstance.get(1);
ComponentInstance.destroy(1);
expect(ComponentInstance.get(1))
.to.be.undefined;
expect(() => {
ComponentInstance.destroy(1);
})
.to.throw();
ComponentInstance.create(1);
expect(ComponentInstance.get(1))
.to.equal(instance);
});

View File

@ -1,12 +1,12 @@
import {expect, test} from 'vitest';
import Arbitrary from './arbitrary.js';
import Component from './component.js';
import Ecs from './ecs.js';
import Schema from './schema.js';
import System from './system.js';
function wrapSpecification(name, specification) {
return class Component extends Arbitrary {
return class WrappedComponent extends Component {
static componentName = name;
static schema = new Schema({
type: 'object',

View File

@ -1,11 +1,11 @@
import {expect, test} from 'vitest';
import Arbitrary from './arbitrary.js';
import Component from './component.js';
import Query from './query.js';
import Schema from './schema.js';
function wrapSpecification(name, specification) {
return class Component extends Arbitrary {
return class WrappedComponent extends Component {
static name = name;
static schema = new Schema({
type: 'object',