From baf09ef95646df223cd0c0f8eaaae28a812143cf Mon Sep 17 00:00:00 2001 From: Agustin Mendez Date: Wed, 6 Feb 2019 15:30:07 -0300 Subject: [PATCH] fix: bulk of fixes (#536) * Fix #502 * Fix #499 * chore: reenable animator and audio * fix decentraland/builder#134, close decentraland/builder#136 * fix #537 --- .circleci/config.yml | 18 ++- packages/atomicHelpers/DebugTelemetry.ts | 142 ------------------ .../atomicHelpers/parcelScenePositions.ts | 20 ++- packages/dcl/WebGLParcelScene.ts | 31 ++-- .../entities/utils/checkParcelSceneLimits.ts | 21 +-- packages/dcl/index.ts | 58 +++---- .../src/decentraland/AnimationClip.ts | 4 +- .../src/decentraland/Components.ts | 6 +- .../decentraland-ecs/src/ecs/Component.ts | 4 +- packages/decentraland-ecs/src/ecs/Entity.ts | 81 ++++++++-- packages/decentraland-ecs/src/ecs/helpers.ts | 12 ++ .../types/dcl/decentraland-ecs.api.json | 98 +++++++++++- .../decentraland-ecs/types/dcl/index.d.ts | 24 ++- .../DisposableComponent.ts | 11 +- .../disposableComponents/GLTFShape.ts | 35 +++-- .../disposableComponents/OBJShape.ts | 14 +- .../ephemeralComponents/Animator.ts | 10 +- .../ephemeralComponents/AudioSource.ts | 7 +- .../components/ephemeralComponents/Gizmos.ts | 31 +++- .../components/ephemeralComponents/Sound.ts | 78 ---------- packages/engine/components/index.ts | 8 +- packages/engine/entities/BaseEntity.ts | 11 +- .../engine/entities/utils/processModels.ts | 31 +++- packages/engine/renderer/camera.ts | 53 ++++++- packages/entryPoints/editor.ts | 20 ++- packages/shared/index.ts | 14 +- .../-100.111.SkeletalAnimation/game.ts | 3 +- public/test-parcels/-102.102.gizmos/game.ts | 2 +- .../test-parcels/200.10.crocs-game/scene.json | 2 +- .../parcelScenePositions.test.ts | 18 +-- test/index.ts | 1 - test/testHelpers.ts | 3 - test/unit/ecs.test.tsx | 116 +++++++++++++- test/unit/entities.test.tsx | 2 +- test/unit/telemetry.test.tsx | 29 ---- 35 files changed, 583 insertions(+), 435 deletions(-) delete mode 100644 packages/atomicHelpers/DebugTelemetry.ts delete mode 100644 packages/engine/components/ephemeralComponents/Sound.ts delete mode 100644 test/unit/telemetry.test.tsx diff --git a/.circleci/config.yml b/.circleci/config.yml index 379573d6c..103dd5b3a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -101,14 +101,18 @@ jobs: - run: sudo apt-get -y -qq install awscli - run: name: Deploy to .zone if tests pass and branch is Master - command: aws s3 sync static s3://client.decentraland.zone/ --exclude "branch/*" --acl public-read -# - run: -# name: Deploy to .today if tests pass and branch is Master -# command: aws s3 sync static s3://client.decentraland.today/ --acl public-read + command: aws s3 sync static s3://explorer.decentraland.zone/ --exclude "branch/*" --acl public-read + # - run: + # name: Deploy to .today if tests pass and branch is Master + # command: aws s3 sync static s3://explorer.decentraland.today/ --acl public-read - run: - name: Deploy tag + name: Deploy tag to .today/tags/$CIRCLE_TAG command: | - [ ! -z "${CIRCLE_TAG}" ] && aws s3 sync static s3://client.decentraland.today/tags/$CIRCLE_TAG --exclude "tags/*" --acl public-read || true + [ ! -z "${CIRCLE_TAG}" ] && aws s3 sync static s3://explorer.decentraland.today/tags/$CIRCLE_TAG --exclude "tags/*" --acl public-read || true + - run: + name: Deploy tag to .today + command: | + [ ! -z "${CIRCLE_TAG}" ] && aws s3 sync static s3://explorer.decentraland.today/ --exclude "tags/*" --acl public-read || true pull-deploy: docker: @@ -121,7 +125,7 @@ jobs: - run: sudo apt-get -y -qq install awscli - run: name: Deploy to S3 under subfolder if tests pass and branch is not master - command: aws s3 sync static s3://client.decentraland.zone/branch/$CIRCLE_BRANCH --acl public-read + command: aws s3 sync static s3://explorer.decentraland.zone/branch/$CIRCLE_BRANCH --acl public-read workflows: version: 2 diff --git a/packages/atomicHelpers/DebugTelemetry.ts b/packages/atomicHelpers/DebugTelemetry.ts deleted file mode 100644 index 6f6e1814d..000000000 --- a/packages/atomicHelpers/DebugTelemetry.ts +++ /dev/null @@ -1,142 +0,0 @@ -export type Dictionary = { - [key: string]: T -} - -export namespace DebugTelemetry { - const initialDate = Date.now() - - export interface ITelemetryData { - metric: string - time: number - data: { [key: string]: number | string } - } - - export const _store: ITelemetryData[] = [] - export let _tags: Dictionary = {} - - export let isEnabled: boolean = false - - /** - * Sets the `isEnabled` flag to `true` - */ - export function startTelemetry(tags: Dictionary = {}): void { - isEnabled = true - _tags = tags - } - - /** - * Sets the `isEnabled` flag to `false` and returns the collected data. - */ - export function stopTelemetry(): ITelemetryData[] { - isEnabled = false - const ret = _store.splice(0) - _store.length = 0 - return ret - } - - /** - * Stores telemetry data in memory. Meant to be called on each frame. - * @param metric The name of the time series - * @param values object with number values (metrics) - * @param tags dictionary with tags, used to filter the metric - */ - export function collect(metric: string, values: Dictionary, tags: Dictionary = {}) { - if (!isEnabled) return - _store.push({ - time: initialDate + performance.now(), - metric, - data: { ...values, ...tags, ..._tags } - }) - } - - function noopMeasure(values: Dictionary) { - /*noop*/ - } - - /** - * Starts a measurement and returns a callback for when it is finalized. - * The callback will receive values: Dictionary - * @param metric the name of the metric to measure - * @param tags extra tags - */ - export function measure(metric: string, tags: Dictionary = {}): (values?: Dictionary) => void { - if (!isEnabled) return noopMeasure - - const start = performance.now() - - return function(values: Dictionary = {}) { - const end = performance.now() - _store.push({ - time: initialDate + start, - metric, - data: { ...values, ...tags, ..._tags, duration: end - start } - }) - } - } - - /** - * Dumps all the collected telemetry data into a JSON file that gets automatically - * downloaded by the browser. - */ - export function save() { - const filename = `telemetry-${+Date.now()}.json` - const data = JSON.stringify(_store, undefined, 4) - - const blob = new Blob([data], { type: 'text/json' }) - const e = document.createEvent('MouseEvents') - const a = document.createElement('a') - - a.download = filename - a.href = window.URL.createObjectURL(blob) - a.dataset.downloadurl = ['text/json', a.download, a.href].join(':') - e.initMouseEvent('click', true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null) - a.dispatchEvent(e) - } -} - -global['dclTelemetry'] = DebugTelemetry - -/** - * This method is a function wrapper that adds measuring instrumentation based - * in the DebugTelemetry.isEnabled flag. Use this to measure recurring STATIC - * function like `updatePhysics`. - * DO NO USE IT FOR DYNAMIC (inline) FUNCTIONS - * - * @param metricName the name of the metric to be recorded - * @param fn the function to be measured - * @returns a function with the same signature as the second parameter - */ -export function instrumentTelemetry(metricName: string, fn: T): T { - return (function() { - if (DebugTelemetry.isEnabled) { - const start = performance.now() - const result = fn.apply(this, arguments) - - DebugTelemetry.collect(metricName, { - duration: performance.now() - start - }) - - return result - } else { - return fn.apply(this, arguments) - } - } as any) as T -} - -/** - * Same as instrumentTelemetry, it decorates a class method - */ -export function instrumentMethodTelemetry(name: string) { - return function( - target: Object, - propertyKey: string | symbol, - descriptor: TypedPropertyDescriptor - ) { - const originalMethod = descriptor.value - - // editing the descriptor/value parameter - descriptor.value = instrumentTelemetry(name, originalMethod) - - return descriptor - } -} diff --git a/packages/atomicHelpers/parcelScenePositions.ts b/packages/atomicHelpers/parcelScenePositions.ts index b4bde04a2..97ef8c1ac 100644 --- a/packages/atomicHelpers/parcelScenePositions.ts +++ b/packages/atomicHelpers/parcelScenePositions.ts @@ -33,35 +33,35 @@ export function isOnLimit(value: number): boolean { return Number.isInteger(value / parcelLimits.parcelSize) } -export function isOnLimits({ maximum, minimum }: BoundingInfo, verificationText: string): boolean { +export function isOnLimits({ maximum, minimum }: BoundingInfo, parcels: Set): boolean { // Computes the world-axis-aligned bounding box of an object (including its children), // accounting for both the object's, and children's, world transforms auxVec3.x = minimum.x auxVec3.z = minimum.z worldToGrid(auxVec3, auxVec2) - if (!verificationText.includes(`${auxVec2.x},${auxVec2.y}`)) { + if (!parcels.has(`${auxVec2.x},${auxVec2.y}`)) { return false } auxVec3.x = isOnLimit(maximum.x) ? minimum.x : maximum.x auxVec3.z = isOnLimit(maximum.z) ? minimum.z : maximum.z worldToGrid(auxVec3, auxVec2) - if (!verificationText.includes(`${auxVec2.x},${auxVec2.y}`)) { + if (!parcels.has(`${auxVec2.x},${auxVec2.y}`)) { return false } auxVec3.x = minimum.x auxVec3.z = isOnLimit(maximum.z) ? minimum.z : maximum.z worldToGrid(auxVec3, auxVec2) - if (!verificationText.includes(`${auxVec2.x},${auxVec2.y}`)) { + if (!parcels.has(`${auxVec2.x},${auxVec2.y}`)) { return false } auxVec3.x = isOnLimit(maximum.x) ? minimum.x : maximum.x auxVec3.z = minimum.z worldToGrid(auxVec3, auxVec2) - if (!verificationText.includes(`${auxVec2.x},${auxVec2.y}`)) { + if (!parcels.has(`${auxVec2.x},${auxVec2.y}`)) { return false } @@ -84,12 +84,18 @@ export function decodeParcelSceneBoundaries(boundaries: string) { return { base, parcels } } +/** + * Converts a position into a string { x: -1, y: 5 } => "-1,5" + */ +export function encodeParcelPosition(base: Vector2Component) { + return `${base.x | 0},${base.y | 0}` +} export function encodeParcelSceneBoundaries(base: Vector2Component, parcels: Vector2Component[]) { - let str = `${base.x | 0},${base.y | 0}` + let str = encodeParcelPosition(base) for (let index = 0; index < parcels.length; index++) { const parcel = parcels[index] - str = str + `;${parcel.x | 0},${parcel.y | 0}` + str = str + `;${encodeParcelPosition(parcel)}` } return str diff --git a/packages/dcl/WebGLParcelScene.ts b/packages/dcl/WebGLParcelScene.ts index bc888710c..cfed84ae9 100644 --- a/packages/dcl/WebGLParcelScene.ts +++ b/packages/dcl/WebGLParcelScene.ts @@ -5,7 +5,7 @@ import { getParcelSceneLimits } from 'atomicHelpers/landHelpers' import { DEBUG, EDITOR } from 'config' import { checkParcelSceneBoundaries } from './entities/utils/checkParcelSceneLimits' import { BaseEntity } from 'engine/entities/BaseEntity' -import { encodeParcelSceneBoundaries, gridToWorld } from 'atomicHelpers/parcelScenePositions' +import { encodeParcelSceneBoundaries, gridToWorld, encodeParcelPosition } from 'atomicHelpers/parcelScenePositions' import { removeEntityHighlight, highlightEntity } from 'engine/components/ephemeralComponents/HighlightBox' import { createAxisEntity, createParcelOutline } from './entities/utils/debugEntities' import { createLogger } from 'shared/logger' @@ -13,12 +13,15 @@ import { DevTools } from 'shared/apis/DevTools' import { ParcelIdentity } from 'shared/apis/ParcelIdentity' import { WebGLScene } from './WebGLScene' import { IEvents } from 'decentraland-ecs/src/decentraland/Types' +import { scene } from 'engine/renderer' export class WebGLParcelScene extends WebGLScene { public encodedPositions: string private setOfEntitiesOutsideBoundaries = new Set() - private validationInterval = null + private parcelSet = new Set() + + private shouldValidateBoundaries = false constructor(public data: EnvironmentData) { super( @@ -46,6 +49,10 @@ export class WebGLParcelScene extends WebGLScene { this.context.logger = this.logger = createLogger(data.data.basePosition.x + ',' + data.data.basePosition.y + ': ') this.encodedPositions = encodeParcelSceneBoundaries(data.data.basePosition, data.data.parcels) + + this.parcelSet.add(encodeParcelPosition(data.data.basePosition)) + data.data.parcels.forEach($ => this.parcelSet.add(encodeParcelPosition($))) + Object.assign(this.context.metricsLimits, getParcelSceneLimits(data.data.parcels.length)) // Set a debug name in the root entity @@ -57,10 +64,6 @@ export class WebGLParcelScene extends WebGLScene { gridToWorld(this.data.data.basePosition.x, this.data.data.basePosition.y, this.context.rootEntity.position) this.context.rootEntity.freezeWorldMatrix() - this.validationInterval = setInterval(() => { - this.checkBoundaries() - }, 5000) - this.context.on('metricsUpdate', data => { if (!EDITOR) { this.checkLimits(data) @@ -69,12 +72,21 @@ export class WebGLParcelScene extends WebGLScene { if (DEBUG) { this.context.onEntityMatrixChangedObservable.add(_entity => { - this.checkBoundaries() + this.shouldValidateBoundaries = true }) this.initDebugEntities() } this.context.updateMetrics() + + scene.onAfterRenderObservable.add(this.afterRender) + } + + afterRender = () => { + if (this.shouldValidateBoundaries) { + this.shouldValidateBoundaries = false + this.checkBoundaries() + } } registerWorker(worker: SceneWorker): void { @@ -94,8 +106,7 @@ export class WebGLParcelScene extends WebGLScene { dispose(): void { super.dispose() - - clearInterval(this.validationInterval) + scene.onAfterRenderObservable.removeCallback(this.afterRender) } /** @@ -132,7 +143,7 @@ export class WebGLParcelScene extends WebGLScene { */ checkBoundaries() { const newSet = new Set() - this.context.entities.forEach(entity => checkParcelSceneBoundaries(this.encodedPositions, newSet, entity)) + this.context.entities.forEach(entity => checkParcelSceneBoundaries(this.parcelSet, newSet, entity)) // remove the highlight from the entities that were outside but they are no longer outside this.setOfEntitiesOutsideBoundaries.forEach($ => { if (!newSet.has($) && this.context.entities.has($.id)) { diff --git a/packages/dcl/entities/utils/checkParcelSceneLimits.ts b/packages/dcl/entities/utils/checkParcelSceneLimits.ts index 4fc02e223..390db406d 100644 --- a/packages/dcl/entities/utils/checkParcelSceneLimits.ts +++ b/packages/dcl/entities/utils/checkParcelSceneLimits.ts @@ -66,22 +66,19 @@ function totalBoundingInfo(meshes: BABYLON.AbstractMesh[]) { * Receives the encoded parcelScene parcels and the entity to traverse */ export function checkParcelSceneBoundaries( - encodedParcels: string, + encodedParcels: Set, objectsOutside: Set, entity: BaseEntity ) { - const numberOfParcels = encodedParcels.replace(/;+/g, ';').split(';').length - const verificationText = ';' + encodedParcels + ';' - - const maxHeight = Math.log2(numberOfParcels) * parcelLimits.height + const maxHeight = Math.log2(encodedParcels.size + 1) * parcelLimits.height const minHeight = -maxHeight - entity.traverseControl(node => { - if (node[ignoreBoundaryCheck]) { + entity.traverseControl(entity => { + if (entity[ignoreBoundaryCheck]) { return 'BREAK' } - const mesh = node.getObject3D(BasicShape.nameInEntity) + const mesh = entity.getObject3D(BasicShape.nameInEntity) if (!mesh) { return 'CONTINUE' @@ -97,17 +94,15 @@ export function checkParcelSceneBoundaries( return 'CONTINUE' } - mesh.computeWorldMatrix(true) - const bbox = totalBoundingInfo(meshes) if (bbox.maximum.y > maxHeight || bbox.minimum.y < minHeight) { - objectsOutside.add(node) + objectsOutside.add(entity) return 'BREAK' } - if (!isOnLimits(bbox, verificationText)) { - objectsOutside.add(node) + if (!isOnLimits(bbox, encodedParcels)) { + objectsOutside.add(entity) return 'BREAK' } diff --git a/packages/dcl/index.ts b/packages/dcl/index.ts index 72ab41856..672775c94 100644 --- a/packages/dcl/index.ts +++ b/packages/dcl/index.ts @@ -5,7 +5,6 @@ import * as TWEEN from '@tweenjs/tween.js' import './api' // Our imports -import { error } from 'engine/logger' import { DEBUG, PREVIEW, NETWORK_HZ, EDITOR } from 'config' import { positionObserver, lastPlayerPosition } from 'shared/world/positionThings' @@ -17,8 +16,6 @@ import { scene, engine, vrHelper, initLocalPlayer, initDCL } from 'engine/render import { enableVirtualJoystick, initKeyboard, enableMouseLock } from 'engine/renderer/input' import { reposition } from 'engine/renderer/ambientLights' -import { DebugTelemetry, instrumentTelemetry } from 'atomicHelpers/DebugTelemetry' - import { quaternionToRotationBABYLON } from 'atomicHelpers/math' import { vrCamera } from 'engine/renderer/camera' @@ -29,6 +26,7 @@ import { initChatSystem, initHudSystem } from './widgets/ui' import { loadedParcelSceneWorkers } from 'shared/world/parcelSceneManager' import { WebGLParcelScene } from './WebGLParcelScene' +import { IParcelSceneLimits } from 'atomicHelpers/landHelpers' let isEngineRunning = false @@ -37,39 +35,27 @@ const parcelMetrics = drawMetrics(getMetrics()) // Draws FPS / ms const stats = createStats() -const _render = instrumentTelemetry('render', function() { - try { - TWEEN.update() +let lastMetrics = 0 - if (vrCamera.position.y < -64) { - vrCamera.position.y = 10 - return - } +function _render() { + TWEEN.update() + + if (vrCamera.position.y < -64) { + vrCamera.position.y = 10 + return + } + + reposition() - reposition() - - let memory = performance['memory'] || {} - - DebugTelemetry.collect('renderStats', { - /* programs: ((renderer.info.programs as any) as Array).length, - geometries: renderer.info.memory.geometries, - calls: renderer.info.render.calls, - faces: renderer.info.render.faces, - points: renderer.info.render.points, - textures: renderer.info.memory.textures, */ - usedHeap: (memory.usedJSHeapSize || 0) / 1048576, - maxHeap: (memory.jsHeapSizeLimit || 0) / 1048576 - }) - - scene.render() - } catch (e) { - DebugTelemetry.collect('renderError', { value: 1 }, { message: e.message }) - error(e) - } finally { + scene.render() + + if (lastMetrics++ > 10) { + lastMetrics = 0 updateMetrics() - stats.update() } -}) + + stats.update() +} const notifyPositionObservers = (() => { const position: BABYLON.Vector3 = BABYLON.Vector3.Zero() @@ -108,10 +94,6 @@ const notifyPositionObservers = (() => { stats.dom.style.visibility = 'visible' } - if (DEBUG) { - window['telemetry'] = DebugTelemetry - } - const networkTime = 1000 / NETWORK_HZ let now = performance.now() let nextNetworkTime = now + networkTime @@ -131,14 +113,14 @@ function getMetrics(): Metrics { .filter(parcelScene => (parcelScene.parcelScene as WebGLParcelScene).context) .map(parcelScene => (parcelScene.parcelScene as WebGLParcelScene).context.metrics) .filter(metrics => !!metrics) - .reduce( + .reduce( (metrics, m) => { Object.keys(metrics).map(key => { metrics[key] += m[key] }) return metrics }, - { triangles: 0, bodies: 0, entities: 0, materials: 0, textures: 0 } + { triangles: 0, bodies: 0, entities: 0, materials: 0, textures: 0, geometries: 0 } ) } diff --git a/packages/decentraland-ecs/src/decentraland/AnimationClip.ts b/packages/decentraland-ecs/src/decentraland/AnimationClip.ts index 610ada785..09503a685 100644 --- a/packages/decentraland-ecs/src/decentraland/AnimationClip.ts +++ b/packages/decentraland-ecs/src/decentraland/AnimationClip.ts @@ -1,5 +1,5 @@ import { ObservableComponent } from '../ecs/Component' -import { uuid } from '../ecs/helpers' +import { newId } from '../ecs/helpers' export type AnimationParams = { looping?: boolean @@ -52,7 +52,7 @@ export class AnimationClip extends ObservableComponent { // @internal @ObservableComponent.readonly - readonly name: string = uuid() + readonly name: string = newId('AnimClip') constructor(clip: string, params: AnimationParams = defaultParams) { super() diff --git a/packages/decentraland-ecs/src/decentraland/Components.ts b/packages/decentraland-ecs/src/decentraland/Components.ts index 69a6c1ac8..e1b86bca1 100644 --- a/packages/decentraland-ecs/src/decentraland/Components.ts +++ b/packages/decentraland-ecs/src/decentraland/Components.ts @@ -1,7 +1,7 @@ import { Component, ObservableComponent, DisposableComponent } from '../ecs/Component' import { Vector3, Quaternion, Matrix, MathTmp, Color3 } from './math' import { AnimationClip } from './AnimationClip' -import { uuid } from '../ecs/helpers' +import { newId } from '../ecs/helpers' import { IEvents } from './Types' export type TranformConstructorArgs = { @@ -43,6 +43,8 @@ export enum CLASS_ID { PRB_MATERIAL = 65, HIGHLIGHT_ENTITY = 66, + + /** @deprecated */ SOUND = 67, AUDIO_CLIP = 200, @@ -667,7 +669,7 @@ export class BasicMaterial extends ObservableComponent { export class OnUUIDEvent extends ObservableComponent { readonly type: string | undefined - readonly uuid: string = uuid() + readonly uuid: string = newId('UUID') @ObservableComponent.field callback!: (event: any) => void diff --git a/packages/decentraland-ecs/src/ecs/Component.ts b/packages/decentraland-ecs/src/ecs/Component.ts index 4cdcac9f0..6ce5e9d10 100644 --- a/packages/decentraland-ecs/src/ecs/Component.ts +++ b/packages/decentraland-ecs/src/ecs/Component.ts @@ -1,4 +1,4 @@ -import { uuid } from './helpers' +import { newId } from './helpers' import { EventConstructor } from './EventManager' const componentSymbol = '__name__symbol_' @@ -157,7 +157,7 @@ export function DisposableComponent(componentName: string, classId: number) { const args = Array.prototype.slice.call(arguments) const ret = new extendedClass(...args) - const id = uuid() + const id = newId('C') Object.defineProperty(ret, componentSymbol, { enumerable: false, diff --git a/packages/decentraland-ecs/src/ecs/Entity.ts b/packages/decentraland-ecs/src/ecs/Entity.ts index 9da113e3e..d5d84e231 100644 --- a/packages/decentraland-ecs/src/ecs/Entity.ts +++ b/packages/decentraland-ecs/src/ecs/Entity.ts @@ -1,7 +1,7 @@ import { getComponentName, ComponentConstructor, getComponentClassId, ComponentLike } from './Component' import { log, Engine } from './Engine' import { EventManager, EventConstructor } from './EventManager' -import { uuid } from './helpers' +import { newId } from './helpers' // tslint:disable:no-use-before-declare @@ -13,7 +13,7 @@ export class Entity { public eventManager: EventManager | null = null public alive: boolean = false - public readonly uuid: string = uuid() + public readonly uuid: string = newId('E') public readonly components: Record = {} // @internal @@ -39,6 +39,10 @@ export class Entity { throw new Error('You passed a function or class as a component, an instance of component is expected') } + if (typeof component !== 'object') { + throw new Error(`You passed a ${typeof component}, an instance of component is expected`) + } + const componentName = getComponentName(component) if (this.components[componentName]) { @@ -53,14 +57,37 @@ export class Entity { /** * Returns a boolean indicating if a component is present in the entity. - * @param component - component class or name + * @param component - component class, instance or name */ - has(component: ComponentConstructor): boolean { - if (typeof component !== 'function') { - throw new Error('Entity#has(component): component is not a class') + has(component: string): boolean + has(component: ComponentConstructor): boolean + has(component: T): boolean + has(component: ComponentConstructor | string): boolean { + const typeOfComponent = typeof component + + if (typeOfComponent !== 'string' && typeOfComponent !== 'object' && typeOfComponent !== 'function') { + throw new Error('Entity#has(component): component is not a class, name or instance') } - const componentName = getComponentName(component) - return !!this.components[componentName] + + if (component == null) return false + + const componentName = typeOfComponent === 'string' ? (component as string) : getComponentName(component as any) + + const storedComponent = this.components[componentName] + + if (!storedComponent) { + return false + } + + if (typeOfComponent === 'object') { + return storedComponent === component + } + + if (typeOfComponent === 'function') { + return storedComponent instanceof (component as ComponentConstructor) + } + + return true } /** @@ -78,11 +105,21 @@ export class Entity { const componentName = typeOfComponent === 'string' ? (component as string) : getComponentName(component as any) - if (!this.components[componentName]) { + const storedComponent = this.components[componentName] + + if (!storedComponent) { throw new Error(`Can not get component "${componentName}" from entity "${this.identifier}"`) } - return this.components[componentName] + if (typeOfComponent === 'function') { + if (storedComponent instanceof (component as ComponentConstructor)) { + return storedComponent + } else { + throw new Error(`Can not get component "${componentName}" from entity "${this.identifier}" (by instance)`) + } + } + + return storedComponent } /** @@ -100,7 +137,21 @@ export class Entity { const componentName = typeOfComponent === 'string' ? (component as string) : getComponentName(component as any) - return this.components[componentName] || null + const storedComponent = this.components[componentName] + + if (!storedComponent) { + return null + } + + if (typeOfComponent === 'function') { + if (storedComponent instanceof (component as ComponentConstructor)) { + return storedComponent + } else { + return null + } + } + + return storedComponent } /** @@ -112,13 +163,15 @@ export class Entity { throw new Error('Entity#getOrCreate(component): component is not a class') } - const componentName = getComponentName(component) + let ret = this.getOrNull(component) - let ret = this.components[componentName] || null if (!ret) { ret = new component() - this.set(ret) + // Safe-guard to only add registered components to entities + getComponentName(ret) + this.set(ret as any) } + return ret } diff --git a/packages/decentraland-ecs/src/ecs/helpers.ts b/packages/decentraland-ecs/src/ecs/helpers.ts index 87bf66540..7593b1ffb 100644 --- a/packages/decentraland-ecs/src/ecs/helpers.ts +++ b/packages/decentraland-ecs/src/ecs/helpers.ts @@ -1,3 +1,15 @@ +let lastGeneratedId = 0 + +/** + * Generates a new prefixed id + * @beta + */ +export function newId(type: string) { + lastGeneratedId++ + if (type.length === 0) throw new Error('newId(type: string): type cannot be empty') + return type + lastGeneratedId.toString(36) +} + /** * @internal */ diff --git a/packages/decentraland-ecs/types/dcl/decentraland-ecs.api.json b/packages/decentraland-ecs/types/dcl/decentraland-ecs.api.json index 595e35aff..323457856 100644 --- a/packages/decentraland-ecs/types/dcl/decentraland-ecs.api.json +++ b/packages/decentraland-ecs/types/dcl/decentraland-ecs.api.json @@ -8382,6 +8382,52 @@ "remarks": [], "isBeta": false }, + "Gizmo": { + "kind": "enum", + "values": { + "MOVE": { + "kind": "enum value", + "value": "\"MOVE\"", + "deprecatedMessage": [], + "summary": [], + "remarks": [], + "isBeta": true + }, + "NONE": { + "kind": "enum value", + "value": "\"NONE\"", + "deprecatedMessage": [], + "summary": [], + "remarks": [], + "isBeta": true + }, + "ROTATE": { + "kind": "enum value", + "value": "\"ROTATE\"", + "deprecatedMessage": [], + "summary": [], + "remarks": [], + "isBeta": true + }, + "SCALE": { + "kind": "enum value", + "value": "\"SCALE\"", + "deprecatedMessage": [], + "summary": [], + "remarks": [], + "isBeta": true + } + }, + "deprecatedMessage": [], + "summary": [ + { + "kind": "text", + "text": "Gizmo identifiers" + } + ], + "remarks": [], + "isBeta": true + }, "Gizmos": { "kind": "class", "extends": "ObservableComponent", @@ -8398,6 +8444,48 @@ "isBeta": true, "isSealed": false, "members": { + "cycle": { + "kind": "property", + "signature": "cycle: boolean;", + "isOptional": false, + "isReadOnly": false, + "isStatic": false, + "type": "boolean", + "deprecatedMessage": [], + "summary": [ + { + "kind": "text", + "text": "Cycle through gizmos using click." + } + ], + "remarks": [], + "isBeta": true, + "isSealed": false, + "isVirtual": false, + "isOverride": false, + "isEventProperty": false + }, + "localReference": { + "kind": "property", + "signature": "localReference: boolean;", + "isOptional": false, + "isReadOnly": false, + "isStatic": false, + "type": "boolean", + "deprecatedMessage": [], + "summary": [ + { + "kind": "text", + "text": "Align the gizmos to match the local reference system" + } + ], + "remarks": [], + "isBeta": true, + "isSealed": false, + "isVirtual": false, + "isOverride": false, + "isEventProperty": false + }, "position": { "kind": "property", "signature": "position: boolean;", @@ -8461,18 +8549,18 @@ "isOverride": false, "isEventProperty": false }, - "updateEntity": { + "selectedGizmo": { "kind": "property", - "signature": "updateEntity: boolean;", - "isOptional": false, + "signature": "selectedGizmo?: Gizmo;", + "isOptional": true, "isReadOnly": false, "isStatic": false, - "type": "boolean", + "type": "Gizmo", "deprecatedMessage": [], "summary": [ { "kind": "text", - "text": "Update entity while dragging. Also let the entity in it's final place after releasing the gizmo." + "text": "If cycle is false, this will be the selected gizmo" } ], "remarks": [], diff --git a/packages/decentraland-ecs/types/dcl/index.d.ts b/packages/decentraland-ecs/types/dcl/index.d.ts index 1efa9ffd7..462533f5c 100644 --- a/packages/decentraland-ecs/types/dcl/index.d.ts +++ b/packages/decentraland-ecs/types/dcl/index.d.ts @@ -1304,6 +1304,17 @@ declare class GLTFShape extends Shape { constructor(src: string) } +/** + * Gizmo identifiers + * @beta + */ +declare enum Gizmo { + MOVE = 'MOVE', + ROTATE = 'ROTATE', + SCALE = 'SCALE', + NONE = 'NONE' +} + declare type GizmoDragEndEvent = { type: 'gizmoDragEnded' transform: { @@ -1338,10 +1349,17 @@ declare class Gizmos extends ObservableComponent { */ scale: boolean /** - * Update entity while dragging. Also let the entity in it's final place after - * releasing the gizmo. + * Cycle through gizmos using click. + */ + cycle: boolean + /** + * If cycle is false, this will be the selected gizmo + */ + selectedGizmo?: Gizmo + /** + * Align the gizmos to match the local reference system */ - updateEntity: boolean + localReference: boolean } /** diff --git a/packages/engine/components/disposableComponents/DisposableComponent.ts b/packages/engine/components/disposableComponents/DisposableComponent.ts index b7e127988..e40ca0b5e 100644 --- a/packages/engine/components/disposableComponents/DisposableComponent.ts +++ b/packages/engine/components/disposableComponents/DisposableComponent.ts @@ -91,14 +91,15 @@ export abstract class BasicShape extends DisposableComponent { if (this.data.visible === false) { entity.removeObject3D(BasicShape.nameInEntity) } else { - const model = this.generateModel() + const mesh = this.generateModel() - model.actionManager = entity.getActionManager() - model.isPickable = true + mesh.cullingStrategy = BABYLON.AbstractMesh.CULLINGSTRATEGY_BOUNDINGSPHERE_ONLY + mesh.actionManager = entity.getActionManager() + mesh.isPickable = true - entity.setObject3D(BasicShape.nameInEntity, model) + entity.setObject3D(BasicShape.nameInEntity, mesh) - model.setEnabled(!!this.data.visible) + mesh.setEnabled(!!this.data.visible) this.setCollisions(entity) } diff --git a/packages/engine/components/disposableComponents/GLTFShape.ts b/packages/engine/components/disposableComponents/GLTFShape.ts index 78bbbeb23..053f85de9 100644 --- a/packages/engine/components/disposableComponents/GLTFShape.ts +++ b/packages/engine/components/disposableComponents/GLTFShape.ts @@ -13,9 +13,12 @@ export class GLTFShape extends DisposableComponent { src: string | null = null loadingDone = false assetContainerEntity = new Map() + entityIsLoading = new Set() onAttach(entity: BaseEntity): void { - if (this.src) { + if (this.src && !this.entityIsLoading.has(entity.uuid)) { + this.entityIsLoading.add(entity.uuid) + const url = resolveUrl(this.context.internalBaseUrl, this.src) const baseUrl = url.substr(0, url.lastIndexOf('/') + 1) @@ -26,6 +29,8 @@ export class GLTFShape extends DisposableComponent { file, scene, assetContainer => { + this.entityIsLoading.delete(entity.uuid) + if (this.assetContainerEntity.has(entity.uuid)) { this.onDetach(entity) } @@ -68,9 +73,10 @@ export class GLTFShape extends DisposableComponent { processColliders(assetContainer, entity.getActionManager()) // Fin the main mesh and add it as the BasicShape.nameInEntity component. - assetContainer.meshes.filter($ => $.name === '__root__').forEach($ => { - entity.setObject3D(BasicShape.nameInEntity, $) - $.rotation.set(0, Math.PI, 0) + assetContainer.meshes.filter($ => $.name === '__root__').forEach(mesh => { + entity.setObject3D(BasicShape.nameInEntity, mesh) + mesh.cullingStrategy = BABYLON.AbstractMesh.CULLINGSTRATEGY_BOUNDINGSPHERE_ONLY + mesh.rotation.set(0, Math.PI, 0) }) this.assetContainerEntity.set(entity.uuid, assetContainer) @@ -79,6 +85,10 @@ export class GLTFShape extends DisposableComponent { assetContainer.addAllToScene() + this.contributions.materialCount = assetContainer.materials.length + this.contributions.geometriesCount = assetContainer.geometries.length + this.contributions.textureCount = assetContainer.textures.length + // This is weird. Verify what does this do. assetContainer.transformNodes.filter($ => $.name === '__root__').forEach($ => { entity.setObject3D(BasicShape.nameInEntity, $) @@ -103,6 +113,8 @@ export class GLTFShape extends DisposableComponent { }, null, (_scene, message, exception) => { + this.entityIsLoading.delete(entity.uuid) + this.context.logger.error('Error loading GLTF', message || exception) this.onDetach(entity) entity.assetContainer = null @@ -117,12 +129,12 @@ export class GLTFShape extends DisposableComponent { ) as any loader.animationStartMode = 0 - } else { - debugger } } onDetach(entity: BaseEntity): void { + this.entityIsLoading.delete(entity.uuid) + const mesh = entity.getObject3D(BasicShape.nameInEntity) if (mesh) { @@ -152,21 +164,14 @@ export class GLTFShape extends DisposableComponent { if (data.visible === false) { this.entities.forEach($ => this.onDetach($)) } else { - this.entities.forEach($ => this.attachTo($)) + this.entities.forEach($ => this.onAttach($)) } } else { - this.entities.forEach($ => this.attachTo($)) + this.entities.forEach($ => this.onAttach($)) } } } } - - dispose() { - super.dispose() - this.assetContainerEntity.forEach($ => { - cleanupAssetContainer($) - }) - } } DisposableComponent.registerClassId(CLASS_ID.GLTF_SHAPE, GLTFShape) diff --git a/packages/engine/components/disposableComponents/OBJShape.ts b/packages/engine/components/disposableComponents/OBJShape.ts index b9f4de96c..965733807 100644 --- a/packages/engine/components/disposableComponents/OBJShape.ts +++ b/packages/engine/components/disposableComponents/OBJShape.ts @@ -27,6 +27,7 @@ export class OBJShape extends DisposableComponent { } assetContainer.meshes.forEach(mesh => { + mesh.cullingStrategy = BABYLON.AbstractMesh.CULLINGSTRATEGY_BOUNDINGSPHERE_ONLY if (!assetContainer.materials.includes(mesh.material)) { assetContainer.materials.push(mesh.material) } @@ -64,6 +65,10 @@ export class OBJShape extends DisposableComponent { $.parent = node }) + this.contributions.materialCount = assetContainer.materials.length + this.contributions.geometriesCount = assetContainer.geometries.length + this.contributions.textureCount = assetContainer.textures.length + node.scaling.set(1, 1, -1) entity.setObject3D(BasicShape.nameInEntity, node) @@ -114,19 +119,14 @@ export class OBJShape extends DisposableComponent { if (data.visible === false) { this.entities.forEach($ => this.onDetach($)) } else { - this.entities.forEach($ => this.attachTo($)) + this.entities.forEach($ => this.onAttach($)) } } else { - this.entities.forEach($ => this.attachTo($)) + this.entities.forEach($ => this.onAttach($)) } } } } - - dispose() { - super.dispose() - this.assetContainerEntity.forEach($ => cleanupAssetContainer($)) - } } DisposableComponent.registerClassId(CLASS_ID.OBJ_SHAPE, OBJShape) diff --git a/packages/engine/components/ephemeralComponents/Animator.ts b/packages/engine/components/ephemeralComponents/Animator.ts index 680f0ba81..cbca79f64 100644 --- a/packages/engine/components/ephemeralComponents/Animator.ts +++ b/packages/engine/components/ephemeralComponents/Animator.ts @@ -2,13 +2,14 @@ import { SkeletalAnimationComponent, SkeletalAnimationValue } from '../../../sha import { BaseComponent } from '../BaseComponent' import { validators } from '../helpers/schemaValidator' import { scene } from '../../renderer' +import { disposeAnimationGroups } from 'engine/entities/utils/processModels' function validateClip(clipDef: SkeletalAnimationValue): SkeletalAnimationValue { if ((clipDef as any) != null && typeof (clipDef as any) === 'object') { - clipDef.weight = validators.float(clipDef.weight, 1) + clipDef.weight = Math.max(Math.min(1, validators.float(clipDef.weight, 1)), 0) clipDef.loop = validators.boolean(clipDef.loop, true) clipDef.playing = validators.boolean(clipDef.playing, true) - clipDef.speed = validators.float(clipDef.speed, 1) + clipDef.speed = Math.max(0, validators.float(clipDef.speed, 1)) if (!clipDef.playing) { clipDef.weight = 0 @@ -66,8 +67,7 @@ export class Animator extends BaseComponent { this.currentAnimations.forEach((value, key) => { if (!usedClipsByName.has(key)) { - value.stop() - value.dispose() + disposeAnimationGroups(value) this.currentAnimations.delete(key) } }) @@ -77,6 +77,6 @@ export class Animator extends BaseComponent { detach() { super.detach() - this.currentAnimations.forEach($ => $.dispose()) + this.currentAnimations.forEach(disposeAnimationGroups) } } diff --git a/packages/engine/components/ephemeralComponents/AudioSource.ts b/packages/engine/components/ephemeralComponents/AudioSource.ts index 4de5d46e8..f31a4d644 100644 --- a/packages/engine/components/ephemeralComponents/AudioSource.ts +++ b/packages/engine/components/ephemeralComponents/AudioSource.ts @@ -1,8 +1,8 @@ import { BaseComponent } from '../BaseComponent' -import { BaseEntity } from 'engine/entities/BaseEntity' +import { BaseEntity } from '../../entities/BaseEntity' import { AudioClip } from '../disposableComponents/AudioClip' import future from 'fp-future' -import { scene } from 'engine/renderer' +import { scene } from '../../renderer/init' const defaultValue = { loop: true, @@ -89,7 +89,7 @@ export class AudioSource extends BaseComponent { { spatialSound: true, distanceModel: 'exponential', - rolloffFactor: 0.5 + rolloffFactor: 1.5 } ) }) @@ -107,6 +107,7 @@ export class AudioSource extends BaseComponent { detach() { super.detach() + this.disposeSound() this.entity.unregisterAfterWorldMatrixUpdate(this.updatePosition) } diff --git a/packages/engine/components/ephemeralComponents/Gizmos.ts b/packages/engine/components/ephemeralComponents/Gizmos.ts index 457085abe..8779a0098 100644 --- a/packages/engine/components/ephemeralComponents/Gizmos.ts +++ b/packages/engine/components/ephemeralComponents/Gizmos.ts @@ -32,6 +32,18 @@ let activeEntity: BaseEntity = null let selectedGizmo: Gizmo = Gizmo.MOVE let currentConfiguration = defaultValue +function isSelectedGizmoValid() { + switch (selectedGizmo) { + case Gizmo.MOVE: + return !!currentConfiguration.position + case Gizmo.ROTATE: + return !!currentConfiguration.rotation + case Gizmo.SCALE: + return !!currentConfiguration.scale + } + return false +} + function switchGizmo() { let nextGizmo = selectedGizmo @@ -137,6 +149,8 @@ export function selectGizmo(type: Gizmo) { } export class Gizmos extends BaseComponent { + active = true + transformValue(data: GizmoConfiguration) { return { ...defaultValue, @@ -151,15 +165,18 @@ export class Gizmos extends BaseComponent { if (currentConfiguration.cycle) { selectGizmo(switchGizmo()) } else { - if (currentConfiguration.selectedGizmo && !selectGizmo(currentConfiguration.selectedGizmo)) { - selectGizmo(switchGizmo()) - } else if (!selectGizmo(selectedGizmo)) { + if (isSelectedGizmoValid()) { + selectGizmo(selectedGizmo) + } else { selectGizmo(switchGizmo()) } } } else { activeEntity = this.entity - if (!selectGizmo(selectedGizmo)) { + + if (isSelectedGizmoValid()) { + selectGizmo(selectedGizmo) + } else { selectGizmo(switchGizmo()) } } @@ -188,7 +205,6 @@ export class Gizmos extends BaseComponent { if (this.entity === activeEntity) { this.configureGizmos() } - // stub } attach(entity: BaseEntity) { @@ -196,11 +212,14 @@ export class Gizmos extends BaseComponent { } detach() { - // TODO: entity.removeListener('onClick', this.activate) + this.active = false + this.entity.removeListener('onClick', this.activate) if (activeEntity === this.entity) { gizmoManager.gizmos.positionGizmo.attachedMesh = null gizmoManager.gizmos.rotationGizmo.attachedMesh = null gizmoManager.gizmos.scaleGizmo.attachedMesh = null } + activeEntity = null + this.entity = null } } diff --git a/packages/engine/components/ephemeralComponents/Sound.ts b/packages/engine/components/ephemeralComponents/Sound.ts deleted file mode 100644 index 0a39f8689..000000000 --- a/packages/engine/components/ephemeralComponents/Sound.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { resolveUrl } from 'atomicHelpers/parseUrl' - -import { warn } from '../../logger' -import { createSchemaValidator } from '../helpers/schemaValidator' -import { scene } from '../../renderer' -import { BaseComponent } from '../BaseComponent' -import { SoundComponent } from 'shared/types' - -export class Sound extends BaseComponent { - static schemaValidator = createSchemaValidator({ - distanceModel: { default: 'inverse', type: 'string' }, - loop: { default: false, type: 'boolean' }, - src: { type: 'string', default: '' }, - volume: { default: 1, type: 'number' }, - rolloffFactor: { default: 1, type: 'number' }, - playing: { default: true, type: 'boolean' } - }) - - sound: BABYLON.Sound - soundUrl: string - - _onAfterWorldMatrixUpdate = () => this.sound && this.sound.setPosition(this.entity.absolutePosition) - - transformValue(value: SoundComponent) { - return Sound.schemaValidator(value) - } - - detach() { - this.disposeAudio() - } - - disposeAudio() { - if (this.sound) { - this.entity.unregisterAfterWorldMatrixUpdate(this._onAfterWorldMatrixUpdate) - this.sound.stop() - this.sound.dispose() - delete this.sound - } - } - - shouldSceneUpdate(newValue) { - return true - } - - update(oldProps: SoundComponent, newProps: SoundComponent) { - const src = resolveUrl(this.entity.context.internalBaseUrl, newProps.src) - - if (!src) { - warn('Audio source was not specified with `src`') - return - } - - if (!this.sound || this.soundUrl !== src) { - this.disposeAudio() - this.sound = new BABYLON.Sound('sound-component', src, scene, null, { - loop: !!newProps.loop, - autoplay: !!newProps.playing, - spatialSound: true, - distanceModel: newProps.distanceModel, - rolloffFactor: newProps.rolloffFactor - }) - this.soundUrl = src - - this.entity.registerAfterWorldMatrixUpdate(this._onAfterWorldMatrixUpdate) - this._onAfterWorldMatrixUpdate() - } - - if (this.sound.isPlaying !== newProps.playing) { - if (newProps.playing) { - this.sound.play() - } else { - this.sound.stop() - } - } - - this.sound.setVolume(newProps.volume) - } -} diff --git a/packages/engine/components/index.ts b/packages/engine/components/index.ts index a2566c720..a572e12ce 100644 --- a/packages/engine/components/index.ts +++ b/packages/engine/components/index.ts @@ -1,9 +1,9 @@ import { Transform } from './ephemeralComponents/Transform' import { Billboard } from './ephemeralComponents/Billboard' import { HighlightBox } from './ephemeralComponents/HighlightBox' -import { Sound } from './ephemeralComponents/Sound' import { TextShape } from './ephemeralComponents/TextShape' import { Gizmos } from './ephemeralComponents/Gizmos' +import { AudioSource } from './ephemeralComponents/AudioSource' import './disposableComponents/BasicMaterial' import './disposableComponents/BoxShape' @@ -31,6 +31,7 @@ import { CLASS_ID } from 'decentraland-ecs/src' import { DEBUG, PREVIEW, EDITOR } from 'config' import { BaseComponent } from './BaseComponent' import { ConstructorOf } from 'engine/entities/BaseEntity' +import { Animator } from './ephemeralComponents/Animator' // We re-export it to avoid circular references from BaseEntity export { BaseComponent } from './BaseComponent' @@ -39,8 +40,9 @@ export const componentRegistry: Record> [CLASS_ID.TRANSFORM]: Transform, [CLASS_ID.BILLBOARD]: Billboard, [CLASS_ID.HIGHLIGHT_ENTITY]: HighlightBox, - [CLASS_ID.SOUND]: Sound, - [CLASS_ID.TEXT_SHAPE]: TextShape + [CLASS_ID.TEXT_SHAPE]: TextShape, + [CLASS_ID.ANIMATION]: Animator, + [CLASS_ID.AUDIO_SOURCE]: AudioSource } if (DEBUG || PREVIEW || EDITOR) { diff --git a/packages/engine/entities/BaseEntity.ts b/packages/engine/entities/BaseEntity.ts index f600fea7d..4ad66d60b 100644 --- a/packages/engine/entities/BaseEntity.ts +++ b/packages/engine/entities/BaseEntity.ts @@ -69,7 +69,6 @@ export class BaseEntity extends BABYLON.AbstractMesh { addUUIDEvent(type: IEventNames, uuid: string): void { this.uuidEvents.set(type, uuid) } - attachDisposableComponent(name: string, component: DisposableComponent) { const current = this.disposableComponents.get(name) if (current && current !== component) { @@ -380,6 +379,16 @@ export class BaseEntity extends BABYLON.AbstractMesh { listenerList.push(fn) } + removeListener(event: T, fn: (data: IEvents[T]) => void) { + let listenerList = this.events.get(event) + if (listenerList) { + const ix = listenerList.indexOf(fn) + if (ix !== -1) { + listenerList.splice(ix, 1) + } + } + } + updateComponent(payload: UpdateEntityComponentPayload) { const name = payload.name this.attrs[name] = payload diff --git a/packages/engine/entities/utils/processModels.ts b/packages/engine/entities/utils/processModels.ts index d5a25760a..09bcabe6f 100644 --- a/packages/engine/entities/utils/processModels.ts +++ b/packages/engine/entities/utils/processModels.ts @@ -1,15 +1,43 @@ import * as BABYLON from 'babylonjs' import { markAsCollider } from './colliders' +import { scene } from 'engine/renderer' function disposeDelegate($: { dispose: Function }) { $.dispose() } + function disposeNodeDelegate($: BABYLON.TransformNode) { $.setEnabled(false) $.parent = null $.dispose(false) } +function disposeSkeleton($: BABYLON.Skeleton) { + $.dispose() + $.bones.forEach($ => { + $.parent = null + $.dispose() + }) +} + +function disposeAnimatable($: BABYLON.Animatable) { + if (!$) return + $.disposeOnEnd = true + $.loopAnimation = false + $.stop() + $._animate(0) +} + +export function disposeAnimationGroups($: BABYLON.AnimationGroup) { + $.animatables.forEach(disposeAnimatable) + + $.targetedAnimations.forEach($ => { + disposeAnimatable(scene.getAnimatableByTarget($.target)) + }) + + $.dispose() +} + export function cleanupAssetContainer($: BABYLON.AssetContainer) { if ($) { $.removeAllFromScene() @@ -17,9 +45,10 @@ export function cleanupAssetContainer($: BABYLON.AssetContainer) { $.rootNodes && $.rootNodes.forEach(disposeNodeDelegate) $.meshes && $.meshes.forEach(disposeNodeDelegate) $.textures && $.textures.forEach(disposeDelegate) + $.animationGroups && $.animationGroups.forEach(disposeAnimationGroups) $.multiMaterials && $.multiMaterials.forEach(disposeDelegate) $.sounds && $.sounds.forEach(disposeDelegate) - $.skeletons && $.skeletons.forEach(disposeDelegate) + $.skeletons && $.skeletons.forEach(disposeSkeleton) $.materials && $.materials.forEach(disposeDelegate) $.lights && $.lights.forEach(disposeDelegate) } diff --git a/packages/engine/renderer/camera.ts b/packages/engine/renderer/camera.ts index 2fbebc950..3a53ca980 100644 --- a/packages/engine/renderer/camera.ts +++ b/packages/engine/renderer/camera.ts @@ -74,6 +74,8 @@ const arcCamera = new BABYLON.ArcRotateCamera( arcCamera.pinchPrecision = 150 arcCamera.wheelPrecision = 150 arcCamera.lowerRadiusLimit = 5 + + setCamera(false) } /// --- EXPORTS --- @@ -112,27 +114,38 @@ function moveCamera(camera: any, directionRotation: BABYLON.Quaternion, speed: n export { vrCamera, arcCamera } +function setUpEvents(attach: boolean) { + const canvas = engine.getRenderingCanvas() + const eventPrefix = BABYLON.Tools.GetPointerPrefix() + + canvas.removeEventListener(eventPrefix + 'move', (scene as any)._onPointerMove) + canvas.removeEventListener(eventPrefix + 'down', (scene as any)._onPointerDown) + window.removeEventListener(eventPrefix + 'up', (scene as any)._onPointerUp) + + if (attach) { + canvas.addEventListener(eventPrefix + 'move', (scene as any)._onPointerMove) + canvas.addEventListener(eventPrefix + 'down', (scene as any)._onPointerDown) + window.addEventListener(eventPrefix + 'up', (scene as any)._onPointerUp) + } +} + export function setCamera(thirdPerson: boolean) { if (thirdPerson && scene.activeCamera === arcCamera) return if (!thirdPerson && scene.activeCamera === vrCamera) return - const canvas = engine.getRenderingCanvas() - if (thirdPerson) { - vrCamera.detachControl(canvas) - arcCamera.attachControl(canvas, true) + setUpEvents(true) arcCamera.target.copyFrom(scene.activeCamera.position) - scene.activeCamera = arcCamera + scene.switchActiveCamera(arcCamera) scene.cameraToUseForPointers = scene.activeCamera } else { - vrCamera.attachControl(canvas) - arcCamera.detachControl(canvas) + setUpEvents(false) vrCamera.position.copyFrom(scene.activeCamera.position) - scene.activeCamera = vrCamera + scene.switchActiveCamera(vrCamera) scene.cameraToUseForPointers = scene.activeCamera } } @@ -156,3 +169,27 @@ export function cameraPositionToRef(ref: BABYLON.Vector3) { ref.copyFrom(scene.activeCamera.position) } } + +export function rayToGround(screenX: number, screenY: number) { + const mouseVec = new BABYLON.Vector3(screenX, screenY, 0) + return unprojectToPlane(mouseVec) +} + +function unprojectToPlane(vec: BABYLON.Vector3) { + const viewport = scene.activeCamera.viewport.toGlobal(engine.getRenderWidth(), engine.getRenderHeight()) + + let onPlane = BABYLON.Vector3.Unproject( + vec, + viewport.width, + viewport.height, + BABYLON.Matrix.Identity(), + scene.activeCamera.getViewMatrix(), + scene.activeCamera.getProjectionMatrix() + ) + + let dir = onPlane.subtract(scene.activeCamera.position).normalize() + let distance = -scene.activeCamera.position.y / dir.y + dir.scaleInPlace(distance) + onPlane = scene.activeCamera.position.add(dir) + return onPlane +} diff --git a/packages/entryPoints/editor.ts b/packages/entryPoints/editor.ts index 9ecd4c275..562dc8616 100644 --- a/packages/entryPoints/editor.ts +++ b/packages/entryPoints/editor.ts @@ -6,7 +6,7 @@ import { initLocalPlayer, domReadyFuture, onWindowResize } from '../engine/rende import { initBabylonClient } from '../dcl' import * as _envHelper from '../engine/renderer/envHelper' -import { canvas } from '../engine/renderer/init' +import { canvas, scene } from '../engine/renderer/init' import { loadedParcelSceneWorkers } from '../shared/world/parcelSceneManager' import { LoadableParcelScene, @@ -26,13 +26,15 @@ import { arcCamera, DEFAULT_CAMERA_ZOOM, setCameraPosition as _setCameraPosition, - cameraPositionToRef + cameraPositionToRef, + rayToGround } from '../engine/renderer/camera' import { setEditorEnvironment } from '../engine/renderer/ambientLights' import { sleep } from '../atomicHelpers/sleep' import * as Gizmos from '../engine/components/ephemeralComponents/Gizmos' import { Gizmo } from '../decentraland-ecs/src/decentraland/Gizmos' import { Vector3 } from 'babylonjs' +import future, { IFuture } from 'fp-future' let didStartPosition = false @@ -237,6 +239,10 @@ export namespace editor { arcCamera.radius = DEFAULT_CAMERA_ZOOM } + export function getMouseWorldPosition(localX: number, localY: number) { + return rayToGround(localX, localY) + } + export function setCameraRotation(alpha: number, beta?: number) { arcCamera.alpha = alpha if (beta !== undefined) { @@ -244,6 +250,16 @@ export namespace editor { } } + export function takeScreenshot(): IFuture { + const ret = future() + + scene.onAfterRenderObservable.addOnce(() => { + ret.resolve(canvas.toDataURL()) + }) + + return ret + } + export const envHelper = _envHelper export const setCameraPosition = _setCameraPosition diff --git a/packages/shared/index.ts b/packages/shared/index.ts index dc1de9551..572a02ea7 100644 --- a/packages/shared/index.ts +++ b/packages/shared/index.ts @@ -32,14 +32,12 @@ async function grantAccess(address: string | null, net: ETHEREUM_NETWORK) { function getNetworkFromDomain(): ETHEREUM_NETWORK { const domain = window.location.host - switch (domain) { - case 'client.decentraland.org': - case 'client.decentraland.today': - return ETHEREUM_NETWORK.MAINNET - case 'client.decentraland.zone': - return ETHEREUM_NETWORK.ROPSTEN - default: - return null + if (domain.endsWith('.decentraland.org') || domain.endsWith('.decentraland.today')) { + return ETHEREUM_NETWORK.MAINNET + } else if (domain.endsWith('.decentraland.zone')) { + return ETHEREUM_NETWORK.ROPSTEN + } else { + return null } } diff --git a/public/test-parcels/-100.111.SkeletalAnimation/game.ts b/public/test-parcels/-100.111.SkeletalAnimation/game.ts index 93d432a3d..5c5d3da21 100644 --- a/public/test-parcels/-100.111.SkeletalAnimation/game.ts +++ b/public/test-parcels/-100.111.SkeletalAnimation/game.ts @@ -17,7 +17,6 @@ shark.set( ) const shark2 = new Entity() -const shape2 = new GLTFShape('shark_anim.gltf') const clip2 = new AnimationClip('shark_skeleton_bite', { weight: 0.7, speed: 5 }) const clip3 = new AnimationClip('shark_skeleton_swim', { weight: 0.7, speed: 0.5 }) const animator2 = shark2.getOrCreate(Animator) @@ -26,7 +25,7 @@ animator2.addClip(clip3) clip2.play() clip3.play() -shark2.set(shape2) +shark2.set(shape) shark2.set( new Transform({ position: new Vector3(6, 1, 6) diff --git a/public/test-parcels/-102.102.gizmos/game.ts b/public/test-parcels/-102.102.gizmos/game.ts index 4dbf3301e..e35c1faed 100644 --- a/public/test-parcels/-102.102.gizmos/game.ts +++ b/public/test-parcels/-102.102.gizmos/game.ts @@ -20,7 +20,7 @@ function createCube(i: number) { gizmo.position = !!(i & 1) gizmo.scale = !!(i & 2) gizmo.rotation = !!(i & 4) - gizmo.cycle = !!(i % 8) + gizmo.cycle = !!(i & 8) cube.set(gizmo) cube.set( diff --git a/public/test-parcels/200.10.crocs-game/scene.json b/public/test-parcels/200.10.crocs-game/scene.json index b35137dde..7da1af26e 100644 --- a/public/test-parcels/200.10.crocs-game/scene.json +++ b/public/test-parcels/200.10.crocs-game/scene.json @@ -11,7 +11,7 @@ "main": "game.js", "tags": [], "scene": { - "parcels": ["200,10", "200,11"], + "parcels": ["200,10", "200,11", "200,12", "200,13"], "base": "200,10" }, "policy": { diff --git a/test/atomicHelpers/parcelScenePositions.test.ts b/test/atomicHelpers/parcelScenePositions.test.ts index 5cf49325e..a71b15d27 100644 --- a/test/atomicHelpers/parcelScenePositions.test.ts +++ b/test/atomicHelpers/parcelScenePositions.test.ts @@ -36,49 +36,49 @@ describe('parcelScenePositions unit tests', () => { describe.skip('vertical limits', () => { it('(20) -> true', () => { const bbox = { maximum: { x: 1, y: 20, z: 1 }, minimum: { x: 0, y: 0, z: 0 } } - const result = isOnLimits(bbox, '0,0') + const result = isOnLimits(bbox, new Set(['0,0'])) expect(result).to.eq(true) }) it('(20.1) -> false', () => { const bbox = { maximum: { x: 1, y: 20.1, z: 1 }, minimum: { x: 0, y: 0, z: 0 } } - const result = isOnLimits(bbox, '0,0') + const result = isOnLimits(bbox, new Set(['0,0'])) expect(result).to.eq(false) }) it('(-20) -> true', () => { const bbox = { maximum: { x: 1, y: 0, z: 1 }, minimum: { x: 0, y: -20, z: 0 } } - const result = isOnLimits(bbox, '0,0') + const result = isOnLimits(bbox, new Set(['0,0'])) expect(result).to.eq(true) }) it('(-20.1) -> false', () => { const bbox = { maximum: { x: 1, y: 0, z: 1 }, minimum: { x: 0, y: -20.1, z: 0 } } - const result = isOnLimits(bbox, '0,0') + const result = isOnLimits(bbox, new Set(['0,0'])) expect(result).to.eq(false) }) }) describe('horizontal limits', () => { it('(10, 10) -> true', () => { const bbox = { maximum: { x: 10, y: 0, z: 10 }, minimum: { x: 0, y: 0, z: 0 } } - const result = isOnLimits(bbox, '0,0') + const result = isOnLimits(bbox, new Set(['0,0'])) expect(result).to.eq(true) }) it('(0, 0) -> true', () => { const bbox = { maximum: { x: 0, y: 0, z: 0 }, minimum: { x: 0, y: 0, z: 0 } } - const result = isOnLimits(bbox, '0,0') + const result = isOnLimits(bbox, new Set(['0,0'])) expect(result).to.eq(true) }) it('a(10, 10) -> true', () => { const bbox = { maximum: { x: 10, y: 0, z: 10 }, minimum: { x: 10, y: 0, z: 10 } } - const result = isOnLimits(bbox, '1,1') + const result = isOnLimits(bbox, new Set(['1,1'])) expect(result).to.eq(true) }) it('(10.1, 10.1) -> false', () => { const bbox = { maximum: { x: 10.1, y: 0, z: 10.1 }, minimum: { x: 0, y: 0, z: 0 } } - const result = isOnLimits(bbox, '0,0') + const result = isOnLimits(bbox, new Set(['0,0'])) expect(result).to.eq(false) }) it('(-0.1, -0.1) -> false', () => { const bbox = { maximum: { x: 1, y: 0, z: 1 }, minimum: { x: -0.1, y: 0, z: -0.1 } } - const result = isOnLimits(bbox, '0,0') + const result = isOnLimits(bbox, new Set(['0,0'])) expect(result).to.eq(false) }) }) diff --git a/test/index.ts b/test/index.ts index 82598244e..87e56caa4 100644 --- a/test/index.ts +++ b/test/index.ts @@ -17,7 +17,6 @@ import './atomicHelpers/vectorHelpers.test' /* UNIT */ import './unit/entities.test' import './unit/passing.test' -import './unit/telemetry.test' import './unit/positions.test' import './unit/ethereum.test' import './unit/schemaValidator.test' diff --git a/test/testHelpers.ts b/test/testHelpers.ts index cb3430f91..617d349b2 100644 --- a/test/testHelpers.ts +++ b/test/testHelpers.ts @@ -14,7 +14,6 @@ import { start, stop } from 'dcl' import { resolveUrl } from 'atomicHelpers/parseUrl' import { sleep, untilNextFrame } from 'atomicHelpers/sleep' -import { DebugTelemetry } from 'atomicHelpers/DebugTelemetry' import { expect } from 'chai' import { bodyReadyFuture } from 'engine/renderer/init' import { BaseEntity } from 'engine/entities/BaseEntity' @@ -29,8 +28,6 @@ import { MemoryTransport } from 'decentraland-rpc' import GamekitScene from '../packages/scene-system/scene.system' import { gridToWorld } from 'atomicHelpers/parcelScenePositions' -DebugTelemetry.startTelemetry(process.env) - const baseUrl = 'http://localhost:8080/local-ipfs/contents/' declare var gc: any diff --git a/test/unit/ecs.test.tsx b/test/unit/ecs.test.tsx index 3ad4125a2..9082f8379 100644 --- a/test/unit/ecs.test.tsx +++ b/test/unit/ecs.test.tsx @@ -21,6 +21,12 @@ import { future } from 'fp-future' import { loadTestParcel, testScene, saveScreenshot, wait } from '../testHelpers' import { sleep } from 'atomicHelpers/sleep' import { BaseEntity } from 'engine/entities/BaseEntity' +import { AudioClip } from 'engine/components/disposableComponents/AudioClip' +import { AudioSource } from 'engine/components/ephemeralComponents/AudioSource' +import { GLTFShape } from 'engine/components/disposableComponents/GLTFShape' +import { Animator } from 'engine/components/ephemeralComponents/Animator' + +declare var describe: any, it: any describe('ECS', () => { describe('unit', () => { @@ -266,6 +272,13 @@ describe('ECS', () => { this.asd = 3 } } + @Component(componentName) + class TheCompoNotTheCompo { + asd = 1 + constructor() { + this.asd = 3 + } + } it('should have symbol in the class as long as the instance', () => { const inst = new TheCompo() @@ -276,7 +289,19 @@ describe('ECS', () => { it('should add the component to an entity using the indicated component name', () => { const entity = new Entity() - entity.add(new TheCompo()) + + const compo = new TheCompo() + + expect(entity.has(compo)).to.eq(false) + expect(entity.has(TheCompo)).to.eq(false) + expect(entity.has(TheCompoNotTheCompo)).to.eq(false) + expect(entity.has(componentName)).to.eq(false) + entity.add(compo) + expect(entity.has(compo)).to.eq(true, 'has(compo)') + expect(entity.has(TheCompo)).to.eq(true, 'has(TheCompo)') + expect(entity.has(TheCompoNotTheCompo)).to.eq(false, 'has(TheCompoNotTheCompo)') + expect(entity.has(componentName)).to.eq(true, 'has(componentName)') + expect(entity.get(TheCompo)).to.be.instanceOf(TheCompo) expect(entity.components[componentName]).to.be.instanceOf(TheCompo) expect(entity.components[componentName]).to.eq(entity.get(TheCompo)) @@ -731,6 +756,95 @@ describe('ECS', () => { scriptingHost.unmounted }) }) + describe('sound', () => { + let audioClips: AudioClip[] = [] + let audioSources: AudioSource[] = [] + + loadTestParcel('test unload', -200, 2, function(_root, futureScene, futureWorker) { + it('must have two audio clips', async () => { + const scene = await futureScene + scene.context.disposableComponents.forEach($ => { + if ($ instanceof AudioClip) { + audioClips.push($) + } + }) + + expect(audioClips.length).to.eq(2) + }) + + it('must have two audio sources', async () => { + const scene = await futureScene + scene.context.entities.forEach($ => { + for (let i in $.components) { + console.log(i, $.components[i]) + if ($.components[i] instanceof AudioSource) { + audioSources.push($.components[i] as any) + } + } + }) + + expect(audioSources.length).to.eq(2) + }) + }) + + describe('after finalizing', () => { + it('must have stopped AudioClips', () => { + for (let clip of audioClips) { + expect(clip.entities.size).eq(0) + } + }) + + it('must have stopped AudioSources', async () => { + for (let source of audioSources) { + expect(source.sound.isPending).eq(true) + } + }) + }) + }) + + describe('gltf animations', () => { + let gltf: GLTFShape[] = [] + let animators: Animator[] = [] + + loadTestParcel('test animatios', -100, 111, function(_root, futureScene, futureWorker) { + it('must have one gltf', async () => { + const scene = await futureScene + scene.context.disposableComponents.forEach($ => { + if ($ instanceof GLTFShape) { + gltf.push($) + } + }) + + expect(gltf.length).to.eq(1) + }) + + it('must have two animators', async () => { + const scene = await futureScene + scene.context.entities.forEach($ => { + for (let i in $.components) { + if ($.components[i] instanceof Animator) { + animators.push($.components[i] as Animator) + } + } + }) + + expect(animators.length).to.eq(2) + }) + + it('wait some seconds', async () => { + await sleep(1000) + }) + }) + + describe('after', () => { + it('must have no entities in the shapes', () => { + for (let shape of gltf) { + expect(shape.assetContainerEntity.size).to.eq(0, 'asset container') + expect(shape.entities.size).to.eq(0, 'entities') + } + }) + }) + }) describe('ecs events', () => { it('should call event only once when event is added before adding entity', async () => { diff --git a/test/unit/entities.test.tsx b/test/unit/entities.test.tsx index abd268c87..619191211 100644 --- a/test/unit/entities.test.tsx +++ b/test/unit/entities.test.tsx @@ -138,6 +138,6 @@ describe('entity tree tests', function() { expect(child.isDisposed()).to.equal(true) expect(entity.isDisposed()).to.equal(true) - expect(removedCalled).to.equal(true) + expect(removedCalled).to.equal(true, 'component.detach should have been called') }) }) diff --git a/test/unit/telemetry.test.tsx b/test/unit/telemetry.test.tsx deleted file mode 100644 index 9e5c8d5d1..000000000 --- a/test/unit/telemetry.test.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { wait, saveScreenshot, enableVisualTests } from '../testHelpers' -import { DebugTelemetry } from 'atomicHelpers/DebugTelemetry' -import { expect } from 'chai' - -enableVisualTests('DebugTelemetry', function() { - it('should enable telemetry', () => { - DebugTelemetry.startTelemetry({ - test: 'telemetry' - }) - }) - - // Wait to collect telemetry data across the render cycles. - wait(1000) - - it('stopping telemetry should return render values', () => { - const values = DebugTelemetry.stopTelemetry() - expect(values.length).greaterThan(0) - expect(values.some($ => $.metric === 'render')).to.equal(true, "At least one 'render' metric should be present") - }) - - // Wait to collect telemetry data across the render cycles. - wait(300) - - it('if DebugTelemetry is deactivated, it should contain no new metrics', () => { - expect(DebugTelemetry._store.length).to.equal(0) - }) - - saveScreenshot('telemetry.png') -})