UNPKG

@dcl/ecs

Version:
268 lines (267 loc) • 11.7 kB
import * as components from '../components'; import { componentNumberFromName } from '../components/component-number'; import { checkNotThenable } from '../runtime/invariant'; import { Schemas } from '../schemas'; import { crdtSceneSystem } from '../systems/crdt'; import { createComponentDefinitionFromSchema } from './lww-element-set-component-definition'; import { createEntityContainer } from './entity'; import { SystemContainer, SYSTEMS_REGULAR_PRIORITY } from './systems'; import { createValueSetComponentDefinitionFromSchema } from './grow-only-value-set-component-definition'; import { removeEntityWithChildren as removeEntityWithChildrenEngine } from '../runtime/helpers/tree'; import { CrdtMessageType } from '../serialization/crdt'; export * from './input'; export * from './readonly'; export * from './types'; function preEngine(options) { const entityContainer = options?.entityContainer ?? createEntityContainer(); const componentsDefinition = new Map(); const systems = SystemContainer(); let sealed = false; function addSystem(fn, priority = SYSTEMS_REGULAR_PRIORITY, name) { systems.add(fn, priority, name); } function removeSystem(selector) { return systems.remove(selector); } function addEntity() { const entity = entityContainer.generateEntity(); return entity; } function removeEntity(entity) { for (const [, component] of componentsDefinition) { // TODO: hack for the moment. // We still need the NetworkEntity to forward this message to the SyncTransport. // If we remove it then we can't notify the other users which entity was deleted. if (component.componentName === 'core-schema::Network-Entity') continue; component.entityDeleted(entity, true); } return entityContainer.removeEntity(entity); } function removeEntityWithChildren(entity) { return removeEntityWithChildrenEngine({ removeEntity, defineComponentFromSchema, getEntitiesWith, defineComponent }, entity); } function registerComponentDefinition(componentName, component) { /* istanbul ignore next */ if (sealed) throw new Error('Engine is already sealed. No components can be added at this stage'); const componentId = componentNumberFromName(componentName); const prev = componentsDefinition.get(componentId); if (prev) { throw new Error(`Component number ${componentId} was already registered.`); } /* istanbul ignore next */ if (component.componentName !== componentName) { throw new Error(`Component name doesn't match componentDefinition.componentName ${componentName} != ${component.componentName}`); } /* istanbul ignore next */ if (component.componentId !== componentId) { throw new Error(`Component number doesn't match componentDefinition.componentId ${componentId} != ${component.componentId}`); } componentsDefinition.set(componentId, component); return component; } function defineComponentFromSchema(componentName, schema) { const componentId = componentNumberFromName(componentName); const prev = componentsDefinition.get(componentId); if (prev) { // TODO: assert spec === prev.spec return prev; } /* istanbul ignore next */ if (sealed) throw new Error('Engine is already sealed. No components can be added at this stage'); const newComponent = createComponentDefinitionFromSchema(componentName, componentId, schema); componentsDefinition.set(componentId, newComponent); return newComponent; } function defineValueSetComponentFromSchema(componentName, schema, options) { const componentId = componentNumberFromName(componentName); const prev = componentsDefinition.get(componentId); if (prev) { // TODO: assert spec === prev.spec return prev; } /* istanbul ignore next */ if (sealed) throw new Error('Engine is already sealed. No components can be added at this stage'); const newComponent = createValueSetComponentDefinitionFromSchema(componentName, componentId, schema, options); componentsDefinition.set(componentId, newComponent); return newComponent; } function defineComponent(componentName, mapSpec, constructorDefault) { const componentId = componentNumberFromName(componentName); const prev = componentsDefinition.get(componentId); if (prev) { // TODO: assert spec === prev.spec return prev; } if (sealed) throw new Error('Engine is already sealed. No components can be added at this stage'); const schemaSpec = Schemas.Map(mapSpec, constructorDefault); const def = createComponentDefinitionFromSchema(componentName, componentId, schemaSpec); const newComponent = { ...def, create(entity, val) { return def.create(entity, val); }, createOrReplace(entity, val) { return def.createOrReplace(entity, val); } }; componentsDefinition.set(componentId, newComponent); return newComponent; } function getComponent(componentIdOrName) { const componentId = typeof componentIdOrName === 'number' ? componentIdOrName : componentNumberFromName(componentIdOrName); const component = componentsDefinition.get(componentId); if (!component) { throw new Error(`Component ${componentIdOrName} not found. You need to declare the components at the beginnig of the engine declaration`); } return component; } function getComponentOrNull(componentIdOrName) { const componentId = typeof componentIdOrName === 'number' ? componentIdOrName : componentNumberFromName(componentIdOrName); return (componentsDefinition.get(componentId) ?? /* istanbul ignore next */ null); } function* getEntitiesWith(...components) { for (const [entity, ...groupComp] of getComponentDefGroup(...components)) { yield [entity, ...groupComp.map((c) => c.get(entity))]; } } function getEntityOrNullByName(value) { const NameComponent = components.Name({ defineComponent }); for (const [entity, name] of getEntitiesWith(NameComponent)) { if (name.value === value) return entity; } return null; } function getEntityByName(value) { const entity = getEntityOrNullByName(value); return entity; } function* getEntitiesByTag(tagName) { const TagComponent = components.Tags({ defineComponent }); for (const [entity, component] of getEntitiesWith(TagComponent)) { if (entity !== 0 && component.tags?.some((tag) => tag === tagName)) { yield entity; } } } function* getComponentDefGroup(...args) { const [firstComponentDef, ...componentDefinitions] = args; for (const [entity] of firstComponentDef.iterator()) { let matches = true; for (const componentDef of componentDefinitions) { if (!componentDef.has(entity)) { matches = false; break; } } if (matches) { yield [entity, ...args]; } } } function getSystems() { return systems.getSystems(); } function componentsIter() { return componentsDefinition.values(); } function removeComponentDefinition(componentIdOrName) { if (sealed) throw new Error('Engine is already sealed. No components can be removed at this stage'); const componentId = typeof componentIdOrName === 'number' ? componentIdOrName : componentNumberFromName(componentIdOrName); componentsDefinition.delete(componentId); } components.Transform({ defineComponentFromSchema }); function seal() { if (!sealed) { sealed = true; } } return { addEntity, removeEntity, removeEntityWithChildren, addSystem, getSystems, removeSystem, defineComponent, defineComponentFromSchema, defineValueSetComponentFromSchema, getEntitiesWith, getComponent, getComponentOrNull: getComponentOrNull, getEntityOrNullByName, getEntityByName, getEntitiesByTag, removeComponentDefinition, registerComponentDefinition, entityContainer, componentsIter, seal }; } /** * Internal constructor of new engines, this is an internal API * @public * @deprecated Prevent manual usage prefer "engine" for scene development */ export function Engine(options) { const partialEngine = preEngine(options); const onChangeFunction = (entity, operation, component, componentValue) => { if (operation === CrdtMessageType.DELETE_ENTITY) { for (const component of partialEngine.componentsIter()) { component?.__onChangeCallbacks(entity, undefined); } } else { component?.__onChangeCallbacks(entity, componentValue); } return options?.onChangeFunction(entity, operation, component, componentValue); }; const crdtSystem = crdtSceneSystem(partialEngine, onChangeFunction); async function update(dt) { await crdtSystem.receiveMessages(); for (const system of partialEngine.getSystems()) { const ret = system.fn(dt); checkNotThenable(ret, `A system (${system.name || 'anonymous'}) returned a thenable. Systems cannot be async functions. Documentation: https://dcl.gg/sdk/sync-systems`); } // get the deleted entities to send the DeleteEntity CRDT commands const deletedEntites = partialEngine.entityContainer.releaseRemovedEntities(); await crdtSystem.sendMessages(deletedEntites); } return { _id: Date.now(), addEntity: partialEngine.addEntity, removeEntity: partialEngine.removeEntity, removeEntityWithChildren: partialEngine.removeEntityWithChildren, addSystem: partialEngine.addSystem, removeSystem: partialEngine.removeSystem, defineComponent: partialEngine.defineComponent, defineComponentFromSchema: partialEngine.defineComponentFromSchema, defineValueSetComponentFromSchema: partialEngine.defineValueSetComponentFromSchema, registerComponentDefinition: partialEngine.registerComponentDefinition, getEntitiesWith: partialEngine.getEntitiesWith, getComponent: partialEngine.getComponent, getComponentOrNull: partialEngine.getComponentOrNull, removeComponentDefinition: partialEngine.removeComponentDefinition, componentsIter: partialEngine.componentsIter, seal: partialEngine.seal, getEntityOrNullByName: partialEngine.getEntityOrNullByName, getEntityByName: partialEngine.getEntityByName, getEntitiesByTag: partialEngine.getEntitiesByTag, update, RootEntity: 0, PlayerEntity: 1, CameraEntity: 2, getEntityState: partialEngine.entityContainer.getEntityState, addTransport: crdtSystem.addTransport, entityContainer: partialEngine.entityContainer }; }