Compare commits
3 Commits
c53d716a37
...
ebb34eef95
Author | SHA1 | Date | |
---|---|---|---|
|
ebb34eef95 | ||
|
5b2db45e94 | ||
|
95a8e5f13e |
|
@ -8,14 +8,14 @@ const specificationsOrClasses = gather(
|
|||
);
|
||||
|
||||
const Components = {};
|
||||
for (const name in specificationsOrClasses) {
|
||||
const specificationOrClass = specificationsOrClasses[name];
|
||||
for (const componentName in specificationsOrClasses) {
|
||||
const specificationOrClass = specificationsOrClasses[componentName];
|
||||
if (specificationOrClass instanceof Base) {
|
||||
Components[name] = specificationOrClass;
|
||||
Components[componentName] = specificationOrClass;
|
||||
}
|
||||
else {
|
||||
Components[name] = class Component extends Arbitrary {
|
||||
static name = name;
|
||||
Components[componentName] = class Component extends Arbitrary {
|
||||
static name = componentName;
|
||||
static schema = new Schema({
|
||||
type: 'object',
|
||||
properties: specificationOrClass,
|
||||
|
|
160
app/ecs/ecs.js
160
app/ecs/ecs.js
|
@ -10,27 +10,22 @@ export default class Ecs {
|
|||
|
||||
$$caret = 1;
|
||||
|
||||
Components = {};
|
||||
|
||||
diff = {};
|
||||
|
||||
static Systems = {};
|
||||
|
||||
Systems = {};
|
||||
|
||||
static Types = {};
|
||||
|
||||
Types = {};
|
||||
|
||||
$$entities = {};
|
||||
|
||||
$$entityFactory = new EntityFactory();
|
||||
|
||||
constructor() {
|
||||
const {Systems, Types} = this.constructor;
|
||||
for (const name in Types) {
|
||||
this.Types[name] = new Types[name](this);
|
||||
constructor({Systems, Components} = {}) {
|
||||
for (const componentName in Components) {
|
||||
this.Components[componentName] = new Components[componentName](this);
|
||||
}
|
||||
for (const name in Systems) {
|
||||
this.Systems[name] = new Systems[name](this);
|
||||
for (const systemName in Systems) {
|
||||
this.Systems[systemName] = new Systems[systemName](this);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -39,7 +34,8 @@ export default class Ecs {
|
|||
const destroying = [];
|
||||
const removing = [];
|
||||
const updating = [];
|
||||
for (const entityId in patch) {
|
||||
for (const entityIdString in patch) {
|
||||
const entityId = parseInt(entityIdString);
|
||||
const components = patch[entityId];
|
||||
if (false === components) {
|
||||
destroying.push(entityId);
|
||||
|
@ -47,12 +43,12 @@ export default class Ecs {
|
|||
}
|
||||
const componentsToRemove = [];
|
||||
const componentsToUpdate = {};
|
||||
for (const i in components) {
|
||||
if (false === components[i]) {
|
||||
componentsToRemove.push(i);
|
||||
for (const componentName in components) {
|
||||
if (false === components[componentName]) {
|
||||
componentsToRemove.push(componentName);
|
||||
}
|
||||
else {
|
||||
componentsToUpdate[i] = components[i];
|
||||
componentsToUpdate[componentName] = components[componentName];
|
||||
}
|
||||
}
|
||||
if (componentsToRemove.length > 0) {
|
||||
|
@ -89,24 +85,24 @@ export default class Ecs {
|
|||
const creating = {};
|
||||
for (let i = 0; i < specificsList.length; i++) {
|
||||
const [entityId, components] = specificsList[i];
|
||||
const componentKeys = [];
|
||||
for (const name in components) {
|
||||
if (this.Types[name]) {
|
||||
componentKeys.push(name);
|
||||
const componentNames = [];
|
||||
for (const componentName in components) {
|
||||
if (this.Components[componentName]) {
|
||||
componentNames.push(componentName);
|
||||
}
|
||||
}
|
||||
entityIds.push(entityId);
|
||||
this.rebuild(entityId, () => componentKeys);
|
||||
for (const component of componentKeys) {
|
||||
if (!creating[component]) {
|
||||
creating[component] = [];
|
||||
this.rebuild(entityId, () => componentNames);
|
||||
for (const componentName of componentNames) {
|
||||
if (!creating[componentName]) {
|
||||
creating[componentName] = [];
|
||||
}
|
||||
creating[component].push([entityId, components[component]]);
|
||||
creating[componentName].push([entityId, components[componentName]]);
|
||||
}
|
||||
this.markChange(entityId, components);
|
||||
}
|
||||
for (const i in creating) {
|
||||
this.Types[i].createMany(creating[i]);
|
||||
this.Components[i].createMany(creating[i]);
|
||||
}
|
||||
this.reindex(entityIds);
|
||||
return entityIds;
|
||||
|
@ -117,14 +113,13 @@ export default class Ecs {
|
|||
}
|
||||
|
||||
deindex(entityIds) {
|
||||
for (const name in this.Systems) {
|
||||
this.Systems[name].deindex(entityIds);
|
||||
for (const systemName in this.Systems) {
|
||||
this.Systems[systemName].deindex(entityIds);
|
||||
}
|
||||
}
|
||||
|
||||
static deserialize(view) {
|
||||
const ecs = new this();
|
||||
const types = Object.keys(ecs.Types);
|
||||
static deserialize(ecs, view) {
|
||||
const componentNames = Object.keys(ecs.Components);
|
||||
const {entities, systems} = decoder.decode(view.buffer);
|
||||
for (const system of systems) {
|
||||
ecs.system(system).active = true;
|
||||
|
@ -135,7 +130,10 @@ export default class Ecs {
|
|||
max = Math.max(max, parseInt(id));
|
||||
specifics.push([
|
||||
parseInt(id),
|
||||
Object.fromEntries(Object.entries(entities[id]).filter(([type]) => types.includes(type))),
|
||||
Object.fromEntries(
|
||||
Object.entries(entities[id])
|
||||
.filter(([componentName]) => componentNames.includes(componentName)),
|
||||
),
|
||||
]);
|
||||
}
|
||||
ecs.$$caret = max + 1;
|
||||
|
@ -158,17 +156,17 @@ export default class Ecs {
|
|||
if (!this.$$entities[entityId]) {
|
||||
throw new Error(`can't destroy non-existent entity ${entityId}`);
|
||||
}
|
||||
for (const component of this.$$entities[entityId].constructor.types) {
|
||||
if (!destroying[component]) {
|
||||
destroying[component] = [];
|
||||
for (const componentName of this.$$entities[entityId].constructor.componentNames) {
|
||||
if (!destroying[componentName]) {
|
||||
destroying[componentName] = [];
|
||||
}
|
||||
destroying[component].push(entityId);
|
||||
destroying[componentName].push(entityId);
|
||||
}
|
||||
this.$$entities[entityId] = undefined;
|
||||
this.diff[entityId] = false;
|
||||
}
|
||||
for (const i in destroying) {
|
||||
this.Types[i].destroyMany(destroying[i]);
|
||||
this.Components[i].destroyMany(destroying[i]);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -203,20 +201,20 @@ export default class Ecs {
|
|||
const inserting = {};
|
||||
const unique = new Set();
|
||||
for (const [entityId, components] of entities) {
|
||||
this.rebuild(entityId, (types) => [...new Set(types.concat(Object.keys(components)))]);
|
||||
this.rebuild(entityId, (componentNames) => [...new Set(componentNames.concat(Object.keys(components)))]);
|
||||
const diff = {};
|
||||
for (const component in components) {
|
||||
if (!inserting[component]) {
|
||||
inserting[component] = [];
|
||||
for (const componentName in components) {
|
||||
if (!inserting[componentName]) {
|
||||
inserting[componentName] = [];
|
||||
}
|
||||
diff[component] = {};
|
||||
inserting[component].push([entityId, components[component]]);
|
||||
diff[componentName] = {};
|
||||
inserting[componentName].push([entityId, components[componentName]]);
|
||||
}
|
||||
unique.add(entityId);
|
||||
this.markChange(entityId, diff);
|
||||
}
|
||||
for (const component in inserting) {
|
||||
this.Types[component].insertMany(inserting[component]);
|
||||
for (const componentName in inserting) {
|
||||
this.Components[componentName].insertMany(inserting[componentName]);
|
||||
}
|
||||
this.reindex(unique.values());
|
||||
}
|
||||
|
@ -229,38 +227,38 @@ export default class Ecs {
|
|||
// Created?
|
||||
else if (!this.diff[entityId]) {
|
||||
const filtered = {};
|
||||
for (const name in components) {
|
||||
filtered[name] = false === components[name]
|
||||
for (const componentName in components) {
|
||||
filtered[componentName] = false === components[componentName]
|
||||
? false
|
||||
: this.Types[name].constructor.filterDefaults(components[name]);
|
||||
: this.Components[componentName].constructor.filterDefaults(components[componentName]);
|
||||
}
|
||||
this.diff[entityId] = filtered;
|
||||
}
|
||||
// Otherwise, merge.
|
||||
else {
|
||||
for (const name in components) {
|
||||
this.diff[entityId][name] = false === components[name]
|
||||
for (const componentName in components) {
|
||||
this.diff[entityId][componentName] = false === components[componentName]
|
||||
? false
|
||||
: this.Types[name].mergeDiff(
|
||||
this.diff[entityId][name] || {},
|
||||
components[name],
|
||||
: this.Components[componentName].mergeDiff(
|
||||
this.diff[entityId][componentName] || {},
|
||||
components[componentName],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rebuild(entityId, types) {
|
||||
rebuild(entityId, componentNames) {
|
||||
let existing = [];
|
||||
if (this.$$entities[entityId]) {
|
||||
existing.push(...this.$$entities[entityId].constructor.types);
|
||||
existing.push(...this.$$entities[entityId].constructor.componentNames);
|
||||
}
|
||||
const Class = this.$$entityFactory.makeClass(types(existing), this.Types);
|
||||
const Class = this.$$entityFactory.makeClass(componentNames(existing), this.Components);
|
||||
this.$$entities[entityId] = new Class(entityId);
|
||||
}
|
||||
|
||||
reindex(entityIds) {
|
||||
for (const name in this.Systems) {
|
||||
this.Systems[name].reindex(entityIds);
|
||||
for (const systemName in this.Systems) {
|
||||
this.Systems[systemName].reindex(entityIds);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -274,18 +272,18 @@ export default class Ecs {
|
|||
for (const [entityId, components] of entities) {
|
||||
unique.add(entityId);
|
||||
const diff = {};
|
||||
for (const component of components) {
|
||||
diff[component] = false;
|
||||
if (!removing[component]) {
|
||||
removing[component] = [];
|
||||
for (const componentName of components) {
|
||||
diff[componentName] = false;
|
||||
if (!removing[componentName]) {
|
||||
removing[componentName] = [];
|
||||
}
|
||||
removing[component].push(entityId);
|
||||
removing[componentName].push(entityId);
|
||||
}
|
||||
this.markChange(entityId, diff);
|
||||
this.rebuild(entityId, (types) => types.filter((type) => !components.includes(type)));
|
||||
this.rebuild(entityId, (componentNames) => componentNames.filter((type) => !components.includes(type)));
|
||||
}
|
||||
for (const component in removing) {
|
||||
this.Types[component].destroyMany(removing[component]);
|
||||
for (const componentName in removing) {
|
||||
this.Components[componentName].destroyMany(removing[componentName]);
|
||||
}
|
||||
this.reindex(unique.values());
|
||||
}
|
||||
|
@ -307,7 +305,7 @@ export default class Ecs {
|
|||
let size = 0;
|
||||
// # of components.
|
||||
size += 2;
|
||||
for (const type in this.Types) {
|
||||
for (const type in this.Components) {
|
||||
size += Schema.sizeOf(type, {type: 'string'});
|
||||
}
|
||||
// # of entities.
|
||||
|
@ -318,19 +316,19 @@ export default class Ecs {
|
|||
return size;
|
||||
}
|
||||
|
||||
system(name) {
|
||||
return this.Systems[name];
|
||||
system(systemName) {
|
||||
return this.Systems[systemName];
|
||||
}
|
||||
|
||||
tick(elapsed) {
|
||||
for (const name in this.Systems) {
|
||||
if (this.Systems[name].active) {
|
||||
this.Systems[name].tick(elapsed);
|
||||
for (const systemName in this.Systems) {
|
||||
if (this.Systems[systemName].active) {
|
||||
this.Systems[systemName].tick(elapsed);
|
||||
}
|
||||
}
|
||||
for (const name in this.Systems) {
|
||||
if (this.Systems[name].active) {
|
||||
this.Systems[name].finalize(elapsed);
|
||||
for (const systemName in this.Systems) {
|
||||
if (this.Systems[systemName].active) {
|
||||
this.Systems[systemName].finalize(elapsed);
|
||||
}
|
||||
}
|
||||
this.tickDestruction();
|
||||
|
@ -338,8 +336,8 @@ export default class Ecs {
|
|||
|
||||
tickDestruction() {
|
||||
const unique = new Set();
|
||||
for (const name in this.Systems) {
|
||||
const System = this.Systems[name];
|
||||
for (const systemName in this.Systems) {
|
||||
const System = this.Systems[systemName];
|
||||
if (System.active) {
|
||||
for (let j = 0; j < System.destroying.length; j++) {
|
||||
unique.add(System.destroying[j]);
|
||||
|
@ -358,9 +356,9 @@ export default class Ecs {
|
|||
entities[id] = this.$$entities[id].toJSON();
|
||||
}
|
||||
const systems = [];
|
||||
for (const name in this.Systems) {
|
||||
if (this.Systems[name].active) {
|
||||
systems.push(name);
|
||||
for (const systemName in this.Systems) {
|
||||
if (this.Systems[systemName].active) {
|
||||
systems.push(systemName);
|
||||
}
|
||||
}
|
||||
return {
|
||||
|
|
|
@ -30,8 +30,8 @@ const Position = wrapSpecification('Position', {
|
|||
test('activates and deactivates systems at runtime', () => {
|
||||
let oneCount = 0;
|
||||
let twoCount = 0;
|
||||
class SystemToggle extends Ecs {
|
||||
static Systems = {
|
||||
const ecs = new Ecs({
|
||||
Systems: {
|
||||
OneSystem: class extends System {
|
||||
tick() {
|
||||
oneCount += 1;
|
||||
|
@ -42,9 +42,8 @@ test('activates and deactivates systems at runtime', () => {
|
|||
twoCount += 1;
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
const ecs = new SystemToggle();
|
||||
},
|
||||
});
|
||||
ecs.tick();
|
||||
expect(oneCount)
|
||||
.to.equal(0);
|
||||
|
@ -69,20 +68,14 @@ test('activates and deactivates systems at runtime', () => {
|
|||
});
|
||||
|
||||
test('creates entities with components', () => {
|
||||
class CreateEcs extends Ecs {
|
||||
static Types = {Empty, Position};
|
||||
}
|
||||
const ecs = new CreateEcs();
|
||||
const ecs = new Ecs({Components: {Empty, Position}});
|
||||
const entity = ecs.create({Empty: {}, Position: {y: 128}});
|
||||
expect(JSON.stringify(ecs.get(entity)))
|
||||
.to.deep.equal(JSON.stringify({Empty: {}, Position: {y: 128}}));
|
||||
});
|
||||
|
||||
test("removes entities' components", () => {
|
||||
class RemoveEcs extends Ecs {
|
||||
static Types = {Empty, Position};
|
||||
}
|
||||
const ecs = new RemoveEcs();
|
||||
const ecs = new Ecs({Components: {Empty, Position}});
|
||||
const entity = ecs.create({Empty: {}, Position: {y: 128}});
|
||||
ecs.remove(entity, ['Position']);
|
||||
expect(JSON.stringify(ecs.get(entity)))
|
||||
|
@ -90,20 +83,14 @@ test("removes entities' components", () => {
|
|||
});
|
||||
|
||||
test('gets entities', () => {
|
||||
class GetEcs extends Ecs {
|
||||
static Types = {Empty, Position};
|
||||
}
|
||||
const ecs = new GetEcs();
|
||||
const ecs = new Ecs({Components: {Empty, Position}});
|
||||
const entity = ecs.create({Empty: {}, Position: {y: 128}});
|
||||
expect(JSON.stringify(ecs.get(entity)))
|
||||
.to.deep.equal(JSON.stringify({Empty: {}, Position: {y: 128}}));
|
||||
});
|
||||
|
||||
test('destroys entities', () => {
|
||||
class DestroyEcs extends Ecs {
|
||||
static Types = {Empty, Position};
|
||||
}
|
||||
const ecs = new DestroyEcs();
|
||||
const ecs = new Ecs({Components: {Empty, Position}});
|
||||
const entity = ecs.create({Empty: {}, Position: {y: 128}});
|
||||
expect(JSON.stringify(ecs.get(entity)))
|
||||
.to.deep.equal(JSON.stringify({Empty: {}, Position: {y: 128}}));
|
||||
|
@ -119,10 +106,7 @@ test('destroys entities', () => {
|
|||
});
|
||||
|
||||
test('inserts components into entities', () => {
|
||||
class InsertEcs extends Ecs {
|
||||
static Types = {Empty, Position};
|
||||
}
|
||||
const ecs = new InsertEcs();
|
||||
const ecs = new Ecs({Components: {Empty, Position}});
|
||||
const entity = ecs.create({Empty: {}});
|
||||
ecs.insert(entity, {Position: {y: 128}});
|
||||
expect(JSON.stringify(ecs.get(entity)))
|
||||
|
@ -138,8 +122,9 @@ test('ticks systems', () => {
|
|||
y: {type: 'int32'},
|
||||
z: {type: 'int32'},
|
||||
});
|
||||
class TickEcs extends Ecs {
|
||||
static Systems = {
|
||||
const ecs = new Ecs({
|
||||
Components: {Momentum, Position},
|
||||
Systems: {
|
||||
Physics: class Physics extends System {
|
||||
|
||||
static queries() {
|
||||
|
@ -157,10 +142,8 @@ test('ticks systems', () => {
|
|||
}
|
||||
|
||||
},
|
||||
}
|
||||
static Types = {Momentum, Position};
|
||||
}
|
||||
const ecs = new TickEcs();
|
||||
},
|
||||
});
|
||||
ecs.system('Physics').active = true;
|
||||
const entity = ecs.create({Momentum: {}, Position: {y: 128}});
|
||||
const position = JSON.stringify(ecs.get(entity).Position);
|
||||
|
@ -174,16 +157,15 @@ test('ticks systems', () => {
|
|||
});
|
||||
|
||||
test('creates many entities when ticking systems', () => {
|
||||
class TickingSystemEcs extends Ecs {
|
||||
static Systems = {
|
||||
const ecs = new Ecs({
|
||||
Systems: {
|
||||
Spawn: class extends System {
|
||||
tick() {
|
||||
this.createManyEntities(Array.from({length: 5}).map(() => []));
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
const ecs = new TickingSystemEcs();
|
||||
},
|
||||
});
|
||||
ecs.system('Spawn').active = true;
|
||||
ecs.create();
|
||||
expect(ecs.get(5))
|
||||
|
@ -194,16 +176,15 @@ test('creates many entities when ticking systems', () => {
|
|||
});
|
||||
|
||||
test('creates entities when ticking systems', () => {
|
||||
class TickingSystemEcs extends Ecs {
|
||||
static Systems = {
|
||||
const ecs = new Ecs({
|
||||
Systems: {
|
||||
Spawn: class extends System {
|
||||
tick() {
|
||||
this.createEntity();
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
const ecs = new TickingSystemEcs();
|
||||
},
|
||||
});
|
||||
ecs.system('Spawn').active = true;
|
||||
ecs.create();
|
||||
expect(ecs.get(2))
|
||||
|
@ -215,8 +196,8 @@ test('creates entities when ticking systems', () => {
|
|||
|
||||
test('schedules entities to be deleted when ticking systems', () => {
|
||||
let entity;
|
||||
class TickingSystemEcs extends Ecs {
|
||||
static Systems = {
|
||||
const ecs = new Ecs({
|
||||
Systems: {
|
||||
Despawn: class extends System {
|
||||
finalize() {
|
||||
entity = ecs.get(1);
|
||||
|
@ -225,9 +206,8 @@ test('schedules entities to be deleted when ticking systems', () => {
|
|||
this.destroyEntity(1);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
const ecs = new TickingSystemEcs();
|
||||
},
|
||||
});
|
||||
ecs.system('Despawn').active = true;
|
||||
ecs.create();
|
||||
ecs.tick(1);
|
||||
|
@ -239,8 +219,9 @@ test('schedules entities to be deleted when ticking systems', () => {
|
|||
|
||||
test('adds components to and remove components from entities when ticking systems', () => {
|
||||
let addLength, removeLength;
|
||||
class TickingSystemEcs extends Ecs {
|
||||
static Systems = {
|
||||
const ecs = new Ecs({
|
||||
Components: {Foo: wrapSpecification('Foo', {bar: {type: 'uint8'}})},
|
||||
Systems: {
|
||||
AddComponent: class extends System {
|
||||
static queries() {
|
||||
return {
|
||||
|
@ -267,10 +248,8 @@ test('adds components to and remove components from entities when ticking system
|
|||
removeLength = Array.from(this.select('default')).length;
|
||||
}
|
||||
},
|
||||
};
|
||||
static Types = {Foo: wrapSpecification('Foo', {bar: {type: 'uint8'}})};
|
||||
}
|
||||
const ecs = new TickingSystemEcs();
|
||||
},
|
||||
});
|
||||
ecs.system('AddComponent').active = true;
|
||||
ecs.create();
|
||||
ecs.tick(1);
|
||||
|
@ -296,10 +275,7 @@ test('generates coalesced diffs for entity creation', () => {
|
|||
});
|
||||
|
||||
test('generates diffs for adding and removing components', () => {
|
||||
class DiffedEcs extends Ecs {
|
||||
static Types = {Position};
|
||||
}
|
||||
const ecs = new DiffedEcs();
|
||||
const ecs = new Ecs({Components: {Position}});
|
||||
let entity;
|
||||
entity = ecs.create();
|
||||
ecs.setClean();
|
||||
|
@ -315,10 +291,7 @@ test('generates diffs for adding and removing components', () => {
|
|||
});
|
||||
|
||||
test('generates diffs for empty components', () => {
|
||||
class DiffedEcs extends Ecs {
|
||||
static Types = {Empty};
|
||||
}
|
||||
const ecs = new DiffedEcs();
|
||||
const ecs = new Ecs({Components: {Empty}});
|
||||
let entity;
|
||||
entity = ecs.create({Empty});
|
||||
expect(ecs.diff)
|
||||
|
@ -330,10 +303,7 @@ test('generates diffs for empty components', () => {
|
|||
});
|
||||
|
||||
test('generates diffs for entity mutations', () => {
|
||||
class DiffedEcs extends Ecs {
|
||||
static Types = {Position};
|
||||
}
|
||||
const ecs = new DiffedEcs();
|
||||
const ecs = new Ecs({Components: {Position}});
|
||||
let entity;
|
||||
entity = ecs.create({Position: {}});
|
||||
ecs.setClean();
|
||||
|
@ -346,10 +316,7 @@ test('generates diffs for entity mutations', () => {
|
|||
});
|
||||
|
||||
test('generates coalesced diffs for components', () => {
|
||||
class DiffedEcs extends Ecs {
|
||||
static Types = {Position};
|
||||
}
|
||||
const ecs = new DiffedEcs();
|
||||
const ecs = new Ecs({Components: {Position}});
|
||||
let entity;
|
||||
entity = ecs.create({Position});
|
||||
ecs.remove(entity, ['Position']);
|
||||
|
@ -361,10 +328,7 @@ test('generates coalesced diffs for components', () => {
|
|||
});
|
||||
|
||||
test('generates coalesced diffs for mutations', () => {
|
||||
class DiffedEcs extends Ecs {
|
||||
static Types = {Position};
|
||||
}
|
||||
const ecs = new DiffedEcs();
|
||||
const ecs = new Ecs({Components: {Position}});
|
||||
let entity;
|
||||
entity = ecs.create({Position});
|
||||
ecs.setClean();
|
||||
|
@ -386,10 +350,7 @@ test('generates diffs for deletions', () => {
|
|||
});
|
||||
|
||||
test('applies creation patches', () => {
|
||||
class PatchedEcs extends Ecs {
|
||||
static Types = {Position};
|
||||
}
|
||||
const ecs = new PatchedEcs();
|
||||
const ecs = new Ecs({Components: {Position}});
|
||||
ecs.apply({16: {Position: {x: 64}}});
|
||||
expect(Array.from(ecs.entities).length)
|
||||
.to.equal(1);
|
||||
|
@ -398,10 +359,7 @@ test('applies creation patches', () => {
|
|||
});
|
||||
|
||||
test('applies update patches', () => {
|
||||
class PatchedEcs extends Ecs {
|
||||
static Types = {Position};
|
||||
}
|
||||
const ecs = new PatchedEcs();
|
||||
const ecs = new Ecs({Components: {Position}});
|
||||
ecs.createSpecific(16, {Position: {x: 64}});
|
||||
ecs.apply({16: {Position: {x: 128}}});
|
||||
expect(Array.from(ecs.entities).length)
|
||||
|
@ -411,10 +369,7 @@ test('applies update patches', () => {
|
|||
});
|
||||
|
||||
test('applies entity deletion patches', () => {
|
||||
class PatchedEcs extends Ecs {
|
||||
static Types = {Position};
|
||||
}
|
||||
const ecs = new PatchedEcs();
|
||||
const ecs = new Ecs({Components: {Position}});
|
||||
ecs.createSpecific(16, {Position: {x: 64}});
|
||||
ecs.apply({16: false});
|
||||
expect(Array.from(ecs.entities).length)
|
||||
|
@ -422,23 +377,17 @@ test('applies entity deletion patches', () => {
|
|||
});
|
||||
|
||||
test('applies component deletion patches', () => {
|
||||
class PatchedEcs extends Ecs {
|
||||
static Types = {Empty, Position};
|
||||
}
|
||||
const ecs = new PatchedEcs();
|
||||
const ecs = new Ecs({Components: {Empty, Position}});
|
||||
ecs.createSpecific(16, {Empty: {}, Position: {x: 64}});
|
||||
expect(ecs.get(16).constructor.types)
|
||||
expect(ecs.get(16).constructor.componentNames)
|
||||
.to.deep.equal(['Empty', 'Position']);
|
||||
ecs.apply({16: {Empty: false}});
|
||||
expect(ecs.get(16).constructor.types)
|
||||
expect(ecs.get(16).constructor.componentNames)
|
||||
.to.deep.equal(['Position']);
|
||||
});
|
||||
|
||||
test('calculates entity size', () => {
|
||||
class SizingEcs extends Ecs {
|
||||
static Types = {Empty, Position};
|
||||
}
|
||||
const ecs = new SizingEcs();
|
||||
const ecs = new Ecs({Components: {Empty, Position}});
|
||||
ecs.createSpecific(1, {Empty: {}, Position: {}});
|
||||
// ID + # of components + Empty + Position + x + y + z
|
||||
// 4 + 2 + 2 + 4 + 2 + 4 + 4 + 4 + 4 = 30
|
||||
|
@ -447,10 +396,7 @@ test('calculates entity size', () => {
|
|||
});
|
||||
|
||||
test('serializes and deserializes', () => {
|
||||
class SerializingEcs extends Ecs {
|
||||
static Types = {Empty, Name, Position};
|
||||
}
|
||||
const ecs = new SerializingEcs();
|
||||
const ecs = new Ecs({Components: {Empty, Name, Position}});
|
||||
ecs.createSpecific(1, {Empty: {}, Position: {x: 64}});
|
||||
ecs.createSpecific(16, {Name: {name: 'foobar'}, Position: {x: 128}});
|
||||
expect(ecs.toJSON())
|
||||
|
@ -461,13 +407,16 @@ test('serializes and deserializes', () => {
|
|||
},
|
||||
systems: [],
|
||||
});
|
||||
const view = SerializingEcs.serialize(ecs);
|
||||
const deserialized = SerializingEcs.deserialize(view);
|
||||
const view = Ecs.serialize(ecs);
|
||||
const deserialized = Ecs.deserialize(
|
||||
new Ecs({Components: {Empty, Name, Position}}),
|
||||
view,
|
||||
);
|
||||
expect(Array.from(deserialized.entities).length)
|
||||
.to.equal(2);
|
||||
expect(deserialized.get(1).constructor.types)
|
||||
expect(deserialized.get(1).constructor.componentNames)
|
||||
.to.deep.equal(['Empty', 'Position']);
|
||||
expect(deserialized.get(16).constructor.types)
|
||||
expect(deserialized.get(16).constructor.componentNames)
|
||||
.to.deep.equal(['Name', 'Position']);
|
||||
expect(JSON.stringify(deserialized.get(1)))
|
||||
.to.equal(JSON.stringify({Empty: {}, Position: {x: 64}}))
|
||||
|
@ -480,17 +429,14 @@ test('serializes and deserializes', () => {
|
|||
});
|
||||
|
||||
test('deserializes from compatible ECS', () => {
|
||||
class DeserializingEcs extends Ecs {
|
||||
static Types = {Empty, Name};
|
||||
}
|
||||
class SerializingEcs extends Ecs {
|
||||
static Types = {Empty, Name, Position};
|
||||
}
|
||||
const ecs = new SerializingEcs();
|
||||
const ecs = new Ecs({Components: {Empty, Name, Position}});
|
||||
ecs.createSpecific(1, {Empty: {}, Position: {x: 64}});
|
||||
ecs.createSpecific(16, {Name: {name: 'foobar'}, Position: {x: 128}});
|
||||
const view = SerializingEcs.serialize(ecs);
|
||||
const deserialized = DeserializingEcs.deserialize(view);
|
||||
const view = Ecs.serialize(ecs);
|
||||
const deserialized = Ecs.deserialize(
|
||||
new Ecs({Components: {Empty, Name}}),
|
||||
view,
|
||||
);
|
||||
expect(deserialized.get(1).toJSON())
|
||||
.to.deep.equal({Empty: {}});
|
||||
expect(deserialized.get(16).toJSON())
|
||||
|
|
|
@ -7,8 +7,8 @@ export default class EntityFactory {
|
|||
|
||||
$$tries = new Node();
|
||||
|
||||
makeClass(types, Types) {
|
||||
const sorted = types.toSorted();
|
||||
makeClass(componentNames, Components) {
|
||||
const sorted = componentNames.toSorted();
|
||||
let walk = this.$$tries;
|
||||
let i = 0;
|
||||
while (i < sorted.length) {
|
||||
|
@ -20,14 +20,14 @@ export default class EntityFactory {
|
|||
}
|
||||
if (!walk.class) {
|
||||
class Entity {
|
||||
static types = sorted;
|
||||
static componentNames = sorted;
|
||||
constructor(id) {
|
||||
this.id = id;
|
||||
}
|
||||
size() {
|
||||
let size = 0;
|
||||
for (const component of this.constructor.types) {
|
||||
const instance = Types[component];
|
||||
for (const componentName of this.constructor.componentNames) {
|
||||
const instance = Components[componentName];
|
||||
size += 2 + 4 + instance.constructor.schema.sizeOf(instance.get(this.id));
|
||||
}
|
||||
// ID + # of components.
|
||||
|
@ -37,7 +37,7 @@ export default class EntityFactory {
|
|||
const properties = {};
|
||||
for (const type of sorted) {
|
||||
properties[type] = {};
|
||||
const get = Types[type].get.bind(Types[type]);
|
||||
const get = Components[type].get.bind(Components[type]);
|
||||
properties[type].get = function() {
|
||||
return get(this.id);
|
||||
};
|
||||
|
|
|
@ -4,15 +4,15 @@ export default class Query {
|
|||
|
||||
$$index = new Set();
|
||||
|
||||
constructor(parameters, Types) {
|
||||
constructor(parameters, Components) {
|
||||
for (let i = 0; i < parameters.length; ++i) {
|
||||
const parameter = parameters[i];
|
||||
switch (parameter.charCodeAt(0)) {
|
||||
case '!'.charCodeAt(0):
|
||||
this.$$criteria.without.push(Types[parameter.slice(1)]);
|
||||
this.$$criteria.without.push(Components[parameter.slice(1)]);
|
||||
break;
|
||||
default:
|
||||
this.$$criteria.with.push(Types[parameter]);
|
||||
this.$$criteria.with.push(Components[parameter]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,13 +18,13 @@ const A = new (wrapSpecification('A', {a: {type: 'int32', defaultValue: 420}}));
|
|||
const B = new (wrapSpecification('B', {b: {type: 'int32', defaultValue: 69}}));
|
||||
const C = new (wrapSpecification('C', {c: {type: 'int32'}}));
|
||||
|
||||
const Types = {A, B, C};
|
||||
Types.A.createMany([[2], [3]]);
|
||||
Types.B.createMany([[1], [2]]);
|
||||
Types.C.createMany([[2], [4]]);
|
||||
const Components = {A, B, C};
|
||||
Components.A.createMany([[2], [3]]);
|
||||
Components.B.createMany([[1], [2]]);
|
||||
Components.C.createMany([[2], [4]]);
|
||||
|
||||
function testQuery(parameters, expected) {
|
||||
const query = new Query(parameters, Types);
|
||||
const query = new Query(parameters, Components);
|
||||
query.reindex([1, 2, 3]);
|
||||
expect(query.count)
|
||||
.to.equal(expected.length);
|
||||
|
@ -51,7 +51,7 @@ test('can query excluding', () => {
|
|||
});
|
||||
|
||||
test('can deindex', () => {
|
||||
const query = new Query(['A'], Types);
|
||||
const query = new Query(['A'], Components);
|
||||
query.reindex([1, 2, 3]);
|
||||
expect(query.count)
|
||||
.to.equal(2);
|
||||
|
@ -74,7 +74,7 @@ test('can reindex', () => {
|
|||
});
|
||||
|
||||
test('can select', () => {
|
||||
const query = new Query(['A'], Types);
|
||||
const query = new Query(['A'], Components);
|
||||
query.reindex([1, 2, 3]);
|
||||
const it = query.select();
|
||||
const result = it.next();
|
||||
|
|
|
@ -15,7 +15,7 @@ export default class System {
|
|||
this.ecs = ecs;
|
||||
const queries = this.constructor.queries();
|
||||
for (const i in queries) {
|
||||
this.queries[i] = new Query(queries[i], ecs.Types);
|
||||
this.queries[i] = new Query(queries[i], ecs.Components);
|
||||
}
|
||||
this.reindex(ecs.entities);
|
||||
}
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
import Ecs from '@/ecs/ecs.js';
|
||||
import Systems from '@/ecs-systems/index.js';
|
||||
import Types from '@/ecs-components/index.js';
|
||||
|
||||
class EngineEcs extends Ecs {
|
||||
static Systems = Systems;
|
||||
static Types = Types;
|
||||
}
|
||||
|
||||
export default EngineEcs;
|
|
@ -1,12 +1,16 @@
|
|||
import {join} from 'node:path';
|
||||
|
||||
import {
|
||||
MOVE_MAP,
|
||||
TPS,
|
||||
} from '@/constants.js';
|
||||
import Ecs from '@/engine/ecs.js';
|
||||
import Ecs from '@/ecs/ecs.js';
|
||||
import Components from '@/ecs-components/index.js';
|
||||
import Systems from '@/ecs-systems/index.js';
|
||||
import {decode, encode} from '@/packets/index.js';
|
||||
|
||||
function join(...parts) {
|
||||
return parts.join('/');
|
||||
}
|
||||
|
||||
export default class Engine {
|
||||
|
||||
incomingActions = [];
|
||||
|
@ -58,7 +62,11 @@ export default class Engine {
|
|||
}
|
||||
|
||||
createEcs() {
|
||||
const ecs = new Ecs();
|
||||
return new Ecs({Components, Systems});
|
||||
}
|
||||
|
||||
async createHomestead(id) {
|
||||
const ecs = this.createEcs();
|
||||
const area = {x: 100, y: 60};
|
||||
ecs.create({
|
||||
AreaSize: {x: area.x * 16, y: area.y * 16},
|
||||
|
@ -87,11 +95,6 @@ export default class Engine {
|
|||
defaultSystems.forEach((defaultSystem) => {
|
||||
ecs.system(defaultSystem).active = true;
|
||||
});
|
||||
return ecs;
|
||||
}
|
||||
|
||||
async createHomestead(id) {
|
||||
const ecs = this.createEcs();
|
||||
const view = Ecs.serialize(ecs);
|
||||
await this.server.writeData(
|
||||
join('homesteads', `${id}`),
|
||||
|
@ -137,7 +140,10 @@ export default class Engine {
|
|||
}
|
||||
|
||||
async loadEcs(path) {
|
||||
this.ecses[path] = Ecs.deserialize(await this.server.readData(path));
|
||||
this.ecses[path] = Ecs.deserialize(
|
||||
this.createEcs(),
|
||||
await this.server.readData(path),
|
||||
);
|
||||
}
|
||||
|
||||
async loadPlayer(id) {
|
||||
|
|
|
@ -5,20 +5,26 @@ import Server from '@/net/server/server.js';
|
|||
|
||||
import Engine from './engine.js';
|
||||
|
||||
test('visibility-based updates', async () => {
|
||||
const engine = new Engine(Server);
|
||||
const data = {};
|
||||
engine.server.readData = async (path) => {
|
||||
if (path in data) {
|
||||
return data[path];
|
||||
class TestServer extends Server {
|
||||
constructor() {
|
||||
super();
|
||||
this.data = {};
|
||||
}
|
||||
async readData(path) {
|
||||
if (path in this.data) {
|
||||
return this.data[path];
|
||||
}
|
||||
const error = new Error();
|
||||
error.code = 'ENOENT';
|
||||
throw error;
|
||||
};
|
||||
engine.server.writeData = async (path, view) => {
|
||||
data[path] = view;
|
||||
};
|
||||
}
|
||||
async writeData(path, view) {
|
||||
this.data[path] = view;
|
||||
}
|
||||
}
|
||||
|
||||
test('visibility-based updates', async () => {
|
||||
const engine = new Engine(TestServer);
|
||||
// Connect an entity.
|
||||
await engine.connectPlayer(0, 0);
|
||||
const ecs = engine.ecses['homesteads/0'];
|
||||
|
|
|
@ -6,12 +6,16 @@ export default class LocalClient extends Client {
|
|||
new URL('../server/worker.js', import.meta.url),
|
||||
{type: 'module'},
|
||||
);
|
||||
this.worker.onmessage = (event) => {
|
||||
this.worker.addEventListener('message', (event) => {
|
||||
if (0 === event.data) {
|
||||
this.worker.terminate();
|
||||
return;
|
||||
}
|
||||
this.accept(event.data);
|
||||
};
|
||||
});
|
||||
}
|
||||
disconnect() {
|
||||
this.worker.terminate();
|
||||
this.worker.postMessage(0);
|
||||
}
|
||||
transmit(packed) {
|
||||
this.worker.postMessage(packed);
|
||||
|
|
|
@ -1,26 +1,53 @@
|
|||
import Engine from '../../engine/engine.js';
|
||||
import {get, set} from 'idb-keyval';
|
||||
|
||||
import {encode} from '@/packets/index.js';
|
||||
|
||||
import Engine from '../../engine/engine.js';
|
||||
import Server from './server.js';
|
||||
|
||||
class WorkerServer extends Server {
|
||||
constructor() {
|
||||
super();
|
||||
this.fs = {};
|
||||
}
|
||||
static qualify(path) {
|
||||
return ['UNIVERSE', path].join('/');
|
||||
}
|
||||
async readData(path) {
|
||||
const data = await get(this.constructor.qualify(path));
|
||||
if ('undefined' !== typeof data) {
|
||||
return data;
|
||||
}
|
||||
const error = new Error();
|
||||
error.code = 'ENOENT';
|
||||
throw error;
|
||||
}
|
||||
async writeData(path, view) {
|
||||
await set(this.constructor.qualify(path), view);
|
||||
}
|
||||
transmit(connection, packed) { postMessage(packed); }
|
||||
}
|
||||
|
||||
const engine = new Engine(WorkerServer);
|
||||
|
||||
onmessage = (event) => {
|
||||
engine.server.accept(undefined, event.data);
|
||||
onmessage = async (event) => {
|
||||
if (0 === event.data) {
|
||||
await engine.disconnectPlayer(0, 0);
|
||||
postMessage(0);
|
||||
return;
|
||||
}
|
||||
engine.server.accept(0, event.data);
|
||||
};
|
||||
|
||||
(async () => {
|
||||
await engine.load();
|
||||
engine.start();
|
||||
await engine.connectPlayer();
|
||||
await engine.connectPlayer(0, 0);
|
||||
postMessage(encode({type: 'ConnectionStatus', payload: 'connected'}));
|
||||
})();
|
||||
|
||||
import.meta.hot.accept('../../engine/engine.js', () => {
|
||||
import.meta.hot.accept('../../engine/engine.js', async () => {
|
||||
await engine.disconnectPlayer(0, 0);
|
||||
postMessage(encode({type: 'ConnectionStatus', payload: 'aborted'}));
|
||||
close();
|
||||
});
|
||||
|
|
|
@ -2,14 +2,16 @@ import {Container} from '@pixi/react';
|
|||
import {useState} from 'react';
|
||||
|
||||
import {RESOLUTION} from '@/constants.js';
|
||||
import Ecs from '@/engine/ecs.js';
|
||||
import Ecs from '@/ecs/ecs.js';
|
||||
import Components from '@/ecs-components/index.js';
|
||||
import Systems from '@/ecs-systems/index.js';
|
||||
import usePacket from '@/hooks/use-packet.js';
|
||||
|
||||
import Entities from './entities.jsx';
|
||||
import TileLayer from './tile-layer.jsx';
|
||||
|
||||
export default function EcsComponent() {
|
||||
const [ecs] = useState(new Ecs());
|
||||
const [ecs] = useState(new Ecs({Components, Systems}));
|
||||
const [entities, setEntities] = useState({});
|
||||
const [mainEntity, setMainEntity] = useState();
|
||||
usePacket('Tick', (payload) => {
|
||||
|
|
|
@ -26,7 +26,7 @@ export default function Ui({disconnected}) {
|
|||
if (disconnected) {
|
||||
handle = setTimeout(() => {
|
||||
setShowDisconnected(true);
|
||||
}, 200);
|
||||
}, 400);
|
||||
}
|
||||
else {
|
||||
setShowDisconnected(false)
|
||||
|
|
|
@ -16,7 +16,7 @@ export default function PlaySpecific() {
|
|||
const [client, setClient] = useState();
|
||||
const [disconnected, setDisconnected] = useState(false);
|
||||
const params = useParams();
|
||||
const [, url] = params['*'].split('/');
|
||||
const [type, url] = params['*'].split('/');
|
||||
useEffect(() => {
|
||||
if (!Client) {
|
||||
return;
|
||||
|
@ -31,6 +31,20 @@ export default function PlaySpecific() {
|
|||
client.disconnect();
|
||||
};
|
||||
}, [Client, url]);
|
||||
// Sneakily use beforeunload to snag some time to save.
|
||||
useEffect(() => {
|
||||
if ('local' !== type) {
|
||||
return;
|
||||
}
|
||||
function onBeforeUnload(event) {
|
||||
client.disconnect();
|
||||
event.preventDefault();
|
||||
}
|
||||
addEventListener('beforeunload', onBeforeUnload);
|
||||
return () => {
|
||||
removeEventListener('beforeunload', onBeforeUnload);
|
||||
};
|
||||
});
|
||||
useEffect(() => {
|
||||
if (!client) {
|
||||
return;
|
||||
|
|
6
package-lock.json
generated
6
package-lock.json
generated
|
@ -15,6 +15,7 @@
|
|||
"@remix-run/react": "^2.9.2",
|
||||
"compression": "^1.7.4",
|
||||
"express": "^4.18.2",
|
||||
"idb-keyval": "^6.2.1",
|
||||
"isbot": "^4.1.0",
|
||||
"morgan": "^1.10.0",
|
||||
"react": "^18.2.0",
|
||||
|
@ -11113,6 +11114,11 @@
|
|||
"postcss": "^8.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/idb-keyval": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.1.tgz",
|
||||
"integrity": "sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg=="
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
"@remix-run/react": "^2.9.2",
|
||||
"compression": "^1.7.4",
|
||||
"express": "^4.18.2",
|
||||
"idb-keyval": "^6.2.1",
|
||||
"isbot": "^4.1.0",
|
||||
"morgan": "^1.10.0",
|
||||
"react": "^18.2.0",
|
||||
|
|
Loading…
Reference in New Issue
Block a user