Dies ist das Repository meines kleinen Portfolios.
Im Hintergrund läuft eine Planetensimulation, geschrieben in JavaScript und Three.js.
Die zu sehenden Texturen stammen von:
https://www.solarsystemscope.com/textures/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1792 lines
44 KiB
1792 lines
44 KiB
/** |
|
* Return the name of a component |
|
* @param {Component} Component |
|
* @private |
|
*/ |
|
|
|
/** |
|
* Get a key from a list of components |
|
* @param {Array(Component)} Components Array of components to generate the key |
|
* @private |
|
*/ |
|
function queryKey(Components) { |
|
var ids = []; |
|
for (var n = 0; n < Components.length; n++) { |
|
var T = Components[n]; |
|
|
|
if (!componentRegistered(T)) { |
|
throw new Error(`Tried to create a query with an unregistered component`); |
|
} |
|
|
|
if (typeof T === "object") { |
|
var operator = T.operator === "not" ? "!" : T.operator; |
|
ids.push(operator + T.Component._typeId); |
|
} else { |
|
ids.push(T._typeId); |
|
} |
|
} |
|
|
|
return ids.sort().join("-"); |
|
} |
|
|
|
// Detector for browser's "window" |
|
const hasWindow = typeof window !== "undefined"; |
|
|
|
// performance.now() "polyfill" |
|
const now = |
|
hasWindow && typeof window.performance !== "undefined" |
|
? performance.now.bind(performance) |
|
: Date.now.bind(Date); |
|
|
|
function componentRegistered(T) { |
|
return ( |
|
(typeof T === "object" && T.Component._typeId !== undefined) || |
|
(T.isComponent && T._typeId !== undefined) |
|
); |
|
} |
|
|
|
class SystemManager { |
|
constructor(world) { |
|
this._systems = []; |
|
this._executeSystems = []; // Systems that have `execute` method |
|
this.world = world; |
|
this.lastExecutedSystem = null; |
|
} |
|
|
|
registerSystem(SystemClass, attributes) { |
|
if (!SystemClass.isSystem) { |
|
throw new Error( |
|
`System '${SystemClass.name}' does not extend 'System' class` |
|
); |
|
} |
|
|
|
if (this.getSystem(SystemClass) !== undefined) { |
|
console.warn(`System '${SystemClass.getName()}' already registered.`); |
|
return this; |
|
} |
|
|
|
var system = new SystemClass(this.world, attributes); |
|
if (system.init) system.init(attributes); |
|
system.order = this._systems.length; |
|
this._systems.push(system); |
|
if (system.execute) { |
|
this._executeSystems.push(system); |
|
this.sortSystems(); |
|
} |
|
return this; |
|
} |
|
|
|
unregisterSystem(SystemClass) { |
|
let system = this.getSystem(SystemClass); |
|
if (system === undefined) { |
|
console.warn( |
|
`Can unregister system '${SystemClass.getName()}'. It doesn't exist.` |
|
); |
|
return this; |
|
} |
|
|
|
this._systems.splice(this._systems.indexOf(system), 1); |
|
|
|
if (system.execute) { |
|
this._executeSystems.splice(this._executeSystems.indexOf(system), 1); |
|
} |
|
|
|
// @todo Add system.unregister() call to free resources |
|
return this; |
|
} |
|
|
|
sortSystems() { |
|
this._executeSystems.sort((a, b) => { |
|
return a.priority - b.priority || a.order - b.order; |
|
}); |
|
} |
|
|
|
getSystem(SystemClass) { |
|
return this._systems.find((s) => s instanceof SystemClass); |
|
} |
|
|
|
getSystems() { |
|
return this._systems; |
|
} |
|
|
|
removeSystem(SystemClass) { |
|
var index = this._systems.indexOf(SystemClass); |
|
if (!~index) return; |
|
|
|
this._systems.splice(index, 1); |
|
} |
|
|
|
executeSystem(system, delta, time) { |
|
if (system.initialized) { |
|
if (system.canExecute()) { |
|
let startTime = now(); |
|
system.execute(delta, time); |
|
system.executeTime = now() - startTime; |
|
this.lastExecutedSystem = system; |
|
system.clearEvents(); |
|
} |
|
} |
|
} |
|
|
|
stop() { |
|
this._executeSystems.forEach((system) => system.stop()); |
|
} |
|
|
|
execute(delta, time, forcePlay) { |
|
this._executeSystems.forEach( |
|
(system) => |
|
(forcePlay || system.enabled) && this.executeSystem(system, delta, time) |
|
); |
|
} |
|
|
|
stats() { |
|
var stats = { |
|
numSystems: this._systems.length, |
|
systems: {}, |
|
}; |
|
|
|
for (var i = 0; i < this._systems.length; i++) { |
|
var system = this._systems[i]; |
|
var systemStats = (stats.systems[system.getName()] = { |
|
queries: {}, |
|
executeTime: system.executeTime, |
|
}); |
|
for (var name in system.ctx) { |
|
systemStats.queries[name] = system.ctx[name].stats(); |
|
} |
|
} |
|
|
|
return stats; |
|
} |
|
} |
|
|
|
class ObjectPool { |
|
// @todo Add initial size |
|
constructor(T, initialSize) { |
|
this.freeList = []; |
|
this.count = 0; |
|
this.T = T; |
|
this.isObjectPool = true; |
|
|
|
if (typeof initialSize !== "undefined") { |
|
this.expand(initialSize); |
|
} |
|
} |
|
|
|
acquire() { |
|
// Grow the list by 20%ish if we're out |
|
if (this.freeList.length <= 0) { |
|
this.expand(Math.round(this.count * 0.2) + 1); |
|
} |
|
|
|
var item = this.freeList.pop(); |
|
|
|
return item; |
|
} |
|
|
|
release(item) { |
|
item.reset(); |
|
this.freeList.push(item); |
|
} |
|
|
|
expand(count) { |
|
for (var n = 0; n < count; n++) { |
|
var clone = new this.T(); |
|
clone._pool = this; |
|
this.freeList.push(clone); |
|
} |
|
this.count += count; |
|
} |
|
|
|
totalSize() { |
|
return this.count; |
|
} |
|
|
|
totalFree() { |
|
return this.freeList.length; |
|
} |
|
|
|
totalUsed() { |
|
return this.count - this.freeList.length; |
|
} |
|
} |
|
|
|
/** |
|
* @private |
|
* @class EventDispatcher |
|
*/ |
|
class EventDispatcher { |
|
constructor() { |
|
this._listeners = {}; |
|
this.stats = { |
|
fired: 0, |
|
handled: 0, |
|
}; |
|
} |
|
|
|
/** |
|
* Add an event listener |
|
* @param {String} eventName Name of the event to listen |
|
* @param {Function} listener Callback to trigger when the event is fired |
|
*/ |
|
addEventListener(eventName, listener) { |
|
let listeners = this._listeners; |
|
if (listeners[eventName] === undefined) { |
|
listeners[eventName] = []; |
|
} |
|
|
|
if (listeners[eventName].indexOf(listener) === -1) { |
|
listeners[eventName].push(listener); |
|
} |
|
} |
|
|
|
/** |
|
* Check if an event listener is already added to the list of listeners |
|
* @param {String} eventName Name of the event to check |
|
* @param {Function} listener Callback for the specified event |
|
*/ |
|
hasEventListener(eventName, listener) { |
|
return ( |
|
this._listeners[eventName] !== undefined && |
|
this._listeners[eventName].indexOf(listener) !== -1 |
|
); |
|
} |
|
|
|
/** |
|
* Remove an event listener |
|
* @param {String} eventName Name of the event to remove |
|
* @param {Function} listener Callback for the specified event |
|
*/ |
|
removeEventListener(eventName, listener) { |
|
var listenerArray = this._listeners[eventName]; |
|
if (listenerArray !== undefined) { |
|
var index = listenerArray.indexOf(listener); |
|
if (index !== -1) { |
|
listenerArray.splice(index, 1); |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* Dispatch an event |
|
* @param {String} eventName Name of the event to dispatch |
|
* @param {Entity} entity (Optional) Entity to emit |
|
* @param {Component} component |
|
*/ |
|
dispatchEvent(eventName, entity, component) { |
|
this.stats.fired++; |
|
|
|
var listenerArray = this._listeners[eventName]; |
|
if (listenerArray !== undefined) { |
|
var array = listenerArray.slice(0); |
|
|
|
for (var i = 0; i < array.length; i++) { |
|
array[i].call(this, entity, component); |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* Reset stats counters |
|
*/ |
|
resetCounters() { |
|
this.stats.fired = this.stats.handled = 0; |
|
} |
|
} |
|
|
|
class Query { |
|
/** |
|
* @param {Array(Component)} Components List of types of components to query |
|
*/ |
|
constructor(Components, manager) { |
|
this.Components = []; |
|
this.NotComponents = []; |
|
|
|
Components.forEach((component) => { |
|
if (typeof component === "object") { |
|
this.NotComponents.push(component.Component); |
|
} else { |
|
this.Components.push(component); |
|
} |
|
}); |
|
|
|
if (this.Components.length === 0) { |
|
throw new Error("Can't create a query without components"); |
|
} |
|
|
|
this.entities = []; |
|
|
|
this.eventDispatcher = new EventDispatcher(); |
|
|
|
// This query is being used by a reactive system |
|
this.reactive = false; |
|
|
|
this.key = queryKey(Components); |
|
|
|
// Fill the query with the existing entities |
|
for (var i = 0; i < manager._entities.length; i++) { |
|
var entity = manager._entities[i]; |
|
if (this.match(entity)) { |
|
// @todo ??? this.addEntity(entity); => preventing the event to be generated |
|
entity.queries.push(this); |
|
this.entities.push(entity); |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* Add entity to this query |
|
* @param {Entity} entity |
|
*/ |
|
addEntity(entity) { |
|
entity.queries.push(this); |
|
this.entities.push(entity); |
|
|
|
this.eventDispatcher.dispatchEvent(Query.prototype.ENTITY_ADDED, entity); |
|
} |
|
|
|
/** |
|
* Remove entity from this query |
|
* @param {Entity} entity |
|
*/ |
|
removeEntity(entity) { |
|
let index = this.entities.indexOf(entity); |
|
if (~index) { |
|
this.entities.splice(index, 1); |
|
|
|
index = entity.queries.indexOf(this); |
|
entity.queries.splice(index, 1); |
|
|
|
this.eventDispatcher.dispatchEvent( |
|
Query.prototype.ENTITY_REMOVED, |
|
entity |
|
); |
|
} |
|
} |
|
|
|
match(entity) { |
|
return ( |
|
entity.hasAllComponents(this.Components) && |
|
!entity.hasAnyComponents(this.NotComponents) |
|
); |
|
} |
|
|
|
toJSON() { |
|
return { |
|
key: this.key, |
|
reactive: this.reactive, |
|
components: { |
|
included: this.Components.map((C) => C.name), |
|
not: this.NotComponents.map((C) => C.name), |
|
}, |
|
numEntities: this.entities.length, |
|
}; |
|
} |
|
|
|
/** |
|
* Return stats for this query |
|
*/ |
|
stats() { |
|
return { |
|
numComponents: this.Components.length, |
|
numEntities: this.entities.length, |
|
}; |
|
} |
|
} |
|
|
|
Query.prototype.ENTITY_ADDED = "Query#ENTITY_ADDED"; |
|
Query.prototype.ENTITY_REMOVED = "Query#ENTITY_REMOVED"; |
|
Query.prototype.COMPONENT_CHANGED = "Query#COMPONENT_CHANGED"; |
|
|
|
/** |
|
* @private |
|
* @class QueryManager |
|
*/ |
|
class QueryManager { |
|
constructor(world) { |
|
this._world = world; |
|
|
|
// Queries indexed by a unique identifier for the components it has |
|
this._queries = {}; |
|
} |
|
|
|
onEntityRemoved(entity) { |
|
for (var queryName in this._queries) { |
|
var query = this._queries[queryName]; |
|
if (entity.queries.indexOf(query) !== -1) { |
|
query.removeEntity(entity); |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* Callback when a component is added to an entity |
|
* @param {Entity} entity Entity that just got the new component |
|
* @param {Component} Component Component added to the entity |
|
*/ |
|
onEntityComponentAdded(entity, Component) { |
|
// @todo Use bitmask for checking components? |
|
|
|
// Check each indexed query to see if we need to add this entity to the list |
|
for (var queryName in this._queries) { |
|
var query = this._queries[queryName]; |
|
|
|
if ( |
|
!!~query.NotComponents.indexOf(Component) && |
|
~query.entities.indexOf(entity) |
|
) { |
|
query.removeEntity(entity); |
|
continue; |
|
} |
|
|
|
// Add the entity only if: |
|
// Component is in the query |
|
// and Entity has ALL the components of the query |
|
// and Entity is not already in the query |
|
if ( |
|
!~query.Components.indexOf(Component) || |
|
!query.match(entity) || |
|
~query.entities.indexOf(entity) |
|
) |
|
continue; |
|
|
|
query.addEntity(entity); |
|
} |
|
} |
|
|
|
/** |
|
* Callback when a component is removed from an entity |
|
* @param {Entity} entity Entity to remove the component from |
|
* @param {Component} Component Component to remove from the entity |
|
*/ |
|
onEntityComponentRemoved(entity, Component) { |
|
for (var queryName in this._queries) { |
|
var query = this._queries[queryName]; |
|
|
|
if ( |
|
!!~query.NotComponents.indexOf(Component) && |
|
!~query.entities.indexOf(entity) && |
|
query.match(entity) |
|
) { |
|
query.addEntity(entity); |
|
continue; |
|
} |
|
|
|
if ( |
|
!!~query.Components.indexOf(Component) && |
|
!!~query.entities.indexOf(entity) && |
|
!query.match(entity) |
|
) { |
|
query.removeEntity(entity); |
|
continue; |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* Get a query for the specified components |
|
* @param {Component} Components Components that the query should have |
|
*/ |
|
getQuery(Components) { |
|
var key = queryKey(Components); |
|
var query = this._queries[key]; |
|
if (!query) { |
|
this._queries[key] = query = new Query(Components, this._world); |
|
} |
|
return query; |
|
} |
|
|
|
/** |
|
* Return some stats from this class |
|
*/ |
|
stats() { |
|
var stats = {}; |
|
for (var queryName in this._queries) { |
|
stats[queryName] = this._queries[queryName].stats(); |
|
} |
|
return stats; |
|
} |
|
} |
|
|
|
class Component { |
|
constructor(props) { |
|
if (props !== false) { |
|
const schema = this.constructor.schema; |
|
|
|
for (const key in schema) { |
|
if (props && props.hasOwnProperty(key)) { |
|
this[key] = props[key]; |
|
} else { |
|
const schemaProp = schema[key]; |
|
if (schemaProp.hasOwnProperty("default")) { |
|
this[key] = schemaProp.type.clone(schemaProp.default); |
|
} else { |
|
const type = schemaProp.type; |
|
this[key] = type.clone(type.default); |
|
} |
|
} |
|
} |
|
|
|
if ( props !== undefined) { |
|
this.checkUndefinedAttributes(props); |
|
} |
|
} |
|
|
|
this._pool = null; |
|
} |
|
|
|
copy(source) { |
|
const schema = this.constructor.schema; |
|
|
|
for (const key in schema) { |
|
const prop = schema[key]; |
|
|
|
if (source.hasOwnProperty(key)) { |
|
this[key] = prop.type.copy(source[key], this[key]); |
|
} |
|
} |
|
|
|
// @DEBUG |
|
{ |
|
this.checkUndefinedAttributes(source); |
|
} |
|
|
|
return this; |
|
} |
|
|
|
clone() { |
|
return new this.constructor().copy(this); |
|
} |
|
|
|
reset() { |
|
const schema = this.constructor.schema; |
|
|
|
for (const key in schema) { |
|
const schemaProp = schema[key]; |
|
|
|
if (schemaProp.hasOwnProperty("default")) { |
|
this[key] = schemaProp.type.copy(schemaProp.default, this[key]); |
|
} else { |
|
const type = schemaProp.type; |
|
this[key] = type.copy(type.default, this[key]); |
|
} |
|
} |
|
} |
|
|
|
dispose() { |
|
if (this._pool) { |
|
this._pool.release(this); |
|
} |
|
} |
|
|
|
getName() { |
|
return this.constructor.getName(); |
|
} |
|
|
|
checkUndefinedAttributes(src) { |
|
const schema = this.constructor.schema; |
|
|
|
// Check that the attributes defined in source are also defined in the schema |
|
Object.keys(src).forEach((srcKey) => { |
|
if (!schema.hasOwnProperty(srcKey)) { |
|
console.warn( |
|
`Trying to set attribute '${srcKey}' not defined in the '${this.constructor.name}' schema. Please fix the schema, the attribute value won't be set` |
|
); |
|
} |
|
}); |
|
} |
|
} |
|
|
|
Component.schema = {}; |
|
Component.isComponent = true; |
|
Component.getName = function () { |
|
return this.displayName || this.name; |
|
}; |
|
|
|
class SystemStateComponent extends Component {} |
|
|
|
SystemStateComponent.isSystemStateComponent = true; |
|
|
|
class EntityPool extends ObjectPool { |
|
constructor(entityManager, entityClass, initialSize) { |
|
super(entityClass, undefined); |
|
this.entityManager = entityManager; |
|
|
|
if (typeof initialSize !== "undefined") { |
|
this.expand(initialSize); |
|
} |
|
} |
|
|
|
expand(count) { |
|
for (var n = 0; n < count; n++) { |
|
var clone = new this.T(this.entityManager); |
|
clone._pool = this; |
|
this.freeList.push(clone); |
|
} |
|
this.count += count; |
|
} |
|
} |
|
|
|
/** |
|
* @private |
|
* @class EntityManager |
|
*/ |
|
class EntityManager { |
|
constructor(world) { |
|
this.world = world; |
|
this.componentsManager = world.componentsManager; |
|
|
|
// All the entities in this instance |
|
this._entities = []; |
|
this._nextEntityId = 0; |
|
|
|
this._entitiesByNames = {}; |
|
|
|
this._queryManager = new QueryManager(this); |
|
this.eventDispatcher = new EventDispatcher(); |
|
this._entityPool = new EntityPool( |
|
this, |
|
this.world.options.entityClass, |
|
this.world.options.entityPoolSize |
|
); |
|
|
|
// Deferred deletion |
|
this.entitiesWithComponentsToRemove = []; |
|
this.entitiesToRemove = []; |
|
this.deferredRemovalEnabled = true; |
|
} |
|
|
|
getEntityByName(name) { |
|
return this._entitiesByNames[name]; |
|
} |
|
|
|
/** |
|
* Create a new entity |
|
*/ |
|
createEntity(name) { |
|
var entity = this._entityPool.acquire(); |
|
entity.alive = true; |
|
entity.name = name || ""; |
|
if (name) { |
|
if (this._entitiesByNames[name]) { |
|
console.warn(`Entity name '${name}' already exist`); |
|
} else { |
|
this._entitiesByNames[name] = entity; |
|
} |
|
} |
|
|
|
this._entities.push(entity); |
|
this.eventDispatcher.dispatchEvent(ENTITY_CREATED, entity); |
|
return entity; |
|
} |
|
|
|
// COMPONENTS |
|
|
|
/** |
|
* Add a component to an entity |
|
* @param {Entity} entity Entity where the component will be added |
|
* @param {Component} Component Component to be added to the entity |
|
* @param {Object} values Optional values to replace the default attributes |
|
*/ |
|
entityAddComponent(entity, Component, values) { |
|
// @todo Probably define Component._typeId with a default value and avoid using typeof |
|
if ( |
|
typeof Component._typeId === "undefined" && |
|
!this.world.componentsManager._ComponentsMap[Component._typeId] |
|
) { |
|
throw new Error( |
|
`Attempted to add unregistered component "${Component.getName()}"` |
|
); |
|
} |
|
|
|
if (~entity._ComponentTypes.indexOf(Component)) { |
|
{ |
|
console.warn( |
|
"Component type already exists on entity.", |
|
entity, |
|
Component.getName() |
|
); |
|
} |
|
return; |
|
} |
|
|
|
entity._ComponentTypes.push(Component); |
|
|
|
if (Component.__proto__ === SystemStateComponent) { |
|
entity.numStateComponents++; |
|
} |
|
|
|
var componentPool = this.world.componentsManager.getComponentsPool( |
|
Component |
|
); |
|
|
|
var component = componentPool |
|
? componentPool.acquire() |
|
: new Component(values); |
|
|
|
if (componentPool && values) { |
|
component.copy(values); |
|
} |
|
|
|
entity._components[Component._typeId] = component; |
|
|
|
this._queryManager.onEntityComponentAdded(entity, Component); |
|
this.world.componentsManager.componentAddedToEntity(Component); |
|
|
|
this.eventDispatcher.dispatchEvent(COMPONENT_ADDED, entity, Component); |
|
} |
|
|
|
/** |
|
* Remove a component from an entity |
|
* @param {Entity} entity Entity which will get removed the component |
|
* @param {*} Component Component to remove from the entity |
|
* @param {Bool} immediately If you want to remove the component immediately instead of deferred (Default is false) |
|
*/ |
|
entityRemoveComponent(entity, Component, immediately) { |
|
var index = entity._ComponentTypes.indexOf(Component); |
|
if (!~index) return; |
|
|
|
this.eventDispatcher.dispatchEvent(COMPONENT_REMOVE, entity, Component); |
|
|
|
if (immediately) { |
|
this._entityRemoveComponentSync(entity, Component, index); |
|
} else { |
|
if (entity._ComponentTypesToRemove.length === 0) |
|
this.entitiesWithComponentsToRemove.push(entity); |
|
|
|
entity._ComponentTypes.splice(index, 1); |
|
entity._ComponentTypesToRemove.push(Component); |
|
|
|
entity._componentsToRemove[Component._typeId] = |
|
entity._components[Component._typeId]; |
|
delete entity._components[Component._typeId]; |
|
} |
|
|
|
// Check each indexed query to see if we need to remove it |
|
this._queryManager.onEntityComponentRemoved(entity, Component); |
|
|
|
if (Component.__proto__ === SystemStateComponent) { |
|
entity.numStateComponents--; |
|
|
|
// Check if the entity was a ghost waiting for the last system state component to be removed |
|
if (entity.numStateComponents === 0 && !entity.alive) { |
|
entity.remove(); |
|
} |
|
} |
|
} |
|
|
|
_entityRemoveComponentSync(entity, Component, index) { |
|
// Remove T listing on entity and property ref, then free the component. |
|
entity._ComponentTypes.splice(index, 1); |
|
var component = entity._components[Component._typeId]; |
|
delete entity._components[Component._typeId]; |
|
component.dispose(); |
|
this.world.componentsManager.componentRemovedFromEntity(Component); |
|
} |
|
|
|
/** |
|
* Remove all the components from an entity |
|
* @param {Entity} entity Entity from which the components will be removed |
|
*/ |
|
entityRemoveAllComponents(entity, immediately) { |
|
let Components = entity._ComponentTypes; |
|
|
|
for (let j = Components.length - 1; j >= 0; j--) { |
|
if (Components[j].__proto__ !== SystemStateComponent) |
|
this.entityRemoveComponent(entity, Components[j], immediately); |
|
} |
|
} |
|
|
|
/** |
|
* Remove the entity from this manager. It will clear also its components |
|
* @param {Entity} entity Entity to remove from the manager |
|
* @param {Bool} immediately If you want to remove the component immediately instead of deferred (Default is false) |
|
*/ |
|
removeEntity(entity, immediately) { |
|
var index = this._entities.indexOf(entity); |
|
|
|
if (!~index) throw new Error("Tried to remove entity not in list"); |
|
|
|
entity.alive = false; |
|
this.entityRemoveAllComponents(entity, immediately); |
|
|
|
if (entity.numStateComponents === 0) { |
|
// Remove from entity list |
|
this.eventDispatcher.dispatchEvent(ENTITY_REMOVED, entity); |
|
this._queryManager.onEntityRemoved(entity); |
|
if (immediately === true) { |
|
this._releaseEntity(entity, index); |
|
} else { |
|
this.entitiesToRemove.push(entity); |
|
} |
|
} |
|
} |
|
|
|
_releaseEntity(entity, index) { |
|
this._entities.splice(index, 1); |
|
|
|
if (this._entitiesByNames[entity.name]) { |
|
delete this._entitiesByNames[entity.name]; |
|
} |
|
entity._pool.release(entity); |
|
} |
|
|
|
/** |
|
* Remove all entities from this manager |
|
*/ |
|
removeAllEntities() { |
|
for (var i = this._entities.length - 1; i >= 0; i--) { |
|
this.removeEntity(this._entities[i]); |
|
} |
|
} |
|
|
|
processDeferredRemoval() { |
|
if (!this.deferredRemovalEnabled) { |
|
return; |
|
} |
|
|
|
for (let i = 0; i < this.entitiesToRemove.length; i++) { |
|
let entity = this.entitiesToRemove[i]; |
|
let index = this._entities.indexOf(entity); |
|
this._releaseEntity(entity, index); |
|
} |
|
this.entitiesToRemove.length = 0; |
|
|
|
for (let i = 0; i < this.entitiesWithComponentsToRemove.length; i++) { |
|
let entity = this.entitiesWithComponentsToRemove[i]; |
|
while (entity._ComponentTypesToRemove.length > 0) { |
|
let Component = entity._ComponentTypesToRemove.pop(); |
|
|
|
var component = entity._componentsToRemove[Component._typeId]; |
|
delete entity._componentsToRemove[Component._typeId]; |
|
component.dispose(); |
|
this.world.componentsManager.componentRemovedFromEntity(Component); |
|
|
|
//this._entityRemoveComponentSync(entity, Component, index); |
|
} |
|
} |
|
|
|
this.entitiesWithComponentsToRemove.length = 0; |
|
} |
|
|
|
/** |
|
* Get a query based on a list of components |
|
* @param {Array(Component)} Components List of components that will form the query |
|
*/ |
|
queryComponents(Components) { |
|
return this._queryManager.getQuery(Components); |
|
} |
|
|
|
// EXTRAS |
|
|
|
/** |
|
* Return number of entities |
|
*/ |
|
count() { |
|
return this._entities.length; |
|
} |
|
|
|
/** |
|
* Return some stats |
|
*/ |
|
stats() { |
|
var stats = { |
|
numEntities: this._entities.length, |
|
numQueries: Object.keys(this._queryManager._queries).length, |
|
queries: this._queryManager.stats(), |
|
numComponentPool: Object.keys(this.componentsManager._componentPool) |
|
.length, |
|
componentPool: {}, |
|
eventDispatcher: this.eventDispatcher.stats, |
|
}; |
|
|
|
for (var ecsyComponentId in this.componentsManager._componentPool) { |
|
var pool = this.componentsManager._componentPool[ecsyComponentId]; |
|
stats.componentPool[pool.T.getName()] = { |
|
used: pool.totalUsed(), |
|
size: pool.count, |
|
}; |
|
} |
|
|
|
return stats; |
|
} |
|
} |
|
|
|
const ENTITY_CREATED = "EntityManager#ENTITY_CREATE"; |
|
const ENTITY_REMOVED = "EntityManager#ENTITY_REMOVED"; |
|
const COMPONENT_ADDED = "EntityManager#COMPONENT_ADDED"; |
|
const COMPONENT_REMOVE = "EntityManager#COMPONENT_REMOVE"; |
|
|
|
class ComponentManager { |
|
constructor() { |
|
this.Components = []; |
|
this._ComponentsMap = {}; |
|
|
|
this._componentPool = {}; |
|
this.numComponents = {}; |
|
this.nextComponentId = 0; |
|
} |
|
|
|
hasComponent(Component) { |
|
return this.Components.indexOf(Component) !== -1; |
|
} |
|
|
|
registerComponent(Component, objectPool) { |
|
if (this.Components.indexOf(Component) !== -1) { |
|
console.warn( |
|
`Component type: '${Component.getName()}' already registered.` |
|
); |
|
return; |
|
} |
|
|
|
const schema = Component.schema; |
|
|
|
if (!schema) { |
|
throw new Error( |
|
`Component "${Component.getName()}" has no schema property.` |
|
); |
|
} |
|
|
|
for (const propName in schema) { |
|
const prop = schema[propName]; |
|
|
|
if (!prop.type) { |
|
throw new Error( |
|
`Invalid schema for component "${Component.getName()}". Missing type for "${propName}" property.` |
|
); |
|
} |
|
} |
|
|
|
Component._typeId = this.nextComponentId++; |
|
this.Components.push(Component); |
|
this._ComponentsMap[Component._typeId] = Component; |
|
this.numComponents[Component._typeId] = 0; |
|
|
|
if (objectPool === undefined) { |
|
objectPool = new ObjectPool(Component); |
|
} else if (objectPool === false) { |
|
objectPool = undefined; |
|
} |
|
|
|
this._componentPool[Component._typeId] = objectPool; |
|
} |
|
|
|
componentAddedToEntity(Component) { |
|
this.numComponents[Component._typeId]++; |
|
} |
|
|
|
componentRemovedFromEntity(Component) { |
|
this.numComponents[Component._typeId]--; |
|
} |
|
|
|
getComponentsPool(Component) { |
|
return this._componentPool[Component._typeId]; |
|
} |
|
} |
|
|
|
const Version = "0.3.1"; |
|
|
|
const proxyMap = new WeakMap(); |
|
|
|
const proxyHandler = { |
|
set(target, prop) { |
|
throw new Error( |
|
`Tried to write to "${target.constructor.getName()}#${String( |
|
prop |
|
)}" on immutable component. Use .getMutableComponent() to modify a component.` |
|
); |
|
}, |
|
}; |
|
|
|
function wrapImmutableComponent(T, component) { |
|
if (component === undefined) { |
|
return undefined; |
|
} |
|
|
|
let wrappedComponent = proxyMap.get(component); |
|
|
|
if (!wrappedComponent) { |
|
wrappedComponent = new Proxy(component, proxyHandler); |
|
proxyMap.set(component, wrappedComponent); |
|
} |
|
|
|
return wrappedComponent; |
|
} |
|
|
|
class Entity { |
|
constructor(entityManager) { |
|
this._entityManager = entityManager || null; |
|
|
|
// Unique ID for this entity |
|
this.id = entityManager._nextEntityId++; |
|
|
|
// List of components types the entity has |
|
this._ComponentTypes = []; |
|
|
|
// Instance of the components |
|
this._components = {}; |
|
|
|
this._componentsToRemove = {}; |
|
|
|
// Queries where the entity is added |
|
this.queries = []; |
|
|
|
// Used for deferred removal |
|
this._ComponentTypesToRemove = []; |
|
|
|
this.alive = false; |
|
|
|
//if there are state components on a entity, it can't be removed completely |
|
this.numStateComponents = 0; |
|
} |
|
|
|
// COMPONENTS |
|
|
|
getComponent(Component, includeRemoved) { |
|
var component = this._components[Component._typeId]; |
|
|
|
if (!component && includeRemoved === true) { |
|
component = this._componentsToRemove[Component._typeId]; |
|
} |
|
|
|
return wrapImmutableComponent(Component, component) |
|
; |
|
} |
|
|
|
getRemovedComponent(Component) { |
|
const component = this._componentsToRemove[Component._typeId]; |
|
|
|
return wrapImmutableComponent(Component, component) |
|
; |
|
} |
|
|
|
getComponents() { |
|
return this._components; |
|
} |
|
|
|
getComponentsToRemove() { |
|
return this._componentsToRemove; |
|
} |
|
|
|
getComponentTypes() { |
|
return this._ComponentTypes; |
|
} |
|
|
|
getMutableComponent(Component) { |
|
var component = this._components[Component._typeId]; |
|
|
|
if (!component) { |
|
return; |
|
} |
|
|
|
for (var i = 0; i < this.queries.length; i++) { |
|
var query = this.queries[i]; |
|
// @todo accelerate this check. Maybe having query._Components as an object |
|
// @todo add Not components |
|
if (query.reactive && query.Components.indexOf(Component) !== -1) { |
|
query.eventDispatcher.dispatchEvent( |
|
Query.prototype.COMPONENT_CHANGED, |
|
this, |
|
component |
|
); |
|
} |
|
} |
|
return component; |
|
} |
|
|
|
addComponent(Component, values) { |
|
this._entityManager.entityAddComponent(this, Component, values); |
|
return this; |
|
} |
|
|
|
removeComponent(Component, forceImmediate) { |
|
this._entityManager.entityRemoveComponent(this, Component, forceImmediate); |
|
return this; |
|
} |
|
|
|
hasComponent(Component, includeRemoved) { |
|
return ( |
|
!!~this._ComponentTypes.indexOf(Component) || |
|
(includeRemoved === true && this.hasRemovedComponent(Component)) |
|
); |
|
} |
|
|
|
hasRemovedComponent(Component) { |
|
return !!~this._ComponentTypesToRemove.indexOf(Component); |
|
} |
|
|
|
hasAllComponents(Components) { |
|
for (var i = 0; i < Components.length; i++) { |
|
if (!this.hasComponent(Components[i])) return false; |
|
} |
|
return true; |
|
} |
|
|
|
hasAnyComponents(Components) { |
|
for (var i = 0; i < Components.length; i++) { |
|
if (this.hasComponent(Components[i])) return true; |
|
} |
|
return false; |
|
} |
|
|
|
removeAllComponents(forceImmediate) { |
|
return this._entityManager.entityRemoveAllComponents(this, forceImmediate); |
|
} |
|
|
|
copy(src) { |
|
// TODO: This can definitely be optimized |
|
for (var ecsyComponentId in src._components) { |
|
var srcComponent = src._components[ecsyComponentId]; |
|
this.addComponent(srcComponent.constructor); |
|
var component = this.getComponent(srcComponent.constructor); |
|
component.copy(srcComponent); |
|
} |
|
|
|
return this; |
|
} |
|
|
|
clone() { |
|
return new Entity(this._entityManager).copy(this); |
|
} |
|
|
|
reset() { |
|
this.id = this._entityManager._nextEntityId++; |
|
this._ComponentTypes.length = 0; |
|
this.queries.length = 0; |
|
|
|
for (var ecsyComponentId in this._components) { |
|
delete this._components[ecsyComponentId]; |
|
} |
|
} |
|
|
|
remove(forceImmediate) { |
|
return this._entityManager.removeEntity(this, forceImmediate); |
|
} |
|
} |
|
|
|
const DEFAULT_OPTIONS = { |
|
entityPoolSize: 0, |
|
entityClass: Entity, |
|
}; |
|
|
|
class World { |
|
constructor(options = {}) { |
|
this.options = Object.assign({}, DEFAULT_OPTIONS, options); |
|
|
|
this.componentsManager = new ComponentManager(this); |
|
this.entityManager = new EntityManager(this); |
|
this.systemManager = new SystemManager(this); |
|
|
|
this.enabled = true; |
|
|
|
this.eventQueues = {}; |
|
|
|
if (hasWindow && typeof CustomEvent !== "undefined") { |
|
var event = new CustomEvent("ecsy-world-created", { |
|
detail: { world: this, version: Version }, |
|
}); |
|
window.dispatchEvent(event); |
|
} |
|
|
|
this.lastTime = now() / 1000; |
|
} |
|
|
|
registerComponent(Component, objectPool) { |
|
this.componentsManager.registerComponent(Component, objectPool); |
|
return this; |
|
} |
|
|
|
registerSystem(System, attributes) { |
|
this.systemManager.registerSystem(System, attributes); |
|
return this; |
|
} |
|
|
|
hasRegisteredComponent(Component) { |
|
return this.componentsManager.hasComponent(Component); |
|
} |
|
|
|
unregisterSystem(System) { |
|
this.systemManager.unregisterSystem(System); |
|
return this; |
|
} |
|
|
|
getSystem(SystemClass) { |
|
return this.systemManager.getSystem(SystemClass); |
|
} |
|
|
|
getSystems() { |
|
return this.systemManager.getSystems(); |
|
} |
|
|
|
execute(delta, time) { |
|
if (!delta) { |
|
time = now() / 1000; |
|
delta = time - this.lastTime; |
|
this.lastTime = time; |
|
} |
|
|
|
if (this.enabled) { |
|
this.systemManager.execute(delta, time); |
|
this.entityManager.processDeferredRemoval(); |
|
} |
|
} |
|
|
|
stop() { |
|
this.enabled = false; |
|
} |
|
|
|
play() { |
|
this.enabled = true; |
|
} |
|
|
|
createEntity(name) { |
|
return this.entityManager.createEntity(name); |
|
} |
|
|
|
stats() { |
|
var stats = { |
|
entities: this.entityManager.stats(), |
|
system: this.systemManager.stats(), |
|
}; |
|
|
|
return stats; |
|
} |
|
} |
|
|
|
class System { |
|
canExecute() { |
|
if (this._mandatoryQueries.length === 0) return true; |
|
|
|
for (let i = 0; i < this._mandatoryQueries.length; i++) { |
|
var query = this._mandatoryQueries[i]; |
|
if (query.entities.length === 0) { |
|
return false; |
|
} |
|
} |
|
|
|
return true; |
|
} |
|
|
|
getName() { |
|
return this.constructor.getName(); |
|
} |
|
|
|
constructor(world, attributes) { |
|
this.world = world; |
|
this.enabled = true; |
|
|
|
// @todo Better naming :) |
|
this._queries = {}; |
|
this.queries = {}; |
|
|
|
this.priority = 0; |
|
|
|
// Used for stats |
|
this.executeTime = 0; |
|
|
|
if (attributes && attributes.priority) { |
|
this.priority = attributes.priority; |
|
} |
|
|
|
this._mandatoryQueries = []; |
|
|
|
this.initialized = true; |
|
|
|
if (this.constructor.queries) { |
|
for (var queryName in this.constructor.queries) { |
|
var queryConfig = this.constructor.queries[queryName]; |
|
var Components = queryConfig.components; |
|
if (!Components || Components.length === 0) { |
|
throw new Error("'components' attribute can't be empty in a query"); |
|
} |
|
|
|
// Detect if the components have already been registered |
|
let unregisteredComponents = Components.filter( |
|
(Component) => !componentRegistered(Component) |
|
); |
|
|
|
if (unregisteredComponents.length > 0) { |
|
throw new Error( |
|
`Tried to create a query '${ |
|
this.constructor.name |
|
}.${queryName}' with unregistered components: [${unregisteredComponents |
|
.map((c) => c.getName()) |
|
.join(", ")}]` |
|
); |
|
} |
|
|
|
var query = this.world.entityManager.queryComponents(Components); |
|
|
|
this._queries[queryName] = query; |
|
if (queryConfig.mandatory === true) { |
|
this._mandatoryQueries.push(query); |
|
} |
|
this.queries[queryName] = { |
|
results: query.entities, |
|
}; |
|
|
|
// Reactive configuration added/removed/changed |
|
var validEvents = ["added", "removed", "changed"]; |
|
|
|
const eventMapping = { |
|
added: Query.prototype.ENTITY_ADDED, |
|
removed: Query.prototype.ENTITY_REMOVED, |
|
changed: Query.prototype.COMPONENT_CHANGED, // Query.prototype.ENTITY_CHANGED |
|
}; |
|
|
|
if (queryConfig.listen) { |
|
validEvents.forEach((eventName) => { |
|
if (!this.execute) { |
|
console.warn( |
|
`System '${this.getName()}' has defined listen events (${validEvents.join( |
|
", " |
|
)}) for query '${queryName}' but it does not implement the 'execute' method.` |
|
); |
|
} |
|
|
|
// Is the event enabled on this system's query? |
|
if (queryConfig.listen[eventName]) { |
|
let event = queryConfig.listen[eventName]; |
|
|
|
if (eventName === "changed") { |
|
query.reactive = true; |
|
if (event === true) { |
|
// Any change on the entity from the components in the query |
|
let eventList = (this.queries[queryName][eventName] = []); |
|
query.eventDispatcher.addEventListener( |
|
Query.prototype.COMPONENT_CHANGED, |
|
(entity) => { |
|
// Avoid duplicates |
|
if (eventList.indexOf(entity) === -1) { |
|
eventList.push(entity); |
|
} |
|
} |
|
); |
|
} else if (Array.isArray(event)) { |
|
let eventList = (this.queries[queryName][eventName] = []); |
|
query.eventDispatcher.addEventListener( |
|
Query.prototype.COMPONENT_CHANGED, |
|
(entity, changedComponent) => { |
|
// Avoid duplicates |
|
if ( |
|
event.indexOf(changedComponent.constructor) !== -1 && |
|
eventList.indexOf(entity) === -1 |
|
) { |
|
eventList.push(entity); |
|
} |
|
} |
|
); |
|
} |
|
} else { |
|
let eventList = (this.queries[queryName][eventName] = []); |
|
|
|
query.eventDispatcher.addEventListener( |
|
eventMapping[eventName], |
|
(entity) => { |
|
// @fixme overhead? |
|
if (eventList.indexOf(entity) === -1) |
|
eventList.push(entity); |
|
} |
|
); |
|
} |
|
} |
|
}); |
|
} |
|
} |
|
} |
|
} |
|
|
|
stop() { |
|
this.executeTime = 0; |
|
this.enabled = false; |
|
} |
|
|
|
play() { |
|
this.enabled = true; |
|
} |
|
|
|
// @question rename to clear queues? |
|
clearEvents() { |
|
for (let queryName in this.queries) { |
|
var query = this.queries[queryName]; |
|
if (query.added) { |
|
query.added.length = 0; |
|
} |
|
if (query.removed) { |
|
query.removed.length = 0; |
|
} |
|
if (query.changed) { |
|
if (Array.isArray(query.changed)) { |
|
query.changed.length = 0; |
|
} else { |
|
for (let name in query.changed) { |
|
query.changed[name].length = 0; |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
toJSON() { |
|
var json = { |
|
name: this.getName(), |
|
enabled: this.enabled, |
|
executeTime: this.executeTime, |
|
priority: this.priority, |
|
queries: {}, |
|
}; |
|
|
|
if (this.constructor.queries) { |
|
var queries = this.constructor.queries; |
|
for (let queryName in queries) { |
|
let query = this.queries[queryName]; |
|
let queryDefinition = queries[queryName]; |
|
let jsonQuery = (json.queries[queryName] = { |
|
key: this._queries[queryName].key, |
|
}); |
|
|
|
jsonQuery.mandatory = queryDefinition.mandatory === true; |
|
jsonQuery.reactive = |
|
queryDefinition.listen && |
|
(queryDefinition.listen.added === true || |
|
queryDefinition.listen.removed === true || |
|
queryDefinition.listen.changed === true || |
|
Array.isArray(queryDefinition.listen.changed)); |
|
|
|
if (jsonQuery.reactive) { |
|
jsonQuery.listen = {}; |
|
|
|
const methods = ["added", "removed", "changed"]; |
|
methods.forEach((method) => { |
|
if (query[method]) { |
|
jsonQuery.listen[method] = { |
|
entities: query[method].length, |
|
}; |
|
} |
|
}); |
|
} |
|
} |
|
} |
|
|
|
return json; |
|
} |
|
} |
|
|
|
System.isSystem = true; |
|
System.getName = function () { |
|
return this.displayName || this.name; |
|
}; |
|
|
|
function Not(Component) { |
|
return { |
|
operator: "not", |
|
Component: Component, |
|
}; |
|
} |
|
|
|
class TagComponent extends Component { |
|
constructor() { |
|
super(false); |
|
} |
|
} |
|
|
|
TagComponent.isTagComponent = true; |
|
|
|
const copyValue = (src) => src; |
|
|
|
const cloneValue = (src) => src; |
|
|
|
const copyArray = (src, dest) => { |
|
if (!src) { |
|
return src; |
|
} |
|
|
|
if (!dest) { |
|
return src.slice(); |
|
} |
|
|
|
dest.length = 0; |
|
|
|
for (let i = 0; i < src.length; i++) { |
|
dest.push(src[i]); |
|
} |
|
|
|
return dest; |
|
}; |
|
|
|
const cloneArray = (src) => src && src.slice(); |
|
|
|
const copyJSON = (src) => JSON.parse(JSON.stringify(src)); |
|
|
|
const cloneJSON = (src) => JSON.parse(JSON.stringify(src)); |
|
|
|
const copyCopyable = (src, dest) => { |
|
if (!src) { |
|
return src; |
|
} |
|
|
|
if (!dest) { |
|
return src.clone(); |
|
} |
|
|
|
return dest.copy(src); |
|
}; |
|
|
|
const cloneClonable = (src) => src && src.clone(); |
|
|
|
function createType(typeDefinition) { |
|
var mandatoryProperties = ["name", "default", "copy", "clone"]; |
|
|
|
var undefinedProperties = mandatoryProperties.filter((p) => { |
|
return !typeDefinition.hasOwnProperty(p); |
|
}); |
|
|
|
if (undefinedProperties.length > 0) { |
|
throw new Error( |
|
`createType expects a type definition with the following properties: ${undefinedProperties.join( |
|
", " |
|
)}` |
|
); |
|
} |
|
|
|
typeDefinition.isType = true; |
|
|
|
return typeDefinition; |
|
} |
|
|
|
/** |
|
* Standard types |
|
*/ |
|
const Types = { |
|
Number: createType({ |
|
name: "Number", |
|
default: 0, |
|
copy: copyValue, |
|
clone: cloneValue, |
|
}), |
|
|
|
Boolean: createType({ |
|
name: "Boolean", |
|
default: false, |
|
copy: copyValue, |
|
clone: cloneValue, |
|
}), |
|
|
|
String: createType({ |
|
name: "String", |
|
default: "", |
|
copy: copyValue, |
|
clone: cloneValue, |
|
}), |
|
|
|
Array: createType({ |
|
name: "Array", |
|
default: [], |
|
copy: copyArray, |
|
clone: cloneArray, |
|
}), |
|
|
|
Ref: createType({ |
|
name: "Ref", |
|
default: undefined, |
|
copy: copyValue, |
|
clone: cloneValue, |
|
}), |
|
|
|
JSON: createType({ |
|
name: "JSON", |
|
default: null, |
|
copy: copyJSON, |
|
clone: cloneJSON, |
|
}), |
|
}; |
|
|
|
function generateId(length) { |
|
var result = ""; |
|
var characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; |
|
var charactersLength = characters.length; |
|
for (var i = 0; i < length; i++) { |
|
result += characters.charAt(Math.floor(Math.random() * charactersLength)); |
|
} |
|
return result; |
|
} |
|
|
|
function injectScript(src, onLoad) { |
|
var script = document.createElement("script"); |
|
// @todo Use link to the ecsy-devtools repo? |
|
script.src = src; |
|
script.onload = onLoad; |
|
(document.head || document.documentElement).appendChild(script); |
|
} |
|
|
|
/* global Peer */ |
|
|
|
function hookConsoleAndErrors(connection) { |
|
var wrapFunctions = ["error", "warning", "log"]; |
|
wrapFunctions.forEach((key) => { |
|
if (typeof console[key] === "function") { |
|
var fn = console[key].bind(console); |
|
console[key] = (...args) => { |
|
connection.send({ |
|
method: "console", |
|
type: key, |
|
args: JSON.stringify(args), |
|
}); |
|
return fn.apply(null, args); |
|
}; |
|
} |
|
}); |
|
|
|
window.addEventListener("error", (error) => { |
|
connection.send({ |
|
method: "error", |
|
error: JSON.stringify({ |
|
message: error.error.message, |
|
stack: error.error.stack, |
|
}), |
|
}); |
|
}); |
|
} |
|
|
|
function includeRemoteIdHTML(remoteId) { |
|
let infoDiv = document.createElement("div"); |
|
infoDiv.style.cssText = ` |
|
align-items: center; |
|
background-color: #333; |
|
color: #aaa; |
|
display:flex; |
|
font-family: Arial; |
|
font-size: 1.1em; |
|
height: 40px; |
|
justify-content: center; |
|
left: 0; |
|
opacity: 0.9; |
|
position: absolute; |
|
right: 0; |
|
text-align: center; |
|
top: 0; |
|
`; |
|
|
|
infoDiv.innerHTML = `Open ECSY devtools to connect to this page using the code: <b style="color: #fff">${remoteId}</b> <button onClick="generateNewCode()">Generate new code</button>`; |
|
document.body.appendChild(infoDiv); |
|
|
|
return infoDiv; |
|
} |
|
|
|
function enableRemoteDevtools(remoteId) { |
|
if (!hasWindow) { |
|
console.warn("Remote devtools not available outside the browser"); |
|
return; |
|
} |
|
|
|
window.generateNewCode = () => { |
|
window.localStorage.clear(); |
|
remoteId = generateId(6); |
|
window.localStorage.setItem("ecsyRemoteId", remoteId); |
|
window.location.reload(false); |
|
}; |
|
|
|
remoteId = remoteId || window.localStorage.getItem("ecsyRemoteId"); |
|
if (!remoteId) { |
|
remoteId = generateId(6); |
|
window.localStorage.setItem("ecsyRemoteId", remoteId); |
|
} |
|
|
|
let infoDiv = includeRemoteIdHTML(remoteId); |
|
|
|
window.__ECSY_REMOTE_DEVTOOLS_INJECTED = true; |
|
window.__ECSY_REMOTE_DEVTOOLS = {}; |
|
|
|
let Version = ""; |
|
|
|
// This is used to collect the worlds created before the communication is being established |
|
let worldsBeforeLoading = []; |
|
let onWorldCreated = (e) => { |
|
var world = e.detail.world; |
|
Version = e.detail.version; |
|
worldsBeforeLoading.push(world); |
|
}; |
|
window.addEventListener("ecsy-world-created", onWorldCreated); |
|
|
|
let onLoaded = () => { |
|
// var peer = new Peer(remoteId); |
|
var peer = new Peer(remoteId, { |
|
host: "peerjs.ecsy.io", |
|
secure: true, |
|
port: 443, |
|
config: { |
|
iceServers: [ |
|
{ url: "stun:stun.l.google.com:19302" }, |
|
{ url: "stun:stun1.l.google.com:19302" }, |
|
{ url: "stun:stun2.l.google.com:19302" }, |
|
{ url: "stun:stun3.l.google.com:19302" }, |
|
{ url: "stun:stun4.l.google.com:19302" }, |
|
], |
|
}, |
|
debug: 3, |
|
}); |
|
|
|
peer.on("open", (/* id */) => { |
|
peer.on("connection", (connection) => { |
|
window.__ECSY_REMOTE_DEVTOOLS.connection = connection; |
|
connection.on("open", function () { |
|
// infoDiv.style.visibility = "hidden"; |
|
infoDiv.innerHTML = "Connected"; |
|
|
|
// Receive messages |
|
connection.on("data", function (data) { |
|
if (data.type === "init") { |
|
var script = document.createElement("script"); |
|
script.setAttribute("type", "text/javascript"); |
|
script.onload = () => { |
|
script.parentNode.removeChild(script); |
|
|
|
// Once the script is injected we don't need to listen |
|
window.removeEventListener( |
|
"ecsy-world-created", |
|
onWorldCreated |
|
); |
|
worldsBeforeLoading.forEach((world) => { |
|
var event = new CustomEvent("ecsy-world-created", { |
|
detail: { world: world, version: Version }, |
|
}); |
|
window.dispatchEvent(event); |
|
}); |
|
}; |
|
script.innerHTML = data.script; |
|
(document.head || document.documentElement).appendChild(script); |
|
script.onload(); |
|
|
|
hookConsoleAndErrors(connection); |
|
} else if (data.type === "executeScript") { |
|
let value = eval(data.script); |
|
if (data.returnEval) { |
|
connection.send({ |
|
method: "evalReturn", |
|
value: value, |
|
}); |
|
} |
|
} |
|
}); |
|
}); |
|
}); |
|
}); |
|
}; |
|
|
|
// Inject PeerJS script |
|
injectScript( |
|
"https://cdn.jsdelivr.net/npm/peerjs@0.3.20/dist/peer.min.js", |
|
onLoaded |
|
); |
|
} |
|
|
|
if (hasWindow) { |
|
const urlParams = new URLSearchParams(window.location.search); |
|
|
|
// @todo Provide a way to disable it if needed |
|
if (urlParams.has("enable-remote-devtools")) { |
|
enableRemoteDevtools(); |
|
} |
|
} |
|
|
|
export { Component, Not, ObjectPool, System, SystemStateComponent, TagComponent, Types, Version, World, Entity as _Entity, cloneArray, cloneClonable, cloneJSON, cloneValue, copyArray, copyCopyable, copyJSON, copyValue, createType, enableRemoteDevtools };
|
|
|