diff --git a/src/components/InteractiveObjects/GenenricObject.js b/src/components/InteractiveObjects/GenenricObject.js index fa2d4ee..b2e7fce 100644 --- a/src/components/InteractiveObjects/GenenricObject.js +++ b/src/components/InteractiveObjects/GenenricObject.js @@ -2,7 +2,7 @@ import { getBoundingBox, getBoundingBoxCenterPoint, getBoundingBoxMaxLength, cen import { EventManager } from '@/lib/EventManager'; class GenericObject extends EventManager{ - emits = ['finish'] + emits = ['finish', 'interaction'] constructor(engine, data){ super(); return new Promise(async(resolve, reject)=>{ @@ -41,6 +41,7 @@ class GenericObject extends EventManager{ }) } } + this.dispatchEvent({type:'interaction'}) }); } diff --git a/src/components/InteractiveObjects/InteractiveObject.js b/src/components/InteractiveObjects/InteractiveObject.js index c77e118..c764892 100644 --- a/src/components/InteractiveObjects/InteractiveObject.js +++ b/src/components/InteractiveObjects/InteractiveObject.js @@ -65,11 +65,15 @@ class InteractiveObject extends EventManager{ // }) this.io.forwardEvents?.(this); if (this.emits?.includes('interaction')){ + //process first interaction this.io.once('interaction', ()=>{ if (obj.introText){ engine.dashboard.updateText(obj.introText, true) } }) + this.io.addEventListener('interaction', ()=>{ + engine.tm?.setGameObject(obj.$go?.id); + }) } break; } diff --git a/src/components/InteractiveObjects/MazeQuizGame/MazeQuizGame.js b/src/components/InteractiveObjects/MazeQuizGame/MazeQuizGame.js index 9cef755..3ab29f4 100644 --- a/src/components/InteractiveObjects/MazeQuizGame/MazeQuizGame.js +++ b/src/components/InteractiveObjects/MazeQuizGame/MazeQuizGame.js @@ -38,7 +38,7 @@ const defaults = { const tl = 4; class MazeQuizGame extends EventManager { - emits = ['finish', 'sceneSwitch'] + emits = ['finish', 'sceneSwitch', 'interaction'] constructor(engine, data) { super(); this.data = data; @@ -97,6 +97,7 @@ class MazeQuizGame extends EventManager { await this.mazeObject.load(); this.object = this.mazeObject.object; resolve(this) + this.dispatchEvent({type:'interaction'}); }) } diff --git a/src/components/InteractiveObjects/SingleQuestion.js b/src/components/InteractiveObjects/SingleQuestion.js index fcc190f..96e7f33 100644 --- a/src/components/InteractiveObjects/SingleQuestion.js +++ b/src/components/InteractiveObjects/SingleQuestion.js @@ -5,7 +5,7 @@ import { TextObject } from './TextObject'; import Utils from '#/app/Utils'; class SingleQuestion extends EventManager { - emits = ['finish'] + emits = ['finish', 'interaction'] constructor(engine, data) { super(); return new Promise(async (resolve, reject)=>{ @@ -20,8 +20,10 @@ class SingleQuestion extends EventManager { qa.object.position.y = -0.15 * (i + 1); answers.add(qa.object); qa.object._answer = a; - engine.clickable.add(qa.object, (i) => { + engine.clickable.add(qa.object, () => { //if (!container.visible) return; + this.dispatchEvent({type:'interaction'}); + engine.tm?.post('answer', i, {answer: a, correct: a == ca}); if (qa.object._answer == ca) { this.dispatchEvent({type:'finish'}) qa.object.outlineColor = new Color(0x55ff55); diff --git a/src/components/InteractiveObjects/VideoPlayer.js b/src/components/InteractiveObjects/VideoPlayer.js index fbf4a97..4e8f377 100644 --- a/src/components/InteractiveObjects/VideoPlayer.js +++ b/src/components/InteractiveObjects/VideoPlayer.js @@ -5,10 +5,10 @@ import { import { EventManager } from '@/lib/EventManager'; class VideoPlayer extends EventManager { - emits = ['finish'] + emits = ['finish', 'interaction'] constructor(engine, data){ super(); - let vi, plane; + let vi, plane, finished = false; return new Promise((resolve, reject)=>{ vi = document.createElement('video'); vi.src = engine.assetPath + data.$go.asset.name; @@ -31,7 +31,9 @@ class VideoPlayer extends EventManager { vi.play(); }else{ vi.pause(); + engine.tm?.post('paused', null, {time: vi.currentTime}); } + this.dispatchEvent({type:'interaction'}) }); const onPlay = ()=>{ @@ -61,6 +63,7 @@ class VideoPlayer extends EventManager { vi.addEventListener('pause', onPause); vi.addEventListener('ended', ()=>{ + finished = true; this.dispatchEvent({type:'finish'}) }) this.video = vi; diff --git a/src/lib/GameEngine.js b/src/lib/GameEngine.js index 2abd0e4..06cb2fe 100644 --- a/src/lib/GameEngine.js +++ b/src/lib/GameEngine.js @@ -17,6 +17,7 @@ import { DashBoard } from './Dashboard.js'; import { MotionEngine } from './MotionEngine.js'; import { Draggable } from './Draggable.js'; import { EventManager } from "./EventManager"; +import { Telemetry } from './Telemetry.js'; THREE.Cache.enabled = true @@ -130,6 +131,10 @@ class GameEngine extends EventManager{ this.gizmo = gizmo; } + if (opts.telemetry){ + this.tm = new Telemetry(opts.telemetry, opts.mode); + } + this.orbitControls = controls; controls.enableZoom = false; //const controls = new MapControls( camera, renderer.domElement ); @@ -588,6 +593,7 @@ class GameEngine extends EventManager{ this.clickable.removeAll(); this.motionQueue.clearAll(); this.ambientSound.stop(); + this.tm?.setScene(null); } async playAmbientSound(source, path){ diff --git a/src/lib/Telemetry.js b/src/lib/Telemetry.js new file mode 100644 index 0000000..905ab7e --- /dev/null +++ b/src/lib/Telemetry.js @@ -0,0 +1,55 @@ +class Telemetry { + game = null; + scene = null; + gameObject = null; + #af = null; + + constructor(apiFunction, mode){ + this.events = []; + this.#af = apiFunction; + } + setGame(game){ + if (this.game == game) { return; } + if (this.game){ + this.#af('game:leave', this.game, {game: this.game, duration: Math.round(performance.now()/1000) - this.gameStart}); + } + if (game){ + this.#af('game:enter', game, {game}); + } + this.game = game; + this.gameStart = Math.round(performance.now()/1000); + } + setScene(scene){ + if (this.scene == scene) { return; } + if (this.scene){ + this.#af('scene:leave', this.scene, { + duration: Math.round(performance.now()/1000) - this.sceneStart, + game: this.game, scene: this.scene + }); + } + if (scene){ + this.#af('scene:enter', scene, {game: this.game, scene}); + } + this.scene = scene; + this.sceneStart = Math.round(performance.now()/1000); + } + setGameObject(gameObject){ + if (this.gameObject == gameObject) { return; } + if (this.gameObject){ + this.#af('gameobject:leave', this.gameObject, { + duration: Math.round(performance.now()/1000) - this.gameObjectStart, + game: this.game, scene: this.scene, gameObject: this.gameObject + }); + } + if (gameObject){ + this.#af('gameobject:enter', gameObject, {game: this.game, scene: this.scene, gameObject}); + } + this.gameObject = gameObject; + this.gameObjectStart = Math.round(performance.now()/1000); + } + post(action, object, data){ + this.#af('gameobject:' + action, object, {game: this.game, scene: this.scene, gameObject: this.gameObject, data}); + } +} + +export { Telemetry } \ No newline at end of file diff --git a/src/mixins/GameEnvironmentMixin.js b/src/mixins/GameEnvironmentMixin.js index 70220fe..5f02f6a 100644 --- a/src/mixins/GameEnvironmentMixin.js +++ b/src/mixins/GameEnvironmentMixin.js @@ -3,30 +3,32 @@ import { VideoPlayer } from '@/components/InteractiveObjects/VideoPlayer'; import { GameEngine } from '@/lib/GameEngine'; import { Hero } from '@/lib/Hero'; import { autoScale, getBoundingBox, getBoundingBoxSize } from '@/lib/MeshUtils'; -let gameEngine = null; +let engine = null; export default { async mounted(){ - gameEngine = new GameEngine(); - this.engine = gameEngine; - //this.gameEngine = gameEngine; - await gameEngine.init(this.$refs.target, { + this.engine = engine = new GameEngine(); + await engine.init(this.$refs.target, { xr: true, gizmo: this.env == 'GameDesigner', stats: this.env != 'GamePlay', designMode: this.env == 'GameDesigner', - depthSense: this.env == 'GameDesigner' ? false : this.store.prefs.xr.depthSense + depthSense: this.env == 'GameDesigner' ? false : this.store.prefs.xr.depthSense, + telemetry: this.$api.user.tm, + mode: this.env }); - //gameEngine.scene.add(new gameEngine.$.GridHelper(100,100)); + //engine.scene.add(new engine.$.GridHelper(100,100)); if (this.env == 'GameDesigner'){ - gameEngine.scene.add(gameEngine.transformControls.getHelper()); + engine.scene.add(engine.transformControls.getHelper()); }else{ - gameEngine.dashboard.enable(); + engine.dashboard.enable(); } this.resize(); - //gameEngine.setCamera(gameEngine.orthographicCamera) - //gameEngine.setCameraOrthographic(); + + engine.tm?.setGame(this.object?.id); + //engine.setCamera(engine.orthographicCamera) + //engine.setCameraOrthographic(); if (!this.scenario) { await this.loadScenario(); } @@ -35,8 +37,9 @@ export default { unmounted(){ window.removeEventListener('resize', this.resize); - gameEngine.clearScene(); - gameEngine.stop(); + engine.clearScene(); + engine.stop(); + engine.tm?.setGame(null); }, computed:{ @@ -69,26 +72,26 @@ export default { await this.loadEnvironment(n, this.object.scenes[n.data.id]); }, mode(n){ - gameEngine.transformControls.setMode(n) + engine.transformControls.setMode(n) }, async 'object.scenario'(n){ await this.loadScenario() }, currentObject(n){ if (this.env == 'GameDesigner'){ - gameEngine.transformControls.attach(n.__o); - gameEngine.gizmo.target = n.__o.position; - gameEngine.camera.updateProjectionMatrix() + engine.transformControls.attach(n.__o); + engine.gizmo.target = n.__o.position; + engine.camera.updateProjectionMatrix() } }, renderType(v){ - gameEngine.renderType = v; + engine.renderType = v; }, cameraType(v){ if (v == 'perspective'){ - gameEngine.setCameraPerspective(); + engine.setCameraPerspective(); }else{ - gameEngine.setCameraOrthographic(); + engine.setCameraOrthographic(); } } }, @@ -122,46 +125,47 @@ export default { * @param target Target scene definition from Game Module */ async loadEnvironment(scene, target){ - //await gameEngine.loadPanorama(`/asset/default/43.webp`); + //await engine.loadPanorama(`/asset/default/43.webp`); let intro; - gameEngine.clearScene(); - gameEngine.activeObjects.visible = false; - await gameEngine.dashboard.ready; - gameEngine.dashboard?.initScene(scene, async ()=>{ + engine.clearScene(); + engine.activeObjects.visible = false; + await engine.dashboard.ready; + engine.dashboard?.initScene(scene, async ()=>{ if (this.scene.data.$audio){ - await gameEngine.playAmbientSound(this.scene.data.$audio.asset.name); - gameEngine.ambientSound.setVolume( 0.5 ); + await engine.playAmbientSound(this.scene.data.$audio.asset.name); + engine.ambientSound.setVolume( 0.5 ); } + engine.tm?.setScene(scene.data.id); if (intro){ intro.play(); }else{ - gameEngine.activeObjects.visible = true; + engine.activeObjects.visible = true; } }); await this.expandScenarioData(scene); - gameEngine.dashboard?.loading(0.05); + engine.dashboard?.loading(0.05); - gameEngine.orbitControls.enableRotate = this.env == 'GameDesigner' + engine.orbitControls.enableRotate = this.env == 'GameDesigner' //this is needed cause when mounted canvas has different size this.resize(); target.objects = target.objects || {}; let l = target.objects; if (this.scene.data.$environment){ - await gameEngine.loadPanorama(this.scene.data.$environment.asset.name); + await engine.loadPanorama(this.scene.data.$environment.asset.name); } if (this.scene.data.$scene){ - let env = await gameEngine.load(this.scene.data.$scene.asset.name); + let env = await engine.load(this.scene.data.$scene.asset.name); this.setObjectAttributes(l, this.scene.data, env.scene, env, null); if (this.env != 'GameDesigner'){ env.scene.traverse(o=>{ if (o.name.startsWith('land') || o.name == 'Sphere'){ console.log('Fixing ground. TODO!!!') - gameEngine.physics.add(o, 'fixed', true, undefined, 'mesh', { root: env.scene}) + engine.physics.add(o, 'fixed', true, undefined, 'mesh', { root: env.scene}) } }) } - gameEngine.activeObjects.add(env.scene); + engine.activeObjects.add(env.scene); } let expectToFinish = 0, finished = 0;; if (this.scene.data.items){ @@ -172,25 +176,25 @@ export default { i.data.shouldBeLocked = true; } } - let io = await new InteractiveObject(gameEngine, i.data) + let io = await new InteractiveObject(engine, i.data) i.__io = io; if (io.emits?.includes('finish')){ expectToFinish++; } this.setObjectAttributes(l, i.data, io.object, io.source, 1); - gameEngine.activeObjects.add(io.object); + engine.activeObjects.add(io.object); if (this.env != 'GameDesigner'){ if (i.data.$go?.type == 'player3d'){ - let hero = new Hero(gameEngine, io); + let hero = new Hero(engine, io); }else{ if (io.source?.animations?.length){ - gameEngine.playAnimation(gameEngine.scene, io.source.animations[0]); + engine.playAnimation(engine.scene, io.source.animations[0]); } if (!i.data.noPhysics){ let bb = getBoundingBox(io.object); let bbs = getBoundingBoxSize(bb); - gameEngine.physics.add(io.object, 'fixed', false, undefined, 'capsule', { + engine.physics.add(io.object, 'fixed', false, undefined, 'capsule', { radius: Math.max(bbs.x, bbs.z)/2, halfHeight: bbs.y/2 }) } @@ -198,7 +202,7 @@ export default { io.addEventListener('finish', ()=>{ finished ++; if (!i.data.pointsGiven){ - gameEngine.dashboard?.addPoints(i.data.points) + engine.dashboard?.addPoints(i.data.points) } i.data.pointsGiven = true; this.scene.data.items.forEach(di=>{ @@ -207,7 +211,7 @@ export default { di.data.activationTriggers.splice(idx, 1); } if (!di.data.activationTriggers?.length && di.__io.object.__active === false && - gameEngine.dashboard.points > di.data.activationScore){ + engine.dashboard.points > di.data.activationScore){ di.__io.activator.activate(); } }); @@ -215,35 +219,36 @@ export default { //GO TO NEXT LEVEL console.log('LEVEL FINISHED') } + engine.tm?.setGameObject(null); }); io.addEventListener('sceneSwitch', (e)=>{ this.scenesList = [this.scenes.find(s=>s.data.id == e.scene)] }) } loaded += 1/this.scene.data.items.length - gameEngine.dashboard?.loading(0.1 + 0.89*loaded); + engine.dashboard?.loading(0.1 + 0.89*loaded); } } if (this.env == 'GameDesigner'){ - gameEngine.activeObjects.visible = true; + engine.activeObjects.visible = true; }else if (this.scene.data.$intro){ - intro = await new VideoPlayer(gameEngine, { + intro = await new VideoPlayer(engine, { $go: this.scene.data.$intro, skipTransition: true, playInHud: true, hideBackground: true }); - gameEngine.activeObjects.add(intro.object); + engine.activeObjects.add(intro.object); intro.video.addEventListener('pause',()=>{ intro.object.removeFromParent(); - gameEngine.clickable.remove(intro.object); //TODO!!!! - gameEngine.activeObjects.visible = true; + engine.clickable.remove(intro.object); //TODO!!!! + engine.activeObjects.visible = true; }); } - gameEngine.dashboard?.loading(1) - gameEngine.physics.start(); + engine.dashboard?.loading(1) + engine.physics.start(); }, targetPointerDown(){ @@ -252,24 +257,24 @@ export default { targetClick(e){ if (this.env == 'GameDesigner'){ if (performance.now() - this.pointerDownTime < 200){ - let intersects = gameEngine.intersect(e, this.$refs.target, gameEngine.activeObjects.children, true); + let intersects = engine.intersect(e, this.$refs.target, engine.activeObjects.children, true); //console.log(intersects) if (intersects.length){ //console.log('attaching controls to', intersects[0].object) - //gameEngine.transformControls.attach(intersects[0].object); + //engine.transformControls.attach(intersects[0].object); //console.log(this.sceneObjects[intersects[0].object.__pn_id]) this.objectsList[0] = this.sceneObjects[intersects[0].object.__pn_id] }else{ - gameEngine.transformControls.detach(); + engine.transformControls.detach(); } } }else{ - gameEngine.onClick(e, this.$refs.target); + engine.onClick(e, this.$refs.target); } }, targetPointer(e, t){ - gameEngine.onPointer(e, this.$refs.target, t); + engine.onPointer(e, this.$refs.target, t); }, setObjectAttributes(l, data, object, source, autoScaleFactor = 1){ @@ -292,21 +297,21 @@ export default { async toggleAnimation(animation){ animation.playing = !animation.playing; - gameEngine.playAnimation(gameEngine.scene, animation.a, animation.playing); + engine.playAnimation(engine.scene, animation.a, animation.playing); }, resize(){ let r = this.$refs.target; console.log('resizing!!', r.clientWidth, r.clientHeight, r) - gameEngine.resize(r.clientWidth, r.clientHeight); + engine.resize(r.clientWidth, r.clientHeight); //this.zoom = Math.min(r.clientWidth / this.viewBox.w, r.clientHeight / this.viewBox.h); }, control(){ - gameEngine.hero.lockControls(); + engine.hero.lockControls(); }, async fullScreen(){ - await gameEngine.renderer.domElement.requestFullscreen() + await engine.renderer.domElement.requestFullscreen() } } } \ No newline at end of file