Using power of Three.js and Ammo.js .Class oriented , script type module. No build no extra time spend needed. Networking with webRTC/Node.js signaling server. First person shooter Multiplayer solution
Using power of Three.js, ammo.js. MagicThree is nice class sorted top level of threejs and ammo.js. Magic-three use the new version threejs 149.
[JS type of script module variant with last version of three.module.js]
Magic-Three is First Person Oriented but can be used for any other case of app flow.
No build needed, just copy/paste for both dev and prod mode.It is the module type of script. Nice fit with npm modules also works direct in browser.
Custom magic Map loader. All 3d objects comes from map.
No package.json [if this repo become npm package then will be back]
Must be fully PWA [cache, server compression, image format webp etc...]
MultiLang support [async load JSON MultiLang file avoid loading all multiLangs]
Basic example: FPS Player controller [bullet , collision]
recommended*** -> New networking driver based on OV/Kurento. https://docs.openvidu.io/en/2.11.0/cheatsheet/send-messages/
⚠️From 23 oct 2024 -> frontend old net code will be removed. server part is still present.
⚠️(only if you wanna use RTCMulti)
In folder ./backend we have package.json to import deps (npm i) for server part.
Run in folder ./backend cmd: npm i and npm run magic for host and broadcaster.
[⚠️ Old ]Networking based on webRtc multiRTC3 library. Signaling server, video chat or stream to texture.
If you wanna use new net just run your own openvidu server on your VPS [i use 2.20.0 version].
Frontend -> Three.js, Ammo.jsBackend -> OV/Kurento OR MultiRTC3import Application from './Application.js';
import config from './config.js';
import myGamePlayMagicMap from './public/assets/maps/free-for-all.js';
let App = new Application(config, myGamePlayMagicMap);
const config = {
cache: false,
stats: false,
camera: {
fov: 60,
near: 0.2,
far: 2000,
order: 'YXZ'
},
map: {
autoplayBgMusic: true,
sky: {
enabled: true,
uniforms: {
turbidity: 0.5,
rayleigh: 3,
mieCoefficient: .05,
mieDirectionalG: .01,
}
},
background: 0xffffff,
floorWidth: 200,
floorHeight: 200,
gravityConstant: 17.5,
directionLight: {
color: 0x3f3f3f,
intensity: 50,
LRTB: 14,
shadow: {
camera: {
near: 2,
far: 50
},
mapSize: {
x: 1024,
y: 1024
}
}
},
ambientLight: {
color: "rgb(250,250,250)"
},
meshShadows: {
castShadow: false,
receiveShadow: true,
computeVertexNormals: true
},
blockingVolumes: {
visible: false
},
collision: {
detectCollision: true
},
nightAndDay: {
enabled : true,
animSun: 1000,
nightFallsAt: 19,
dawnAt: 6
}
},
playerController: {
type: 'FPS', // FPS | orbit
movementType: 'velocity', // velocity | kinematic
cameraInitPosition: {x: 0, y: 0, z: 72},
alwaysRun: false,
mobile: {
hudControls: true
},
movementSpeed: {
forward: 20, backward: 10,
left: 9, right: 9,
jump: 11, jumpLimitInterval: 2000
},
physicsBody: {
typeOfPlayerCapsule: 'cube', // ball
visible: false,
radius: 2, cubeCapsuleScale: [2.5, 2.5, 2.5],
mass: 10
},
bullet: {
mass: 20,
radius: 0.07,
power: 200,
bulletLiveTime: 1000
},
playerData: {
energy: 1000
},
playerItems: {
munition: 200
},
onEvent : {
onDie: "justHideNetPlayer" // "justHideNetPlayer" | "reload"
}
},
useRCSAccount: true,
networking: {
broadcasterPort: 9001, // use dev stage
broadcasterInit: false,
// domain: "maximumroulette.com",
domain: "localhost",
networkDeepLogs: true,
/**
* masterServerKey is channel access id used to connect
* endpoint p2p. Multimedia server channel/multiRTC3 used.
*/
masterServerKey: "magic.three.main.channel",
runBroadcasterOnInt: false,
broadcastAutoConnect: false,
/**
* If you dont wanna initially camera call
* you need to set audio AND video to `false`
* Data works by default.
*/
broadcasterSessionDefaults: {
sessionAudio: true,
sessionVideo: true,
sessionData: true,
enableFileSharing: true,
},
stunList: [
"stun:maximumroulette.com:5349?transport=udp",
"stun:maximumroulette.com:3478?transport=udp",
"maximumroulette.com:3478",
"maximumroulette.com:5349",
"stun:stun.l.google.com:19302",
"stun:stun1.l.google.com:19302",
"stun:stun.l.google.com:19302?transport=udp",
],
getBroadcastSockRoute() {
return getProtocolFromAddressBar() + getDomain() + ":" + this.broadcasterPort + "/";
}
},
networking2: {
masterChannel: "magic",
runKureOnInt: true,
publishAudio: true, // Whether you want to start publishing with your audio unmuted or not
publishVideo: true, // Whether you want to start publishing with your video enabled or not
}
}
Blocking Volumes implemented for map - map.objMtlsArray :
Nice for walls and env staff. Forced simple cube physics body with mass = 0.
Frontend done in script type "module" ant it's so powerfull. No build time lost.
dispatchEvent(new CustomEvent('destroyObject', {detail: e.connection.connectionId}))i use OV 2.20.0 client part.
I borrowed a service with an address https://maximumroulette.com:2020
Interface send(arg) function is the same like other variant of networking.
Because on mobile devices it is not recommended video chat streming on load also in game play for now. New config flag:
mobilePublishVideo: false,
mobilePublishAudio: false
Check for user internet speed with navigator.connection.downlink
For low net disable streaming.
Easy running also on VPS:
Working example:
let map = {
breakable: [
{
name: "myBreakAbleBox1",
mass: 100,
scale: {x: 2, y: 5, z: 2},
pos: {x: 3, y: 1, z: 1},
quat: [0, 0, 0, 1],
matFlag: 'Black' // new
}
],
boxs: [
{
name: "myMidBox1",
net: true,
mass: 10,
scale: {x: 5, y: 5, z: 5},
pos: {x: 0, y: 1, z: 20},
quat: [0, 0, 0, 1],
matFlag: 'Bronze'
}
],
tubes: [
{
name: "myTube1",
mass: 1000,
scale: [5, 5, 20, 32],
pos: {x: -20, y: 1, z: -80},
quat: [0, 0, 0, 1]
},
{
name: "myTube2",
mass: 1000,
scale: [5, 5, 20, 32],
pos: {x: 20, y: 1, z: -80},
quat: [0, 0, 0, 1]
}
],
torus: [
{
name: "myTorus1",
mass: 1000,
scale: [10, 3, 16, 100],
pos: {x: 30, y: 1, z: 1},
quat: [0, 0, 0, 1]
}
],
pointLights: [
{
name: 'l1',
color: 0xff0040,
radius: 2,
intensity: 150,
pos: {x: 30, y: 12, z: 10},
helper: true
},
{
name: 'l2',
color: 0xeeee40,
radius: 2,
intensity: 510,
pos: {x: -30, y: 12, z: 10},
helper: true
}
],
objMtls: [
{
path: 'assets/objects/env/wall1.obj',
name: 'myWall_1',
pos: {x:-100, y:-0.5, z:-42}
}
],
objMtlsArray: [
{
path: 'assets/objects/env/wall1.obj',
name: 'myWall',
instances: [
{pos: {x: -100, y: -0.5, z: -62}},
{
pos: {x: 52.8, y: -0.5, z: 86.5},
rot: {x: 0, y: 90, z: 0}
}
]
}
]
};
export default map;
I have performance stable at ~90% value. I load extra fbx animation 22Mb to test little more better. Image formats like WebP and AVIF often provide better compression than PNG or JPEG, which means faster downloads and less data consumption. I use freeware GIMP he had a webp format support for exports.
Lighthouse screenshot:

