Skip to content

Commit

Permalink
Improved modularity, disposability and multi-instance support (#45)
Browse files Browse the repository at this point in the history
* Improved modularity, disposability and multi-instance support

* Default to accepting key input

* Linting fixes
  • Loading branch information
MarcusLongmuir authored Oct 16, 2023
1 parent aaef76d commit 87d19dc
Show file tree
Hide file tree
Showing 47 changed files with 2,284 additions and 1,371 deletions.
1 change: 0 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
"plugin:import/errors",
"plugin:import/warnings",
"plugin:import/typescript",
"plugin:prettier/recommended",
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:react-hooks/recommended'
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes
67 changes: 67 additions & 0 deletions example/local-multi-web-client/build.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import * as esbuild from "esbuild";
import { copy } from "esbuild-plugin-copy";

const buildMode = "--build";
const watchMode = "--watch";

const helpString = `Mode must be provided as one of ${buildMode} or ${watchMode}`;

const args = process.argv.splice(2);

if (args.length !== 1) {
console.error(helpString);
process.exit(1);
}

const mode = args[0];

const buildOptions: esbuild.BuildOptions = {
entryPoints: {
index: "src/index.ts",
},
bundle: true,
write: true,
sourcemap: "linked",
outdir: "./build/",
assetNames: "[dir]/[name]-[hash]",
preserveSymlinks: true,
loader: {
".html": "text",
".svg": "file",
".png": "file",
".jpg": "file",
".glb": "file",
".hdr": "file",
},
outbase: "../",
sourceRoot: "./src",
publicPath: "/local-multi-web-client/",
plugins: [
copy({
resolveFrom: "cwd",
assets: {
from: ["./public/**/*"],
to: ["./build/"],
},
}),
],
};

switch (mode) {
case buildMode:
esbuild.build(buildOptions).catch(() => process.exit(1));
break;
case watchMode:
esbuild
.context({
...buildOptions,
banner: {
js: ` (() => new WebSocket((window.location.protocol === "https:" ? "wss://" : "ws://")+window.location.host+'/local-multi-web-client-build').addEventListener('message', () => location.reload()))();`,
},
})
.then((context) => context.watch())
.catch(() => process.exit(1));
break;
default:
console.error(helpString);
}
27 changes: 27 additions & 0 deletions example/local-multi-web-client/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "@example/local-multi-web-client",
"private": true,
"version": "0.8.0",
"files": [
"/build"
],
"type": "module",
"scripts": {
"build": "rimraf ./build && tsx ./build.ts --build",
"iterate": "tsx ./build.ts --watch",
"type-check": "tsc --noEmit",
"lint": "eslint \"./{src,test}/**/*.{js,jsx,ts,tsx}\" --max-warnings 0",
"lint-fix": "eslint \"./{src,test}/**/*.{js,jsx,ts,tsx}\" --fix"
},
"dependencies": {
"@mml-io/3d-web-client-core": "^0.8.0",
"@mml-io/3d-web-text-chat": "^0.8.0",
"@mml-io/3d-web-user-networking": "^0.8.0",
"@mml-io/3d-web-voice-chat": "^0.8.0",
"mml-web-runner": "0.9.0",
"three": "0.153.0"
},
"devDependencies": {
"@types/three": "0.153.0"
}
}
1 change: 1 addition & 0 deletions example/local-multi-web-client/public/favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions example/local-multi-web-client/public/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg" href="/local-multi-web-client/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MML 3D Web Experience</title>
<link rel="stylesheet" href="/local-multi-web-client/style.css" />
</head>
<body>
<div id="app"></div>
<div id="text-chat-ui"></div>
<div id="voice-chat-ui"></div>
<script type="application/javascript" src="/local-multi-web-client/index.js"></script>
</body>
</html>
56 changes: 56 additions & 0 deletions example/local-multi-web-client/public/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: black;
background-color: #121212;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}

a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}

body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
overflow: hidden;
}

h1 {
font-size: 3.2em;
line-height: 1.1;
}

#app {
max-width: 100vw;
margin: 0 auto;
padding: 0;
text-align: center;
}

@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}
184 changes: 184 additions & 0 deletions example/local-multi-web-client/src/LocalAvatarClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import {
CameraManager,
CharacterDescription,
CharacterManager,
CharacterModelLoader,
CharacterState,
CollisionsManager,
Composer,
KeyInputManager,
MMLCompositionScene,
TimeManager,
} from "@mml-io/3d-web-client-core";
import { EditableNetworkedDOM, NetworkedDOM } from "@mml-io/networked-dom-document";
import { MMLWebRunnerClient } from "mml-web-runner";
import { AudioListener, Euler, Scene, Vector3 } from "three";

import hdrUrl from "../../assets/hdr/industrial_sunset_2k.hdr";
import airAnimationFileUrl from "../../assets/models/unreal-air.glb";
import idleAnimationFileUrl from "../../assets/models/unreal-idle.glb";
import jogAnimationFileUrl from "../../assets/models/unreal-jog.glb";
import meshFileUrl from "../../assets/models/unreal-mesh.glb";
import sprintAnimationFileUrl from "../../assets/models/unreal-run.glb";

import { LocalAvatarServer } from "./LocalAvatarServer";
import { Room } from "./Room";

