Comparing version 0.0.1 to 0.1.0
@@ -8,3 +8,5 @@ import { System } from "../../build/ecsy.module.js"; | ||
return { | ||
entities: [Circle, Movement] | ||
queries: { | ||
entities: { components: [Circle, Movement] } | ||
} | ||
}; | ||
@@ -36,6 +38,2 @@ } | ||
var dx = circle.position.x; | ||
var dy = circle.position.y; | ||
var dist = Math.sqrt(dx * dx + dy * dy); | ||
if (circle.position.y + circle.radius < 0) | ||
@@ -59,8 +57,9 @@ circle.position.y = canvasHeight + circle.radius; | ||
return { | ||
entities: [Circle] | ||
queries: { | ||
entities: { components: [Circle] } | ||
} | ||
}; | ||
} | ||
execute(delta) { | ||
var ctx = this.world.components.canvasContext.ctx; | ||
execute() { | ||
let entities = this.queries.entities; | ||
@@ -90,2 +89,5 @@ | ||
} | ||
if (entity.hasComponent(Intersecting) && entity.getComponent(Intersecting).points.length === 0) { | ||
entity.removeComponent(Intersecting); | ||
} | ||
} | ||
@@ -111,8 +113,10 @@ } | ||
return { | ||
circles: [Circle], | ||
intersectingCircles: [Intersecting] | ||
queries: { | ||
circles: { components: [Circle] }, | ||
intersectingCircles: { components: [Intersecting] } | ||
} | ||
}; | ||
} | ||
execute(delta) { | ||
execute() { | ||
let canvasComponent = this.world.components.canvasContext; | ||
@@ -119,0 +123,0 @@ let ctx = canvasComponent.ctx; |
{ | ||
"name": "ecsy", | ||
"version": "0.0.1", | ||
"version": "0.1.0", | ||
"description": "Entity Component System in JS", | ||
@@ -10,3 +10,3 @@ "main": "build/ecsy.js", | ||
"build": "rollup -c", | ||
"dev": "concurrently --names \"ROLLUP,HTTP\" -c \"bgBlue.bold,bgGreen.bold\" \"rollup -c -w -m inline\" \"http-server -c-1 -p 8080\"", | ||
"dev": "concurrently --names \"ROLLUP,HTTP\" -c \"bgBlue.bold,bgGreen.bold\" \"rollup -c -w -m inline\" \"http-server -c-1 -p 8080 --cors\"", | ||
"lint": "eslint src", | ||
@@ -13,0 +13,0 @@ "start": "npm run dev", |
@@ -22,7 +22,7 @@ # ecsy | ||
- Reactive support: | ||
- Support for reactive systems (React to changes on entities and components) | ||
- Support for reactive behaviour on systems (React to changes on entities and components) | ||
- System can query mutable or immutable components | ||
- Predictable: | ||
- Systems will run on the order they were registered | ||
- Reactive events will not generate a random callback when emited but queued | ||
- Systems will run on the order they were registered or based on the priority defined when registering them | ||
- Reactive events will not generate a random callback when emited but queued and be processed in order | ||
- Modern Javascript: ES6, classes, modules,... | ||
@@ -72,3 +72,5 @@ - Pool for components and entities | ||
return { | ||
entities: [Rotating, Transform] | ||
queries: { | ||
entities: { components: [Rotating, Transform] } | ||
} | ||
}; | ||
@@ -109,18 +111,36 @@ } | ||
return { | ||
entities: [Transform] | ||
queries: { | ||
entities: { | ||
components: [Rotating, Transform] | ||
events: { | ||
added: { | ||
event: "EntityAdded" | ||
}, | ||
removed: { | ||
event: "EntityRemoved" | ||
}, | ||
changed: { | ||
event: "EntityChanged" | ||
}, | ||
rotatingChanged: { | ||
event: "ComponentChanged", | ||
components: [Rotating] | ||
}, | ||
transformChanged: { | ||
event: "ComponentChanged", | ||
components: [Transform] | ||
} | ||
} | ||
} | ||
} | ||
}; | ||
} | ||
onEntitiesAdded(entities) { | ||
console.log('OnAdded', this.queries.entities.added); | ||
execute() { | ||
console.log('OnAdded', this.events.entities.added); | ||
console.log('OnRemoved', this.events.entities.removed); | ||
console.log('OnChanged entities', this.events.entities.changed); | ||
console.log('OnChanged Rotating Component', this.events.entities.rotatingChanged); | ||
console.log('OnChanged Transform Component', this.events.entities.transformChanged); | ||
} | ||
onEntitiesRemoved(entities) { | ||
console.log('OnRemoved', this.queries.entities.removed); | ||
} | ||
onEntitiesChanged(entities) { | ||
console.log('OnChanged entities', this.queries.entities.changed); | ||
} | ||
onComponentChanged(entities, component) { | ||
console.log('OnChanged components', entities, component); | ||
} | ||
} | ||
@@ -127,0 +147,0 @@ |
@@ -56,4 +56,8 @@ import Entity from "./Entity.js"; | ||
if (values) { | ||
for (var name in values) { | ||
component[name] = values[name]; | ||
if (component.copy) { | ||
component.copy(values); | ||
} else { | ||
for (var name in values) { | ||
component[name] = values[name]; | ||
} | ||
} | ||
@@ -60,0 +64,0 @@ } |
export { World } from "./World.js"; | ||
export { System } from "./System.js"; | ||
export { ReactiveSystem } from "./ReactiveSystem.js"; | ||
export { SchemaTypes } from "./SchemaTypes.js"; |
@@ -16,3 +16,3 @@ import EventDispatcher from "./EventDispatcher.js"; | ||
// This query is being used by a ReactiveSystem | ||
// This query is being used by a reactive system | ||
this.reactive = false; | ||
@@ -19,0 +19,0 @@ |
/** | ||
* @class System | ||
*/ | ||
import Query from "./Query.js"; | ||
export class System { | ||
toJSON() { | ||
var json = { | ||
name: this.constructor.name, | ||
enabled: this.enabled, | ||
executeTime: this.executeTime, | ||
priority: this.priority, | ||
queries: {}, | ||
events: {} | ||
}; | ||
if (this.config) { | ||
var queries = this.config.queries; | ||
for (let queryName in queries) { | ||
let query = queries[queryName]; | ||
json.queries[queryName] = { | ||
key: this._queries[queryName].key | ||
}; | ||
if (query.events) { | ||
let events = (json.queries[queryName]["events"] = {}); | ||
for (let eventName in query.events) { | ||
let event = query.events[eventName]; | ||
events[eventName] = { | ||
eventName: event.event, | ||
numEntities: this.events[queryName][eventName].length | ||
}; | ||
if (event.components) { | ||
events[eventName].components = event.components.map(c => c.name); | ||
} | ||
} | ||
} | ||
} | ||
let events = this.config.events; | ||
for (let eventName in events) { | ||
json.events[eventName] = { | ||
eventName: events[eventName] | ||
}; | ||
} | ||
} | ||
return json; | ||
} | ||
constructor(world, attributes) { | ||
this.world = world; | ||
this.enabled = true; | ||
this.queryComponents = this.init ? this.init() : null; | ||
// @todo Better naming :) | ||
this._queries = {}; | ||
this.queries = {}; | ||
this._events = {}; | ||
this.events = {}; | ||
this.priority = 0; | ||
if (attributes) { | ||
if (attributes.priority) { | ||
this.priority = attributes.priority; | ||
// Used for stats | ||
this.executeTime = 0; | ||
if (attributes && attributes.priority) { | ||
this.priority = attributes.priority; | ||
} | ||
this.config = this.init ? this.init() : null; | ||
if (!this.config) return; | ||
if (this.config.queries) { | ||
for (var name in this.config.queries) { | ||
var queryConfig = this.config.queries[name]; | ||
var Components = queryConfig.components; | ||
if (!Components || Components.length === 0) { | ||
throw new Error("'components' attribute can't be empty in a query"); | ||
} | ||
var query = this.world.entityManager.queryComponents(Components); | ||
this._queries[name] = query; | ||
this.queries[name] = query.entities; | ||
if (queryConfig.events) { | ||
this.events[name] = {}; | ||
let events = this.events[name]; | ||
for (let eventName in queryConfig.events) { | ||
let event = queryConfig.events[eventName]; | ||
events[eventName] = []; | ||
const eventMapping = { | ||
EntityAdded: Query.prototype.ENTITY_ADDED, | ||
EntityRemoved: Query.prototype.ENTITY_REMOVED, | ||
EntityChanged: Query.prototype.ENTITY_CHANGED | ||
}; | ||
if (eventMapping[event.event]) { | ||
query.eventDispatcher.addEventListener( | ||
eventMapping[event.event], | ||
entity => { | ||
events[eventName].push(entity); | ||
} | ||
); | ||
} else if (event.event === "ComponentChanged") { | ||
query.reactive = true; | ||
query.eventDispatcher.addEventListener( | ||
Query.prototype.COMPONENT_CHANGED, | ||
(entity, component) => { | ||
if (event.components.indexOf(component.constructor) !== -1) { | ||
events[eventName].push(entity); | ||
} | ||
} | ||
); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
for (var name in this.queryComponents) { | ||
var Components = this.queryComponents[name]; | ||
var query = this.world.entityManager.queryComponents(Components); | ||
this._queries[name] = query; | ||
this.queries[name] = query.entities; | ||
if (this.config.events) { | ||
for (let name in this.config.events) { | ||
var event = this.config.events[name]; | ||
this.events[name] = []; | ||
this.world.addEventListener(event, data => { | ||
this.events[name].push(data); | ||
}); | ||
} | ||
} | ||
@@ -34,2 +138,15 @@ } | ||
} | ||
clearEvents() { | ||
for (var name in this.events) { | ||
var event = this.events[name]; | ||
if (Array.isArray(event)) { | ||
this.events[name].length = 0; | ||
} else { | ||
for (name in event) { | ||
event[name].length = 0; | ||
} | ||
} | ||
} | ||
} | ||
} |
@@ -1,3 +0,1 @@ | ||
import { ReactiveSystem } from "./ReactiveSystem.js"; | ||
/** | ||
@@ -47,24 +45,10 @@ * @class SystemManager | ||
if (system.enabled) { | ||
if (system instanceof ReactiveSystem) { | ||
if (system.onEntitiesAdded && system.counters.added) { | ||
system.onEntitiesAdded(); | ||
} | ||
if (system.onEntitiesRemoved && system.counters.removed) { | ||
system.onEntitiesRemoved(); | ||
} | ||
if (system.onEntitiesChanged && system.counters.changed) { | ||
system.onEntitiesChanged(); | ||
} | ||
} else if (system.execute) { | ||
if (system.execute) { | ||
let startTime = performance.now(); | ||
system.execute(delta, time); | ||
system.executeTime = performance.now() - startTime; | ||
} | ||
system.clearEvents(); | ||
} | ||
}); | ||
this.systems.forEach(system => { | ||
system = this.systems[name]; | ||
if (system instanceof ReactiveSystem) { | ||
system.clearQueries(); | ||
} | ||
}); | ||
} | ||
@@ -71,0 +55,0 @@ |
@@ -5,2 +5,3 @@ import { SystemManager } from "./SystemManager.js"; | ||
import { componentPropertyName } from "./Utils.js"; | ||
import EventDispatcher from "./EventDispatcher.js"; | ||
@@ -18,4 +19,22 @@ /** | ||
this.components = {}; | ||
this.eventQueues = {}; | ||
this.eventDispatcher = new EventDispatcher(); | ||
var event = new CustomEvent("ecsy-world-created", { detail: this }); | ||
window.dispatchEvent(event); | ||
} | ||
emitEvent(eventName, data) { | ||
this.eventDispatcher.dispatchEvent(eventName, data); | ||
} | ||
addEventListener(eventName, callback) { | ||
this.eventDispatcher.addEventListener(eventName, callback); | ||
} | ||
removeEventListener(eventName, callback) { | ||
this.eventDispatcher.removeEventListener(eventName, callback); | ||
} | ||
/** | ||
@@ -22,0 +41,0 @@ * Register a singleton component |
@@ -5,17 +5,12 @@ import test from "ava"; | ||
class SystemA extends System {} | ||
class SystemB extends System {} | ||
class SystemC extends System {} | ||
class SystemD extends System {} | ||
class SystemE extends System {} | ||
/** | ||
* TODO | ||
* - IDs | ||
* - tags | ||
*/ | ||
test("init", t => { | ||
var world = new World(); | ||
class SystemA extends System {} | ||
class SystemB extends System {} | ||
class SystemC extends System {} | ||
class SystemD extends System {} | ||
class SystemE extends System {} | ||
// Register empty system | ||
world | ||
@@ -49,3 +44,3 @@ .registerSystem(SystemA) | ||
); | ||
/* | ||
/* | ||
world = new World(); | ||
@@ -66,2 +61,256 @@ world | ||
*/ | ||
world.execute(); | ||
}); | ||
test("empty_queries", t => { | ||
var world = new World(); | ||
class SystemEmpty0 extends System {} | ||
class SystemEmpty1 extends System { | ||
init() {} | ||
} | ||
class SystemEmpty2 extends System { | ||
init() { | ||
return {}; | ||
} | ||
} | ||
class SystemEmpty3 extends System { | ||
init() { | ||
return { queries: {} }; | ||
} | ||
} | ||
class SystemEmpty4 extends System { | ||
init() { | ||
return { | ||
queries: { | ||
entities: {} | ||
} | ||
}; | ||
} | ||
} | ||
class SystemEmpty5 extends System { | ||
init() { | ||
return { | ||
queries: { | ||
entities: { components: [] } | ||
} | ||
}; | ||
} | ||
} | ||
// Register empty system | ||
world | ||
.registerSystem(SystemEmpty0) | ||
.registerSystem(SystemEmpty1) | ||
.registerSystem(SystemEmpty2) | ||
.registerSystem(SystemEmpty3); | ||
t.deepEqual(world.systemManager.systems[0].queries, {}); | ||
t.deepEqual(world.systemManager.systems[1].queries, {}); | ||
t.deepEqual(world.systemManager.systems[2].queries, {}); | ||
t.deepEqual(world.systemManager.systems[3].queries, {}); | ||
const error = t.throws(() => { | ||
world.registerSystem(SystemEmpty4); | ||
}, Error); | ||
t.is(error.message, "'components' attribute can't be empty in a query"); | ||
const error2 = t.throws(() => { | ||
world.registerSystem(SystemEmpty5); | ||
}, Error); | ||
t.is(error2.message, "'components' attribute can't be empty in a query"); | ||
}); | ||
test("queries", t => { | ||
var world = new World(); | ||
world.registerComponent(FooComponent).registerComponent(BarComponent); | ||
for (var i = 0; i < 15; i++) { | ||
var entity = world.createEntity(); | ||
if (i < 10) entity.addComponent(FooComponent); | ||
if (i >= 5) entity.addComponent(BarComponent); | ||
} | ||
class SystemFoo extends System { | ||
init() { | ||
return { | ||
queries: { | ||
entities: { components: [FooComponent] } | ||
} | ||
}; | ||
} | ||
} | ||
class SystemBar extends System { | ||
init() { | ||
return { | ||
queries: { | ||
entities: { components: [BarComponent] } | ||
} | ||
}; | ||
} | ||
} | ||
class SystemBoth extends System { | ||
init() { | ||
return { | ||
queries: { | ||
entities: { components: [FooComponent, BarComponent] } | ||
} | ||
}; | ||
} | ||
} | ||
// Register empty system | ||
world | ||
.registerSystem(SystemFoo) | ||
.registerSystem(SystemBar) | ||
.registerSystem(SystemBoth); | ||
// Foo | ||
t.is(world.systemManager.systems[0].queries.entities.length, 10); | ||
// Bar | ||
t.is(world.systemManager.systems[1].queries.entities.length, 10); | ||
// Both | ||
t.is(world.systemManager.systems[2].queries.entities.length, 5); | ||
}); | ||
test("reactive", t => { | ||
var world = new World(); | ||
class ReactiveSystem extends System { | ||
init() { | ||
return { | ||
queries: { | ||
entities: { | ||
components: [FooComponent, BarComponent], | ||
events: { | ||
added: { | ||
event: "EntityAdded" | ||
}, | ||
removed: { | ||
event: "EntityRemoved" | ||
}, | ||
changed: { | ||
event: "EntityChanged" | ||
}, | ||
fooChanged: { | ||
event: "ComponentChanged", | ||
components: [FooComponent] | ||
}, | ||
barChanged: { | ||
event: "ComponentChanged", | ||
components: [BarComponent] | ||
}, | ||
foobarChanged: { | ||
event: "ComponentChanged", | ||
components: [FooComponent, BarComponent] | ||
} | ||
} | ||
} | ||
} | ||
}; | ||
} | ||
} | ||
// Register empty system | ||
world.registerSystem(ReactiveSystem); | ||
world.registerComponent(FooComponent).registerComponent(BarComponent); | ||
for (var i = 0; i < 15; i++) { | ||
world | ||
.createEntity() | ||
.addComponent(FooComponent) | ||
.addComponent(BarComponent); | ||
} | ||
// Entities from the standard query | ||
t.is(world.systemManager.systems[0].queries.entities.length, 15); | ||
// Added entities | ||
t.is(world.systemManager.systems[0].events.entities.added.length, 15); | ||
world.execute(); // After execute, events should be cleared | ||
t.is(world.systemManager.systems[0].events.entities.added.length, 0); | ||
// Add a new one | ||
world | ||
.createEntity() | ||
.addComponent(FooComponent) | ||
.addComponent(BarComponent); | ||
t.is(world.systemManager.systems[0].events.entities.added.length, 1); | ||
world.execute(); // After execute, events should be cleared | ||
t.is(world.systemManager.systems[0].events.entities.added.length, 0); | ||
// Changing | ||
world.entityManager._entities[0].getMutableComponent(FooComponent); | ||
t.is(world.systemManager.systems[0].events.entities.changed.length, 1); | ||
t.is(world.systemManager.systems[0].events.entities.fooChanged.length, 1); | ||
t.is(world.systemManager.systems[0].events.entities.barChanged.length, 0); | ||
t.is(world.systemManager.systems[0].events.entities.foobarChanged.length, 0); | ||
world.execute(); // After execute, events should be cleared | ||
t.is(world.systemManager.systems[0].events.entities.changed.length, 0); | ||
t.is(world.systemManager.systems[0].events.entities.fooChanged.length, 0); | ||
world.entityManager._entities[0].getMutableComponent(BarComponent); | ||
t.is(world.systemManager.systems[0].events.entities.changed.length, 1); | ||
t.is(world.systemManager.systems[0].events.entities.fooChanged.length, 0); | ||
t.is(world.systemManager.systems[0].events.entities.barChanged.length, 1); | ||
t.is(world.systemManager.systems[0].events.entities.foobarChanged.length, 0); | ||
world.execute(); // After execute, events should be cleared | ||
t.is(world.systemManager.systems[0].events.entities.changed.length, 0); | ||
t.is(world.systemManager.systems[0].events.entities.barChanged.length, 0); | ||
// Check if the entity is already on the list? | ||
world.entityManager._entities[0].getMutableComponent(FooComponent); | ||
world.entityManager._entities[0].getMutableComponent(BarComponent); | ||
t.is(world.systemManager.systems[0].events.entities.changed.length, 1); | ||
t.is(world.systemManager.systems[0].events.entities.fooChanged.length, 1); | ||
t.is(world.systemManager.systems[0].events.entities.barChanged.length, 1); | ||
t.is(world.systemManager.systems[0].events.entities.foobarChanged.length, 1); | ||
world.execute(); // After execute, events should be cleared | ||
t.is(world.systemManager.systems[0].events.entities.changed.length, 0); | ||
t.is(world.systemManager.systems[0].events.entities.fooChanged.length, 0); | ||
t.is(world.systemManager.systems[0].events.entities.barChanged.length, 0); | ||
t.is(world.systemManager.systems[0].events.entities.foobarChanged.length, 0); | ||
// Dispose an entity | ||
world.entityManager._entities[0].dispose(); | ||
t.is(world.systemManager.systems[0].events.entities.removed.length, 1); | ||
world.execute(); // After execute, events should be cleared | ||
t.is(world.systemManager.systems[0].events.entities.removed.length, 0); | ||
// Removed | ||
world.entityManager._entities[0].removeComponent(FooComponent); | ||
t.is(world.systemManager.systems[0].events.entities.removed.length, 1); | ||
world.execute(); // After execute, events should be cleared | ||
t.is(world.systemManager.systems[0].events.entities.removed.length, 0); | ||
// Added componets to the previous one | ||
world.entityManager._entities[0].addComponent(FooComponent); | ||
t.is(world.systemManager.systems[0].events.entities.added.length, 1); | ||
world.execute(); // After execute, events should be cleared | ||
t.is(world.systemManager.systems[0].events.entities.added.length, 0); | ||
// Remove all components from the first 5 entities | ||
for (i = 0; i < 5; i++) { | ||
world.entityManager._entities[i].removeAllComponents(); | ||
} | ||
t.is(world.systemManager.systems[0].events.entities.removed.length, 5); | ||
world.execute(); // After execute, events should be cleared | ||
t.is(world.systemManager.systems[0].events.entities.removed.length, 0); | ||
// Dispose all entities | ||
world.entityManager.removeAllEntities(); | ||
t.is(world.systemManager.systems[0].events.entities.removed.length, 10); | ||
world.execute(); // After execute, events should be cleared | ||
t.is(world.systemManager.systems[0].events.entities.removed.length, 0); | ||
}); |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
293129
4792
175
42