From 75efb844a373df7a3c413925223d20c42a45a1cd Mon Sep 17 00:00:00 2001 From: Gheric Speiginer Date: Wed, 12 Apr 2023 14:14:41 -0700 Subject: [PATCH] Update EntityUUID.ts Add strong entity typings, add missing types to ECS queries --- .../properties/InstancingNodeEditor.tsx | 1 - .../editor/src/components/properties/Util.ts | 15 +- .../gltf/extensions/EEECSExporterExtension.ts | 15 +- .../src/avatar/AvatarAnimationSystem.ts | 5 +- .../src/avatar/AvatarControllerSystem.ts | 9 +- .../src/avatar/functions/moveAvatar.test.ts | 11 +- packages/engine/src/ecs/classes/Engine.ts | 36 +- packages/engine/src/ecs/classes/Entity.ts | 15 +- .../src/ecs/functions/ComponentFunctions.ts | 158 ++++---- ...EntityFunctions.tsx => EntityFunctions.ts} | 12 +- .../src/ecs/functions/EntityTree.test.ts | 8 +- .../engine/src/ecs/functions/EntityTree.ts | 77 +++- .../src/ecs/functions/SystemFunctions.tsx | 17 +- .../interaction/systems/InteractiveSystem.ts | 2 +- .../interaction/systems/MountPointSystem.ts | 29 +- .../src/scene/components/ColliderComponent.ts | 8 +- .../src/scene/components/ErrorComponent.ts | 2 +- .../src/scene/components/GroupComponent.tsx | 6 +- .../src/scene/components/ImageComponent.ts | 201 +++++----- .../src/scene/components/MediaComponent.ts | 358 +++++++++--------- .../src/scene/components/NameComponent.ts | 2 +- .../components/TransformComponent.ts | 11 +- .../src/transform/systems/TransformSystem.ts | 32 +- 23 files changed, 554 insertions(+), 476 deletions(-) rename packages/engine/src/ecs/functions/{EntityFunctions.tsx => EntityFunctions.ts} (72%) diff --git a/packages/editor/src/components/properties/InstancingNodeEditor.tsx b/packages/editor/src/components/properties/InstancingNodeEditor.tsx index 9c869d8e94..47dd399ea9 100644 --- a/packages/editor/src/components/properties/InstancingNodeEditor.tsx +++ b/packages/editor/src/components/properties/InstancingNodeEditor.tsx @@ -8,7 +8,6 @@ import { ComponentType, getComponent, getMutableComponent, - getOrAddComponent, hasComponent, useComponent } from '@etherealengine/engine/src/ecs/functions/ComponentFunctions' diff --git a/packages/editor/src/components/properties/Util.ts b/packages/editor/src/components/properties/Util.ts index 48216fb38e..3e5161cda2 100644 --- a/packages/editor/src/components/properties/Util.ts +++ b/packages/editor/src/components/properties/Util.ts @@ -1,11 +1,6 @@ -import { Engine } from '@etherealengine/engine/src/ecs/classes/Engine' import { Entity } from '@etherealengine/engine/src/ecs/classes/Entity' import { SceneState } from '@etherealengine/engine/src/ecs/classes/Scene' -import { - Component, - ComponentType, - SerializedComponentType -} from '@etherealengine/engine/src/ecs/functions/ComponentFunctions' +import { Component, SerializedComponentType } from '@etherealengine/engine/src/ecs/functions/ComponentFunctions' import { EntityOrObjectUUID, getEntityNodeArrayFromEntities } from '@etherealengine/engine/src/ecs/functions/EntityTree' import { iterateEntityNode } from '@etherealengine/engine/src/ecs/functions/EntityTree' import { UUIDComponent } from '@etherealengine/engine/src/scene/components/UUIDComponent' @@ -17,7 +12,7 @@ import { EditorState } from '../../services/EditorServices' import { SelectionState } from '../../services/SelectionServices' export type EditorPropType = { - entity: Entity + entity: Entity component?: Component multiEdit?: boolean } @@ -56,9 +51,9 @@ export const updateProperties = ( } export function traverseScene( - callback: (node: Entity) => T, - predicate: (node: Entity) => boolean = () => true, - snubChildren: boolean = false + callback: (node: Entity) => T, + predicate: (node: Entity) => boolean = () => true, + snubChildren = false ): T[] { const result: T[] = [] iterateEntityNode(getState(SceneState).sceneEntity, (node) => result.push(callback(node)), predicate, snubChildren) diff --git a/packages/engine/src/assets/exporters/gltf/extensions/EEECSExporterExtension.ts b/packages/engine/src/assets/exporters/gltf/extensions/EEECSExporterExtension.ts index f6b1ceb44d..d134a29bba 100644 --- a/packages/engine/src/assets/exporters/gltf/extensions/EEECSExporterExtension.ts +++ b/packages/engine/src/assets/exporters/gltf/extensions/EEECSExporterExtension.ts @@ -1,11 +1,8 @@ -import { Event, Object3D } from 'three' - -import { ComponentJson } from '@etherealengine/common/src/interfaces/SceneInterface' - import { ComponentMap, getComponent, getMutableComponent, + getOptionalComponent, hasComponent } from '../../../../ecs/functions/ComponentFunctions' import { ColliderComponent } from '../../../../scene/components/ColliderComponent' @@ -27,16 +24,18 @@ export class EEECSExporterExtension extends ExporterExtension implements GLTFExp const data = new Array<[string, any]>() for (const field of gltfLoaded) { switch (field) { - case 'entity': - const name = getComponent(entity, NameComponent) + case 'entity': { + const name = getOptionalComponent(entity, NameComponent) data.push(['xrengine.entity', name]) break - default: - const component = ComponentMap.get(field)! + } + default: { + const component = ComponentMap.get(field)! as any const compData = component.toJSON(entity, getMutableComponent(entity, component)) for (const [field, value] of Object.entries(compData)) { data.push([`xrengine.${component.name}.${field}`, value]) } + } } } nodeDef.extensions = nodeDef.extensions ?? {} diff --git a/packages/engine/src/avatar/AvatarAnimationSystem.ts b/packages/engine/src/avatar/AvatarAnimationSystem.ts index d4c49293cf..b377fbe8ac 100644 --- a/packages/engine/src/avatar/AvatarAnimationSystem.ts +++ b/packages/engine/src/avatar/AvatarAnimationSystem.ts @@ -35,6 +35,7 @@ import { DistanceFromCameraComponent, FrustumCullCameraComponent } from '../transform/components/DistanceComponents' +import { LocalTransformComponent, TransformComponent } from '../transform/components/TransformComponent' import { updateGroupChildren } from '../transform/systems/TransformSystem' import { XRLeftHandComponent, XRRightHandComponent } from '../xr/XRComponents' import { getCameraMode, isMobileXRHeadset, ReferenceSpace, XRState } from '../xr/XRState' @@ -112,7 +113,7 @@ export default async function AvatarAnimationSystem() { }) Engine.instance.priorityAvatarEntities = priorityQueue.priorityEntities - const filterPriorityEntities = (entity: Entity) => + const filterPriorityEntities = (entity: Entity) => Engine.instance.priorityAvatarEntities.has(entity) || entity === Engine.instance.localClientEntity const filterFrustumCulledEntities = (entity: Entity) => @@ -339,7 +340,7 @@ export default async function AvatarAnimationSystem() { for (const entity of loopAnimationEntities) updateGroupChildren(entity) for (const entity of Engine.instance.priorityAvatarEntities) { - const avatarRig = getComponent(entity, AvatarRigComponent) + const avatarRig = getComponent(entity as Entity<[typeof AvatarRigComponent]>, AvatarRigComponent) if (avatarRig) { avatarRig.rig.Hips.updateWorldMatrix(true, true) avatarRig.helper?.updateMatrixWorld(true) diff --git a/packages/engine/src/avatar/AvatarControllerSystem.ts b/packages/engine/src/avatar/AvatarControllerSystem.ts index 61b36cd9c0..4ee14a8436 100755 --- a/packages/engine/src/avatar/AvatarControllerSystem.ts +++ b/packages/engine/src/avatar/AvatarControllerSystem.ts @@ -24,7 +24,7 @@ import { respawnAvatar } from './functions/respawnAvatar' import { AvatarInputSettingsReceptor } from './state/AvatarInputSettingsState' export default async function AvatarControllerSystem() { - const localControllerQuery = defineQuery([AvatarControllerComponent, LocalInputTagComponent]) + const localControllerQuery = defineQuery([AvatarControllerComponent, LocalInputTagComponent, UUIDComponent]) const controllerQuery = defineQuery([AvatarControllerComponent]) const sessionChangedActions = createActionQueue(XRAction.sessionChanged.matches) @@ -70,8 +70,12 @@ export default async function AvatarControllerSystem() { const controlledEntity = Engine.instance.localClientEntity - if (hasComponent(controlledEntity, AvatarControllerComponent)) { + if ( + hasComponent(controlledEntity, AvatarControllerComponent) && + hasComponent(controlledEntity, RigidBodyComponent) + ) { const controller = getComponent(controlledEntity, AvatarControllerComponent) + const rigidbody = getComponent(controlledEntity, RigidBodyComponent) if (controller.movementEnabled) { /** Support multiple peers controlling the same avatar by detecting movement and overriding network authority. @@ -95,7 +99,6 @@ export default async function AvatarControllerSystem() { } } - const rigidbody = getComponent(controlledEntity, RigidBodyComponent) if (rigidbody.position.y < -10) respawnAvatar(controlledEntity) } } diff --git a/packages/engine/src/avatar/functions/moveAvatar.test.ts b/packages/engine/src/avatar/functions/moveAvatar.test.ts index dd49322dbf..8382431345 100644 --- a/packages/engine/src/avatar/functions/moveAvatar.test.ts +++ b/packages/engine/src/avatar/functions/moveAvatar.test.ts @@ -6,6 +6,7 @@ import { getMutableState } from '@etherealengine/hyperflux' import { destroyEngine, Engine } from '../../ecs/classes/Engine' import { EngineState } from '../../ecs/classes/EngineState' +import { Entity } from '../../ecs/classes/Entity' import { getComponent } from '../../ecs/functions/ComponentFunctions' import { createEngine } from '../../initializeEngine' import { WorldNetworkAction } from '../../networking/functions/WorldNetworkAction' @@ -46,8 +47,8 @@ describe('moveAvatar function tests', () => { const camera = new PerspectiveCamera(60, 800 / 600, 0.1, 10000) - const velocity = getComponent(entity, RigidBodyComponent).linearVelocity - const avatar = getComponent(entity, AvatarControllerComponent) + const velocity = getComponent(entity as Entity, RigidBodyComponent).linearVelocity + const avatar = getComponent(entity as Entity, AvatarControllerComponent) avatar.gamepadWorldMovement.setZ(-1) @@ -79,7 +80,7 @@ describe('moveAvatar function tests', () => { const camera = new PerspectiveCamera(60, 800 / 600, 0.1, 10000) - const velocity = getComponent(entity, RigidBodyComponent).linearVelocity + const velocity = getComponent(entity as Entity, RigidBodyComponent).linearVelocity // velocity starts at 0 strictEqual(velocity.x, 0) @@ -114,7 +115,7 @@ describe('moveAvatar function tests', () => { const camera = new PerspectiveCamera(60, 800 / 600, 0.1, 10000) - const velocity = getComponent(entity, RigidBodyComponent).linearVelocity + const velocity = getComponent(entity as Entity, RigidBodyComponent).linearVelocity // velocity starts at 0 strictEqual(velocity.x, 0) @@ -146,7 +147,7 @@ describe('moveAvatar function tests', () => { const camera = new PerspectiveCamera(60, 800 / 600, 0.1, 10000) - const velocity = getComponent(entity, RigidBodyComponent).linearVelocity + const velocity = getComponent(entity as Entity, RigidBodyComponent).linearVelocity // velocity starts at 0 strictEqual(velocity.x, 0) diff --git a/packages/engine/src/ecs/classes/Engine.ts b/packages/engine/src/ecs/classes/Engine.ts index 1ab916c40b..c254fea714 100755 --- a/packages/engine/src/ecs/classes/Engine.ts +++ b/packages/engine/src/ecs/classes/Engine.ts @@ -20,7 +20,9 @@ import { NetworkId } from '@etherealengine/common/src/interfaces/NetworkId' import { ComponentJson } from '@etherealengine/common/src/interfaces/SceneInterface' import { GLTFLoader } from '../../assets/loaders/gltf/GLTFLoader' +import { AvatarAnimationComponent } from '../../avatar/components/AvatarAnimationComponent' import { AvatarComponent } from '../../avatar/components/AvatarComponent' +import { AvatarControllerComponent } from '../../avatar/components/AvatarControllerComponent' import { CameraComponent } from '../../camera/components/CameraComponent' import { SceneLoaderType } from '../../common/constants/PrefabFunctionType' import { nowMilliseconds } from '../../common/functions/nowMilliseconds' @@ -31,6 +33,7 @@ import { NetworkObjectComponent } from '../../networking/components/NetworkObjec import { NetworkState } from '../../networking/NetworkState' import { SerializationSchema } from '../../networking/serialization/Utils' import { PhysicsWorld } from '../../physics/classes/Physics' +import { RigidBodyComponent } from '../../physics/components/RigidBodyComponent' import { addObjectToGroup } from '../../scene/components/GroupComponent' import { NameComponent } from '../../scene/components/NameComponent' import { PortalComponent } from '../../scene/components/PortalComponent' @@ -41,7 +44,6 @@ import { setTransformComponent, TransformComponent } from '../../transform/compo import { Widget } from '../../xrui/Widgets' import { Component, - ComponentType, defineQuery, EntityRemovedComponent, getComponent, @@ -207,7 +209,7 @@ export class Engine { /** * The xr origin reference space entity */ - originEntity: Entity = UndefinedEntity + originEntity: Entity<[typeof TransformComponent]> /** * The xr origin group @@ -217,7 +219,7 @@ export class Engine { /** * The camera entity */ - cameraEntity: Entity = UndefinedEntity + cameraEntity: Entity<[typeof TransformComponent, typeof CameraComponent]> /** * Reference to the three.js camera object. @@ -250,13 +252,13 @@ export class Engine { buttons = {} as Readonly - reactiveQueryStates = new Set<{ query: Query; result: State; components: QueryComponents }>() + reactiveQueryStates = new Set<{ query: Query; result: State; components: QueryComponents }>() #entityQuery = defineQuery([Not(EntityRemovedComponent)]) entityQuery = () => this.#entityQuery() as Entity[] // @todo move to EngineState - activePortal = null as ComponentType | null + activePortal = null as typeof PortalComponent._TYPE | null /** * Custom systems injected into this world @@ -326,7 +328,15 @@ export class Engine { getUserAvatarEntity(userId: UserId) { return this.getOwnedNetworkObjectsWithComponent(userId, AvatarComponent).find((eid) => { return getComponent(eid, AvatarComponent).primary - })! + })! as Entity< + [ + typeof NetworkObjectComponent, + typeof AvatarComponent, + typeof AvatarControllerComponent, + typeof AvatarAnimationComponent, + typeof RigidBodyComponent + ] + > } /** @@ -335,12 +345,10 @@ export class Engine { * @param component * @returns */ - getOwnedNetworkObjectWithComponent(userId: UserId, component: Component) { - return ( - this.getOwnedNetworkObjects(userId).find((eid) => { - return hasComponent(eid, component) - }) || UndefinedEntity - ) + getOwnedNetworkObjectWithComponent(userId: UserId, component: C) { + return (this.getOwnedNetworkObjects(userId).find((eid) => { + return hasComponent(eid, component) + }) || UndefinedEntity) as Entity<[C, typeof NetworkObjectComponent]> } /** @@ -349,10 +357,10 @@ export class Engine { * @param component * @returns */ - getOwnedNetworkObjectsWithComponent(userId: UserId, component: Component) { + getOwnedNetworkObjectsWithComponent(userId: UserId, component: C) { return this.getOwnedNetworkObjects(userId).filter((eid) => { return hasComponent(eid, component) - }) + }) as any as Entity<[typeof NetworkObjectComponent, C]>[] } /** ID of last network created. */ diff --git a/packages/engine/src/ecs/classes/Entity.ts b/packages/engine/src/ecs/classes/Entity.ts index 2de8e5f202..1183f0f8c8 100755 --- a/packages/engine/src/ecs/classes/Entity.ts +++ b/packages/engine/src/ecs/classes/Entity.ts @@ -1,5 +1,18 @@ import { OpaqueType } from '@etherealengine/common/src/interfaces/OpaqueType' -export type Entity = OpaqueType<'entity'> & number +type FilterComponents = T //extends {isComponent:true} ? T : never + +/** + * Entity or Entity<[]> means an entity that can NOT be used with any component without type errors + * Entity<[Component1, Component2]> means an entity that is typed to ONLY have Component1 and Component2 + */ +export type Entity = OpaqueType<'entity'> & + number & { + __components: { + [key in FilterComponents[number]['name']]: true + } & { + [key: string]: true + } + } export const UndefinedEntity = 0 as Entity diff --git a/packages/engine/src/ecs/functions/ComponentFunctions.ts b/packages/engine/src/ecs/functions/ComponentFunctions.ts index c0e1d64ea0..a679c2b864 100755 --- a/packages/engine/src/ecs/functions/ComponentFunctions.ts +++ b/packages/engine/src/ecs/functions/ComponentFunctions.ts @@ -12,6 +12,7 @@ import { useForceUpdate } from '@etherealengine/common/src/utils/useForceUpdate' import { startReactor } from '@etherealengine/hyperflux' import { hookstate, NO_PROXY, none, State, useHookstate } from '@etherealengine/hyperflux/functions/StateFunctions' +import { TransformComponent } from '../../transform/components/TransformComponent' import { Engine } from '../classes/Engine' import { Entity } from '../classes/Entity' import { EntityReactorProps, EntityReactorRoot } from './EntityFunctions' @@ -25,71 +26,72 @@ export const ComponentMap = new Map>() globalThis.ComponentMap = ComponentMap type PartialIfObject = T extends object ? Partial : T - -type OnInitValidateNotState = T extends State ? 'onAdd must not return a State object' : T - type SomeStringLiteral = 'a' | 'b' | 'c' // just a dummy string literal union type StringLiteral = string extends T ? SomeStringLiteral : string const createExistenceMap = () => hookstate({} as Record, subscribable()) export interface ComponentPartial< + Name extends string = string, ComponentType = any, - Schema extends bitECS.ISchema = {}, - JSON = ComponentType, - SetJSON = PartialIfObject>, + Schema extends bitECS.ISchema = bitECS.ISchema, + JSON = unknown, + SetJSON = unknown, ErrorTypes = never > { - name: string + name: Name schema?: Schema - onInit?: (this: SoAComponentType, entity: Entity) => ComponentType & OnInitValidateNotState - toJSON?: (entity: Entity, component: State) => JSON - onSet?: (entity: Entity, component: State, json?: SetJSON) => void - onRemove?: (entity: Entity, component: State) => void | Promise - reactor?: React.FC + onInit?: (entity: Entity) => ComponentType + toJSON?: (entity: Entity<[{ name: Name }]>, component: State) => JSON + onSet?: (entity: Entity<[{ name: Name }]>, component: State, json?: SetJSON) => void + onRemove?: (entity: Entity<[{ name: Name }]>, component: State) => void | Promise + reactor?: React.FC> errors?: ErrorTypes[] } export interface Component< + Name extends string = string, ComponentType = any, - Schema extends bitECS.ISchema = {}, - JSON = ComponentType, - SetJSON = PartialIfObject>, + Schema extends bitECS.ISchema = bitECS.ISchema, + JSON = unknown, + SetJSON = unknown, ErrorTypes = string > { isComponent: true - name: string + name: Name schema?: Schema - onInit: (this: SoAComponentType, entity: Entity) => ComponentType & OnInitValidateNotState + onInit: (entity: Entity) => ComponentType toJSON: (entity: Entity, component: State) => JSON onSet: (entity: Entity, component: State, json?: SetJSON) => void onRemove: (entity: Entity, component: State) => void - reactor?: HookableFunction> - reactorMap: Map + reactor?: HookableFunction>> + reactorMap: Map> existenceMap: ReturnType stateMap: Record | undefined> valueMap: Record errors: ErrorTypes[] + _TYPE: ComponentType } export type SoAComponentType = bitECS.ComponentType -export type ComponentType = NonNullable +export type ComponentType = C['_TYPE'] export type SerializedComponentType = ReturnType export type SetComponentType = Parameters[2] export type ComponentErrorsType = C['errors'][number] export const defineComponent = < + ComponentName extends string, ComponentType = true, - Schema extends bitECS.ISchema = {}, + Schema extends bitECS.ISchema = bitECS.ISchema, JSON = ComponentType, ComponentExtras = unknown, SetJSON = PartialIfObject>, Error extends StringLiteral = '' >( - def: ComponentPartial & ComponentExtras + def: ComponentPartial & ComponentExtras ) => { - const Component = bitECS.defineComponent(def.schema, INITIAL_COMPONENT_SIZE) as ComponentExtras & - SoAComponentType & - Component + const Component = bitECS.defineComponent(def.schema, INITIAL_COMPONENT_SIZE) as SoAComponentType & + Component & + ComponentExtras Component.isComponent = true Component.onInit = (entity) => undefined as any Component.onSet = (entity, component, json) => {} @@ -113,11 +115,15 @@ export const defineComponent = < /** * @deprecated use `defineComponent` */ -export const createMappedComponent = ( - name: string, +export const createMappedComponent = < + ComponentName extends string, + ComponentType, + Schema extends bitECS.ISchema = bitECS.ISchema +>( + name: ComponentName, schema?: Schema ) => { - const Component = defineComponent({ + const Component = defineComponent({ name, schema, onSet: (entity, component, json: any) => { @@ -128,19 +134,16 @@ export const createMappedComponent = ( +export const getOptionalComponentState = ( entity: Entity, - component: Component -): State | undefined => { + component: C +): State | undefined => { // if (entity === UndefinedEntity) return undefined - if (component.existenceMap[entity].value) return component.stateMap[entity] + if (component.existenceMap[entity].value) return component.stateMap[entity] as State return undefined } -export const getMutableComponent = ( - entity: Entity, - component: Component -): State => { +export const getMutableComponent = (entity: Entity<[C]>, component: C): State => { const componentState = getOptionalComponentState(entity, component)! // TODO: uncomment the following after enabling es-lint no-unnecessary-condition rule // if (!componentState?.value) throw new Error(`[getComponent]: entity does not have ${component.name}`) @@ -152,18 +155,12 @@ export const getMutableComponent = ( */ export const getComponentState = getMutableComponent -export const getOptionalComponent = ( - entity: Entity, - component: Component -): ComponentType | undefined => { - return component.valueMap[entity] as ComponentType | undefined +export const getOptionalComponent = (entity: Entity, component: C): C['_TYPE'] | undefined => { + return component.valueMap[entity] } -export const getComponent = ( - entity: Entity, - component: Component -): ComponentType => { - return component.valueMap[entity] as ComponentType +export const getComponent = , C extends Component>(entity: E, component: C) => { + return component.valueMap[entity] as C['_TYPE'] } /** @@ -174,7 +171,7 @@ export const getComponent = ( * @param Component * @param args * - * @returns the component + * @returns the component state */ export const setComponent = ( entity: Entity, @@ -187,21 +184,21 @@ export const setComponent = ( if (!bitECS.entityExists(Engine.instance, entity)) { throw new Error('[setComponent]: entity does not exist') } - let value = args if (!hasComponent(entity, Component)) { - value = Component.onInit(entity) ?? args + // @ts-ignore + const value = Component.onInit(entity) Component.existenceMap[entity].set(true) if (!Component.stateMap[entity]) { const state = hookstate(value, subscribable()) - Component.stateMap[entity] = state + Component.stateMap[entity] = state as any state.subscribe(() => { - Component.valueMap[entity] = Component.stateMap[entity]?.get(NO_PROXY) + Component.valueMap[entity] = Component.stateMap[entity]?.get(NO_PROXY) as any }) } else Component.stateMap[entity]!.set(value) bitECS.addComponent(Engine.instance, Component, entity, false) // don't clear data on-add if (Component.reactor && !Component.reactorMap.has(entity)) { - const root = startReactor(Component.reactor) as EntityReactorRoot - root.entity = entity + const root = startReactor(Component.reactor) as EntityReactorRoot<[C]> + root.entity = entity as Entity<[C]> Component.reactorMap.set(entity, root) } } @@ -211,13 +208,14 @@ export const setComponent = ( const root = Component.reactorMap.get(entity) if (!root?.isRunning) root?.run() }) + return Component.stateMap[entity]! } /** * Experimental API */ export const updateComponent = ( - entity: Entity, + entity: Entity<[C]>, Component: C, props: Partial> ) => { @@ -265,10 +263,13 @@ export const addComponent = ( args: SetComponentType | undefined = undefined ) => { if (hasComponent(entity, Component)) throw new Error(`${Component.name} already exists on entity ${entity}`) - setComponent(entity, Component, args) + return setComponent(entity, Component, args) as State } -export const hasComponent = (entity: Entity, component: C) => { +export const hasComponent = ( + entity: Entity, + component: TestingComponent +): entity is Entity<[...KnownComponents, TestingComponent]> => { return component.existenceMap[entity]?.value ?? false } @@ -297,7 +298,7 @@ export const getAllComponents = (entity: Entity): Component[] => { return bitECS.getEntityComponents(Engine.instance, entity) as Component[] } -export const getAllComponentData = (entity: Entity): { [name: string]: ComponentType } => { +export const getAllComponentData = (entity: Entity): { [name: string]: any } => { return Object.fromEntries(getAllComponents(entity).map((C) => [C.name, getComponent(entity, C)])) } @@ -308,7 +309,7 @@ export const getComponentCountOfType = (component: C): numb return length } -export const getAllComponentsOfType = >(component: C): ComponentType[] => { +export const getAllComponentsOfType = >(component: C): C['_TYPE'][] => { const query = defineQuery([component]) const entities = query() bitECS.removeQuery(Engine.instance, query._query) @@ -327,36 +328,47 @@ export const removeAllComponents = (entity: Entity) => { } } -export const serializeComponent = >(entity: Entity, Component: C) => { +export const serializeComponent = (entity: Entity<[C]>, Component: C) => { const component = getMutableComponent(entity, Component) return Component.toJSON(entity, component) as ReturnType } -export function defineQuery(components: (bitECS.Component | bitECS.QueryModifier)[]) { +export interface Query { + (): Entity[] + enter(): Entity[] + exit(): Entity[] + _query: bitECS.Query + _enterQuery: bitECS.Query + _exitQuery: bitECS.Query +} + +export function defineQuery( + components: A +): Query { const query = bitECS.defineQuery([...components, bitECS.Not(EntityRemovedComponent)]) as bitECS.Query const enterQuery = bitECS.enterQuery(query) const exitQuery = bitECS.exitQuery(query) - const wrappedQuery = () => query(Engine.instance) as Entity[] - wrappedQuery.enter = () => enterQuery(Engine.instance) as Entity[] - wrappedQuery.exit = () => exitQuery(Engine.instance) as Entity[] + const wrappedQuery = () => query(Engine.instance) as Entity[] + wrappedQuery.enter = () => enterQuery(Engine.instance) as Entity[] + wrappedQuery.exit = () => exitQuery(Engine.instance) as Entity[] wrappedQuery._query = query wrappedQuery._enterQuery = enterQuery wrappedQuery._exitQuery = exitQuery return wrappedQuery } -export function removeQuery(query: ReturnType) { +export function removeQuery(query: Query) { bitECS.removeQuery(Engine.instance, query._query) bitECS.removeQuery(Engine.instance, query._enterQuery) bitECS.removeQuery(Engine.instance, query._exitQuery) } -export type QueryComponents = (Component | bitECS.QueryModifier | bitECS.Component)[] +export type QueryComponents = (Component | bitECS.QueryModifier | bitECS.Component)[] /** * Use a query in a reactive context (a React component) */ -export function useQuery(components: QueryComponents) { +export function useQuery(components: C) { const result = useHookstate([] as Entity[]) const forceUpdate = useForceUpdate() @@ -366,7 +378,7 @@ export function useQuery(components: QueryComponents) { // (component state can't be modified after a component is unmounted) useLayoutEffect(() => { const query = defineQuery(components) - result.set(query()) + result.set(query() as Entity[]) const queryState = { query, result, components } Engine.instance.reactiveQueryStates.add(queryState) return () => { @@ -420,7 +432,7 @@ function _use(promise) { /** * Use a component in a reactive context (a React component) */ -export function useComponent>(entity: Entity, Component: C) { +export function useComponent, C extends Component>(entity: E, Component: C) { const hasComponent = useHookstate(Component.existenceMap[entity]).value // use() will suspend the component (by throwing a promise) and resume when the promise is resolved if (!hasComponent) @@ -434,21 +446,19 @@ export function useComponent>(entity: Entity, Component }) }) ) - return useHookstate(Component.stateMap[entity]) as any as State> // todo fix any cast + return useHookstate(Component.stateMap[entity]) as State } /** * Use a component in a reactive context (a React component) */ -export function useOptionalComponent>(entity: Entity, Component: C) { +export function useOptionalComponent(entity: Entity, Component: C) { const hasComponent = useHookstate(Component.existenceMap[entity]).value - if (!Component.stateMap[entity]) Component.stateMap[entity] = hookstate(undefined) - const component = useHookstate(Component.stateMap[entity]) as any as State> // todo fix any cast + if (!Component.stateMap[entity]) Component.stateMap[entity] = hookstate(undefined) as any + const component = useHookstate(Component.stateMap[entity]) as State return hasComponent ? component : undefined } -export type Query = ReturnType - export const EntityRemovedComponent = defineComponent({ name: 'EntityRemovedComponent' }) globalThis.EE_getComponent = getComponent diff --git a/packages/engine/src/ecs/functions/EntityFunctions.tsx b/packages/engine/src/ecs/functions/EntityFunctions.ts similarity index 72% rename from packages/engine/src/ecs/functions/EntityFunctions.tsx rename to packages/engine/src/ecs/functions/EntityFunctions.ts index 2b4af35c02..f5b3057746 100644 --- a/packages/engine/src/ecs/functions/EntityFunctions.tsx +++ b/packages/engine/src/ecs/functions/EntityFunctions.ts @@ -6,10 +6,10 @@ import { Engine } from '../classes/Engine' import { Entity } from '../classes/Entity' import { EntityRemovedComponent, removeAllComponents, setComponent } from './ComponentFunctions' -export const createEntity = (): Entity => { +export const createEntity = () => { let entity = bitECS.addEntity(Engine.instance) if (entity === 0) entity = bitECS.addEntity(Engine.instance) // always discard entity 0 since we do a lot of `if (entity)` checks - return entity as Entity + return entity as Entity } export const removeEntity = (entity: Entity, immediately = false) => { @@ -27,10 +27,10 @@ export const entityExists = (entity: Entity) => { return bitECS.entityExists(Engine.instance, entity) } -export interface EntityReactorRoot extends ReactorRoot { - entity: Entity +export interface EntityReactorRoot extends ReactorRoot { + entity: Entity } -export interface EntityReactorProps { - root: EntityReactorRoot +export interface EntityReactorProps { + root: EntityReactorRoot } diff --git a/packages/engine/src/ecs/functions/EntityTree.test.ts b/packages/engine/src/ecs/functions/EntityTree.test.ts index dbcbeb31a1..b777bd36a8 100644 --- a/packages/engine/src/ecs/functions/EntityTree.test.ts +++ b/packages/engine/src/ecs/functions/EntityTree.test.ts @@ -47,7 +47,7 @@ describe('EntityTreeComponent', () => { }) it('should set given values', () => { - const sceneEntity = getState(SceneState).sceneEntity + const sceneEntity = getState(SceneState).sceneEntity as Entity const entity = createEntity() const testUUID = 'test-uuid' as EntityUUID @@ -68,7 +68,7 @@ describe('EntityTreeComponent', () => { }) it('should set child at a given index', () => { - const sceneEntity = getState(SceneState).sceneEntity + const sceneEntity = getState(SceneState).sceneEntity as Entity setComponent(createEntity(), EntityTreeComponent, { parentEntity: sceneEntity, @@ -111,7 +111,7 @@ describe('EntityTreeComponent', () => { }) it('should remove entity from maps', () => { - const sceneEntity = getState(SceneState).sceneEntity + const sceneEntity = getState(SceneState).sceneEntity as Entity const entity = createEntity() setComponent(entity, EntityTreeComponent, { parentEntity: sceneEntity, uuid: 'test-uuid' as EntityUUID }) @@ -141,7 +141,7 @@ describe('EntityTreeFunctions', () => { describe('initializeEntityTree function', () => { it('will initialize entity tree', () => { initializeSceneEntity() - const sceneEntity = getState(SceneState).sceneEntity + const sceneEntity = getState(SceneState).sceneEntity as Entity assert(sceneEntity) assert(getComponent(sceneEntity, NameComponent), 'scene') assert(hasComponent(sceneEntity, VisibleComponent)) diff --git a/packages/engine/src/ecs/functions/EntityTree.ts b/packages/engine/src/ecs/functions/EntityTree.ts index cfedc84824..4e83574892 100644 --- a/packages/engine/src/ecs/functions/EntityTree.ts +++ b/packages/engine/src/ecs/functions/EntityTree.ts @@ -19,7 +19,7 @@ import { import { computeTransformMatrix } from '../../transform/systems/TransformSystem' import { Engine } from '../classes/Engine' import { EngineState } from '../classes/EngineState' -import { Entity, UndefinedEntity } from '../classes/Entity' +import { Entity } from '../classes/Entity' import { SceneState } from '../classes/Scene' import { defineComponent, @@ -33,7 +33,7 @@ import { import { createEntity, entityExists, removeEntity } from '../functions/EntityFunctions' type EntityTreeSetType = { - parentEntity: Entity | null + parentEntity: Entity<[{ name: 'EntityTreeComponent' }, typeof UUIDComponent, typeof TransformComponent]> | null uuid?: EntityUUID childIndex?: number } @@ -46,19 +46,35 @@ type EntityTreeSetType = { * @param {Readonly} children */ export const EntityTreeComponent = defineComponent({ - name: 'EntityTreeComponent', + name: 'EntityTreeComponent' as const, onInit: (entity) => { return { // api - parentEntity: null as Entity | null, + parentEntity: null as Entity< + [{ name: 'EntityTreeComponent' }, typeof UUIDComponent, typeof TransformComponent] + > | null, + // internal - children: [] as Entity[], - rootEntity: null as Entity | null + children: [] as Entity< + [ + { name: 'EntityTreeComponent' }, + typeof UUIDComponent, + typeof TransformComponent, + typeof LocalTransformComponent + ] + >[], + rootEntity: null as Entity<[{ name: 'EntityTreeComponent' }, typeof UUIDComponent]> | null } }, - onSet: (entity, component, json?: Readonly) => { + onSet: ( + entity: Entity< + [{ name: 'EntityTreeComponent' }, typeof UUIDComponent, typeof TransformComponent, typeof LocalTransformComponent] + >, + component, + json?: Readonly + ) => { if (!json) return // If a previous parentEntity, remove this entity from its children @@ -102,7 +118,7 @@ export const EntityTreeComponent = defineComponent({ }, onRemove: (entity, component) => { - if (entity === Engine.instance.originEntity) return + if (entity === (Engine.instance.originEntity as Entity)) return // TODO: this logic should be elsewhere if (component.parentEntity.value && entityExists(component.parentEntity.value)) { const parent = getMutableComponent(component.parentEntity.value, EntityTreeComponent) @@ -143,7 +159,7 @@ export function initializeSceneEntity(): void { /** * Recursively destroys all the children entities of the passed entity */ -export function destroyEntityTree(rootEntity: Entity): void { +export function destroyEntityTree(rootEntity: Entity<[typeof EntityTreeComponent]>): void { const children = getComponent(rootEntity, EntityTreeComponent).children.slice() for (const child of children) { destroyEntityTree(child) @@ -154,7 +170,7 @@ export function destroyEntityTree(rootEntity: Entity): void { /** * Recursively removes all the children from the entity tree */ -export function removeFromEntityTree(rootEntity: Entity): void { +export function removeFromEntityTree(rootEntity: Entity<[typeof EntityTreeComponent]>): void { const children = getComponent(rootEntity, EntityTreeComponent).children.slice() for (const child of children) { removeFromEntityTree(child) @@ -168,7 +184,11 @@ export function removeFromEntityTree(rootEntity: Entity): void { * @param node Child node to be added * @param index Index at which child node will be added */ -export function addEntityNodeChild(entity: Entity, parentEntity: Entity, uuid?: EntityUUID): void { +export function addEntityNodeChild( + entity: Entity<[typeof TransformComponent, typeof LocalTransformComponent]>, + parentEntity: Entity<[typeof EntityTreeComponent, typeof TransformComponent, typeof UUIDComponent]>, + uuid?: EntityUUID +): void { if ( !hasComponent(entity, EntityTreeComponent) || parentEntity !== getComponent(entity, EntityTreeComponent).parentEntity @@ -198,7 +218,7 @@ export function addEntityNodeChild(entity: Entity, parentEntity: Entity, uuid?: // } } -export function serializeNodeToWorld(entity: Entity) { +export function serializeNodeToWorld(entity: Entity<[typeof EntityTreeComponent, typeof UUIDComponent]>) { const entityTreeNode = getComponent(entity, EntityTreeComponent) const nodeUUID = getComponent(entity, UUIDComponent) const jsonEntity = getState(SceneState).sceneData!.scene.entities[nodeUUID] as EntityJson @@ -216,7 +236,10 @@ export function serializeNodeToWorld(entity: Entity) { * @param node * @param tree */ -export function removeEntityNodeRecursively(entity: Entity, serialize = false) { +export function removeEntityNodeRecursively( + entity: Entity<[typeof EntityTreeComponent, typeof UUIDComponent]>, + serialize = false +) { traverseEntityNode(entity, (childEntity) => { if (serialize) serializeNodeToWorld(childEntity) removeEntity(childEntity) @@ -228,7 +251,10 @@ export function removeEntityNodeRecursively(entity: Entity, serialize = false) { * @param entity * @param tree */ -export function removeEntityNode(entity: Entity, serialize = false) { +export function removeEntityNode( + entity: Entity<[typeof EntityTreeComponent, typeof UUIDComponent]>, + serialize = false +) { const entityTreeNode = getComponent(entity, EntityTreeComponent) for (const childEntity of entityTreeNode.children) { @@ -244,7 +270,11 @@ export function removeEntityNode(entity: Entity, serialize = false) { * @param newParent Parent node * @param index Index at which passed node will be set as child in parent node's children arrays */ -export function reparentEntityNode(entity: Entity, parentEntity: Entity | null, index?: number): void { +export function reparentEntityNode( + entity: Entity<[typeof TransformComponent, typeof LocalTransformComponent, typeof EntityTreeComponent]>, + parentEntity: Entity<[typeof EntityTreeComponent, typeof TransformComponent, typeof UUIDComponent]> | null, + index?: number +): void { const entityTreeNode = getComponent(entity, EntityTreeComponent) if (entityTreeNode.parentEntity === parentEntity) return if (parentEntity) addEntityNodeChild(entity, parentEntity) @@ -254,7 +284,7 @@ export function reparentEntityNode(entity: Entity, parentEntity: Entity | null, /** * Returns all entities in the tree */ -export function getAllEntitiesInTree(entity: Entity) { +export function getAllEntitiesInTree(entity: Entity<[typeof EntityTreeComponent, typeof UUIDComponent]>) { const entities = [] as Entity[] traverseEntityNode(entity, (childEntity) => { entities.push(childEntity) @@ -270,7 +300,11 @@ export function getAllEntitiesInTree(entity: Entity) { * @param index index of the curren node in it's parent * @param tree Entity Tree */ -export function traverseEntityNode(entity: Entity, cb: (entity: Entity, index: number) => void, index = 0): void { +export function traverseEntityNode( + entity: Entity<[typeof EntityTreeComponent, typeof UUIDComponent]>, + cb: (entity: Entity<[typeof EntityTreeComponent, typeof UUIDComponent]>, index: number) => void, + index = 0 +): void { const entityTreeNode = getComponent(entity, EntityTreeComponent) if (!entityTreeNode) return @@ -293,10 +327,10 @@ export function traverseEntityNode(entity: Entity, cb: (entity: Entity, index: n * @param snubChildren If true, will not traverse children of a node if pred returns false */ export function iterateEntityNode( - entity: Entity, + entity: Entity<[typeof EntityTreeComponent]>, cb: (entity: Entity, index: number) => void, pred: (entity: Entity) => boolean = (x) => true, - snubChildren: boolean = false + snubChildren = false ): void { const frontier = [[entity]] while (frontier.length > 0) { @@ -327,7 +361,10 @@ export function iterateEntityNode( * @param cb Callback function which will be called for every traverse * @param tree Entity Tree */ -export function traverseEntityNodeParent(entity: Entity, cb: (parent: Entity) => void): void { +export function traverseEntityNodeParent( + entity: Entity<[typeof EntityTreeComponent]>, + cb: (parent: Entity) => void +): void { const entityTreeNode = getComponent(entity, EntityTreeComponent) if (entityTreeNode.parentEntity) { const parent = entityTreeNode.parentEntity diff --git a/packages/engine/src/ecs/functions/SystemFunctions.tsx b/packages/engine/src/ecs/functions/SystemFunctions.tsx index 6657953b0e..9f1e73a917 100755 --- a/packages/engine/src/ecs/functions/SystemFunctions.tsx +++ b/packages/engine/src/ecs/functions/SystemFunctions.tsx @@ -9,16 +9,16 @@ import { nowMilliseconds } from '../../common/functions/nowMilliseconds' import { Engine } from '../classes/Engine' import { EngineState } from '../classes/EngineState' import { Entity } from '../classes/Entity' -import { defineQuery, EntityRemovedComponent, Query, QueryComponents, useQuery } from './ComponentFunctions' +import { defineQuery, EntityRemovedComponent, QueryComponents, useQuery } from './ComponentFunctions' import { EntityReactorProps, removeEntity } from './EntityFunctions' import { SystemUpdateType } from './SystemUpdateType' const logger = multiLogger.child({ component: 'engine:ecs:SystemFunctions' }) -export type CreateSystemSyncFunctionType = (props?: A) => SystemDefintion -export type CreateSystemFunctionType = (props?: A) => Promise -export type SystemModule = { default: CreateSystemFunctionType } -export type SystemLoader = () => Promise> +export type CreateSystemSyncFunctionType = (props?: A) => SystemDefintion +export type CreateSystemFunctionType = (props?: A) => Promise +export type SystemModule = { default: CreateSystemFunctionType } +export type SystemLoader = () => Promise> export interface SystemDefintion { execute: () => void @@ -325,7 +325,7 @@ export const unloadSystem = (uuid: string) => { function QueryReactor(props: { root: ReactorRoot query: QueryComponents - ChildEntityReactor: React.FC + ChildEntityReactor: React.FC> }) { const entities = useQuery(props.query) return ( @@ -341,7 +341,10 @@ function QueryReactor(props: { ) } -export const startQueryReactor = (Components: QueryComponents, ChildEntityReactor: React.FC) => { +export const startQueryReactor = ( + Components: QueryComponents, + ChildEntityReactor: React.FC> +) => { if (!ChildEntityReactor.name) Object.defineProperty(ChildEntityReactor, 'name', { value: 'ChildEntityReactor' }) return startReactor(function HyperfluxQueryReactor({ root }: ReactorProps) { return diff --git a/packages/engine/src/interaction/systems/InteractiveSystem.ts b/packages/engine/src/interaction/systems/InteractiveSystem.ts index a0166d2b07..d9c2f71528 100755 --- a/packages/engine/src/interaction/systems/InteractiveSystem.ts +++ b/packages/engine/src/interaction/systems/InteractiveSystem.ts @@ -56,7 +56,7 @@ const vec3 = new Vector3() export const onInteractableUpdate = (entity: Entity, xrui: ReturnType) => { const transform = getComponent(xrui.entity, TransformComponent) - if (!transform || !getComponent(Engine.instance.localClientEntity, TransformComponent)) return + if (!transform || !hasComponent(Engine.instance.localClientEntity, TransformComponent)) return transform.position.copy(getComponent(entity, TransformComponent).position) transform.rotation.copy(getComponent(entity, TransformComponent).rotation) transform.position.y += 1 diff --git a/packages/engine/src/interaction/systems/MountPointSystem.ts b/packages/engine/src/interaction/systems/MountPointSystem.ts index 30f1e56496..d8c072b9ef 100755 --- a/packages/engine/src/interaction/systems/MountPointSystem.ts +++ b/packages/engine/src/interaction/systems/MountPointSystem.ts @@ -11,9 +11,9 @@ import { Engine } from '../../ecs/classes/Engine' import { EngineActions, EngineState } from '../../ecs/classes/EngineState' import { addComponent, - ComponentType, defineQuery, getComponent, + getOptionalComponentState, hasComponent, removeComponent, removeQuery @@ -68,8 +68,15 @@ export default async function MountPointSystem() { } const mountPointActionQueue = createActionQueue(EngineActions.interactedWithObject.matches) - const mountPointQuery = defineQuery([MountPointComponent]) - const sittingIdleQuery = defineQuery([SittingComponent]) + const mountPointQuery = defineQuery([MountPointComponent, TransformComponent]) + const sittingIdleQuery = defineQuery([ + SittingComponent, + TransformComponent, + AvatarControllerComponent, + AvatarAnimationComponent, + AvatarComponent, + RigidBodyComponent + ]) const execute = () => { for (const entity of mountPointQuery.enter()) { @@ -88,13 +95,14 @@ export default async function MountPointSystem() { if (!action.targetEntity || !hasComponent(action.targetEntity!, MountPointComponent)) continue const avatarEntity = Engine.instance.getUserAvatarEntity(action.$from) - const mountPoint = getComponent(action.targetEntity!, MountPointComponent) + const mountPoint = getComponent(action.targetEntity, MountPointComponent) if (mountPoint.type === MountPoint.seat) { const avatar = getComponent(avatarEntity, AvatarComponent) if (hasComponent(avatarEntity, SittingComponent)) continue + if (!hasComponent(action.targetEntity, TransformComponent)) continue - const transform = getComponent(action.targetEntity!, TransformComponent) + const transform = getComponent(action.targetEntity, TransformComponent) const rigidBody = getComponent(avatarEntity, RigidBodyComponent) rigidBody.body.setTranslation( { @@ -105,17 +113,16 @@ export default async function MountPointSystem() { true ) rigidBody.body.setLinvel({ x: 0, y: 0, z: 0 }, true) - addComponent(avatarEntity, SittingComponent, { - mountPointEntity: action.targetEntity!, + const sitting = addComponent(avatarEntity, SittingComponent, { + mountPointEntity: action.targetEntity, state: AvatarStates.SIT_ENTER }) - const sitting = getComponent(avatarEntity, SittingComponent) getComponent(avatarEntity, AvatarControllerComponent).movementEnabled = false const avatarAnimationComponent = getComponent(avatarEntity, AvatarAnimationComponent) changeState(avatarAnimationComponent.animationGraph, AvatarStates.SIT_IDLE) - sitting.state = AvatarStates.SIT_IDLE + sitting.state.set(AvatarStates.SIT_IDLE) } } @@ -160,7 +167,9 @@ export default async function MountPointSystem() { changeState(avatarAnimationComponent.animationGraph, AvatarStates.LOCOMOTION) removeComponent(entity, SittingComponent) - getComponent(Engine.instance.localClientEntity, AvatarControllerComponent).movementEnabled = true + getOptionalComponentState(Engine.instance.localClientEntity, AvatarControllerComponent)?.movementEnabled.set( + true + ) } } } diff --git a/packages/engine/src/scene/components/ColliderComponent.ts b/packages/engine/src/scene/components/ColliderComponent.ts index 0ca3cb8b39..32450d2618 100644 --- a/packages/engine/src/scene/components/ColliderComponent.ts +++ b/packages/engine/src/scene/components/ColliderComponent.ts @@ -103,13 +103,13 @@ export const ColliderComponent = defineComponent({ reactor: function ({ root }) { const entity = root.entity - const transformComponent = useComponent(entity, TransformComponent) + const transformComponent = useComponent(entity as any, TransformComponent) const colliderComponent = useComponent(entity, ColliderComponent) const isLoadedFromGLTF = useOptionalComponent(entity, GLTFLoadedComponent) const groupComponent = useOptionalComponent(entity, GroupComponent) useEffect(() => { - if (!!isLoadedFromGLTF?.value) { + if (isLoadedFromGLTF?.value) { const colliderComponent = getComponent(entity, ColliderComponent) if (hasComponent(entity, RigidBodyComponent)) { @@ -133,7 +133,7 @@ export const ColliderComponent = defineComponent({ } else { const rigidbodyTypeChanged = !hasComponent(entity, RigidBodyComponent) || - colliderComponent.bodyType.value !== getComponent(entity, RigidBodyComponent).body.bodyType() + colliderComponent.bodyType.value !== getComponent(entity as any, RigidBodyComponent).body.bodyType() if (rigidbodyTypeChanged) { const rigidbody = getOptionalComponent(entity, RigidBodyComponent)?.body @@ -166,7 +166,7 @@ export const ColliderComponent = defineComponent({ } } - const rigidbody = getComponent(entity, RigidBodyComponent) + const rigidbody = getComponent(entity as any, RigidBodyComponent) /** * This component only supports one collider, always at index 0 diff --git a/packages/engine/src/scene/components/ErrorComponent.ts b/packages/engine/src/scene/components/ErrorComponent.ts index 117f78ed46..5c47b7b718 100644 --- a/packages/engine/src/scene/components/ErrorComponent.ts +++ b/packages/engine/src/scene/components/ErrorComponent.ts @@ -12,7 +12,7 @@ export type ErrorComponentType = { } } -export const ErrorComponent = defineComponent({ +export const ErrorComponent = defineComponent({ name: 'ErrorComponent', onInit: () => ({} as ErrorComponentType) }) diff --git a/packages/engine/src/scene/components/GroupComponent.tsx b/packages/engine/src/scene/components/GroupComponent.tsx index 77742f4015..ae716374ff 100644 --- a/packages/engine/src/scene/components/GroupComponent.tsx +++ b/packages/engine/src/scene/components/GroupComponent.tsx @@ -49,12 +49,12 @@ export function addObjectToGroup(entity: Entity, object: Object3D) { obj.entity = entity if (!hasComponent(entity, GroupComponent)) addComponent(entity, GroupComponent, []) - if (getComponent(entity, GroupComponent).includes(obj)) return // console.warn('[addObjectToGroup]: Tried to add an object that is already included', entity, object) + if (getComponent(entity as any, GroupComponent).includes(obj)) return // console.warn('[addObjectToGroup]: Tried to add an object that is already included', entity, object) if (!hasComponent(entity, TransformComponent)) setTransformComponent(entity) - getMutableComponent(entity, GroupComponent).merge([obj]) + getMutableComponent(entity as any, GroupComponent).merge([obj]) - const transform = getComponent(entity, TransformComponent) + const transform = getComponent(entity as any, TransformComponent) obj.position.copy(transform.position) obj.quaternion.copy(transform.rotation) obj.scale.copy(transform.scale) diff --git a/packages/engine/src/scene/components/ImageComponent.ts b/packages/engine/src/scene/components/ImageComponent.ts index 3ec8e876ee..bd354aafef 100644 --- a/packages/engine/src/scene/components/ImageComponent.ts +++ b/packages/engine/src/scene/components/ImageComponent.ts @@ -19,6 +19,7 @@ import { useHookstate } from '@etherealengine/hyperflux' import { AssetLoader } from '../../assets/classes/AssetLoader' import { AssetClass } from '../../assets/enum/AssetClass' +import { Entity } from '../../ecs/classes/Entity' import { defineComponent, hasComponent, @@ -97,7 +98,105 @@ export const ImageComponent = defineComponent({ errors: ['MISSING_TEXTURE_SOURCE', 'UNSUPPORTED_ASSET_CLASS', 'LOADING_ERROR', 'INVALID_URL'], - reactor: ImageReactor + reactor: function ImageReactor({ root }) { + const entity = root.entity + const image = useComponent(entity, ImageComponent) + const texture = useHookstate(null as Texture | null) + const imageValue = image.value + const source = + imageValue.resource?.jpegStaticResource?.LOD0_url || + imageValue.resource?.gifStaticResource?.LOD0_url || + imageValue.resource?.pngStaticResource?.LOD0_url || + imageValue.resource?.ktx2StaticResource?.LOD0_url || + imageValue.resource?.source || + imageValue.source + + useEffect( + function updateTextureSource() { + if (!source) { + return addError(entity, ImageComponent, `MISSING_TEXTURE_SOURCE`) + } + + const assetType = AssetLoader.getAssetClass(source) + if (assetType !== AssetClass.Image) { + return addError(entity, ImageComponent, `UNSUPPORTED_ASSET_CLASS`) + } + + AssetLoader.loadAsync(source) + .then((_texture) => { + texture.set(_texture) + }) + .catch((e) => { + addError(entity, ImageComponent, `LOADING_ERROR`, e.message) + }) + + return () => { + // TODO: abort load request, pending https://github.com/mrdoob/three.js/pull/23070 + } + }, + [image.resource] + ) + + useEffect( + function updateTexture() { + if (!image) return + if (!texture.value) return + + clearErrors(entity, ImageComponent) + + texture.value.encoding = sRGBEncoding + texture.value.minFilter = LinearMipmapLinearFilter + + image.mesh.material.map.ornull?.value.dispose() + image.mesh.material.map.set(texture.value) + image.mesh.visible.set(true) + image.mesh.material.value.needsUpdate = true + + // upload to GPU immediately + EngineRenderer.instance.renderer.initTexture(texture.value) + + const imageMesh = image.mesh.value + addObjectToGroup(entity, imageMesh) + + return () => { + removeObjectFromGroup(entity, imageMesh) + } + }, + [texture] + ) + + useEffect( + function updateGeometry() { + if (!image.mesh.material.map.value) return + + const flippedTexture = image.mesh.material.map.value.flipY + switch (image.projection.value) { + case ImageProjection.Equirectangular360: + image.mesh.value.geometry = flippedTexture ? SPHERE_GEO : SPHERE_GEO_FLIPPED + image.mesh.scale.value.set(-1, 1, 1) + break + case ImageProjection.Flat: + default: + image.mesh.value.geometry = flippedTexture ? PLANE_GEO : PLANE_GEO_FLIPPED + resizeImageMesh(image.mesh.value) + } + }, + [image.mesh.material.map, image.projection] + ) + + useEffect( + function updateMaterial() { + const material = image.mesh.material.value + material.transparent = image.alphaMode.value === ImageAlphaMode.Blend + material.alphaTest = image.alphaMode.value === 'Mask' ? image.alphaCutoff.value : 0 + material.side = image.side.value + material.needsUpdate = true + }, + [image.alphaMode, image.alphaCutoff, image.side] + ) + + return null + } }) const _size = new Vector2() @@ -131,103 +230,3 @@ function flipNormals(geometry: G) { } export const SCENE_COMPONENT_IMAGE = 'image' - -export function ImageReactor({ root }: EntityReactorProps) { - const entity = root.entity - const image = useComponent(entity, ImageComponent) - const texture = useHookstate(null as Texture | null) - const imageValue = image.value - const source = - imageValue.resource?.jpegStaticResource?.LOD0_url || - imageValue.resource?.gifStaticResource?.LOD0_url || - imageValue.resource?.pngStaticResource?.LOD0_url || - imageValue.resource?.ktx2StaticResource?.LOD0_url || - imageValue.resource?.source || - imageValue.source - - useEffect( - function updateTextureSource() { - if (!source) { - return addError(entity, ImageComponent, `MISSING_TEXTURE_SOURCE`) - } - - const assetType = AssetLoader.getAssetClass(source) - if (assetType !== AssetClass.Image) { - return addError(entity, ImageComponent, `UNSUPPORTED_ASSET_CLASS`) - } - - AssetLoader.loadAsync(source) - .then((_texture) => { - texture.set(_texture) - }) - .catch((e) => { - addError(entity, ImageComponent, `LOADING_ERROR`, e.message) - }) - - return () => { - // TODO: abort load request, pending https://github.com/mrdoob/three.js/pull/23070 - } - }, - [image.resource] - ) - - useEffect( - function updateTexture() { - if (!image) return - if (!texture.value) return - - clearErrors(entity, ImageComponent) - - texture.value.encoding = sRGBEncoding - texture.value.minFilter = LinearMipmapLinearFilter - - image.mesh.material.map.ornull?.value.dispose() - image.mesh.material.map.set(texture.value) - image.mesh.visible.set(true) - image.mesh.material.value.needsUpdate = true - - // upload to GPU immediately - EngineRenderer.instance.renderer.initTexture(texture.value) - - const imageMesh = image.mesh.value - addObjectToGroup(entity, imageMesh) - - return () => { - removeObjectFromGroup(entity, imageMesh) - } - }, - [texture] - ) - - useEffect( - function updateGeometry() { - if (!image.mesh.material.map.value) return - - const flippedTexture = image.mesh.material.map.value.flipY - switch (image.projection.value) { - case ImageProjection.Equirectangular360: - image.mesh.value.geometry = flippedTexture ? SPHERE_GEO : SPHERE_GEO_FLIPPED - image.mesh.scale.value.set(-1, 1, 1) - break - case ImageProjection.Flat: - default: - image.mesh.value.geometry = flippedTexture ? PLANE_GEO : PLANE_GEO_FLIPPED - resizeImageMesh(image.mesh.value) - } - }, - [image.mesh.material.map, image.projection] - ) - - useEffect( - function updateMaterial() { - const material = image.mesh.material.value - material.transparent = image.alphaMode.value === ImageAlphaMode.Blend - material.alphaTest = image.alphaMode.value === 'Mask' ? image.alphaCutoff.value : 0 - material.side = image.side.value - material.needsUpdate = true - }, - [image.alphaMode, image.alphaCutoff, image.side] - ) - - return null -} diff --git a/packages/engine/src/scene/components/MediaComponent.ts b/packages/engine/src/scene/components/MediaComponent.ts index 92d215135c..79a490a015 100644 --- a/packages/engine/src/scene/components/MediaComponent.ts +++ b/packages/engine/src/scene/components/MediaComponent.ts @@ -239,206 +239,204 @@ export const MediaComponent = defineComponent({ return component }, - reactor: MediaReactor, + errors: ['LOADING_ERROR', 'UNSUPPORTED_ASSET_CLASS', 'INVALID_URL'], + + reactor: function MediaReactor({ root }) { + const entity = root.entity as Entity<[typeof MediaComponent, typeof MediaElementComponent]> + const media = useComponent(entity, MediaComponent) + const mediaElement = useOptionalComponent(entity, MediaElementComponent) + const audioContext = getState(AudioState).audioContext + const gainNodeMixBuses = getState(AudioState).gainNodeMixBuses + + useEffect( + function updatePlay() { + if (!mediaElement) return + if (media.paused.value) { + mediaElement.element.value.pause() + } else { + mediaElement.element.value.play() + } + }, + [media.paused, mediaElement] + ) - errors: ['LOADING_ERROR', 'UNSUPPORTED_ASSET_CLASS', 'INVALID_URL'] -}) + useEffect( + function updateTrackMetadata() { + clearErrors(entity, MediaComponent) -export function MediaReactor({ root }: EntityReactorProps) { - const entity = root.entity - const media = useComponent(entity, MediaComponent) - const mediaElement = useOptionalComponent(entity, MediaElementComponent) - const audioContext = getState(AudioState).audioContext - const gainNodeMixBuses = getState(AudioState).gainNodeMixBuses - - useEffect( - function updatePlay() { - if (!mediaElement) return - if (media.paused.value) { - mediaElement.element.value.pause() - } else { - mediaElement.element.value.play() - } - }, - [media.paused, mediaElement] - ) + const paths = media.resources.value.map((resource) => getResourceURL(resource)) - useEffect( - function updateTrackMetadata() { - clearErrors(entity, MediaComponent) + for (const path of paths) { + const assetClass = AssetLoader.getAssetClass(path).toLowerCase() + if (assetClass !== 'audio' && assetClass !== 'video') { + return addError(entity, MediaComponent, 'UNSUPPORTED_ASSET_CLASS') + } + } - const paths = media.resources.value.map((resource) => getResourceURL(resource)) + const metadataListeners = [] as Array<{ tempElement: HTMLMediaElement; listener: () => void }> + + for (const [i, path] of paths.entries()) { + const assetClass = AssetLoader.getAssetClass(path).toLowerCase() + const tempElement = document.createElement(assetClass) as HTMLMediaElement + const listener = () => media.trackDurations[i].set(tempElement.duration) + metadataListeners.push({ tempElement, listener }) + tempElement.addEventListener('loadedmetadata', listener) + tempElement.crossOrigin = 'anonymous' + tempElement.preload = 'metadata' + tempElement.src = path + } - for (const path of paths) { - const assetClass = AssetLoader.getAssetClass(path).toLowerCase() - if (assetClass !== 'audio' && assetClass !== 'video') { - return addError(entity, MediaComponent, 'UNSUPPORTED_ASSET_CLASS') + return () => { + for (const { tempElement, listener } of metadataListeners) { + tempElement.removeEventListener('loadedmetadata', listener) + tempElement.src = '' + tempElement.load() + } + } + }, + [media.resources] + ) + + useEffect( + function updateMediaElement() { + if (!isClient) return + + const track = media.track.value + const resource = media.resources[track].value + const path = getResourceURL(resource) + if (!path) { + if (hasComponent(entity, MediaElementComponent)) removeComponent(entity, MediaElementComponent) + return } - } - const metadataListeners = [] as Array<{ tempElement: HTMLMediaElement; listener: () => void }> + const mediaElement = getOptionalComponent(entity, MediaElementComponent) - for (const [i, path] of paths.entries()) { const assetClass = AssetLoader.getAssetClass(path).toLowerCase() - const tempElement = document.createElement(assetClass) as HTMLMediaElement - const listener = () => media.trackDurations[i].set(tempElement.duration) - metadataListeners.push({ tempElement, listener }) - tempElement.addEventListener('loadedmetadata', listener) - tempElement.crossOrigin = 'anonymous' - tempElement.preload = 'metadata' - tempElement.src = path - } - return () => { - for (const { tempElement, listener } of metadataListeners) { - tempElement.removeEventListener('loadedmetadata', listener) - tempElement.src = '' - tempElement.load() + if (assetClass !== 'audio' && assetClass !== 'video') { + addError(entity, MediaComponent, 'UNSUPPORTED_ASSET_CLASS') + return } - } - }, - [media.resources] - ) - - useEffect( - function updateMediaElement() { - if (!isClient) return - - const track = media.track.value - const resource = media.resources[track].value - const path = getResourceURL(resource) - if (!path) { - if (hasComponent(entity, MediaElementComponent)) removeComponent(entity, MediaElementComponent) - return - } - - const mediaElement = getOptionalComponent(entity, MediaElementComponent) - const assetClass = AssetLoader.getAssetClass(path).toLowerCase() - - if (assetClass !== 'audio' && assetClass !== 'video') { - addError(entity, MediaComponent, 'UNSUPPORTED_ASSET_CLASS') - return - } + if (!mediaElement || mediaElement.element.nodeName.toLowerCase() !== assetClass) { + setComponent(entity, MediaElementComponent, { + element: document.createElement(assetClass) as HTMLMediaElement + }) + const mediaElementState = getMutableComponent(entity, MediaElementComponent) + + const element = mediaElementState.element.value + + element.crossOrigin = 'anonymous' + element.preload = 'auto' + element.muted = false + element.setAttribute('playsinline', 'true') + + const signal = mediaElementState.abortController.signal.value + + element.addEventListener( + 'playing', + () => { + media.waiting.set(false) + clearErrors(entity, MediaElementComponent) + }, + { signal } + ) + element.addEventListener('waiting', () => media.waiting.set(true), { signal }) + element.addEventListener( + 'error', + (err) => { + addError(entity, MediaElementComponent, 'MEDIA_ERROR', err.message) + if (!media.paused.value) media.track.set(getNextTrack(media.value)) + media.waiting.set(false) + }, + { signal } + ) + + element.addEventListener( + 'ended', + () => { + if (media.playMode.value === PlayMode.single) return + media.track.set(getNextTrack(media.value)) + media.waiting.set(false) + }, + { signal } + ) + + const audioNodes = createAudioNodeGroup( + element, + audioContext.createMediaElementSource(element), + media.isMusic.value ? gainNodeMixBuses.music : gainNodeMixBuses.soundEffects + ) + + audioNodes.gain.gain.setTargetAtTime(media.volume.value, audioContext.currentTime, 0.1) + } - if (!mediaElement || mediaElement.element.nodeName.toLowerCase() !== assetClass) { - setComponent(entity, MediaElementComponent, { - element: document.createElement(assetClass) as HTMLMediaElement - }) + setComponent(entity, MediaElementComponent) const mediaElementState = getMutableComponent(entity, MediaElementComponent) - const element = mediaElementState.element.value - - element.crossOrigin = 'anonymous' - element.preload = 'auto' - element.muted = false - element.setAttribute('playsinline', 'true') - - const signal = mediaElementState.abortController.signal.value + mediaElementState.hls.value?.destroy() + mediaElementState.hls.set(undefined) - element.addEventListener( - 'playing', - () => { - media.waiting.set(false) - clearErrors(entity, MediaElementComponent) - }, - { signal } - ) - element.addEventListener('waiting', () => media.waiting.set(true), { signal }) - element.addEventListener( - 'error', - (err) => { - addError(entity, MediaElementComponent, 'MEDIA_ERROR', err.message) - if (!media.paused.value) media.track.set(getNextTrack(media.value)) - media.waiting.set(false) - }, - { signal } - ) - - element.addEventListener( - 'ended', - () => { - if (media.playMode.value === PlayMode.single) return - media.track.set(getNextTrack(media.value)) - media.waiting.set(false) - }, - { signal } - ) - - const audioNodes = createAudioNodeGroup( - element, - audioContext.createMediaElementSource(element), - media.isMusic.value ? gainNodeMixBuses.music : gainNodeMixBuses.soundEffects - ) - - audioNodes.gain.gain.setTargetAtTime(media.volume.value, audioContext.currentTime, 0.1) - } - - setComponent(entity, MediaElementComponent) - const mediaElementState = getMutableComponent(entity, MediaElementComponent) - - mediaElementState.hls.value?.destroy() - mediaElementState.hls.set(undefined) - - if (isHLS(path)) { - mediaElementState.hls.set(setupHLS(entity, path)) - mediaElementState.hls.value!.attachMedia(mediaElementState.element.value) - } else { - mediaElementState.element.src.set(path) - } - }, - [media.resources, media.track] - ) - - useEffect( - function updateVolume() { - const volume = media.volume.value - const element = getOptionalComponent(entity, MediaElementComponent)?.element as HTMLMediaElement - if (!element) return - const audioNodes = AudioNodeGroups.get(element) - if (audioNodes) { - audioNodes.gain.gain.setTargetAtTime(volume, audioContext.currentTime, 0.1) + if (isHLS(path)) { + mediaElementState.hls.set(setupHLS(entity, path)) + mediaElementState.hls.value!.attachMedia(mediaElementState.element.value) + } else { + mediaElementState.element.src.set(path) + } + }, + [media.resources, media.track] + ) + + useEffect( + function updateVolume() { + const volume = media.volume.value + const element = getOptionalComponent(entity, MediaElementComponent)?.element as HTMLMediaElement + if (!element) return + const audioNodes = AudioNodeGroups.get(element) + if (audioNodes) { + audioNodes.gain.gain.setTargetAtTime(volume, audioContext.currentTime, 0.1) + } + }, + [media.volume] + ) + + useEffect( + function updateMixbus() { + if (!mediaElement?.value) return + const element = mediaElement.element.get({ noproxy: true }) + const audioNodes = AudioNodeGroups.get(element) + if (audioNodes) { + audioNodes.gain.disconnect(audioNodes.mixbus) + audioNodes.mixbus = media.isMusic.value ? gainNodeMixBuses.music : gainNodeMixBuses.soundEffects + audioNodes.gain.connect(audioNodes.mixbus) + } + }, + [mediaElement, media.isMusic] + ) + + const debugEnabled = useHookstate(getMutableState(RendererState).nodeHelperVisibility) + + useEffect(() => { + if (debugEnabled.value && !media.helper.value) { + const helper = new Mesh(new PlaneGeometry(), new MeshBasicMaterial({ transparent: true, side: DoubleSide })) + helper.name = `audio-helper-${root.entity}` + AssetLoader.loadAsync(AUDIO_TEXTURE_PATH).then((AUDIO_HELPER_TEXTURE) => { + helper.material.map = AUDIO_HELPER_TEXTURE + }) + setObjectLayers(helper, ObjectLayers.NodeHelper) + addObjectToGroup(root.entity, helper) + media.helper.set(helper) } - }, - [media.volume] - ) - useEffect( - function updateMixbus() { - if (!mediaElement?.value) return - const element = mediaElement.element.get({ noproxy: true }) - const audioNodes = AudioNodeGroups.get(element) - if (audioNodes) { - audioNodes.gain.disconnect(audioNodes.mixbus) - audioNodes.mixbus = media.isMusic.value ? gainNodeMixBuses.music : gainNodeMixBuses.soundEffects - audioNodes.gain.connect(audioNodes.mixbus) + if (!debugEnabled.value && media.helper.value) { + removeObjectFromGroup(root.entity, media.helper.value) + media.helper.set(none) } - }, - [mediaElement, media.isMusic] - ) + }, [debugEnabled]) - const debugEnabled = useHookstate(getMutableState(RendererState).nodeHelperVisibility) - - useEffect(() => { - if (debugEnabled.value && !media.helper.value) { - const helper = new Mesh(new PlaneGeometry(), new MeshBasicMaterial({ transparent: true, side: DoubleSide })) - helper.name = `audio-helper-${root.entity}` - AssetLoader.loadAsync(AUDIO_TEXTURE_PATH).then((AUDIO_HELPER_TEXTURE) => { - helper.material.map = AUDIO_HELPER_TEXTURE - }) - setObjectLayers(helper, ObjectLayers.NodeHelper) - addObjectToGroup(root.entity, helper) - media.helper.set(helper) - } - - if (!debugEnabled.value && media.helper.value) { - removeObjectFromGroup(root.entity, media.helper.value) - media.helper.set(none) - } - }, [debugEnabled]) - - return null -} + return null + } +}) export const SCENE_COMPONENT_MEDIA = 'media' diff --git a/packages/engine/src/scene/components/NameComponent.ts b/packages/engine/src/scene/components/NameComponent.ts index 306d25617d..eb3e019b5f 100755 --- a/packages/engine/src/scene/components/NameComponent.ts +++ b/packages/engine/src/scene/components/NameComponent.ts @@ -6,7 +6,7 @@ import { defineComponent } from '../../ecs/functions/ComponentFunctions' export const NameComponent = defineComponent({ name: 'NameComponent', - onInit: () => undefined as any as string, + onInit: () => '' as string, onSet: (entity, component, name?: string) => { if (typeof name !== 'string') throw new Error('NameComponent expects a non-empty string') diff --git a/packages/engine/src/transform/components/TransformComponent.ts b/packages/engine/src/transform/components/TransformComponent.ts index b73463a091..301b0337e8 100755 --- a/packages/engine/src/transform/components/TransformComponent.ts +++ b/packages/engine/src/transform/components/TransformComponent.ts @@ -1,14 +1,13 @@ -import { Types } from 'bitecs' +import { Not, Types } from 'bitecs' import { Euler, Matrix4, Quaternion, Vector3 } from 'three' import { DeepReadonly } from '@etherealengine/common/src/DeepReadonly' import { proxifyQuaternionWithDirty, proxifyVector3WithDirty } from '../../common/proxies/createThreejsProxy' -import { Engine } from '../../ecs/classes/Engine' import { Entity, UndefinedEntity } from '../../ecs/classes/Entity' import { defineComponent, - getComponent, + defineQuery, getOptionalComponent, hasComponent, setComponent @@ -85,9 +84,7 @@ export const TransformComponent = defineComponent({ }, toJSON(entity, comp) { - const component = hasComponent(entity, LocalTransformComponent) - ? getComponent(entity, LocalTransformComponent) - : comp.value + const component = getOptionalComponent(entity, LocalTransformComponent) ?? comp.value return { position: new Vector3().copy(component.position), rotation: new Quaternion().copy(component.rotation), @@ -99,7 +96,7 @@ export const TransformComponent = defineComponent({ delete TransformComponent.dirtyTransforms[entity] }, - dirtyTransforms: {} as Record + dirtyTransforms: {} as Record, boolean> }) export const LocalTransformComponent = defineComponent({ diff --git a/packages/engine/src/transform/systems/TransformSystem.ts b/packages/engine/src/transform/systems/TransformSystem.ts index 22e448bcb7..9f0e141b23 100755 --- a/packages/engine/src/transform/systems/TransformSystem.ts +++ b/packages/engine/src/transform/systems/TransformSystem.ts @@ -41,7 +41,11 @@ import { import { TransformSerialization } from '../TransformSerialization' const transformQuery = defineQuery([TransformComponent]) -const nonDynamicLocalTransformQuery = defineQuery([LocalTransformComponent, Not(RigidBodyDynamicTagComponent)]) +const nonDynamicLocalTransformQuery = defineQuery([ + TransformComponent, + LocalTransformComponent, + Not(RigidBodyDynamicTagComponent) +] as const) const rigidbodyTransformQuery = defineQuery([TransformComponent, RigidBodyComponent]) const fixedRigidBodyQuery = defineQuery([TransformComponent, RigidBodyComponent, RigidBodyFixedTagComponent]) const groupQuery = defineQuery([GroupComponent, TransformComponent]) @@ -53,12 +57,12 @@ const distanceFromLocalClientQuery = defineQuery([TransformComponent, DistanceFr const distanceFromCameraQuery = defineQuery([TransformComponent, DistanceFromCameraComponent]) const frustumCulledQuery = defineQuery([TransformComponent, FrustumCullCameraComponent]) -export const computeLocalTransformMatrix = (entity: Entity) => { +export const computeLocalTransformMatrix = (entity: Entity<[typeof LocalTransformComponent]>) => { const localTransform = getComponent(entity, LocalTransformComponent) localTransform.matrix.compose(localTransform.position, localTransform.rotation, localTransform.scale) } -export const computeTransformMatrix = (entity: Entity) => { +export const computeTransformMatrix = (entity: Entity<[typeof TransformComponent]>) => { const transform = getComponent(entity, TransformComponent) updateTransformFromComputedTransform(entity) updateTransformFromLocalTransform(entity) @@ -66,7 +70,7 @@ export const computeTransformMatrix = (entity: Entity) => { transform.matrixInverse.copy(transform.matrix).invert() } -export const teleportRigidbody = (entity: Entity) => { +export const teleportRigidbody = (entity: Entity<[typeof TransformComponent, typeof RigidBodyComponent]>) => { const transform = getComponent(entity, TransformComponent) const rigidBody = getComponent(entity, RigidBodyComponent) const isAwake = !rigidBody.body.isSleeping() @@ -113,7 +117,7 @@ export const lerpTransformFromRigidbody = (entity: Entity, alpha: number) => { TransformComponent.dirtyTransforms[entity] = true } -export const copyTransformToRigidBody = (entity: Entity) => { +export const copyTransformToRigidBody = (entity: Entity<[typeof RigidBodyComponent]>) => { RigidBodyComponent.position.x[entity] = TransformComponent.position.x[entity] RigidBodyComponent.position.y[entity] = TransformComponent.position.y[entity] RigidBodyComponent.position.z[entity] = TransformComponent.position.z[entity] @@ -126,7 +130,7 @@ export const copyTransformToRigidBody = (entity: Entity) => { rigidbody.body.setRotation(rigidbody.rotation, false) } -const updateTransformFromLocalTransform = (entity: Entity) => { +const updateTransformFromLocalTransform = (entity: Entity<[typeof TransformComponent]>) => { const localTransform = getOptionalComponent(entity, LocalTransformComponent) const isDynamicRigidbody = hasComponent(entity, RigidBodyDynamicTagComponent) const parentTransform = localTransform?.parentEntity @@ -146,7 +150,7 @@ const updateTransformFromComputedTransform = (entity: Entity) => { return true } -export const updateGroupChildren = (entity: Entity) => { +export const updateGroupChildren = (entity: Entity<[typeof GroupComponent]>) => { const group = getComponent(entity, GroupComponent) as any as (Mesh & Camera)[] // drop down one level and update children for (const root of group) { @@ -157,7 +161,7 @@ export const updateGroupChildren = (entity: Entity) => { } } -const getDistanceSquaredFromTarget = (entity: Entity, targetPosition: Vector3) => { +const getDistanceSquaredFromTarget = (entity: Entity<[typeof TransformComponent]>, targetPosition: Vector3) => { return getComponent(entity, TransformComponent).position.distanceToSquared(targetPosition) } @@ -213,7 +217,7 @@ export default async function TransformSystem() { if (mesh.isMesh) mesh.geometry.computeBoundingBox() } - const computeBoundingBox = (entity: Entity) => { + const computeBoundingBox = (entity: Entity<[typeof BoundingBoxComponent, typeof GroupComponent]>) => { const box = getComponent(entity, BoundingBoxComponent).box const group = getComponent(entity, GroupComponent) @@ -225,7 +229,7 @@ export default async function TransformSystem() { } } - const updateBoundingBox = (entity: Entity) => { + const updateBoundingBox = (entity: Entity<[typeof BoundingBoxComponent, typeof GroupComponent]>) => { const box = getComponent(entity, BoundingBoxComponent).box const group = getComponent(entity, GroupComponent) box.makeEmpty() @@ -238,11 +242,13 @@ export default async function TransformSystem() { !hasComponent(entity, RigidBodyKinematicPositionBasedTagComponent) && !hasComponent(entity, RigidBodyKinematicVelocityBasedTagComponent) - const filterAwakeRigidbodies = (entity: Entity) => !getComponent(entity, RigidBodyComponent).body.isSleeping() + const filterAwakeRigidbodies = (entity: Entity<[typeof RigidBodyComponent]>) => + !getComponent(entity, RigidBodyComponent).body.isSleeping() - const filterSleepingRigidbodies = (entity: Entity) => getComponent(entity, RigidBodyComponent).body.isSleeping() + const filterSleepingRigidbodies = (entity: Entity<[typeof RigidBodyComponent]>) => + getComponent(entity, RigidBodyComponent).body.isSleeping() - let sortedTransformEntities = [] as Entity[] + const sortedTransformEntities = [] as Entity<[typeof TransformComponent]>[] /** override Skeleton.update, as it is called inside */ const skeletonUpdate = Skeleton.prototype.update