diff --git a/scene/lobby.pr b/scene/lobby.pr index b89d5b8..d96f48e 100644 --- a/scene/lobby.pr +++ b/scene/lobby.pr @@ -79,17 +79,19 @@ > >>> - - - - ] - >> - > - ]> - >> - >>> +; +; +; +; ] +; >> +; > +; ]> +; >> +; >>> + +[] diff --git a/src/engine.ts b/src/engine.ts index bfd7b8d..545ace0 100644 --- a/src/engine.ts +++ b/src/engine.ts @@ -1,15 +1,19 @@ import { + AbstractMesh, DualShockPad, Engine, FreeCamera, FreeCameraGamepadInput, Gamepad as b_Gamepad, + KeyboardEventTypes, Mesh, Quaternion, Scene, Vector3, + WebXRCamera, WebXRDefaultExperience, WebXRSessionManager, + WebXRState, } from '@babylonjs/core/Legacy/legacy'; import { log } from './log.js'; @@ -86,14 +90,20 @@ export class GamepadState { } export class RunningEngine { - camera!: FreeCamera; - xrSessionManager: WebXRSessionManager | null = null; - gamepadInput!: FreeCameraGamepadInput; + // The active camera - plainCamera or xrCamera, depending. + camera: FreeCamera; + + plainCamera: FreeCamera; + gamepadInput: FreeCameraGamepadInput; + + xrCamera: WebXRCamera | null = null; leanBase: { position: Vector3 } | null = null; recenterBase: { rotation: Quaternion } | null = null; - padStates: Map = new Map(); + padStates = new Map(); + keysDown = new Set(); + keysChanged = new Map(); static async start( interactivity: Interactivity, @@ -106,8 +116,14 @@ export class RunningEngine { }, options0); const engine = new Engine(options.canvas, true); const scene = new Scene(engine); - const xr = await scene.createDefaultXRExperienceAsync({}); - return new RunningEngine(options, engine, interactivity, scene, xr); + return new RunningEngine( + options, + engine, + interactivity, + scene, + (await WebXRSessionManager.IsSessionSupportedAsync('immersive-vr') + ? await scene.createDefaultXRExperienceAsync({}) + : null)); } private constructor ( @@ -115,41 +131,49 @@ export class RunningEngine { public engine: Engine, public interactivity: Interactivity, public scene: Scene, - public xr: WebXRDefaultExperience, + public xr: WebXRDefaultExperience | null, ) { - this.xrSessionManager = this.xr.baseExperience?.sessionManager ?? null; + this.plainCamera = new FreeCamera("camera", this.options.initialPos.clone(), this.scene); + this.plainCamera.rotation = this.options.initialRotation; + this.plainCamera.minZ = 0.1; + this.plainCamera.inertia = 0.75; + this.plainCamera.speed = 0.5; + this.plainCamera.keysUp.push(87 /* W */); + this.plainCamera.keysLeft.push(65 /* A */); + this.plainCamera.keysDown.push(83 /* S */); + this.plainCamera.keysRight.push(68 /* D */); + this.plainCamera.keysUpward.push(69 /* E */); + this.plainCamera.keysDownward.push(81 /* Q */); + this.plainCamera.attachControl(true); - if (this.xrSessionManager) { - this.camera = this.xr.baseExperience.camera; - } else { - this.camera = new FreeCamera("camera", - this.options.initialPos.clone(), - this.scene); - this.camera.rotation = this.options.initialRotation; - this.camera.minZ = 0.1; - this.camera.inertia = 0.75; - this.camera.speed = 0.5; - this.camera.attachControl(true); + scene.onKeyboardObservable.add(info => { + if (info.event.metaKey) return; + this.keysChanged.set(info.event.keyCode, info.type === KeyboardEventTypes.KEYDOWN); + }); + + this._setupCamera(this.plainCamera); + + if (this.xr) { + this.xr.baseExperience.onStateChangedObservable.add(state => this.xrStateChanged(state)); + this.xrCamera = this.xr.baseExperience.camera; + this._setupCamera(this.xrCamera); } + this.camera = this.plainCamera; + this.gamepadInput = new FreeCameraGamepadInput(); this.gamepadInput.gamepadMoveSensibility = 320; this.gamepadInput.gamepadAngularSensibility = 100; - this.camera.inputs.add(this.gamepadInput); + this.plainCamera.inputs.add(this.gamepadInput); this.gamepadInput.attachControl(); this.scene.gravity = new Vector3(0, -9.81 / 90, 0); this.scene.collisionsEnabled = true; - this.camera.checkCollisions = true; - this.camera.applyGravity = false; - (this.camera as any)._needMoveForGravity = true; - this.camera.ellipsoid = new Vector3(0.25, this.options.initialPos.y / 2, 0.25); - if (this.options.canvas) { const canvas = this.options.canvas; - if (this.xrSessionManager) { - canvas.onclick = () => this.xrEnable(); + if (this.xrAvailable) { + canvas.onclick = () => this.xrToggle(); } else { canvas.onclick = () => canvas.requestPointerLock?.(); } @@ -160,6 +184,7 @@ export class RunningEngine { Array.from(navigator.getGamepads()).forEach(gp => { if (gp !== null) this.checkGamepadInput(gp); }); + this.checkKeys(); this.scene.render(); } catch (e) { console.error('Error in render loop', e); @@ -170,6 +195,36 @@ export class RunningEngine { window.addEventListener("resize", () => this.engine.resize()); } + _setupCamera(c: FreeCamera) { + c.checkCollisions = true; + c.applyGravity = false; + (c as any)._needMoveForGravity = true; + c.ellipsoid = new Vector3(0.25, this.options.initialPos.y / 2, 0.25); + } + + get inXR(): boolean { + return (this.xr !== null) && (this.xr.baseExperience.state === WebXRState.IN_XR); + } + + get xrAvailable(): boolean { + return this.xr !== null; + } + + set onCollide(c: (m: AbstractMesh) => void) { + this.plainCamera.onCollide = c; + if (this.xrCamera) this.xrCamera.onCollide = c; + } + + get position(): Vector3 { + return this.camera.position; + } + + get rotation(): Vector3 { + return this.inXR + ? this.camera.rotationQuaternion.toEulerAngles() + : this.camera.rotation; + } + padStateFor(gp: Gamepad): GamepadState { const state = this.padStates.get(gp.index); if (state) { @@ -186,58 +241,82 @@ export class RunningEngine { checkGamepadInput(gp: Gamepad) { const state = this.padStateFor(gp); - if (state.latch(2)) location.reload(); - if (state.latch(5)) this.turn180(); + if (state.latch(9 /* options */)) location.reload(); + if (state.latch(3 /* triangle */)) this.turn180(); - if (this.xrSessionManager) { - this.updateLean(gp); - if (state.latch(0)) this.xrTeleport(); - if (state.latch(1)) this.xrTouch(); - if (state.latch(3)) this.xrEnable(); + if (this.xrAvailable) { + if (state.latch(16 /* ps */)) this.xrToggle(); + } - if (state.latch(4)) { + if (this.inXR) { + this.xrUpdateLean(gp); + if (state.latch(0 /* cross */)) this.xrTeleport(); + if (state.latch(1 /* circle */)) this.xrTouch(); + + if (state.latch(8 /* share */)) { this.recenterBase = { - rotation: this.xr.baseExperience.camera.rotationQuaternion.clone(), + rotation: this.xrCamera!.rotationQuaternion.clone(), }; } - if (state.isDown(4) && this.recenterBase) { - this.xr.baseExperience.camera.rotationQuaternion.copyFrom( + if (state.isDown(8 /* share */) && this.recenterBase) { + this.xrCamera!.rotationQuaternion.copyFrom( this.recenterBase.rotation); } } } - updateLean(gp: Gamepad) { + checkKeys() { + for (const [keyCode, state] of this.keysChanged.entries()) { + if (state) { + this.keysDown.add(keyCode); + switch (keyCode) { + case 32 /* space */: this.jump(); break; + default: break; + } + } else { + this.keysDown.delete(keyCode); + } + } + this.keysChanged.clear(); + } + + jump() { + if (Math.abs(this.camera.cameraDirection.y) < 0.1) { + this.camera.cameraDirection.y += 1; + } + } + + xrUpdateLean(gp: Gamepad) { const pos = new Vector3((gp.axes[0]), (-gp.axes[1]), (-gp.axes[3])); if (pos.length() > 0.0625) { if (this.leanBase === null) { - this.leanBase = { position: this.xr.baseExperience.camera.position }; + this.leanBase = { position: this.xrCamera!.position }; } - this.xr.baseExperience.camera.position = - pos.applyRotationQuaternion(this.xr.baseExperience.camera.absoluteRotation) + this.xrCamera!.position = + pos.applyRotationQuaternion(this.xrCamera!.absoluteRotation) .scale(0.25) .add(this.leanBase.position); } else { if (this.leanBase !== null) { - this.xr.baseExperience.camera.position = this.leanBase.position; + this.xrCamera!.position = this.leanBase.position; } this.leanBase = null; } } turn180() { - const r = this.xrSessionManager - ? this.xr.baseExperience.camera.rotationQuaternion.toEulerAngles() + const r = this.inXR + ? this.xrCamera!.rotationQuaternion.toEulerAngles() : this.camera.rotation; r.y += Math.PI; r.y %= 2 * Math.PI; - if (this.xrSessionManager) { - this.xr.baseExperience.camera.rotationQuaternion.copyFrom(r.toQuaternion()); + if (this.inXR) { + this.xrCamera!.rotationQuaternion.copyFrom(r.toQuaternion()); } } xrTouch() { - const ray = this.xr.baseExperience.camera.getForwardRay(); + const ray = this.xrCamera!.getForwardRay(); const meshes = this.interactivity.touchableMeshes(); const hit = this.scene.pickWithRay(ray, m => meshes.indexOf(m as any) !== -1); @@ -248,9 +327,9 @@ export class RunningEngine { } xrTeleport() { - if (!this.xrSessionManager) return; + if (!this.inXR) return; - const ray = this.xr.baseExperience.camera.getForwardRay(); + const ray = this.xrCamera!.getForwardRay(); const meshes = this.interactivity.floorMeshes(); const hit = this.scene.pickWithRay(ray, m => meshes.indexOf(m as any) !== -1); @@ -259,17 +338,37 @@ export class RunningEngine { if (hit.pickedPoint === null) return; const pos = hit.pickedPoint.add(new Vector3(0, 1.6, 0)); - this.xr.baseExperience.camera.position = pos; - this.xr.baseExperience.camera.rotation = Vector3.Zero(); + this.xrCamera!.position = pos; + this.xrCamera!.rotation = Vector3.Zero(); if (this.leanBase !== null) this.leanBase.position = pos; } - xrEnable() { - if (!this.xrSessionManager) return; - this.xr.baseExperience.enterXRAsync('immersive-vr', 'local').then(() => { - this.xr.baseExperience.camera.position = this.options.initialPos.clone(); - this.xr.baseExperience.camera.rotation = this.options.initialRotation; - this.xr.baseExperience.sessionManager.session.onselect = () => this.xrTeleport(); - }); + xrStateChanged(state: WebXRState) { + switch (state) { + case WebXRState.IN_XR: + break; + case WebXRState.ENTERING_XR: + this.camera = this.xrCamera!; + this.camera.position = this.plainCamera.position.clone(); + this.camera.rotationQuaternion = this.options.initialRotation.toQuaternion(); + break; + case WebXRState.EXITING_XR: + this.camera = this.plainCamera; + this.camera.position = this.xrCamera!.position.clone(); + this.camera.rotation = this.xrCamera!.rotationQuaternion.toEulerAngles(); + break; + case WebXRState.NOT_IN_XR: + break; + } + } + + xrToggle() { + if (this.inXR) { + this.xr!.baseExperience.exitXRAsync(); + } else { + this.xr!.baseExperience.enterXRAsync('immersive-vr', 'local').then(() => { + this.xr!.baseExperience.sessionManager.session.onselect = () => this.xrTeleport(); + }); + } } } diff --git a/src/index.ts b/src/index.ts index 37c4d83..6ee5046 100644 --- a/src/index.ts +++ b/src/index.ts @@ -70,15 +70,14 @@ async function enterScene( const rootMesh = new Mesh('--root-' + (+new Date()), runningEngine.scene); - const camera = runningEngine.camera; - camera.applyGravity = false; - camera.position = initialPosition.clone(); + runningEngine.camera.applyGravity = false; + runningEngine.camera.position = initialPosition.clone(); interpretScene(id, runningEngine, rootMesh, sceneDs); let lastTouchTime = 0; let lastTouchSpriteName = ""; - camera.onCollide = (other: AbstractMesh) => { + runningEngine.onCollide = (other: AbstractMesh) => { if (other.metadata?.touchable) { const now = +new Date(); const touched = other.metadata?.spriteName ?? ""; @@ -97,11 +96,8 @@ async function enterScene( } }; - const currentPosition = () => Shapes.Vector3(camera.position); - const currentRotation = () => Shapes.Vector3( - camera instanceof WebXRCamera - ? camera.rotationQuaternion.toEulerAngles() - : camera.rotation); + const currentPosition = () => Shapes.Vector3(runningEngine.position); + const currentRotation = () => Shapes.Vector3(runningEngine.rotation); field position: Shapes.Vector3 = currentPosition(); field rotation: Shapes.Vector3 = currentRotation(); @@ -137,7 +133,7 @@ async function enterScene( switch (dest._variant) { case "local": if (dest.value === sceneDs) { - camera.position = newPos; + runningEngine.camera.position = newPos; } else { runningEngine.scene.removeMesh(rootMesh, true); Turn.active.stop(currentSceneFacet, () => {