const characterDescription: CharacterDescription = {
airAnimationFileUrl,
idleAnimationFileUrl,
jogAnimationFileUrl,
meshFileUrl,
sprintAnimationFileUrl,
modelScale: 1,
};

export class LocalAvatarClient {
public element: HTMLDivElement;

private readonly scene = new Scene();
private readonly audioListener = new AudioListener();
private readonly characterModelLoader = new CharacterModelLoader();
public readonly composer: Composer;
private readonly timeManager = new TimeManager();
private readonly keyInputManager = new KeyInputManager(() => {
return this.cameraManager.dragging;
});
private readonly characterManager: CharacterManager;
private readonly cameraManager: CameraManager;

private readonly collisionsManager = new CollisionsManager(this.scene);
private readonly remoteUserStates = new Map<number, CharacterState>();

private mmlComposition: MMLCompositionScene;
private resizeObserver: ResizeObserver;
private documentRunnerClients = new Set<MMLWebRunnerClient>();
private animationFrameRequest: number | null = null;

constructor(
private localAvatarServer: LocalAvatarServer,
private localClientId: number,
spawnPosition: Vector3,
spawnRotation: Euler,
) {
this.element = document.createElement("div");
this.element.style.position = "absolute";
this.element.style.width = "100%";
this.element.style.height = "100%";

document.addEventListener("mousedown", () => {
if (this.audioListener.context.state === "suspended") {
this.audioListener.context.resume();
}
});

this.cameraManager = new CameraManager(
this.element,
this.collisionsManager,
Math.PI / 2,
Math.PI / 2,
);
this.cameraManager.camera.add(this.audioListener);

this.composer = new Composer(this.scene, this.cameraManager.camera, true);
this.composer.useHDRI(hdrUrl);
this.element.appendChild(this.composer.renderer.domElement);

this.resizeObserver = new ResizeObserver(() => {
this.composer.fitContainer();
});
this.resizeObserver.observe(this.element);

this.localAvatarServer.addClient(
localClientId,
(clientId: number, userNetworkingClientUpdate: null | CharacterState) => {
if (userNetworkingClientUpdate === null) {
this.remoteUserStates.delete(clientId);
} else {
this.remoteUserStates.set(clientId, userNetworkingClientUpdate);
}
},
);

this.characterManager = new CharacterManager(
this.composer,
this.characterModelLoader,
this.collisionsManager,
this.cameraManager,
this.timeManager,
this.keyInputManager,
this.remoteUserStates,
(characterState: CharacterState) => {
localAvatarServer.send(localClientId, characterState);
},
);
this.scene.add(this.characterManager.group);

this.mmlComposition = new MMLCompositionScene(
this.element,
this.composer.renderer,
this.scene,
this.cameraManager.camera,
this.audioListener,
this.collisionsManager,
() => {
return this.characterManager.getLocalCharacterPositionAndRotation();
},
);
this.scene.add(this.mmlComposition.group);

const room = new Room();
this.collisionsManager.addMeshesGroup(room);
this.scene.add(room);

this.characterManager.spawnCharacter(
characterDescription!,
localClientId,
true,
spawnPosition,
spawnRotation,
);
}

public dispose() {
if (this.animationFrameRequest !== null) {
cancelAnimationFrame(this.animationFrameRequest);
}
for (const documentRunnerClient of this.documentRunnerClients) {
documentRunnerClient.dispose();
}
this.localAvatarServer.removeClient(this.localClientId);
this.documentRunnerClients.clear();
this.resizeObserver.disconnect();
this.mmlComposition.dispose();
this.characterManager.clear();
this.cameraManager.dispose();
this.composer.dispose();
this.element.remove();
}

public update(): void {
this.timeManager.update();
this.characterManager.update();
this.cameraManager.update();
this.composer.sun?.updateCharacterPosition(this.characterManager.character?.position);
this.composer.render(this.timeManager);
this.animationFrameRequest = requestAnimationFrame(() => {
this.update();
});
}

public addDocument(
mmlDocument: NetworkedDOM | EditableNetworkedDOM,
windowTarget: Window,
remoteHolderElement: HTMLElement,
) {
const mmlWebRunnerClient = new MMLWebRunnerClient(
windowTarget,
remoteHolderElement,
this.mmlComposition.mmlScene,
);
mmlWebRunnerClient.connect(mmlDocument);
this.documentRunnerClients.add(mmlWebRunnerClient);
}
}
27 changes: 27 additions & 0 deletions example/local-multi-web-client/src/LocalAvatarServer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { CharacterState } from "@mml-io/3d-web-client-core";

export class LocalAvatarServer {
private callbacks = new Map<
number,
(clientId: number, userNetworkingClientUpdate: null | CharacterState) => void
>();

send(clientId: number, userNetworkingClientUpdate: null | CharacterState) {
this.callbacks.forEach((callback, callbackClientId) => {
if (callbackClientId !== clientId) {
callback(clientId, userNetworkingClientUpdate);
}
});
}

addClient(
clientId: number,
callback: (clientId: number, userNetworkingClientUpdate: null | CharacterState) => void,
) {
this.callbacks.set(clientId, callback);
}

removeClient(clientId: number) {
this.callbacks.delete(clientId);
}
}
Loading

0 comments on commit 87d19dc

Please sign in to comment.