#12 initial commit

This commit is contained in:
2026-02-07 18:36:33 +02:00
parent c28286e305
commit 9cf14ff2a8
8 changed files with 141 additions and 64 deletions
@@ -2,7 +2,7 @@ import { getBoundingBox, getBoundingBoxCenterPoint, getBoundingBoxMaxLength, cen
import { EventManager } from '@/lib/EventManager'; import { EventManager } from '@/lib/EventManager';
class GenericObject extends EventManager{ class GenericObject extends EventManager{
emits = ['finish'] emits = ['finish', 'interaction']
constructor(engine, data){ constructor(engine, data){
super(); super();
return new Promise(async(resolve, reject)=>{ return new Promise(async(resolve, reject)=>{
@@ -41,6 +41,7 @@ class GenericObject extends EventManager{
}) })
} }
} }
this.dispatchEvent({type:'interaction'})
}); });
} }
@@ -65,11 +65,15 @@ class InteractiveObject extends EventManager{
// }) // })
this.io.forwardEvents?.(this); this.io.forwardEvents?.(this);
if (this.emits?.includes('interaction')){ if (this.emits?.includes('interaction')){
//process first interaction
this.io.once('interaction', ()=>{ this.io.once('interaction', ()=>{
if (obj.introText){ if (obj.introText){
engine.dashboard.updateText(obj.introText, true) engine.dashboard.updateText(obj.introText, true)
} }
}) })
this.io.addEventListener('interaction', ()=>{
engine.tm?.setGameObject(obj.$go?.id);
})
} }
break; break;
} }
@@ -38,7 +38,7 @@ const defaults = {
const tl = 4; const tl = 4;
class MazeQuizGame extends EventManager { class MazeQuizGame extends EventManager {
emits = ['finish', 'sceneSwitch'] emits = ['finish', 'sceneSwitch', 'interaction']
constructor(engine, data) { constructor(engine, data) {
super(); super();
this.data = data; this.data = data;
@@ -97,6 +97,7 @@ class MazeQuizGame extends EventManager {
await this.mazeObject.load(); await this.mazeObject.load();
this.object = this.mazeObject.object; this.object = this.mazeObject.object;
resolve(this) resolve(this)
this.dispatchEvent({type:'interaction'});
}) })
} }
@@ -5,7 +5,7 @@ import { TextObject } from './TextObject';
import Utils from '#/app/Utils'; import Utils from '#/app/Utils';
class SingleQuestion extends EventManager { class SingleQuestion extends EventManager {
emits = ['finish'] emits = ['finish', 'interaction']
constructor(engine, data) { constructor(engine, data) {
super(); super();
return new Promise(async (resolve, reject)=>{ return new Promise(async (resolve, reject)=>{
@@ -20,8 +20,10 @@ class SingleQuestion extends EventManager {
qa.object.position.y = -0.15 * (i + 1); qa.object.position.y = -0.15 * (i + 1);
answers.add(qa.object); answers.add(qa.object);
qa.object._answer = a; qa.object._answer = a;
engine.clickable.add(qa.object, (i) => { engine.clickable.add(qa.object, () => {
//if (!container.visible) return; //if (!container.visible) return;
this.dispatchEvent({type:'interaction'});
engine.tm?.post('answer', i, {answer: a, correct: a == ca});
if (qa.object._answer == ca) { if (qa.object._answer == ca) {
this.dispatchEvent({type:'finish'}) this.dispatchEvent({type:'finish'})
qa.object.outlineColor = new Color(0x55ff55); qa.object.outlineColor = new Color(0x55ff55);
@@ -5,10 +5,10 @@ import {
import { EventManager } from '@/lib/EventManager'; import { EventManager } from '@/lib/EventManager';
class VideoPlayer extends EventManager { class VideoPlayer extends EventManager {
emits = ['finish'] emits = ['finish', 'interaction']
constructor(engine, data){ constructor(engine, data){
super(); super();
let vi, plane; let vi, plane, finished = false;
return new Promise((resolve, reject)=>{ return new Promise((resolve, reject)=>{
vi = document.createElement('video'); vi = document.createElement('video');
vi.src = engine.assetPath + data.$go.asset.name; vi.src = engine.assetPath + data.$go.asset.name;
@@ -31,7 +31,9 @@ class VideoPlayer extends EventManager {
vi.play(); vi.play();
}else{ }else{
vi.pause(); vi.pause();
engine.tm?.post('paused', null, {time: vi.currentTime});
} }
this.dispatchEvent({type:'interaction'})
}); });
const onPlay = ()=>{ const onPlay = ()=>{
@@ -61,6 +63,7 @@ class VideoPlayer extends EventManager {
vi.addEventListener('pause', onPause); vi.addEventListener('pause', onPause);
vi.addEventListener('ended', ()=>{ vi.addEventListener('ended', ()=>{
finished = true;
this.dispatchEvent({type:'finish'}) this.dispatchEvent({type:'finish'})
}) })
this.video = vi; this.video = vi;
+6
View File
@@ -17,6 +17,7 @@ import { DashBoard } from './Dashboard.js';
import { MotionEngine } from './MotionEngine.js'; import { MotionEngine } from './MotionEngine.js';
import { Draggable } from './Draggable.js'; import { Draggable } from './Draggable.js';
import { EventManager } from "./EventManager"; import { EventManager } from "./EventManager";
import { Telemetry } from './Telemetry.js';
THREE.Cache.enabled = true THREE.Cache.enabled = true
@@ -130,6 +131,10 @@ class GameEngine extends EventManager{
this.gizmo = gizmo; this.gizmo = gizmo;
} }
if (opts.telemetry){
this.tm = new Telemetry(opts.telemetry, opts.mode);
}
this.orbitControls = controls; this.orbitControls = controls;
controls.enableZoom = false; controls.enableZoom = false;
//const controls = new MapControls( camera, renderer.domElement ); //const controls = new MapControls( camera, renderer.domElement );
@@ -588,6 +593,7 @@ class GameEngine extends EventManager{
this.clickable.removeAll(); this.clickable.removeAll();
this.motionQueue.clearAll(); this.motionQueue.clearAll();
this.ambientSound.stop(); this.ambientSound.stop();
this.tm?.setScene(null);
} }
async playAmbientSound(source, path){ async playAmbientSound(source, path){
+55
View File
@@ -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 }
+63 -58
View File
@@ -3,30 +3,32 @@ import { VideoPlayer } from '@/components/InteractiveObjects/VideoPlayer';
import { GameEngine } from '@/lib/GameEngine'; import { GameEngine } from '@/lib/GameEngine';
import { Hero } from '@/lib/Hero'; import { Hero } from '@/lib/Hero';
import { autoScale, getBoundingBox, getBoundingBoxSize } from '@/lib/MeshUtils'; import { autoScale, getBoundingBox, getBoundingBoxSize } from '@/lib/MeshUtils';
let gameEngine = null; let engine = null;
export default { export default {
async mounted(){ async mounted(){
gameEngine = new GameEngine(); this.engine = engine = new GameEngine();
this.engine = gameEngine; await engine.init(this.$refs.target, {
//this.gameEngine = gameEngine;
await gameEngine.init(this.$refs.target, {
xr: true, xr: true,
gizmo: this.env == 'GameDesigner', gizmo: this.env == 'GameDesigner',
stats: this.env != 'GamePlay', stats: this.env != 'GamePlay',
designMode: this.env == 'GameDesigner', 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'){ if (this.env == 'GameDesigner'){
gameEngine.scene.add(gameEngine.transformControls.getHelper()); engine.scene.add(engine.transformControls.getHelper());
}else{ }else{
gameEngine.dashboard.enable(); engine.dashboard.enable();
} }
this.resize(); this.resize();
//gameEngine.setCamera(gameEngine.orthographicCamera)
//gameEngine.setCameraOrthographic(); engine.tm?.setGame(this.object?.id);
//engine.setCamera(engine.orthographicCamera)
//engine.setCameraOrthographic();
if (!this.scenario) { if (!this.scenario) {
await this.loadScenario(); await this.loadScenario();
} }
@@ -35,8 +37,9 @@ export default {
unmounted(){ unmounted(){
window.removeEventListener('resize', this.resize); window.removeEventListener('resize', this.resize);
gameEngine.clearScene(); engine.clearScene();
gameEngine.stop(); engine.stop();
engine.tm?.setGame(null);
}, },
computed:{ computed:{
@@ -69,26 +72,26 @@ export default {
await this.loadEnvironment(n, this.object.scenes[n.data.id]); await this.loadEnvironment(n, this.object.scenes[n.data.id]);
}, },
mode(n){ mode(n){
gameEngine.transformControls.setMode(n) engine.transformControls.setMode(n)
}, },
async 'object.scenario'(n){ async 'object.scenario'(n){
await this.loadScenario() await this.loadScenario()
}, },
currentObject(n){ currentObject(n){
if (this.env == 'GameDesigner'){ if (this.env == 'GameDesigner'){
gameEngine.transformControls.attach(n.__o); engine.transformControls.attach(n.__o);
gameEngine.gizmo.target = n.__o.position; engine.gizmo.target = n.__o.position;
gameEngine.camera.updateProjectionMatrix() engine.camera.updateProjectionMatrix()
} }
}, },
renderType(v){ renderType(v){
gameEngine.renderType = v; engine.renderType = v;
}, },
cameraType(v){ cameraType(v){
if (v == 'perspective'){ if (v == 'perspective'){
gameEngine.setCameraPerspective(); engine.setCameraPerspective();
}else{ }else{
gameEngine.setCameraOrthographic(); engine.setCameraOrthographic();
} }
} }
}, },
@@ -122,46 +125,47 @@ export default {
* @param target Target scene definition from Game Module * @param target Target scene definition from Game Module
*/ */
async loadEnvironment(scene, target){ async loadEnvironment(scene, target){
//await gameEngine.loadPanorama(`/asset/default/43.webp`); //await engine.loadPanorama(`/asset/default/43.webp`);
let intro; let intro;
gameEngine.clearScene(); engine.clearScene();
gameEngine.activeObjects.visible = false; engine.activeObjects.visible = false;
await gameEngine.dashboard.ready; await engine.dashboard.ready;
gameEngine.dashboard?.initScene(scene, async ()=>{ engine.dashboard?.initScene(scene, async ()=>{
if (this.scene.data.$audio){ if (this.scene.data.$audio){
await gameEngine.playAmbientSound(this.scene.data.$audio.asset.name); await engine.playAmbientSound(this.scene.data.$audio.asset.name);
gameEngine.ambientSound.setVolume( 0.5 ); engine.ambientSound.setVolume( 0.5 );
} }
engine.tm?.setScene(scene.data.id);
if (intro){ if (intro){
intro.play(); intro.play();
}else{ }else{
gameEngine.activeObjects.visible = true; engine.activeObjects.visible = true;
} }
}); });
await this.expandScenarioData(scene); 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 is needed cause when mounted canvas has different size
this.resize(); this.resize();
target.objects = target.objects || {}; target.objects = target.objects || {};
let l = target.objects; let l = target.objects;
if (this.scene.data.$environment){ 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){ 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); this.setObjectAttributes(l, this.scene.data, env.scene, env, null);
if (this.env != 'GameDesigner'){ if (this.env != 'GameDesigner'){
env.scene.traverse(o=>{ env.scene.traverse(o=>{
if (o.name.startsWith('land') || o.name == 'Sphere'){ if (o.name.startsWith('land') || o.name == 'Sphere'){
console.log('Fixing ground. TODO!!!') 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;; let expectToFinish = 0, finished = 0;;
if (this.scene.data.items){ if (this.scene.data.items){
@@ -172,25 +176,25 @@ export default {
i.data.shouldBeLocked = true; i.data.shouldBeLocked = true;
} }
} }
let io = await new InteractiveObject(gameEngine, i.data) let io = await new InteractiveObject(engine, i.data)
i.__io = io; i.__io = io;
if (io.emits?.includes('finish')){ if (io.emits?.includes('finish')){
expectToFinish++; expectToFinish++;
} }
this.setObjectAttributes(l, i.data, io.object, io.source, 1); 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 (this.env != 'GameDesigner'){
if (i.data.$go?.type == 'player3d'){ if (i.data.$go?.type == 'player3d'){
let hero = new Hero(gameEngine, io); let hero = new Hero(engine, io);
}else{ }else{
if (io.source?.animations?.length){ 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){ if (!i.data.noPhysics){
let bb = getBoundingBox(io.object); let bb = getBoundingBox(io.object);
let bbs = getBoundingBoxSize(bb); 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 radius: Math.max(bbs.x, bbs.z)/2, halfHeight: bbs.y/2
}) })
} }
@@ -198,7 +202,7 @@ export default {
io.addEventListener('finish', ()=>{ io.addEventListener('finish', ()=>{
finished ++; finished ++;
if (!i.data.pointsGiven){ if (!i.data.pointsGiven){
gameEngine.dashboard?.addPoints(i.data.points) engine.dashboard?.addPoints(i.data.points)
} }
i.data.pointsGiven = true; i.data.pointsGiven = true;
this.scene.data.items.forEach(di=>{ this.scene.data.items.forEach(di=>{
@@ -207,7 +211,7 @@ export default {
di.data.activationTriggers.splice(idx, 1); di.data.activationTriggers.splice(idx, 1);
} }
if (!di.data.activationTriggers?.length && di.__io.object.__active === false && 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(); di.__io.activator.activate();
} }
}); });
@@ -215,35 +219,36 @@ export default {
//GO TO NEXT LEVEL //GO TO NEXT LEVEL
console.log('LEVEL FINISHED') console.log('LEVEL FINISHED')
} }
engine.tm?.setGameObject(null);
}); });
io.addEventListener('sceneSwitch', (e)=>{ io.addEventListener('sceneSwitch', (e)=>{
this.scenesList = [this.scenes.find(s=>s.data.id == e.scene)] this.scenesList = [this.scenes.find(s=>s.data.id == e.scene)]
}) })
} }
loaded += 1/this.scene.data.items.length 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'){ if (this.env == 'GameDesigner'){
gameEngine.activeObjects.visible = true; engine.activeObjects.visible = true;
}else if (this.scene.data.$intro){ }else if (this.scene.data.$intro){
intro = await new VideoPlayer(gameEngine, { intro = await new VideoPlayer(engine, {
$go: this.scene.data.$intro, $go: this.scene.data.$intro,
skipTransition: true, skipTransition: true,
playInHud: true, playInHud: true,
hideBackground: true hideBackground: true
}); });
gameEngine.activeObjects.add(intro.object); engine.activeObjects.add(intro.object);
intro.video.addEventListener('pause',()=>{ intro.video.addEventListener('pause',()=>{
intro.object.removeFromParent(); intro.object.removeFromParent();
gameEngine.clickable.remove(intro.object); //TODO!!!! engine.clickable.remove(intro.object); //TODO!!!!
gameEngine.activeObjects.visible = true; engine.activeObjects.visible = true;
}); });
} }
gameEngine.dashboard?.loading(1) engine.dashboard?.loading(1)
gameEngine.physics.start(); engine.physics.start();
}, },
targetPointerDown(){ targetPointerDown(){
@@ -252,24 +257,24 @@ export default {
targetClick(e){ targetClick(e){
if (this.env == 'GameDesigner'){ if (this.env == 'GameDesigner'){
if (performance.now() - this.pointerDownTime < 200){ 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) //console.log(intersects)
if (intersects.length){ if (intersects.length){
//console.log('attaching controls to', intersects[0].object) //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]) //console.log(this.sceneObjects[intersects[0].object.__pn_id])
this.objectsList[0] = this.sceneObjects[intersects[0].object.__pn_id] this.objectsList[0] = this.sceneObjects[intersects[0].object.__pn_id]
}else{ }else{
gameEngine.transformControls.detach(); engine.transformControls.detach();
} }
} }
}else{ }else{
gameEngine.onClick(e, this.$refs.target); engine.onClick(e, this.$refs.target);
} }
}, },
targetPointer(e, t){ targetPointer(e, t){
gameEngine.onPointer(e, this.$refs.target, t); engine.onPointer(e, this.$refs.target, t);
}, },
setObjectAttributes(l, data, object, source, autoScaleFactor = 1){ setObjectAttributes(l, data, object, source, autoScaleFactor = 1){
@@ -292,21 +297,21 @@ export default {
async toggleAnimation(animation){ async toggleAnimation(animation){
animation.playing = !animation.playing; animation.playing = !animation.playing;
gameEngine.playAnimation(gameEngine.scene, animation.a, animation.playing); engine.playAnimation(engine.scene, animation.a, animation.playing);
}, },
resize(){ resize(){
let r = this.$refs.target; let r = this.$refs.target;
console.log('resizing!!', r.clientWidth, r.clientHeight, r) 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); //this.zoom = Math.min(r.clientWidth / this.viewBox.w, r.clientHeight / this.viewBox.h);
}, },
control(){ control(){
gameEngine.hero.lockControls(); engine.hero.lockControls();
}, },
async fullScreen(){ async fullScreen(){
await gameEngine.renderer.domElement.requestFullscreen() await engine.renderer.domElement.requestFullscreen()
} }
} }
} }