diff --git a/.gitignore b/.gitignore index 942c9c1..1d850b8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,8 @@ +*.js.map /dummykey.crt /dummykey.key /dummykey.pem /index.js -/index.js.map /lib /node_modules /src.ts/ diff --git a/protocols/schemas/scene.prs b/protocols/schemas/scene.prs index 26aa4e1..a9fd37e 100644 --- a/protocols/schemas/scene.prs +++ b/protocols/schemas/scene.prs @@ -6,5 +6,7 @@ PortalDestination = / @remote @sturdyref sturdy.SturdyRef> . +Gravity = . + ; Message Touch = . diff --git a/scene/00-rooms.pr b/scene/00-rooms.pr new file mode 100644 index 0000000..38579aa --- /dev/null +++ b/scene/00-rooms.pr @@ -0,0 +1,7 @@ + + + +? [ + let ?scene = dataspace + +] diff --git a/scene/05-common-scene.pr b/scene/05-common-scene.pr new file mode 100644 index 0000000..4b863fd --- /dev/null +++ b/scene/05-common-scene.pr @@ -0,0 +1,9 @@ +? [ + $scene [ + ? [ + $config ? [ + $scene + ] + ] + ] +] diff --git a/scene/10-lobby.pr b/scene/10-lobby.pr new file mode 100644 index 0000000..a923d8d --- /dev/null +++ b/scene/10-lobby.pr @@ -0,0 +1,65 @@ +? $scene [ + >> + > + + + ] + >>>> + + + + + + 0.75] + >>>>> + + + >>>>> + + + + > + > + > + ]>>>>> + + + + + >>>>>> + + + + + >>>>> + + + + + >>>>>> + + + >>>> +] diff --git a/scene/10-other.pr b/scene/10-other.pr new file mode 100644 index 0000000..e3c30be --- /dev/null +++ b/scene/10-other.pr @@ -0,0 +1,12 @@ +? $scene [ + >> + + + ] + >>>> + + + >>>> +] diff --git a/scene/example.pr b/scene/example.pr deleted file mode 100644 index 0a1130f..0000000 --- a/scene/example.pr +++ /dev/null @@ -1,66 +0,0 @@ ->> - - - ] - >>>> - - - - - - 0.75] - >>>>> - - - >>>>> - - - - > - > - > - ]>>>>> - -let ?ballDs = dataspace - - - - - >>>>>> - - - - - >>>>> - - - - - >>>>>> - - - >>>> - -[] diff --git a/src/engine.ts b/src/engine.ts index 3b9a372..57c4ef8 100644 --- a/src/engine.ts +++ b/src/engine.ts @@ -45,9 +45,7 @@ if ((navigator as any).oscpu?.startsWith('Linux')) { }; } -export type CreateScene = (engine: Engine) => Promise; -export type CreatedScene = { - scene: Scene, +export type Interactivity = { floorMeshes: () => Mesh[], touchableMeshes: () => Mesh[], }; @@ -87,9 +85,9 @@ export class GamepadState { } export class RunningEngine { - camera: FreeCamera; - xrSessionManager: WebXRSessionManager | null; - gamepadInput: FreeCameraGamepadInput; + camera!: FreeCamera; + xrSessionManager: WebXRSessionManager | null = null; + gamepadInput!: FreeCameraGamepadInput; leanBase: { position: Vector3 } | null = null; recenterBase: { rotation: Quaternion } | null = null; @@ -97,7 +95,7 @@ export class RunningEngine { padStates: Map = new Map(); static async start( - createScene: CreateScene, + interactivity: Interactivity, options0: Partial = {}, ): Promise { const options = Object.assign({ @@ -106,15 +104,16 @@ export class RunningEngine { canvas: document.getElementById("renderCanvas") as HTMLCanvasElement, }, options0); const engine = new Engine(options.canvas, true); - const createdScene = await createScene(engine); - const xr = await createdScene.scene.createDefaultXRExperienceAsync({}); - return new RunningEngine(options, engine, createdScene, xr); + const scene = new Scene(engine); + const xr = await scene.createDefaultXRExperienceAsync({}); + return new RunningEngine(options, engine, interactivity, scene, xr); } private constructor ( public options: EngineOptions, public engine: Engine, - public createdScene: CreatedScene, + public interactivity: Interactivity, + public scene: Scene, public xr: WebXRDefaultExperience, ) { this.xrSessionManager = this.xr.baseExperience?.sessionManager ?? null; @@ -124,7 +123,7 @@ export class RunningEngine { } else { this.camera = new FreeCamera("camera", this.options.initialPos, - this.createdScene.scene); + this.scene); this.camera.rotation = this.options.initialRotation; this.camera.minZ = 0.1; this.camera.inertia = 0.75; @@ -138,12 +137,13 @@ export class RunningEngine { this.camera.inputs.add(this.gamepadInput); this.gamepadInput.attachControl(); - this.createdScene.scene.gravity = new Vector3(0, -9.81 / 90, 0); - this.createdScene.scene.collisionsEnabled = true; + this.scene.gravity = new Vector3(0, -9.81 / 90, 0); + this.scene.collisionsEnabled = true; this.camera.checkCollisions = true; - this.camera.applyGravity = true; - this.camera.ellipsoid = new Vector3(0.25, 0.8, 0.25); + 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; @@ -158,8 +158,9 @@ export class RunningEngine { Array.from(navigator.getGamepads()).forEach(gp => { if (gp !== null) this.checkGamepadInput(gp); }); - this.createdScene.scene.render(); + this.scene.render(); }); + window.addEventListener("resize", () => this.engine.resize()); } @@ -226,8 +227,8 @@ export class RunningEngine { xrTouch() { const ray = this.xr.baseExperience.camera.getForwardRay(); - const meshes = this.createdScene.touchableMeshes(); - const hit = this.createdScene.scene.pickWithRay(ray, m => meshes.indexOf(m as any) !== -1); + const meshes = this.interactivity.touchableMeshes(); + const hit = this.scene.pickWithRay(ray, m => meshes.indexOf(m as any) !== -1); if (hit === null) return; if (meshes.indexOf(hit.pickedMesh as any) === -1) return; @@ -239,8 +240,8 @@ export class RunningEngine { if (!this.xrSessionManager) return; const ray = this.xr.baseExperience.camera.getForwardRay(); - const meshes = this.createdScene.floorMeshes(); - const hit = this.createdScene.scene.pickWithRay(ray, m => meshes.indexOf(m as any) !== -1); + const meshes = this.interactivity.floorMeshes(); + const hit = this.scene.pickWithRay(ray, m => meshes.indexOf(m as any) !== -1); if (hit === null) return; if (meshes.indexOf(hit.pickedMesh as any) === -1) return; diff --git a/src/index.ts b/src/index.ts index 32893e6..c908547 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import { is, Dataspace, Embedded, Reader, Ref, Schemas, Sturdy, Turn } from "@syndicate-lang/core"; +import { is, embed, stringify, Dataflow, Dataspace, Embedded, Reader, Ref, Schemas, Sturdy, Turn, Value } from "@syndicate-lang/core"; import * as html from "@syndicate-lang/html"; import * as timer from "@syndicate-lang/timer"; import * as wsRelay from "@syndicate-lang/ws-relay"; @@ -14,16 +14,17 @@ import { Mesh, Scene, TargetCamera, + Vector3, WebXRCamera, } from '@babylonjs/core/Legacy/legacy'; import { activeFloorMeshes, activeTouchableMeshes, ShapeTree, builder as B } from './shapes.js'; -import { RunningEngine, CreatedScene } from './engine.js'; +import { RunningEngine } from './engine.js'; import { uuid } from './uuid.js'; assertion type SceneHandle(ds: Embedded); -function interpretScene(myId: string, scene: Scene, sceneDs: Ref) { +function interpretScene(myId: string, scene: Scene, rootMesh: Mesh, sceneDs: Ref) { at sceneDs { during Shapes.Sprite({ "name": $name: string }) => spawn named `sprite:${name}` { if (name === myId) { @@ -31,22 +32,153 @@ function interpretScene(myId: string, scene: Scene, sceneDs: Ref) { } else { console.log('+shape', name); on stop console.log('-shape', name); - spriteMain(name, scene, sceneDs); + spriteMain(name, scene, rootMesh, sceneDs); } } } } -function spriteMain(spriteName: string, scene: Scene, sceneDs: Ref) { +function spriteMain(spriteName: string, scene: Scene, rootMesh: Mesh, sceneDs: Ref) { at sceneDs { let currentShape = ShapeTree.empty(spriteName, scene); + currentShape.node.parent = rootMesh; on stop currentShape.remove(); during Shapes.Sprite({ "name": spriteName, "shape": $shape: Shapes.Shape }) => { currentShape = currentShape.reconcile(spriteName, spriteName, shape); + currentShape.node.parent = rootMesh; } } } +async function enterScene( + id: string, + runningEngine: RunningEngine, + ds: Ref, + sceneDs: Ref, + email: Dataflow.Field, +) { + const currentSceneFacet = Turn.activeFacet; + + console.log('enterScene', sceneDs); + + const rootMesh = new Mesh('--root-' + (+new Date()), runningEngine.scene); + interpretScene(id, runningEngine.scene, rootMesh, sceneDs); + + const camera = runningEngine.camera; + camera.applyGravity = false; + camera.position = runningEngine.options.initialPos; + + let lastTouchTime = 0; + let lastTouchSpriteName = ""; + camera.onCollide = (other: AbstractMesh) => { + if (other.metadata?.touchable) { + const now = +new Date(); + const touched = other.metadata?.spriteName ?? ""; + if ((now - lastTouchTime > 500) || (lastTouchSpriteName !== touched)) { + currentSceneFacet.turn(() => { + at sceneDs { + send message SceneProtocol.Touch({ + subject: id, + object: touched, + }); + } + }); + lastTouchTime = now; + lastTouchSpriteName = touched; + } + } + }; + + const currentPosition = () => Shapes.Vector3(camera.position); + const currentRotation = () => Shapes.Vector3( + camera instanceof WebXRCamera + ? camera.rotationQuaternion.toEulerAngles() + : camera.rotation); + + field position: Shapes.Vector3 = currentPosition(); + field rotation: Shapes.Vector3 = currentRotation(); + + const refreshPeriod = Math.floor(1000 / 10); + at ds { + on message timer.PeriodicTick(refreshPeriod) => { + const newPosition = currentPosition(); + const newRotation = currentRotation(); + if (!is(Shapes.fromVector3(position.value), Shapes.fromVector3(newPosition))) { + position.value = newPosition; + } + if (!is(Shapes.fromVector3(rotation.value), Shapes.fromVector3(newRotation))) { + rotation.value = newRotation; + } + } + } + + at sceneDs { + during SceneProtocol.Gravity($direction: Shapes.Vector3) => { + camera.applyGravity = true; + on stop camera.applyGravity = false; + const frameRate = 60; + runningEngine.scene.gravity = new Vector3(direction.x / frameRate, + direction.y / frameRate, + direction.z / frameRate); + } + + on message SceneProtocol.Touch({ "subject": id, "object": $o }) => { + console.log('touched', o); + react { + let needStop = true; + on asserted SceneProtocol.Portal({ + "name": o, + "destination": $dest: SceneProtocol.PortalDestination + }) => { + needStop = false; + switch (dest._variant) { + case "local": + runningEngine.scene.removeMesh(rootMesh, true); + Turn.active.stop(currentSceneFacet, () => { + react { + enterScene(id, runningEngine, ds, dest.value, email); + } + }); + break; + default: + break; + } + } + const checkFacet = Turn.activeFacet; + Turn.active.sync(sceneDs).then(() => checkFacet.turn(() => { + if (needStop) { + stop {} + } + })); + } + } + + assert Shapes.Sprite({ + name: id, + shape: B.nonphysical( + B.move(position.value, B.many([ + B.move({ x: 0, y: -0.9, z: 0 }, + B.rotate({ x: 0, y: rotation.value.y, z: 0 }, + B.scale({ x: 0.4, y: 1.4, z: 0.1 }, B.box()))), + B.rotate(rotation.value, + B.scale({ x: 0.15, y: 0.23, z: 0.18 }, B.many([ + B.box(), + B.move({ x: 0, y: 0, z: 0.501 }, + B.texture( + Shapes.TextureSpec.uvAlpha({ + path: `https://www.gravatar.com/avatar/${md5(new TextEncoder().encode(email.value.trim()))}?s=256&d=wavatar`, + scale: Shapes.Vector2({ x:1, y:1 }), + offset: Shapes.Vector2({ x:0, y:0 }), + alpha: 1 + }), + B.rotate({ x: 0, y: Math.PI, z: 0 }, + B.plane()))), + ]))), + ]))), + }); + } +} + function wsurl(): string { const scheme = (document.location.protocol.toLowerCase() === 'https:') ? 'wss' : 'ws'; return `${scheme}://${document.location.host}/ws`; @@ -82,99 +214,12 @@ function bootApp(ds: Ref, runningEngine: RunningEngine) { send message wsRelay.ForceRelayDisconnect(relayAddr); } - at remoteDs { - during SceneHandle($sceneDs_e: Embedded) => { - const thisFacet = Turn.activeFacet; - const sceneDs = sceneDs_e.embeddedValue; - interpretScene(id, runningEngine.createdScene.scene, sceneDs); - - const camera = runningEngine.camera; - camera.onCollide = (other: AbstractMesh) => { - if (other.metadata?.touchable) { - thisFacet.turn(() => { - at sceneDs { - send message SceneProtocol.Touch({ - subject: id, - object: other.metadata?.spriteName, - }); - } - }); + react { + at remoteDs { + stop on asserted SceneHandle($sceneDs_e: Embedded) => { + react { + enterScene(id, runningEngine, ds, sceneDs_e.embeddedValue, email); } - }; - - field position: Shapes.Vector3 = Shapes.Vector3({ x:0, y:0, z:0 }); - field rotation: Shapes.Vector3 = Shapes.Vector3({ x:0, y:0, z:0 }); - - at ds { - const refreshPeriod = Math.floor(1000 / 10); - on message timer.PeriodicTick(refreshPeriod) => { - if (camera instanceof TargetCamera) { - const newPosition = Shapes.Vector3(camera.position); - const newRotation = Shapes.Vector3(camera instanceof WebXRCamera - ? camera.rotationQuaternion.toEulerAngles() - : camera.rotation); - if (!is(Shapes.fromVector3(position.value), Shapes.fromVector3(newPosition))) { - position.value = newPosition; - } - if (!is(Shapes.fromVector3(rotation.value), Shapes.fromVector3(newRotation))) { - rotation.value = newRotation; - } - } - } - } - - at sceneDs { - on message SceneProtocol.Touch({ "subject": id, "object": $o }) => { - console.log('touch!', o); - react { - on stop console.log('portal check ending', o); - stop on asserted SceneProtocol.Portal({ - "name": o, - "destination": $dest: SceneProtocol.PortalDestination - }) => react { - console.log('portal!', dest); - switch (dest._variant) { - case "local": - at dest.value { - assert 909909909; - } - break; - default: - break; - } - } - const checkFacet = Turn.activeFacet; - Turn.active.sync(sceneDs).then(() => checkFacet.turn(() => { - console.log('synced'); - stop {} - })); - } - } - - assert Shapes.Sprite({ - name: id, - shape: B.nonphysical( - B.move(position.value, B.many([ - B.move({ x: 0, y: -0.9, z: 0 }, - B.rotate({ x: 0, y: rotation.value.y, z: 0 }, - B.scale({ x: 0.4, y: 1.4, z: 0.1 }, - B.box()))), - B.rotate(rotation.value, - B.scale({ x: 0.15, y: 0.23, z: 0.18 }, B.many([ - B.box(), - B.move({ x: 0, y: 0, z: 0.501 }, - B.texture( - Shapes.TextureSpec.uvAlpha({ - path: `https://www.gravatar.com/avatar/${md5(new TextEncoder().encode(email.value.trim()))}?s=256&d=wavatar`, - scale: Shapes.Vector2({ x:1, y:1 }), - offset: Shapes.Vector2({ x:0, y:0 }), - alpha: 1 - }), - B.rotate({ x: 0, y: Math.PI, z: 0 }, - B.plane()))), - ]))), - ]))), - }); } } } @@ -184,12 +229,10 @@ function bootApp(ds: Ref, runningEngine: RunningEngine) { } window.addEventListener('load', async () => { - const runningEngine = await RunningEngine.start( - async (engine: Engine): Promise => ({ - scene: new Scene(engine), - floorMeshes: () => activeFloorMeshes, - touchableMeshes: () => activeTouchableMeshes, - })); + const runningEngine = await RunningEngine.start({ + floorMeshes: () => activeFloorMeshes, + touchableMeshes: () => activeTouchableMeshes, + }); Dataspace.boot(ds => { html.boot(ds); timer.boot(ds);