Introduction:
bitbucket repo:
https://HamboGameJam@bitbucket.org/HamboGameJam/vue_demo.git
Work based on the repository from iso74:
However changed it from Vue2+VueX to Vue3+Pinia … Ionic and Capacitor were also added …
- Vue - Javascript framework for building web applications. Popular choice for building complex, data-drive applications.
- VueX - State management pattern and library for Vue.js, uses a single, centralized store to manage global state.
- Pinia - Lightweight, modular state management library for Vue.js that stores state in separate module for automatic caching and reactivity.
- Ionic - Framework for building hybrid mobile apps using web technologies such as: html, css and Javascript.
- Capacitor - Cross-platform app runtime that allows you to build web apps that run on native platforms like Androis and iOs. Provides a set of APIs for accessing native device features like camera, gps and file system.
We use Vue to build our Web application,
Ionic for providing the mobile app framework and
Capacitor to access native device features and run the app on top of native platform…
The application is made from 2 vue objects:
- Base - Contains the markup of the main page and also contains Stage object.
- Stage - Contains the necessary canvas html object where Playcanvas app will mount.
Here is the Pinia store file; holder of all playcanvas code/logic
/* eslint-disable */
import { /*createPinia,*/ defineStore } from 'pinia';
import * as pc from 'playcanvas';
import settings from '@/components/settings.js';
import { useLoadModules } from '@/components/useLoadModules.js';
//import { useRotate } from '@/assets/scripts/useRotate.js';
import { CapacitorHttp } from '@capacitor/core';
//import { Filesystem, Directory, Encoding } from '@capacitor/filesystem';
export const usePiniaStore = defineStore('piniaStore', {
state: () => ({
canvasStage: null,
configJson: null,
isConfigured: false,
jsonEntitiesGLB: null,
pc: null,
pcApp: null,
sceneCurrent: "",
watchingAppConfigured: false,
}),
actions: {
configureCSS( canvas, fillMode, width, height ) {
//const canvas = this.canvasStage
if (canvas.classList) {
canvas.classList.add('fill-mode-' + fillMode);
}
// Dynamic CSS media query for aspect ratio changes
this.appWidth = width;
this.appHeight = height;
},
pcCreateInputDevices() {
console.log('🍍🍍🍍 Pinia Store > pcCreateInputDevices');
const canvas = this.canvasStage;
const elementInput = new pc.ElementInput(canvas, {
useMouse: settings.INPUT_SETTINGS.useMouse,
useTouch: settings.INPUT_SETTINGS.useTouch,
});
const keyboard = settings.INPUT_SETTINGS.useKeyboard
? new pc.Keyboard(window)
: null;
console.log('🍍🍍 Pinia Store > createInputDevices > keyboard:', keyboard);
const mouse = settings.INPUT_SETTINGS.useMouse
? new pc.Mouse(canvas)
: null;
console.log('🍍🍍 Pinia Store > createInputDevices > mouse:', mouse);
const gamepads = settings.INPUT_SETTINGS.useGamepads
? new pc.GamePads()
: null;
console.log('🍍🍍 Pinia Store > createInputDevices > gamepads:', gamepads);
const touch = settings.INPUT_SETTINGS.useTouch && pc.platform.touch
? new pc.TouchDevice(canvas)
: null;
console.log('🍍🍍 Pinia Store > createInputDevices > touch:', touch);
const devices = {
elementInput,
keyboard,
mouse,
gamepads,
touch,
};
return devices;
},
pcDisablePreloadingAssets() {
console.log('🍍🍍🍍 Pinia Store > pcDisablePreloadingAssets');
const app = this.pcApp;
/*
app.assets.on("add", (asset) => {
// Disable the default preloading of assets.
// You can also just mark all assets to not preload from the editor.
if (asset.type !== "script" && asset.type !== "wasm" && asset.type !== "folder") {
console.log('🍍🍍🍍 Pinia Store > Asset '+asset+' Not Preloading!');
asset.preload = false;
}
});
*/
},
pcEnableSceneEntityRenderers() {
console.log('🍍🍍🍍 Pinia Store > pcEnableSceneEntityRenderers');
const entityNames = ["Luigi", "QBert", "PacMan"];
for (const entityName of entityNames) {
for (const render of this.pcApp.root.findByName(entityName).findComponents('render')) {
render.enabled = true;
}
}
},
pcKickstartApp() {
console.log('🍍🍍🍍 kk Pinia Store > pcKickstartApp');
const app = this.pcApp;
app.fire('preload:end');
app.start();
this.pcUpdate();
},
pcOnAppCreated() {
console.log('🍍🍍🍍 Pinia Store > pcOnAppCreated');
this.pcLoadModules();
},
pcOnAppPreconfigured() {
console.log('🍍🍍🍍 Pinia Store > pcOnAppPreconfigured');
this.pcAppConfigure( settings.CONFIG_FILENAME );
},
pcOnGLBFilesLoaded() {
console.log('🍍🍍🍍 Pinia Store > pcOnGLBFilesLoaded');
this.pcApp.off("app:glbFilesLoaded");
this.pcSpawnGLBEntities( this.jsonEntitiesGLB );
},
pcOnGLBEntitiesSpawned() {
console.log('🍍🍍🍍 Pinia Store > pcOnGLBEntitiesSpawned');
this.pcApp.off("app:glbEntitiesSpawned");
//this.pcSpawnEntitiesByCode();
this.pcKickstartApp();
},
pcOnModulesLoaded() {
console.log('🍍🍍🍍 Pinia Store > pcOnModulesLoaded ');
this.pcAppPreconfigure();
},
pcOnSceneLoaded() {
console.log('🍍🍍🍍 Pinia Store > pcOnSceneLoaded');
this.pcApp.off("app:sceneLoaded");
//this.pcLoadGLBEntities( this.sceneCurrent ); ////LOVE
/* entities spawned by scene alone*/
this.pcEnableSceneEntityRenderers();
this.pcKickstartApp();
// errors for displaying enabled render objects pcSpawnEntitiesByCode_0.png
/* entities spawned by json and code; code instantiation uses
using loadedmaterial pcSpawnEntitiesByCode_2.png
this.pcKickstartApp();
this.pcEnableSceneEntityRenderers();
*/
/* enable the renderers of the entities loaded from scene.json */
//this.pcSpawnEntitiesByCode();
//this.pcKickstartApp();
/* load up only the scene and GLB */
//this.pcKickstartApp();
//this.pcSpawnEntitiesByCode();
//this.pcLoadGLBEntities( this.sceneCurrent );
/**/
},
pcSpawnCubeEntity( name ){
console.log('🍍🍍🍍 Pinia Store > pcSpawnCubeEntity ',name );
const box = new pc.Entity( name );
box.addComponent('render', {
type: 'box'
});
return box;
},
pcSpawnEntitiesByCode()
{
console.log('🍍🍍🍍 Pinia Store > pcSpawnEntitiesByCode');
const entityLah = this.pcSpawnCubeEntity('Lah');
const entityDee = this.pcSpawnCubeEntity('Dee');
const entityDah = this.pcSpawnCubeEntity('Dah');
const rootEntity = this.pcApp.root.findByGuid('Root');
rootEntity.addChild( entityLah );
rootEntity.addChild( entityDee );
rootEntity.addChild( entityDah );
entityLah.setPosition( new pc.Vec3( 3, 0,-7) );
entityDee.setPosition( new pc.Vec3( 0, 0,-7) );
entityDah.setPosition( new pc.Vec3(-3, 0,-7) );
},
pcSpawnGLBEntities( jsonEntitiesGLB ) {
console.log('🍍🍍🍍 Pinia Store > pcSpawnGLBEntities( jsonEntitiesGLB ) '+jsonEntitiesGLB);
for (const jsonEntityName in jsonEntitiesGLB) {
const jsonEntityGLB = jsonEntitiesGLB[jsonEntityName];
const entityName = jsonEntityGLB.name.split('_')[0];
const glbFileName = jsonEntityGLB.name.split('_')[2] + ".glb";
const assetContainer = this.pcApp.assets.find(glbFileName);
const resourceContainer = assetContainer.resource;
const entityContainer = resourceContainer.instantiateRenderEntity();
const entityParent = this.pcApp.root.findByGuid(jsonEntityGLB.parent);
const isEnabled = jsonEntityGLB.enabled;
console.log('🍍🍍 Pinia Store > pcSpawnGLBEntities > jsonEntityGLB '+jsonEntityGLB+", parent "+ parent);
entityContainer.name = entityName;
entityParent.addChild(entityContainer);
entityContainer.enabled = isEnabled;
entityContainer.setPosition( jsonEntityGLB.position[0],
jsonEntityGLB.position[1],
jsonEntityGLB.position[2] );
entityContainer.setRotation( jsonEntityGLB.rotation[0],
jsonEntityGLB.rotation[1],
jsonEntityGLB.rotation[2] );
entityContainer.setLocalScale( jsonEntityGLB.scale[0],
jsonEntityGLB.scale[1],
jsonEntityGLB.scale[2] );
}
this.pcOnGLBEntitiesSpawned();
},
pcSpawnMaterialEntitiesByCode()
{
console.log('🍍🍍🍍 Pinia Store > pcSpawnEntitiesByCode');
const entityLah = this.pcSpawnCubeEntity('Lah');
const entityDee = this.pcSpawnCubeEntity('Dee');
const entityDah = this.pcSpawnCubeEntity('Dah');
const rootEntity = this.pcApp.root.findByGuid('Root');
rootEntity.addChild( entityLah );
rootEntity.addChild( entityDee );
rootEntity.addChild( entityDah );
entityLah.setPosition( new pc.Vec3( 3, 0,-7) );
entityDee.setPosition( new pc.Vec3( 0, 0,-7) );
entityDah.setPosition( new pc.Vec3(-3, 0,-7) );
const assetMaterialOrange = this.pcApp.assets.find("materialOrange", "material");
const entityNames = ["Lah", "Dee", "Dah"];
for (const entityName of entityNames) {
for (const render of this.pcApp.root.findByName(entityName).findComponents('render')) {
for (const meshInstance of render.meshInstances) {
meshInstance.material = assetMaterialOrange.resource;
}
}
}
},
setCanvas(data) {
console.log('🍍🍍🍍 Pinia Store > setCanvas this.canvasStage = '+ data);
this.canvasStage = data;
},
async pcAppBaseCreate(canvas) {
console.log('🍍🍍🍍 Pinia Store > pcAppBaseCreate');
const gfxOptions = {
deviceTypes: [pc.DEVICETYPE_WEBGPU],
glslangUrl: settings.LIB_PATH+'glslang.js',
twgslUrl: settings.LIB_PATH+'twgsl.js'
};
const device = await pc.createGraphicsDevice(canvas, gfxOptions);
device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
const createOptions = new pc.AppOptions();
createOptions.graphicsDevice = device;
createOptions.mouse = new pc.Mouse(document.body);
createOptions.touch = new pc.TouchDevice(document.body);
createOptions.keyboard = new pc.Keyboard(document.body);
createOptions.componentSystems = [
pc.CameraComponentSystem,
pc.LightComponentSystem,
pc.ModelComponentSystem,
pc.RenderComponentSystem,
pc.ScriptComponentSystem,
pc.SpriteComponentSystem
];
createOptions.resourceHandlers = [
pc.AnimationHandler,
pc.AudioHandler,
pc.ContainerHandler,
pc.CubemapHandler,
pc.FontHandler,
//pc.JsonHandler, ???
pc.MaterialHandler,
pc.ModelHandler,
pc.RenderHandler,
pc.SceneHandler,
pc.ScriptHandler,
pc.SpriteHandler,
pc.TextureAtlasHandler,
pc.TextureHandler
];
const app = new pc.AppBase(canvas);
app.init(createOptions);
this.pcApp = app;
this.pcOnAppCreated();
},
async pcAppPreconfigure() {
console.log('🍍🍍🍍 Pinia Store > pcAppPreconfigure');
//this event :preconfigured
this.pcGetConfigJSON().then((configJson) => {
this.configJson = configJson;
this.pcOnAppPreconfigured();
});
},
pcAppConfigure( configFilename ) {
console.log('🍍1🍍1🍍 Pinia Store > pcAppConfigure');
this.pcApp.on('app:configured', () => {
console.log('🍍 Pinia Store > pcOnAppConfigured');
this.pcOnAppConfigured();
});
const app = this.pcApp;
app.configure( configFilename,
function(err) {
if(err) {
console.error(err)
}
const aFillMode = this.configJson.application_properties.fillMode;
const canvasStg = this.canvasStage;
const iWidth = app.graphicsDevice.width;
const iHeight = app.graphicsDevice.height;
console.log('🍍🍍 Pinia Store > pcAppConfigure > app.configure > aFillMode,canvasStg,iWidth,iHeight:', aFillMode,canvasStg,iWidth,iHeight);
this.configureCSS( canvasStg, aFillMode, iWidth, iHeight );
setTimeout(
function() {
// Reflow the canvas
this.reflow();
window.addEventListener('resize', this.reflow, false);
window.addEventListener('orientationchange', this.reflow, false);
this.pcPreloadAssets();
}.bind(this),
250
);
}.bind(this)
);
},
async pcGetConfigJSON() {
console.log('🍍w🍍w🍍 @@@@ Pinia Store > getConfig '+settings.CONFIG_FILENAME);
const configFile = await fetch(settings.CONFIG_FILENAME);
const configData = await configFile.json();
return configData;
},
async pcKickstart() {
console.log('🍍🍍🍍 KK Pinia Store > pcKickstart');
this.pcAppBaseCreate(this.canvasStage);
},
async pcLoadGLBFiles(glbFileNames) {
console.log('🍍🍍🍍 Pinia Store > pcLoadGLBFiles ');
//Step 1 - Create the data to feed pc.assetListLoader
const assets_ = {};
for (let i = 0; i < glbFileNames.length; i++) {
const glbFileName = glbFileNames[i];
if(!this.pcApp.assets.find(glbFileName))
{
let glbUrl = process.env.BASE_URL + settings.GLB_PATH + glbFileName;
console.log('🍍🍍 Pinia Store > pcLoadGLBFiles > '+glbFileName +" "+glbUrl );
assets_[glbFileName] = ( new pc.Asset( glbFileName, 'container', { url: glbUrl } ) );
}
}
/////////////////////////////////////////////////////////////
//Step 2 - Load files... and trigger callback when all data is loaded
const assetListLoader = new pc.AssetListLoader(Object.values(assets_), this.pcApp.assets);
assetListLoader.load((err, failed) => {
if (err) {
console.error(`🍍🍍 Pinia Store > pcLoadGLBFiles > ${failed.length} assets failed to load`);
} else {
console.log(`🍍🍍 Pinia Store > pcLoadGLBFiles > assets loaded`);
this.pcApp.fire("app:glbFilesLoaded");
}
});
},
async pcLoadModules() {
console.log('🍍🍍🍍 Pinia Store > pcLoadModules');
pc.WasmModule.setConfig('Ammo', {
glueUrl: '/assets/scripts/ammo.wasm.js',
wasmUrl: '/assets/scripts/ammo.wasm.wasm',
fallbackUrl: '/assets/scripts/ammo.js'
});
console.log('🍍🍍🍍 Pinia Store > pcLoadModules > pc.WasmModule.getConfig(\'Ammo\')', pc.WasmModule.getConfig('Ammo'));
pc.WasmModule.getInstance('Ammo', this.pcOnModulesLoaded.bind(this));
},
async pcLoadModules2() {
console.log('🍍🍍🍍 Pinia Store > pcLoadModules');
// Using Mixins in Vue 3
const { loadModules } = useLoadModules();
loadModules(settings.PRELOAD_MODULES, settings.ASSET_PREFIX, function() {
console.log('🍍🍍 Pinia Store > pcLoadModules > this.pcOnModulesLoaded()');
this.pcOnModulesLoaded();
}.bind(this));
},
async pcLoadGLBEntities(sceneName) {
this.pcApp.on("app:glbFilesLoaded", this.pcOnGLBFilesLoaded, this);
let glbSceneName = sceneName.replace('.json', '_glb.json');
// remove the trailing front slash; otherwise it wont get the proper path
let glbScenePath = (process.env.BASE_URL + settings.SCENE_PATH).slice(1);
console.log('🍍🍍🍍 Pinia Store > pcLoadGLBEntities > (glbScenePath+glbSceneName)'+(glbScenePath+glbSceneName) );
try{
CapacitorHttp.get({
url: (glbScenePath+glbSceneName),
responseType: 'json'
})
.then(response => {
console.log('🍍🍍 Pinia Store > pcLoadGLBEntities > (response.data)'+response.data );
const uniqueGLBFileNames = [];
this.jsonEntitiesGLB = response.data.entities;
for (const entityName in this.jsonEntitiesGLB) {
const glbFileName = ( entityName.split('_glb_')[1] ) + ".glb";
if (uniqueGLBFileNames.indexOf(glbFileName) === -1) {
uniqueGLBFileNames.push(glbFileName);
}
}
this.pcLoadGLBFiles(uniqueGLBFileNames);
})
.catch(error => {
console.log('🍍🍍44 Pinia Store > pcLoadGLBEntities > No GLB Entities found');
});
} catch(error) {
console.log('🍍🍍45 Pinia Store > pcLoadGLBEntities > ERROR :', error);
}
},
async pcLoadScene( sceneName ) {
console.log('🍍🍍🍍 Pinia Store > pcLoadScene(sceneName) > Loading Scene named: ', sceneName);
this.sceneCurrent = sceneName;
const scenePath = process.env.BASE_URL +
settings.SCENE_PATH +
sceneName;
this.pcApp.on("app:sceneLoaded", this.pcOnSceneLoaded, this);
console.log('🍍🍍 Pinia Store > pcLoadScene > pcLoadSceneData(sceneName) > Scene path: ', scenePath);
this.pcApp.scenes.loadScene( scenePath,
function(err, scene ){
console.log('🍍 Pinia Store > loadScene');
if(!err)
{
console.log('🍍 Pinia Store > loadScene > OnSceneLoaded ', scene );
this.pcApp.fire("app:sceneLoaded");
}
else
{
console.error('🍍 Pinia Store > onLoadScene FAIL: '+ err);
}
}.bind(this)
);
console.log('🍍🍍 Pinia Store > pcLoadScene > End of Function');
},
async pcOnAppConfigured() {
console.log('🍍🍍🍍 Pinia Store > pcOnAppConfigured');
this.pcApp.off("app:configured");
this.pcLoadScene('scene8.json');
},
async pcPreloadAssets() {
console.log('🍍🍍🍍 Pinia Store > pcPreloadAssets');
const app = this.pcApp;
app.preload(
function(err) {
if (err) {
console.error('🍍🍍 Pinia Store > preloadAssets > Preload Error: ', err);
}
// Trigger event when the app is configured and assets are preloaded
console.log('$$$🍍🍍 Pinia Store > preloadAssets > Assets preloaded.');
app.fire('app:configured');
}.bind(this)
);
},
async pcUpdate() {
console.log('🍍🍍🍍 Pinia Store > pcUpdate');
const app = this.pcApp;
app.on('update', (dt) => {
console.log('Derpesaurus '+dt);
console.log( 'Derp I '+app.root.findByGuid('Root') == null);
/*console.log( 'Derp II '+app.hasEvent('app:configured') );*/
//box.rotate(10*dt,20*dt,30*dt);
});
},
reflow() {
const app = this.pcApp;
const canvas = this.canvasStage
const fillMode = app._fillMode;
console.log('🍍🍍🍍 Pinia Store > reflow: this, app, canvas:'+this+','+app+','+canvas);
app.resizeCanvas(canvas.width, canvas.height);
canvas.style.width = '';
canvas.style.height = '';
if (fillMode === pc.FILLMODE_NONE || fillMode === pc.FILLMODE_KEEP_ASPECT)
{
if ( (fillMode === pc.FILLMODE_NONE &&
canvas.clientHeight < window.innerHeight) ||
(canvas.clientWidth / canvas.clientHeight) >=
(window.innerWidth / window.innerHeight ) )
{
canvas.style.marginTop =
Math.floor((window.innerHeight - canvas.clientHeight) / 2) + 'px';
} else {
canvas.style.marginTop = '';
}
}
},
},
});
Slide 1 - Entities WITH RENDER COMPONENT “ENABLED” loaded from scene.json files threw exceptions when entity’s material+renderer+shader init is triggered
Slide 2 - Entities WITH RENDER COMPONENT “DISABLED” loaded from scene.json files are loaded into memory, camera(with red clear color) and light components are deployed
Slide 3 - Entities WITH RENDER COMPONENT “ENABLED” AFTER APPLICATION STARTS… yields no errors HOWEVER ENTITIES ARE NOT RENDERED!
Slide 4 - Entities WITH RENDER COMPONENT “ENABLED” BEFORE APPLICATION STARTS… yields same errors as Slide1
Slide 5 - Entities being created on runtime by code are rendered:
pcSpawnCubeEntity( name ){
console.log( Pinia Store > pcSpawnCubeEntity ',name );
const box = new pc.Entity( name );
box.addComponent('render', {
type: 'box'
});
return box;
},
pcSpawnEntitiesByCode()
{
console.log('Pinia Store > pcSpawnEntitiesByCode');
const entityLah = this.pcSpawnCubeEntity('Lah');
const entityDee = this.pcSpawnCubeEntity('Dee');
const entityDah = this.pcSpawnCubeEntity('Dah');
const rootEntity = this.pcApp.root.findByGuid('Root');
rootEntity.addChild( entityLah );
rootEntity.addChild( entityDee );
rootEntity.addChild( entityDah );
entityLah.setPosition( new pc.Vec3( 3, 0,-7) );
entityDee.setPosition( new pc.Vec3( 0, 0,-7) );
entityDah.setPosition( new pc.Vec3(-3, 0,-7) );
},
Slide 6 - Enabling the render components of the entities loaded from Json after instantiating the objects by code throws exception
Slide 7 - Out of frustations I’ve tried to device a way to render objects using glb files:
- Derive a list of unique glb filenames derived from the entity’s scene name.
- Create a list of asset container objects to be fed into pc.assetListLoader.
- On finished loading , the assets should be on the AssetRegistry.
- We iterate the glb entity list once more to instantiate, position, rotate and scale the glb asset containers using the data of the entity derived from the scene.
The errors thrown are eerily similar… render object complaining about initialization of material+shaders …
Thanks in advance =D !!!