No need for PWA at dev/localhost work. In final time you can use .prod.js compressed files to make full optimised app with better preformance.
Only on startup for now:
addEventListener('multi-lang', () => {
// You can setup inline or you can use data-label="KEY"
byId('header.title').innerHTML = t('title');
byId('player.munition.label').innerHTML = t('munition');
...
});
New networking based on kurento/openvidu server.
Force streaming on mobile with URL Params ?video=true&audio=true:
https://maximumroulette.com/apps/magic/public/module.html?video=true&audio=true
You need to start your own openvidu server.
Using css vars from vars.css.
Script:
import {setCssVar} from "./utility.js"
export class MagicTheme {
Light() {
console.log('THEME LIGHT SET')
setCssVar("--bg", "#6b6b6b33")
setCssVar("--text", "hsl(1, 20%, 100%)")
setCssVar("--text2", "rgb(0, 0, 0)")
setCssVar("--err", "orangered")
setCssVar("--bgBlocker", "rgba(150, 150, 150, 0.9)")
setCssVar("--bgTransparent1", "rgba(0, 0, 0, 0.1)")
setCssVar("--LG1", "linear-gradient(87deg,#ff6f00,#b5830f,#df494b,#fff,#fff,#e90b0f)")
setCssVar("--mainFont", "Accuratist")
}
Dark() {
setCssVar("--bg", "#0d2d4e")
setCssVar("--text", "hsl(0, 0%, 100%)")
setCssVar("--text2", "rgb(255, 253, 192)")
setCssVar("--err", "red")
setCssVar("--bgBlocker", "rgba(10, 10, 10, 0.9)")
setCssVar("--bgTransparent1", "rgba(0, 0, 0, 0.1)")
setCssVar("--LG1", "linear-gradient(87deg,#00b3ff,#510fb5,#49cbdf,#000000,#000000,#1d0be9)")
setCssVar("--mainFont", "stormfaze")
}
Green() {
setCssVar("--bg", "#000")
setCssVar("--text", "hsl(107.39deg 82.83% 47.02%)")
setCssVar("--text2", "rgb(42 199 49)")
setCssVar("--err", "red")
setCssVar("--bgBlocker", "rgba(10, 10, 10, 0.9)")
setCssVar("--bgTransparent1", "rgba(0, 0, 0, 0.1)")
setCssVar("--LG1", "linear-gradient(87deg,#10f30f,#fff,#10f30f,#000000,#10f30f,#000000)")
setCssVar("--mainFont", "WARGAMES")
}
constructor() {
addEventListener("theme", (e) => {
this[e.detail]();
})
}
}
Login example:
async login() {
let route = this.apiDomain || location.origin;
byId('loginRCSBtn').disabled = true;
byId('registerBtn').disabled = true;
let args = {
emailField: (byId('arg-email') != null ? byId('arg-email').value : null),
passwordField: (byId('arg-pass') != null ? byId('arg-pass').value : null)
}
fetch(route + '/rocket/login', {
method: 'POST',
headers: jsonHeaders,
body: JSON.stringify(args)
}).then((d) => {
return d.json();
}).then((r) => {
console.log(r.message);
notify.show(`${r.message}`)
if(r.message == "User logged") {
this.email = byId('arg-email').value;
byId('myAccountLoginForm').style.display = 'none';
sessionStorage.setItem('RocketAcount', JSON.stringify(r.flag))
}
}).catch((err) => {
console.log('[My Account Error]', err)
notify.show("Next Login call try in 5 secounds...")
setTimeout(() => {
this.preventDBLOG = false;
this.preventDBREG = false;
byId('registerBtn').disabled = false;
byId('loginRCSBtn').disabled = false;
}, 5000)
return;
})
}
fetch("http://maximumroulette.com/rocket/login", {
"headers": {
"accept": "application/json",
"accept-language": "en-US,en;q=0.9,ru;q=0.8",
"cache-control": "no-cache",
"content-type": "application/json",
"pragma": "no-cache"
},
"referrer": "http://maximumroulette.com/apps/my-admin/",
"referrerPolicy": "strict-origin-when-cross-origin",
"body": "{\"emailField\":\"[email protected]\",\"passwordField\":\"123123123\"}",
"method": "POST",
"mode": "cors",
"credentials": "omit"
});
(prepared in blender) you can open it in any 3d editor: https://drive.google.com/drive/folders/194gsNMBvljJgK_2nyM4paA-veBZl8_Tf?usp=sharing