#72 epic refactoring

This commit is contained in:
2026-03-21 17:45:27 +02:00
parent da326ddf96
commit 8dd88174af
25 changed files with 665 additions and 672 deletions
@@ -23,7 +23,7 @@
<script> <script>
import { GameEngine } from '@/lib/GameEngine.js'; import { GameEngine } from '@/lib/GameEngine.js';
import { autoScale } from '@/lib/MeshUtils'; import { autoScale } from '@/lib/MeshUtils';
let gameEngine = null; let engine = null;
export default{ export default{
props:{ props:{
@@ -45,8 +45,8 @@ export default{
} }
}, },
beforeUnmount() { beforeUnmount() {
gameEngine?.dispose(); engine?.dispose();
gameEngine = null; engine = null;
}, },
watch:{ watch:{
object(n){ object(n){
@@ -54,8 +54,8 @@ export default{
}, },
async obj(){ async obj(){
if (!this.obj) return; if (!this.obj) return;
gameEngine = new GameEngine(); engine = new GameEngine();
await gameEngine.init(this.$refs.target, { await engine.init(this.$refs.target, {
gizmo: true, gizmo: true,
xr: true, xr: true,
mode: 'ObjectPreview' mode: 'ObjectPreview'
@@ -71,41 +71,45 @@ export default{
methods:{ methods:{
async loadAsset() { async loadAsset() {
if (this.forRendering) { if (this.forRendering) {
gameEngine.resetScene(); engine.resetScene();
if (this.obj.type == 'panorama2d') { if (this.obj.type == 'panorama2d') {
await gameEngine.loadPanorama(this.obj.asset.name); await engine.loadPanorama(this.obj.asset.name);
// let t = await gameEngine.loadTexture(`/asset/default/${this.obj.asset.name}`); // let t = await engine.loadTexture(`/asset/default/${this.obj.asset.name}`);
// t.mapping = gameEngine.$.EquirectangularReflectionMapping; // t.mapping = engine.$.EquirectangularReflectionMapping;
// gameEngine.scene.background = t; // engine.scene.background = t;
// gameEngine.scene.environment = t; // engine.scene.environment = t;
// gameEngine.scene.add(gameEngine.camera); // engine.scene.add(engine.camera);
} else { } else {
let gltf = await gameEngine.load(this.obj.asset.name); let gltf = await engine.load(this.obj.asset.name);
//console.debug('GLTF', gltf); //console.debug('GLTF', gltf);
this.loadedAsset = gltf; this.loadedAsset = gltf;
this.animations = gltf.animations.map(a => ({ this.animations = gltf.animations.map(a => ({
name: a.name, id: a.uuid name: a.name, id: a.uuid
})); }));
autoScale(gltf.scene); autoScale(gltf.scene);
let bb = new gameEngine.$.Box3().setFromObject(gltf.scene); let bb = new engine.$.Box3().setFromObject(gltf.scene);
//console.log(bb) //console.log(bb)
gameEngine.camera.position.set(bb.max.x, bb.max.y, bb.max.z); engine.camera.position.set(bb.max.x, bb.max.y, bb.max.z);
gameEngine.orbitControls.target.set((bb.max.x + bb.min.x) / 2, (bb.max.y + bb.min.y) / 2, (bb.max.z + bb.min.z) / 2) engine.orbitControls.target.set((bb.max.x + bb.min.x) / 2, (bb.max.y + bb.min.y) / 2, (bb.max.z + bb.min.z) / 2)
gameEngine.orbitControls.update(); engine.orbitControls.update();
gameEngine.activeObjects.add(gltf.scene); engine.activeObjects.add(gltf.scene);
//gameEngine.scene.add(gameEngine.light); //engine.scene.add(engine.light);
} }
} }
}, },
async toggleAnimation(animation){ async toggleAnimation(animation){
animation.playing = !animation.playing; animation.playing = !animation.playing;
gameEngine.playAnimation( engine.playAnimation(
gameEngine.scene, engine.scene,
this.loadedAsset.animations.find(a=>a.uuid == animation.id), this.loadedAsset.animations.find(a=>a.uuid == animation.id),
animation.playing animation.playing
); );
}, },
async captureScreenshot(){
return await engine.captureScreenshot();
}
} }
} }
</script> </script>
+124 -24
View File
@@ -14,23 +14,23 @@
<v-btn value="perspective" icon="mdi-cone"></v-btn> <v-btn value="perspective" icon="mdi-cone"></v-btn>
<v-btn value="orthographic" icon="mdi-cone-off"></v-btn> <v-btn value="orthographic" icon="mdi-cone-off"></v-btn>
</v-btn-toggle> </v-btn-toggle>
<template v-if="currentObject?.__o"> <div v-if="currentObject" :version="version">
<v-card subtitle="Position" class="ma-1" color="light-blue-darken-4"> <v-card subtitle="Position" class="ma-1" color="light-blue-darken-4">
<v-number-input :precision="null" controlVariant="stacked" hide-details density="compact" label="x" v-model="currentObject.__o.position.x"></v-number-input> <v-number-input :precision="null" controlVariant="stacked" hide-details density="compact" label="x" v-model="currentObject.position.x"></v-number-input>
<v-number-input :precision="null" controlVariant="stacked" hide-details density="compact" label="y" v-model="currentObject.__o.position.y"></v-number-input> <v-number-input :precision="null" controlVariant="stacked" hide-details density="compact" label="y" v-model="currentObject.position.y"></v-number-input>
<v-number-input :precision="null" controlVariant="stacked" hide-details density="compact" label="z" v-model="currentObject.__o.position.z"></v-number-input> <v-number-input :precision="null" controlVariant="stacked" hide-details density="compact" label="z" v-model="currentObject.position.z"></v-number-input>
</v-card> </v-card>
<v-card subtitle="Rotation" class="ma-1" color="green-darken-4"> <v-card subtitle="Rotation" class="ma-1" color="green-darken-4">
<v-number-input :precision="null" controlVariant="stacked" hide-details density="compact" label="x" v-model="currentObject.__o.rotation.x"></v-number-input> <v-number-input :precision="null" controlVariant="stacked" hide-details density="compact" label="x" v-model="currentObject.rotation.x"></v-number-input>
<v-number-input :precision="null" controlVariant="stacked" hide-details density="compact" label="y" v-model="currentObject.__o.rotation.y"></v-number-input> <v-number-input :precision="null" controlVariant="stacked" hide-details density="compact" label="y" v-model="currentObject.rotation.y"></v-number-input>
<v-number-input :precision="null" controlVariant="stacked" hide-details density="compact" label="z" v-model="currentObject.__o.rotation.z"></v-number-input> <v-number-input :precision="null" controlVariant="stacked" hide-details density="compact" label="z" v-model="currentObject.rotation.z"></v-number-input>
</v-card> </v-card>
<v-card subtitle="Scale" class="ma-1" color="pink-darken-4"> <v-card subtitle="Scale" class="ma-1" color="pink-darken-4">
<v-number-input :precision="null" controlVariant="stacked" hide-details density="compact" label="x" v-model="currentObject.__o.scale.x"></v-number-input> <v-number-input :precision="null" controlVariant="stacked" hide-details density="compact" label="x" v-model="currentObject.scale.x"></v-number-input>
<v-number-input :precision="null" controlVariant="stacked" hide-details density="compact" label="y" v-model="currentObject.__o.scale.y"></v-number-input> <v-number-input :precision="null" controlVariant="stacked" hide-details density="compact" label="y" v-model="currentObject.scale.y"></v-number-input>
<v-number-input :precision="null" controlVariant="stacked" hide-details density="compact" label="z" v-model="currentObject.__o.scale.z"></v-number-input> <v-number-input :precision="null" controlVariant="stacked" hide-details density="compact" label="z" v-model="currentObject.scale.z"></v-number-input>
</v-card> </v-card>
</template> </div>
</v-navigation-drawer> </v-navigation-drawer>
<div class="container my-3 position-relative game-designer-canvas"> <div class="container my-3 position-relative game-designer-canvas">
<div ref="target" @click="targetClick" @pointerdown="targetPointerDown"></div> <div ref="target" @click="targetClick" @pointerdown="targetPointerDown"></div>
@@ -47,11 +47,9 @@
</v-slide-group> </v-slide-group>
</v-toolbar> </v-toolbar>
<v-navigation-drawer location="right"> <v-navigation-drawer location="right">
<v-list v-model:selected="scenesList" selectable color="primary"> <v-list selectable color="primary" @update:selected="loadScene($event[0])" :items="scenes"></v-list>
<v-list-item v-for="(s, i) in scenes" :key="i" :title="s.data.title" :value="s"></v-list-item> <v-list selectable @update:selected="selectObject($event[0])" color="secondary">
</v-list> <v-list-item v-for="(v, k) in sceneObjects" :title="v.__title" :subtitle="k" :value="k">
<v-list selectable v-model:selected="objectsList" color="secondary">
<v-list-item v-for="(v, k) in sceneObjects" :title="v.__title" :subtitle="k" :value=v>
<template v-slot:prepend> <template v-slot:prepend>
<v-btn variant="plain" density="comfortable" size="small" v-if="v.__o" <v-btn variant="plain" density="comfortable" size="small" v-if="v.__o"
:icon="`mdi-eye${v.__o.visible ? '' : '-off'}`" :icon="`mdi-eye${v.__o.visible ? '' : '-off'}`"
@@ -64,35 +62,137 @@
</template> </template>
<script> <script>
import { GameEngine } from '@/lib/GameEngine';
import { GameManager } from '@/lib/GameManager';
import { markRaw } from 'vue';
import GameEnvironmentMixin from '@/mixins/GameEnvironmentMixin'; let engine = null, manager = null;
export default { export default {
mixins: [GameEnvironmentMixin],
props:{ props:{
modelValue: Object, modelValue: Object,
}, },
data(){ data(){
return { return {
env: 'GameDesigner', env: 'GameDesigner',
scenesList: [], scenes: [],
objectsList: [], currentScene: null,
sceneObjects: {},
currentObject: null,
objectAnimations: [],
mode: 'translate', mode: 'translate',
pointerDownTime: 0, pointerDownTime: 0,
scenario: null,
renderType: 'ST', renderType: 'ST',
cameraType: 'perspective' cameraType: 'perspective',
version: 0
} }
}, },
async mounted(){
engine = new GameEngine();
await engine.init(this.$refs.target, {
xr: true,
gizmo: true,
stats: true,
mode: 'GameDesigner'
});
manager = await new GameManager(engine, this.modelValue, null, {
onObjectLoad:(sceneObjects, data, object3d, source)=>{
['position', 'scale', 'rotation', 'visible'].forEach(p=>{
sceneObjects[data.id][p] = object3d[p];
})
sceneObjects[data.id].__o = markRaw(object3d);
sceneObjects[data.id].__animations = markRaw(source.animations || []);
sceneObjects[data.id].__title = data.title;
}
});
this.scenes = manager.scenarioData.scenes.map(s=>({
title: s.data.title,
value: s.data.id
}))
window.addEventListener('resize', this.resize);
},
async unmounted(){
this.debug('Disposing scene')
window.removeEventListener('resize', this.resize);
engine.transformControls.removeEventListener('change', this.update)
engine.tm?.setGame(null);
engine.dispose();
this.debug('Disposed scene', JSON.stringify(engine.renderer.info.memory));
engine = null;
manager = null;
},
watch:{ watch:{
async 'object.scenario'(n){ mode(n){
await this.loadScenario() engine.transformControls.setMode(n)
}, },
renderType(v){
engine.renderType = v;
},
cameraType(v){
if (v == 'perspective'){
engine.setCameraPerspective();
}else{
engine.setCameraOrthographic();
}
}
}, },
methods:{ methods:{
async loadScene(sceneId){
engine.transformControls.removeEventListener('change', this.update)
await manager.loadScene(sceneId)
this.currentScene = manager.scenarioData.scenes.find(sc=>sc.data.id == sceneId);
this.sceneObjects = this.modelValue.scenes?.[sceneId]?.objects
engine.transformControls.addEventListener('change', this.update)
},
selectObject(oid){
this.currentObject = this.sceneObjects[oid];
engine.transformControls.attach(this.currentObject.__o);
engine.gizmo.target = this.currentObject.__o.position;
engine.camera.updateProjectionMatrix();
this.objectAnimations = this.currentObject?.__animations?.map(a => ({
name: a.name, id: a.uuid, a
}));
},
targetClick(e){
if (performance.now() - this.pointerDownTime < 200){
let intersects = engine.intersect(e, this.$refs.target, engine.activeObjects.children, true);
if (intersects.length){
this.selectObject(intersects[0].object.__pn_id)
}else{
engine.transformControls.detach();
}
}
},
targetPointerDown(){
this.pointerDownTime = performance.now();
},
targetPointer(e, t){
engine.onPointer(e, this.$refs.target, t);
},
async toggleAnimation(animation){
animation.playing = !animation.playing;
engine.playAnimation(engine.scene, animation.a, animation.playing);
},
resize(){
let r = this.$refs.target;
this.debug('resizing!!', r.clientWidth, r.clientHeight, r)
engine.resize(r.clientWidth, r.clientHeight);
//this.zoom = Math.min(r.clientWidth / this.viewBox.w, r.clientHeight / this.viewBox.h);
},
update(){
this.version ++;
}
} }
} }
</script> </script>
+44 -31
View File
@@ -6,44 +6,57 @@
<div ref="target" @click="targetClick" class="canvas-wrapper" <div ref="target" @click="targetClick" class="canvas-wrapper"
@mousedown="targetPointer($event, 'start')" @mousedown="targetPointer($event, 'start')"
@mousemove="targetPointer($event, 'drag')" @mousemove="targetPointer($event, 'drag')"
@mouseup="targetPointer($event, 'end')" @mouseup="targetPointer($event, 'end')"></div>
@pointerdown="targetPointerDown"></div>
</div> </div>
<video class="d-none" src="" ref="videoPlayer"></video>
</template> </template>
<script> <script>
import { GameEngine } from '@/lib/GameEngine';
import { useAppStore } from '@/stores/app'; import { GameManager } from '@/lib/GameManager';
let engine = null, manager = null;
import GameEnvironmentMixin from '@/mixins/GameEnvironmentMixin';
const store = useAppStore();
export default { export default {
mixins:[GameEnvironmentMixin], props:['id'],
props:{ async mounted(){
modelValue: Object, engine = new GameEngine();
await engine.init(this.$refs.target, {
xr: true,
mode: 'GamePlay'
});
manager = await new GameManager(engine, this.id);
window.addEventListener('resize', this.resize);
manager.loadScene(manager.scenarioData.scenes[0].data.id)
}, },
watch:{
scenario(n){ async unmounted(){
this.debug('Scenario changed', n); this.debug('Disposing scene')
if (n){ window.removeEventListener('resize', this.resize);
this.scenesList = [this.scenes?.[0]]; engine.tm?.setGame(null);
} engine.dispose();
this.debug('Disposed scene', JSON.stringify(engine.renderer.info.memory));
engine = null;
manager = null;
},
methods:{
targetClick(e){
engine.onClick(e, this.$refs.target);
},
targetPointer(e, t){
engine.onPointer(e, this.$refs.target, t);
},
resize(){
let r = this.$refs.target;
this.debug('resizing', r.clientWidth, r.clientHeight, r)
engine.resize(r.clientWidth, r.clientHeight);
},
async fullScreen(){
await engine.renderer.domElement.requestFullscreen()
} }
}, }
data(){
return {
env: 'GamePlay',
scenesList: [],
objectsList: [],
pointerDownTime: 0,
scenario: null,
renderType: 'ST',
cameraType: 'perspective',
store
}
},
} }
</script> </script>
+80 -43
View File
@@ -1,61 +1,98 @@
<template> <template>
<v-navigation-drawer width="133">
<v-btn-toggle variant="tonal" density="comfortable" class="ma-3" v-model="renderType" color="light-blue-darken-4">
<v-btn value="ST" icon="mdi-video-3d-variant"></v-btn>
<v-btn value="VR" icon="mdi-google-cardboard"></v-btn>
<v-btn value="AG" icon="mdi-glasses"></v-btn>
</v-btn-toggle>
<v-btn-toggle variant="tonal" density="comfortable" class="ma-3" v-model="mode" color="green-darken-4">
<v-btn class="text-none" value="translate" icon="mdi-cursor-move"></v-btn>
<v-btn class="text-none" value="rotate" icon="mdi-rotate-orbit"></v-btn>
<v-btn class="text-none" value="scale" icon="mdi-resize"></v-btn>
</v-btn-toggle>
<v-btn-toggle variant="tonal" v-model="cameraType" class="ma-3" density="comfortable" color="orange-darken-4">
<v-btn value="perspective" icon="mdi-cone"></v-btn>
<v-btn value="orthographic" icon="mdi-cone-off"></v-btn>
</v-btn-toggle>
<v-btn-toggle variant="tonal" v-model="store.prefs.xr.depthSense" class="ma-3" density="comfortable" color="green-darken-2">
<v-btn :value="true" icon="mdi-cube-outline"></v-btn>
</v-btn-toggle>
<v-divider class="my-2"></v-divider>
<v-btn variant="tonal" icon="mdi-walk" @click="control" v-tooltip="`Pointer lock controls mode`" class="ma-1" size="small"></v-btn>
<v-btn variant="tonal" icon="mdi-image-frame" @click="setGameHeader" v-tooltip="`Capture screenshot as game header image`" class="ma-1" size="small"></v-btn>
<v-btn icon="mdi-fullscreen" @click="fullScreen" variant="tonal" v-tooltip="l.fullScreen" size="small" class="ma-1" ></v-btn>
</v-navigation-drawer>
<div class="container my-3 position-relative game-designer-canvas"> <div class="container my-3 position-relative game-designer-canvas">
<div ref="target" @click="targetClick" class="canvas-wrapper" <div ref="target" @click="targetClick" class="canvas-wrapper"
@mousedown="targetPointer($event, 'start')" @mousedown="targetPointer($event, 'start')"
@mousemove="targetPointer($event, 'drag')" @mousemove="targetPointer($event, 'drag')"
@mouseup="targetPointer($event, 'end')" @mouseup="targetPointer($event, 'end')" ></div>
@pointerdown="targetPointerDown"></div>
</div> </div>
<v-navigation-drawer location="right"> <v-navigation-drawer width="133" rail location="right" class="mt-3">
<v-list v-model:selected="scenesList" selectable color="primary"> <v-menu>
<v-list-item v-for="(s, i) in scenes" :key="i" :title="s.data.title" :value="s"></v-list-item> <template v-slot:activator="{ props }">
</v-list> <v-btn icon="mdi-panorama-outline" color="primary" v-bind="props" v-tooltip="'Select scene'"></v-btn>
</template>
<v-list selectable color="primary" @update:selected="loadScene($event[0])" :items="scenes"></v-list>
</v-menu>
<v-divider class="my-2"></v-divider>
<v-btn variant="text" icon="mdi-walk" @click="control" v-tooltip="`Pointer lock controls mode`"></v-btn>
<v-btn variant="text" icon="mdi-image-frame" @click="setGameHeader" v-tooltip="`Capture screenshot as game header image`"></v-btn>
<v-btn icon="mdi-fullscreen" @click="fullScreen" variant="text" v-tooltip="l.fullScreen"></v-btn>
<v-btn-toggle variant="text" v-model="store.prefs.xr.depthSense" color="green-darken-2" v-tooltip="`Toggle XR depth sense`">
<v-btn :value="true" icon="mdi-cube-outline"></v-btn>
</v-btn-toggle>
</v-navigation-drawer> </v-navigation-drawer>
<video class="d-none" src="" ref="videoPlayer"></video>
</template> </template>
<script> <script>
import GameEnvironmentMixin from '@/mixins/GameEnvironmentMixin'; import { GameEngine } from '@/lib/GameEngine';
import { GameManager } from '@/lib/GameManager';
let engine = null, manager = null;
export default { export default {
mixins:[GameEnvironmentMixin], props:['id'],
props:{
modelValue: Object,
},
data(){ data(){
return { return {
env: 'GamePreview', scenes: []
scenesList: [], }
objectsList: [], },
mode: 'translate', async mounted(){
pointerDownTime: 0, engine = new GameEngine();
scenario: null, await engine.init(this.$refs.target, {
renderType: 'ST', xr: true,
cameraType: 'perspective', gizmo: false,
stats: true,
depthSense: this.store.prefs.xr.depthSense,
mode: 'GamePreview'
});
manager = await new GameManager(engine, this.id);
this.scenes = manager.scenarioData.scenes.map(s=>({
title: s.data.title,
value: s.data.id
}))
window.addEventListener('resize', this.resize);
},
async unmounted(){
this.debug('Disposing scene')
window.removeEventListener('resize', this.resize);
engine.tm?.setGame(null);
engine.dispose();
this.debug('Disposed scene', JSON.stringify(engine.renderer.info.memory));
engine = null;
manager = null;
},
methods:{
loadScene(sceneId){
manager.loadScene(sceneId)
},
targetClick(e){
engine.onClick(e, this.$refs.target);
},
targetPointer(e, t){
engine.onPointer(e, this.$refs.target, t);
},
resize(){
let r = this.$refs.target;
this.debug('resizing', r.clientWidth, r.clientHeight, r)
engine.resize(r.clientWidth, r.clientHeight);
},
control(){
engine.pointerControls.lock(true);
},
async fullScreen(){
await engine.renderer.domElement.requestFullscreen()
},
async setGameHeader(){
let screenshot = await engine.captureScreenshot();
let fd = new FormData();
fd.append('file', screenshot);
await this.$api.game.setHeader(this.modelValue.id, fd);
} }
} }
} }
</script> </script>
@@ -1,6 +1,6 @@
import { Color, Group, DoubleSide, RepeatWrapping, MeshStandardMaterial, VideoTexture } from "three" import { Color, Group, DoubleSide, RepeatWrapping, MeshStandardMaterial, VideoTexture } from "three"
import { EventManager } from '@/lib/EventManager'; import { EventManager } from '@/lib/EventManager';
import { centerOrigin } from "@/lib/MeshUtils"; import { centerOrigin, clearMaterial, clearObject } from "@/lib/MeshUtils";
import Utils from "#/app/Utils"; import Utils from "#/app/Utils";
class ClassicPuzzle extends EventManager { class ClassicPuzzle extends EventManager {
@@ -20,7 +20,7 @@ class ClassicPuzzle extends EventManager {
vi.addEventListener('loadedmetadata', async ()=>{ vi.addEventListener('loadedmetadata', async ()=>{
map = new VideoTexture( vi ); map = new VideoTexture( vi );
resolve(); resolve();
}); }, { once: true }); //'once' is needed in order to avoid memory leaks (the listener gets removed after called)
}) })
}else{ }else{
map = await engine.loadTexture(data.$go.asset.name); map = await engine.loadTexture(data.$go.asset.name);
@@ -101,6 +101,12 @@ class ClassicPuzzle extends EventManager {
engine.draggable.remove(done0) engine.draggable.remove(done0)
container.add(dragZone); container.add(dragZone);
this.object = centerOrigin(container); this.object = centerOrigin(container);
this.dispose = () => {
//console.log('disposing!!!!!!!')
clearMaterial(defaultMaterial);
clearObject(gltf.scene);
super.dispose();
}
resolve(this); resolve(this);
}) })
} }
@@ -92,23 +92,15 @@ class InteractiveObject extends EventManager{
if (obj.distance) { if (obj.distance) {
engine.hideIfFar(this.object, obj.distance) engine.hideIfFar(this.object, obj.distance)
// const o = this.object;
// let dstm = obj.distance;
// let v = new Vector3();
// o.visible = false;
// engine.addEventListener('beforeRender', function () {
// o.getWorldPosition(v);
// var dst = engine.cameraWorld.position.distanceTo(v);
// if (dst <= dstm && !o.visible) {
// o.visible = true;
// }else if (dst > dstm && o.visible){
// o.visible = false;
// }
// });
} }
this.object.userData._io = this;
resolve(this); resolve(this);
}); });
} }
dispose(){
super.dispose();
this.io?.dispose?.();
}
} }
class VisibilityActivator{ class VisibilityActivator{
@@ -1,6 +1,6 @@
import { BoxGeometry, Mesh, MeshStandardMaterial, Group, Vector3, CatmullRomCurve3, Color } from 'three'; import { BoxGeometry, Mesh, MeshStandardMaterial, Group, Vector3, CatmullRomCurve3, Color } from 'three';
import { LineMaterial, LineGeometry, Line2 } from 'three/examples/jsm/Addons.js'; import { LineMaterial, LineGeometry, Line2 } from 'three/examples/jsm/Addons.js';
import { centerOrigin } from '@/lib/MeshUtils'; import { centerOrigin, clearMaterial } from '@/lib/MeshUtils';
import { EventManager } from '@/lib/EventManager'; import { EventManager } from '@/lib/EventManager';
import Utils from '#/app/Utils'; import Utils from '#/app/Utils';
@@ -111,6 +111,12 @@ class PairMatchingGame extends EventManager {
this.object = centerOrigin(container); this.object = centerOrigin(container);
this.dispose = () => {
clearMaterial(material);
bm.dispose();
super.dispose();
}
resolve(this); resolve(this);
}) })
} }
@@ -36,7 +36,7 @@ class Particles {
gg.translate(position.x, position.y, position.z); gg.translate(position.x, position.y, position.z);
arr.push(gg); arr.push(gg);
var gg = geometry.clone(); gg = geometry.clone();
gg.rotateY(angle + Math.PI); gg.rotateY(angle + Math.PI);
gg.translate(position.x, position.y, position.z); gg.translate(position.x, position.y, position.z);
arr.push(gg); arr.push(gg);
@@ -1,5 +1,5 @@
import { BoxGeometry, Mesh, MeshBasicMaterial, Group, VideoTexture } from 'three'; import { BoxGeometry, Mesh, MeshBasicMaterial, Group, VideoTexture } from 'three';
import { centerOrigin } from '@/lib/MeshUtils'; import { centerOrigin, clearMaterial } from '@/lib/MeshUtils';
import { EventManager } from '@/lib/EventManager'; import { EventManager } from '@/lib/EventManager';
class PuzzleGame1 extends EventManager { class PuzzleGame1 extends EventManager {
@@ -24,7 +24,7 @@ class PuzzleGame1 extends EventManager {
vi.addEventListener('loadedmetadata', async ()=>{ vi.addEventListener('loadedmetadata', async ()=>{
map = new VideoTexture( vi ); map = new VideoTexture( vi );
resolve(); resolve();
}); }, { once: true });
}) })
}else{ }else{
map = await engine.loadTexture(data.$go.asset.name); map = await engine.loadTexture(data.$go.asset.name);
@@ -73,9 +73,9 @@ class PuzzleGame1 extends EventManager {
container.add(mesh); container.add(mesh);
} }
container.children[0].onBeforeRender = () => { // container.children[0].onBeforeRender = () => {
this.update(); // this.update();
}; // };
var check = () => { var check = () => {
let i = 0; let i = 0;
@@ -121,8 +121,17 @@ class PuzzleGame1 extends EventManager {
} }
}; };
engine.addEventListener('beforeRender', this.update)
this.object = centerOrigin(container); this.object = centerOrigin(container);
this.dispose = () => {
//console.log('disposing PG1')
clearMaterial(material);
bm.dispose();
super.dispose();
}
resolve(this); resolve(this);
}) })
} }
@@ -17,7 +17,7 @@ class PuzzleGame2 extends EventManager {
vi.addEventListener('loadedmetadata', async ()=>{ vi.addEventListener('loadedmetadata', async ()=>{
map = new VideoTexture( vi ); map = new VideoTexture( vi );
resolve(); resolve();
}); }, { once: true });
}) })
}else{ }else{
map = await engine.loadTexture(data.$go.asset.name); map = await engine.loadTexture(data.$go.asset.name);
@@ -109,9 +109,9 @@ class PuzzleGame2 extends EventManager {
} }
last = container.children[lidx]; last = container.children[lidx];
container.children[0].onBeforeRender = () => { // container.children[0].onBeforeRender = () => {
this.update(); // this.update();
}; // };
this.shuffle(); this.shuffle();
@@ -164,6 +164,8 @@ class PuzzleGame2 extends EventManager {
//engine.dashboard.addPoints(10); //engine.dashboard.addPoints(10);
} }
}; };
engine.addEventListener('beforeRender', this.update)
this.object = centerOrigin(container) this.object = centerOrigin(container)
resolve(this) resolve(this)
}); });
@@ -45,9 +45,10 @@ var PuzzleGame4 = function(context, gltf, w, h){
context.clickable.add(c, clickFn); context.clickable.add(c, clickFn);
}) })
this.object.children[0].onBeforeRender = ()=>{ // this.object.children[0].onBeforeRender = ()=>{
this.update(); // this.update();
} // }
engine.addEventListener('beforeRender', this.update)
}); });
var check = ()=>{ var check = ()=>{
@@ -77,7 +77,7 @@ class VideoPlayer extends EventManager {
} }
resolve(this); resolve(this);
}) }, { once: true })
vi.src = engine.assetPath + data.$go.asset.name; vi.src = engine.assetPath + data.$go.asset.name;
}) })
} }
+92
View File
@@ -0,0 +1,92 @@
import axios from 'axios';
import Utils from '#/app/Utils';
const $ax = axios.create({
baseURL: '/api/',
transformRequest: [
(data, headers)=>{
if (data && !(data instanceof FormData)){
data = Utils.deepMerge({}, data, (k, v)=>{
return k.startsWith('__') ? undefined : v;
})
data = JSON.stringify(data);
headers['Content-Type'] = 'application/json;charset=utf-8';
}
return data;
}
, ...axios.defaults.transformRequest
]
})
const api = {
gameObject:{
async save(data){
return await $ax.put('/game-object', data);
},
async load(id){
return await $ax.get(`/game-object/${id}`);
},
async search(query){
return await $ax.post('/game-object', query);
},
async remove(id){
return await $ax.delete(`/game-object/${id}`)
},
async getTags(q){
return await $ax.post('/game-object/tags', {q});
}
},
scenario:{
async save(data){
return await $ax.put('/scenario', data);
},
async load(id){
return await $ax.get(`/scenario/${id}`);
},
async search(query){
return await $ax.post('/scenario', query);
},
async remove(id){
return await $ax.delete(`/scenario/${id}`)
}
},
game:{
async save(data){
return await $ax.put('/game', data);
},
async load(id){
return await $ax.get(`/game/${id}`);
},
async search(query){
return await $ax.post('/game', query);
},
async remove(id){
return await $ax.delete(`/game/${id}`)
},
async setHeader(id, data){
return await $ax.post(`/game/${id}/header`, data);
}
},
user:{
async tm(action, object, data){
return await $ax.post('/user/tm', {action, object, data});
},
async signin(data){
return await $ax.post('/user/signin', data);
},
async signup(data){
return await $ax.post('/user/signup', data);
},
async signout(){
return await $ax.get('/user/signout');
},
async load(){
return await $ax.get('/user/info');
},
async update(data){
return await $ax.post('/user/update', data);
}
}
}
export { api };
+6
View File
@@ -16,6 +16,12 @@ class EventManager extends EventDispatcher{
removeAllListenersOfType(type){ removeAllListenersOfType(type){
this._listeners?.[ type ]?.splice(0, this._listeners[ type ].length) this._listeners?.[ type ]?.splice(0, this._listeners[ type ].length)
} }
removeAllListeners(){
this._listeners && Object.keys(this._listeners).forEach(k=>this.removeAllListenersOfType(k));
}
dispose(){
this.removeAllListeners();
}
} }
export { EventManager } export { EventManager }
+7 -43
View File
@@ -20,6 +20,7 @@ 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'; import { Telemetry } from './Telemetry.js';
import { clearObject } from './MeshUtils.js';
THREE.Cache.enabled = true THREE.Cache.enabled = true
@@ -181,10 +182,7 @@ class GameEngine extends EventManager{
this.motionQueue = new MotionEngine(); this.motionQueue = new MotionEngine();
this.assetPath = assetPath; this.assetPath = assetPath;
this.tm = new Telemetry(opts.mode);
if (opts.telemetry){
this.tm = new Telemetry(opts.telemetry, opts.mode);
}
// controls.enableDamping = true; // controls.enableDamping = true;
// controls.screenSpacePanning = true; // controls.screenSpacePanning = true;
@@ -662,44 +660,8 @@ class GameEngine extends EventManager{
]) ])
} }
clearObject(o){
let disposables = []
o.traverse(object => {
if (object.isMesh) {
disposables.push(object);
}
if (object.isLight){
object.shadow?.map?.dispose();
object.shadow?.dispose();
object.dispose();
}
});
disposables.forEach(object=>{
object.removeFromParent();
object.geometry.dispose();
if (object.material.isMaterial) {
this.clearMaterial(object.material)
} else {
for (const material of object.material) this.clearMaterial(material)
}
})
}
clearMaterial(material) {
material.dispose();
for (const key of Object.keys(material)) {
const value = material[key]
if (value && typeof value == 'object' && 'minFilter' in value) {
//console.log('Disposing', value.name, this.renderer.info.memory.textures );
value.dispose();
//console.log('Disposed', value.name, this.renderer.info.memory.textures );
}
}
}
clearScene(){ clearScene(){
this.hero?.dispose(); this.hero?.dispose();
this.dashboard?.reset();
this.transformControls?.dispose(); this.transformControls?.dispose();
this.pointerControls.dispose(); this.pointerControls.dispose();
//this.activeObjects.clear(); //this.activeObjects.clear();
@@ -709,7 +671,7 @@ class GameEngine extends EventManager{
this.gizmo?.dispose(); this.gizmo?.dispose();
this.motionQueue.clearAll(); this.motionQueue.clearAll();
this.ambientSound.stop(); this.ambientSound.stop();
this.loadedObjects.forEach(o=>this.clearObject(o.scene)) this.loadedObjects.forEach(o=>clearObject(o.scene))
GameEngine.loadedTextures.forEach(t=>{ GameEngine.loadedTextures.forEach(t=>{
//console.log('Disposing', t.name, this.renderer.info.memory.textures ); //console.log('Disposing', t.name, this.renderer.info.memory.textures );
t.dispose(); t.dispose();
@@ -717,7 +679,7 @@ class GameEngine extends EventManager{
}); });
this.scene.background?.dispose?.(); this.scene.background?.dispose?.();
this.scene.environment?.dispose?.(); this.scene.environment?.dispose?.();
this.clearObject(this.scene); clearObject(this.scene);
this.scene = null; this.scene = null;
this.tm?.setScene(null); this.tm?.setScene(null);
this.removeAllListenersOfType('beforeRender'); this.removeAllListenersOfType('beforeRender');
@@ -726,6 +688,7 @@ class GameEngine extends EventManager{
} }
async resetScene(){ async resetScene(){
this.dashboard?.reset();//moved from clearscene to resetscene, because an updateText sync callback waits infinitely after gameengine destroy
this.clearScene(); this.clearScene();
await this.initScene(); await this.initScene();
} }
@@ -741,14 +704,15 @@ class GameEngine extends EventManager{
this.stats?.dom?.remove(); this.stats?.dom?.remove();
this.renderer.domElement.removeEventListener('wheel', this._wheelEvent) this.renderer.domElement.removeEventListener('wheel', this._wheelEvent)
this.renderer.domElement.remove(); this.renderer.domElement.remove();
super.dispose();
//console.log('Engine Disposed', this.renderer.info.memory.textures ); //console.log('Engine Disposed', this.renderer.info.memory.textures );
} }
static textureLoader = new THREE.TextureLoader(); static textureLoader = new THREE.TextureLoader();
static audioLoader = new THREE.AudioLoader(); static audioLoader = new THREE.AudioLoader();
static draco = new DRACOLoader().setDecoderPath('/3rdparty/draco/'); static draco = new DRACOLoader().setDecoderPath('/3rdparty/draco/');
static gltfLoader = new GLTFLoader().setDRACOLoader(this.draco);
static ktxLoader = new KTX2Loader().setTranscoderPath( '/3rdparty/basis/' ); static ktxLoader = new KTX2Loader().setTranscoderPath( '/3rdparty/basis/' );
static gltfLoader = new GLTFLoader().setDRACOLoader(this.draco).setKTX2Loader(this.ktxLoader);
static loadedTextures = [] static loadedTextures = []
+186
View File
@@ -0,0 +1,186 @@
import { InteractiveObject } from "@/components/InteractiveObjects/InteractiveObject";
import { VideoPlayer } from '@/components/InteractiveObjects/VideoPlayer';
import { Hero } from "./Hero";
import { getBoundingBox, getBoundingBoxSize, autoScale } from "./MeshUtils";
import { api } from "./Api";
class GameManager{
constructor(engine, gameData, scenarioData, opts = {}){
return new Promise(async (resolve, reject)=>{
if (typeof gameData != 'object'){
gameData = (await api.game.load(gameData)).data;
}
if (!scenarioData){
scenarioData = (await api.scenario.load(gameData.scenario)).data;
}
this.gameData = gameData;
this.scenarioData = scenarioData;
this.opts = opts;
this.loadScene = async function(sceneId){
let scene = scenarioData.scenes.find(sc=>sc.data.id == sceneId);
let sceneObjects = gameData.scenes[sceneId].objects || {};
engine.tm?.setGame(gameData.id);
let intro;
await engine.resetScene();
engine.activeObjects.visible = false;
await engine.dashboard?.ready;
await this.expandScenarioData(scene);
engine.dashboard?.initScene(scene, async ()=>{
if (scene.data.$audio){
await engine.playAmbientSound(scene.data.$audio.asset.name);
engine.ambientSound.setVolume( 0.5 );
}
engine.tm?.setScene(scene.data.id);
if (intro){
intro.play();
}else{
engine.activeObjects.visible = true;
}
});
engine.dashboard?.loading(0.05);
//this is needed cause when mounted canvas has different size
//this.resize();
if (scene.data.$environment){
await engine.loadPanorama(scene.data.$environment.asset.name);
}
if (scene.data.$scene){
let env = await engine.load(scene.data.$scene.asset.name);
this.setObjectAttributes(sceneObjects, scene.data, env.scene, env, null);
if (engine.opts.mode != 'GameDesigner'){
env.scene.traverse(o=>{
if (o.name.startsWith('land') || o.name == 'Sphere'){
//this.debug('Fixing ground. TODO!!!')
engine.physics.add(o, 'fixed', true, undefined, 'mesh', { root: env.scene})
}
})
}
engine.activeObjects.add(env.scene);
}
let expectToFinish = 0, finished = 0, iobjs = [];
if (scene.data.items){
let loaded = 0;
for (let i of scene.data.items) {
//this.debug('Loading', i.data.id);
if (engine.opts.mode != 'GameDesigner'){
if (i.data.activationTriggers?.length || i.data.activationScore){
i.data.shouldBeLocked = true;
}
}
let io = await new InteractiveObject(engine, i.data)
iobjs.push({io, data: i.data})
//i.__io = io;
if (io.emits?.includes('finish')){
expectToFinish++;
}
this.setObjectAttributes(sceneObjects, i.data, io.object, io.source, 1);
engine.activeObjects.add(io.object);
if (engine.opts.mode != 'GameDesigner'){
if (i.data.$go?.type == 'player3d'){
let hero = new Hero(engine, io);
}else{
if (io.source?.animations?.length){
engine.playAnimation(engine.scene, io.source.animations[0]);
}
if (!i.data.noPhysics){
let bb = getBoundingBox(io.object);
let bbs = getBoundingBoxSize(bb);
engine.physics.add(io.object, 'fixed', false, undefined, 'capsule', {
radius: Math.max(bbs.x, bbs.z)/2, halfHeight: bbs.y/2
})
}
}
io.addEventListener('finish', ()=>{
finished ++;
if (!i.data.pointsGiven){
engine.dashboard?.addPoints(i.data.points)
}
i.data.pointsGiven = true;
iobjs.forEach(di=>{
if (di.data.activationTriggers?.includes(i.data.id)){
let idx = di.data.activationTriggers.indexOf(i.data.id)
di.data.activationTriggers.splice(idx, 1);
}
if (!di.data.activationTriggers?.length && di.io.object.__active === false &&
engine.dashboard.points > di.data.activationScore){
di.io.activator.activate();
}
});
// if (finished == expectToFinish){
// //GO TO NEXT LEVEL
// this.debug('LEVEL FINISHED')
// }
engine.tm?.setGameObject(null);
});
io.addEventListener('sceneSwitch', (e)=>{
//this.scenesList = [this.scenes.find(s=>s.data.id == e.scene)]
//switch to next scene:
this.loadScene(e.scene);
iobjs.forEach(di=>di.io.dispose?.());
})
}
loaded += 1/scene.data.items.length
engine.dashboard?.loading(0.1 + 0.89*loaded);
}
}
if (engine.opts.mode == 'GameDesigner'){
engine.activeObjects.visible = true;
}else if (scene.data.$intro){
intro = await new VideoPlayer(engine, {
$go: scene.data.$intro,
skipTransition: true,
playInHud: true,
immersive: true
});
engine.activeObjects.add(intro.object);
intro.video.addEventListener('pause',()=>{
intro.object.removeFromParent();
engine.clickable.remove(intro.object); //TODO!!!!
engine.activeObjects.visible = true;
});
}
engine.dashboard?.loading(1)
engine.physics.start();
//this.debug('Scene loaded', engine.renderer.info.memory)
}
resolve(this);
})
}
async expandScenarioData(scene){
if (scene.expanded) return;
const promises = [];
['environment', 'scene', 'intro', 'audio'].filter(e=>scene.data[e]).forEach(e=>{
promises.push(api.gameObject.load(scene.data[e]).then(r=>scene.data['$'+e] = r.data))
})
for (let i of scene.data.items || []) {
Object.keys(i.data).filter(k=>k == 'go' || k.startsWith('go_')).forEach(k=>{
promises.push(api.gameObject.load(i.data[k]).then(r=>i.data['$'+k] = r.data));
})
}
await Promise.all(promises);
scene.expanded = true;
}
setObjectAttributes(sceneObjects, data, object3d, source, autoScaleFactor = 1){
if (sceneObjects[data.id]){
['position', 'scale', 'rotation'].forEach(p=>{
object3d[p].copy(sceneObjects[data.id][p])
})
}else if (!data.type || data.type == 'GenericObject'){
autoScale(object3d, autoScaleFactor);
}
sceneObjects[data.id] = sceneObjects[data.id] || {};
if (this.opts.onObjectLoad){
this.opts.onObjectLoad(sceneObjects, data, object3d, source)
}
object3d.__pn_id = data.id;
}
}
export{ GameManager }
+40 -1
View File
@@ -86,7 +86,46 @@ function bottomOrigin(object){
return group; return group;
} }
function clearObject(o){
let disposables = []
o.traverse(object => {
if (object.isMesh) {
disposables.push(object);
}
if (object.isLight){
object.shadow?.map?.dispose();
object.shadow?.dispose();
object.dispose();
}
if (object.userData?._io){
object.userData._io.dispose?.();
}
});
disposables.forEach(object=>{
object.removeFromParent();
object.geometry.dispose();
if (object.material.isMaterial) {
clearMaterial(object.material)
} else {
for (const material of object.material) clearMaterial(material)
}
})
}
function clearMaterial(material) {
material.dispose();
for (const key of Object.keys(material)) {
const value = material[key]
if (value && typeof value == 'object' && 'minFilter' in value) {
//console.log('Disposing', value.name, this.renderer.info.memory.textures );
value.dispose();
//console.log('Disposed', value.name, this.renderer.info.memory.textures );
}
}
}
export { export {
assignParams, assignMaterial, autoScale, centerOrigin, wrapInGroup, bottomOrigin, assignParams, assignMaterial, autoScale, centerOrigin, wrapInGroup, bottomOrigin,
getBoundingBox, getBoundingBoxSize, getBoundingBoxMaxLength, getBoundingBoxCenterPoint getBoundingBox, getBoundingBoxSize, getBoundingBoxMaxLength, getBoundingBoxCenterPoint,
clearObject, clearMaterial
} }
+1 -1
View File
@@ -103,7 +103,7 @@ class Physics{
if (colliderSettings.root){ if (colliderSettings.root){
collider.setTranslationWrtParent(colliderSettings.root.position) collider.setTranslationWrtParent(colliderSettings.root.position)
collider.setRotationWrtParent(colliderSettings.root.quaternion) collider.setRotationWrtParent(colliderSettings.root.quaternion)
console.log(colliderSettings.root.position, mesh.position) //console.log(colliderSettings.root.position, mesh.position)
} }
const physicsObject = { mesh, collider, rigidBody, fn: postPhysicsFn, autoAnimate } const physicsObject = { mesh, collider, rigidBody, fn: postPhysicsFn, autoAnimate }
this.physicsObjects.push(physicsObject) this.physicsObjects.push(physicsObject)
+3 -2
View File
@@ -1,12 +1,13 @@
import { api } from "./Api";
class Telemetry { class Telemetry {
game = null; game = null;
scene = null; scene = null;
gameObject = null; gameObject = null;
#af = null; #af = null;
constructor(apiFunction, mode){ constructor(mode){
this.mode = mode; this.mode = mode;
this.#af = apiFunction; this.#af = api.user.tm;
} }
setGame(game){ setGame(game){
if (this.game == game) { return; } if (this.game == game) { return; }
-320
View File
@@ -1,320 +0,0 @@
import { InteractiveObject } from '@/components/InteractiveObjects/InteractiveObject';
import { VideoPlayer } from '@/components/InteractiveObjects/VideoPlayer';
import { GameEngine } from '@/lib/GameEngine';
import { Hero } from '@/lib/Hero';
import { autoScale, getBoundingBox, getBoundingBoxSize } from '@/lib/MeshUtils';
let engine = null;
export default {
async mounted(){
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,
telemetry: this.$api.user.tm,
mode: this.env
});
//engine.scene.add(new engine.$.GridHelper(100,100));
this.resize();
if (!this.scenario) {
await this.loadScenario();
}
window.addEventListener('resize', this.resize);
},
async unmounted(){
this.debug('Disposing scene')
window.removeEventListener('resize', this.resize);
engine.tm?.setGame(null);
engine.dispose();
this.debug('Disposed scene', engine.renderer.info.memory);
engine = null;
},
computed:{
scenes(){
return this.scenario?.scenes || [];
},
scene(){
return this.scenesList[0];
},
currentObject(){
return this.objectsList[0];
},
sceneObjects(){
return this.object.scenes?.[this.scene?.data?.id]?.objects;
},
object(){
return this.modelValue;
},
objectAnimations(){
return this.currentObject?.__g?.animations?.map(a => ({
name: a.name, id: a.uuid, a
}));
}
},
watch:{
async scene(n){
this.object.scenes = this.object.scenes || {}
this.object.scenes[n.data.id] = this.object.scenes[n.data.id] || {}
await this.loadEnvironment(n, this.object.scenes[n.data.id]);
},
mode(n){
engine.transformControls.setMode(n)
},
currentObject(n){
if (this.env == 'GameDesigner'){
engine.transformControls.attach(n.__o);
engine.gizmo.target = n.__o.position;
engine.camera.updateProjectionMatrix()
}
},
renderType(v){
engine.renderType = v;
},
cameraType(v){
if (v == 'perspective'){
engine.setCameraPerspective();
}else{
engine.setCameraOrthographic();
}
}
},
methods:{
async loadScenario(){
if (this.object.scenario){
this.scenario = (await this.$api.scenario.load(this.object.scenario)).data;
}else{
this.scenario = null;
}
},
async expandScenarioData(scene){
const promises = [];
['environment', 'scene', 'intro', 'audio'].filter(e=>scene.data[e]).forEach(e=>{
promises.push(this.$api.gameObject.load(scene.data[e]).then(r=>scene.data['$'+e] = r.data))
})
for (let i of scene.data.items || []) {
Object.keys(i.data).filter(k=>k == 'go' || k.startsWith('go_')).forEach(k=>{
promises.push(this.$api.gameObject.load(i.data[k]).then(r=>i.data['$'+k] = r.data));
})
}
await Promise.all(promises);
},
/**
* loads all environment objects
* @param scene Scene object from the Scenario Module
* @param target Target scene definition from Game Module
*/
async loadEnvironment(scene, target){
this.debug('loading environment');
engine.tm?.setGame(this.object?.id);
//await engine.loadPanorama(`/asset/default/43.webp`);
let intro;
await engine.resetScene();
engine.activeObjects.visible = false;
await engine.dashboard?.ready;
engine.dashboard?.initScene(scene, async ()=>{
if (this.scene.data.$audio){
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{
engine.activeObjects.visible = true;
}
});
await this.expandScenarioData(scene);
engine.dashboard?.loading(0.05);
//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){
this.debug('loading panorama', this.scene.data.$environment.asset.name)
await engine.loadPanorama(this.scene.data.$environment.asset.name);
}
if (this.scene.data.$scene){
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'){
this.debug('Fixing ground. TODO!!!')
engine.physics.add(o, 'fixed', true, undefined, 'mesh', { root: env.scene})
}
})
}
engine.activeObjects.add(env.scene);
}
let expectToFinish = 0, finished = 0;;
if (this.scene.data.items){
let loaded = 0;
for (let i of this.scene.data.items) {
this.debug('Loading', i.data.id);
if (this.env != 'GameDesigner'){
if (i.data.activationTriggers?.length || i.data.activationScore){
i.data.shouldBeLocked = true;
}
}
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);
engine.activeObjects.add(io.object);
if (this.env != 'GameDesigner'){
if (i.data.$go?.type == 'player3d'){
let hero = new Hero(engine, io);
}else{
if (io.source?.animations?.length){
engine.playAnimation(engine.scene, io.source.animations[0]);
}
if (!i.data.noPhysics){
let bb = getBoundingBox(io.object);
let bbs = getBoundingBoxSize(bb);
engine.physics.add(io.object, 'fixed', false, undefined, 'capsule', {
radius: Math.max(bbs.x, bbs.z)/2, halfHeight: bbs.y/2
})
}
}
io.addEventListener('finish', ()=>{
finished ++;
if (!i.data.pointsGiven){
engine.dashboard?.addPoints(i.data.points)
}
i.data.pointsGiven = true;
this.scene.data.items.forEach(di=>{
if (di.data.activationTriggers?.includes(i.data.id)){
let idx = di.data.activationTriggers.indexOf(i.data.id)
di.data.activationTriggers.splice(idx, 1);
}
if (!di.data.activationTriggers?.length && di.__io.object.__active === false &&
engine.dashboard.points > di.data.activationScore){
di.__io.activator.activate();
}
});
if (finished == expectToFinish){
//GO TO NEXT LEVEL
this.debug('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
engine.dashboard?.loading(0.1 + 0.89*loaded);
}
}
if (this.env == 'GameDesigner'){
engine.activeObjects.visible = true;
}else if (this.scene.data.$intro){
intro = await new VideoPlayer(engine, {
$go: this.scene.data.$intro,
skipTransition: true,
playInHud: true,
immersive: true
});
engine.activeObjects.add(intro.object);
intro.video.addEventListener('pause',()=>{
intro.object.removeFromParent();
engine.clickable.remove(intro.object); //TODO!!!!
engine.activeObjects.visible = true;
});
}
engine.dashboard?.loading(1)
engine.physics.start();
this.debug('Scene loaded', engine.renderer.info.memory)
},
targetPointerDown(){
this.pointerDownTime = performance.now();
},
targetClick(e){
if (this.env == 'GameDesigner'){
if (performance.now() - this.pointerDownTime < 200){
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)
//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{
engine.transformControls.detach();
}
}
}else{
engine.onClick(e, this.$refs.target);
}
},
targetPointer(e, t){
engine.onPointer(e, this.$refs.target, t);
},
setObjectAttributes(l, data, object, source, autoScaleFactor = 1){
if (l[data.id]){
['position', 'scale', 'rotation'].forEach(p=>{
object[p].copy(l[data.id][p])
})
}else if (!data.type || data.type == 'GenericObject'){
autoScale(object, autoScaleFactor);
}
l[data.id] = l[data.id] || {};
['position', 'scale', 'rotation', 'visible'].forEach(p=>{
l[data.id][p] = object[p];
})
l[data.id].__o = object;
l[data.id].__g = source;
l[data.id].__title = data.title;
object.__pn_id = data.id;
},
async toggleAnimation(animation){
animation.playing = !animation.playing;
engine.playAnimation(engine.scene, animation.a, animation.playing);
},
resize(){
let r = this.$refs.target;
this.debug('resizing!!', r.clientWidth, r.clientHeight, r)
engine.resize(r.clientWidth, r.clientHeight);
//this.zoom = Math.min(r.clientWidth / this.viewBox.w, r.clientHeight / this.viewBox.h);
},
control(){
engine.pointerControls.lock(true);
},
async fullScreen(){
await engine.renderer.domElement.requestFullscreen()
},
async setGameHeader(){
let screenshot = await engine.captureScreenshot();
let fd = new FormData();
fd.append('file', screenshot);
await this.$api.game.setHeader(this.modelValue.id, fd);
}
}
}
+1 -1
View File
@@ -118,7 +118,7 @@ export default {
}, },
async captureThumbnail() { async captureThumbnail() {
this.object.thumb = await this.$refs.assetPreview.gameEngine.captureScreenshot(); this.object.thumb = await this.$refs.assetPreview.captureScreenshot();
await this.save({ thumbOnly: true }); await this.save({ thumbOnly: true });
}, },
+3 -4
View File
@@ -4,7 +4,7 @@
<v-tab value="game"> <v-tab value="game">
<v-icon icon="mdi-pencil"></v-icon> {{ id == 'add' ? l.createGame : l.editGame }} <v-icon icon="mdi-pencil"></v-icon> {{ id == 'add' ? l.createGame : l.editGame }}
</v-tab> </v-tab>
<v-tab value="gameDesigner" v-if="object.scenario"> <v-tab value="gameDesigner" v-if="this.id != 'add'">
<v-icon icon="mdi-movie-open-outline"></v-icon> {{ l.gameDesigner }} <v-icon icon="mdi-movie-open-outline"></v-icon> {{ l.gameDesigner }}
</v-tab> </v-tab>
</v-tabs> </v-tabs>
@@ -13,7 +13,7 @@
<v-form class="pa-4" v-model="valid"> <v-form class="pa-4" v-model="valid">
<v-text-field :label="l.name" v-model="object.name" :rules="[rules.required]"></v-text-field> <v-text-field :label="l.name" v-model="object.name" :rules="[rules.required]"></v-text-field>
<v-textarea :label="l.description" v-model="object.description"></v-textarea> <v-textarea :label="l.description" v-model="object.description"></v-textarea>
<v-select :label="l.scenario" :items="scenarios" v-model="object.scenario" item-title="name" item-value="id"></v-select> <v-select :label="l.scenario" :items="scenarios" :disabled="this.id != 'add'" v-model="object.scenario" item-title="name" item-value="id"></v-select>
</v-form> </v-form>
</v-tabs-window-item> </v-tabs-window-item>
<v-tabs-window-item value="gameDesigner"> <v-tabs-window-item value="gameDesigner">
@@ -62,9 +62,8 @@ export default {
async save(params) { async save(params) {
this.loading = true; this.loading = true;
try { try {
console.log('saving', this.object) this.debug('saving', this.object)
let result = await this.$api.game.save(this.object); let result = await this.$api.game.save(this.object);
//Object.assign(this.object, result.data.object);
this.object.id = result.data.object.id; this.object.id = result.data.object.id;
if (this.id == 'add') { if (this.id == 'add') {
this.$router.replace({ params: { id: this.object.id } }); this.$router.replace({ params: { id: this.object.id } });
+3 -35
View File
@@ -1,47 +1,15 @@
<template> <template>
<v-container max-width="1400"> <v-container max-width="1400">
<GamePreview v-model="object" v-if="object"></GamePreview> <GamePreview :id="$route.params?.id"></GamePreview>
</v-container> </v-container>
</template> </template>
<script> <script>
export default { export default {
data() {
return {
object: null,
valid: false,
rules: {
required: v => v ? true : this.l.fieldRequired,
requiredFile: v => (v?.length || this.id != 'add') ? true : this.l.fieldRequired
},
loading: false,
panel: [],
scenarios: []
}
},
async mounted(){
if (this.id && this.id != 'add') {
this.object = (await this.$api.game.load(this.id)).data;
//this.$api.user.tm('test', 'test', {data: 'test'})
}
this.scenarios = (await this.$api.scenario.search()).data.data;
},
watch:{
'object.scenario'(v){
if (v){
this.object.thumb = this.scenarios?.find(s=>s.id == v)?.sceneThumb?.[0];
}
}
},
computed: {
id() {
return this.$route.params?.id;
}
},
methods:{
}
} }
</script> </script>
<route> <route>
+2 -27
View File
@@ -1,35 +1,10 @@
<template> <template>
<v-container max-width="1400"> <v-container max-width="1400">
<GamePlay v-model="object" v-if="object"></GamePlay> <GamePlay :id="$route.params?.id"></GamePlay>
</v-container> </v-container>
</template> </template>
<script> <script>
export default { export default {}
data() {
return {
object: null,
valid: false,
rules: {
required: v => v ? true : this.l.fieldRequired,
requiredFile: v => (v?.length || this.id != 'add') ? true : this.l.fieldRequired
},
loading: false,
panel: [],
scenarios: []
}
},
async mounted(){
this.object = (await this.$api.game.load(this.id)).data;
//this.$api.user.tm('test', 'test', {data: 'test'})
},
computed: {
id() {
return this.$route.params?.id;
}
},
methods:{
}
}
</script> </script>
+2 -89
View File
@@ -1,94 +1,7 @@
import axios from 'axios'; import { api } from "@/lib/Api";
import Utils from '#/app/Utils';
const $ax = axios.create({
baseURL: '/api/',
transformRequest: [
(data, headers)=>{
if (data && !(data instanceof FormData)){
data = Utils.deepMerge({}, data, (k, v)=>{
return k.startsWith('__') ? undefined : v;
})
data = JSON.stringify(data);
headers['Content-Type'] = 'application/json;charset=utf-8';
}
return data;
}
, ...axios.defaults.transformRequest
]
})
export default { export default {
install: (app, options) => { install: (app, options) => {
app.config.globalProperties.$api = { app.config.globalProperties.$api = api
gameObject:{
async save(data){
return await $ax.put('/game-object', data);
},
async load(id){
return await $ax.get(`/game-object/${id}`);
},
async search(query){
return await $ax.post('/game-object', query);
},
async remove(id){
return await $ax.delete(`/game-object/${id}`)
},
async getTags(q){
return await $ax.post('/game-object/tags', {q});
}
},
scenario:{
async save(data){
return await $ax.put('/scenario', data);
},
async load(id){
return await $ax.get(`/scenario/${id}`);
},
async search(query){
return await $ax.post('/scenario', query);
},
async remove(id){
return await $ax.delete(`/scenario/${id}`)
}
},
game:{
async save(data){
return await $ax.put('/game', data);
},
async load(id){
return await $ax.get(`/game/${id}`);
},
async search(query){
return await $ax.post('/game', query);
},
async remove(id){
return await $ax.delete(`/game/${id}`)
},
async setHeader(id, data){
return await $ax.post(`/game/${id}/header`, data);
}
},
user:{
async tm(action, object, data){
return await $ax.post('/user/tm', {action, object, data});
},
async signin(data){
return await $ax.post('/user/signin', data);
},
async signup(data){
return await $ax.post('/user/signup', data);
},
async signout(){
return await $ax.get('/user/signout');
},
async load(){
return await $ax.get('/user/info');
},
async update(data){
return await $ax.post('/user/update', data);
}
}
}
} }
} }