three.js and other engines
three.js (the shell keeps its board, bar and touch pad; three.js draws on the shell’s canvas). Load three.js as the global THREE (<script src="lib:three"></script> on the tickcli server, which makes it from the installed three package, 0.171 or newer; or your own bundle), then room.js, game.js, game3d.js:
const R3 = TickcliThree; // client/game3d.js: renderer(g), resize(renderer, camera, g), place(obj, state), pose(obj), pool(parent, make)let renderer, scene, camera, me, others;Game({ context: "webgl", // no 2D context: g.canvas is yours; draw(g) runs every frame multiplayer: { max: 8, player: { x: [-20, 20], y: [0, 5], z: [-20, 20], qx: [-1, 1], qy: [-1, 1], qz: [-1, 1], qw: [-1, 1] } }, setup(g) { renderer = renderer || R3.renderer(g); /* scene, camera, me = new THREE.Mesh(...) */ others = others || R3.pool(scene, (p) => makeAvatar(p)); }, update(g, dt) { /* move me */ g.me.set(R3.pose(me)); }, // position + quaternion, rounded host(g, dt) { g.shared.set({ gem: R3.pose(gem) }); }, // host-moved things ease too draw(g) { R3.resize(renderer, camera, g); // size, pixel ratio (the quality ladder), camera aspect others.sync(g.players.filter((p) => !p.me)); // one object per other player, eased, removed when they leave renderer.render(scene, camera); },});A rotation is a quaternion in four fields qx, qy, qz, qw (three.js and Babylon order). Other screens ease it as one turn (slerp, the short way round, always of length 1); players declare each as [-1, 1] and send 3 decimals. In webgl mode the positions the host shares keep 2 decimals (multiplayer.decimals changes it; 2D games keep whole numbers). g.pointer.nx, ny (-1..1) go straight into a Raycaster. Example: examples/games/arena3d.html.
Any engine that runs its own loop (Phaser, PixiJS, Babylon.js, Kaplay, PlayCanvas, Excalibur, your own requestAnimationFrame): headless: true. The shell adds only the lobby, notices and countdown over your page (clicks pass through everywhere else) and leaves your page, canvas, loop and touches alone. The room is the same: setup, host, onAction, onFx, g.me.set, g.shared.set, g.players (eased), g.gameOver().
const g = Game({ headless: true, title: "My game", multiplayer: { max: 8, player: { x: [0, 800], y: [0, 600] } }, host(g, dt) { /* rules on one player: g.shared.set({...}) */ } });
// in your engine's update, once a frame: the whole room is these two linesif (g.state === "playing") g.me.set({ x: hero.x, y: hero.y });g.sync(make, place, remove); // one object per other player: make(p) when they join, place(sprite, state) every frame (eased), remove(sprite) when they leave| Engine | Call from | make / place / remove |
|---|---|---|
| Phaser 4 | the scene’s update() |
this.add.circle(0, 0, 12, 0xff0000) / s.setPosition(st.x, st.y) / s.destroy() |
| PixiJS 8 | app.ticker.add(...) |
app.stage.addChild(new PIXI.Graphics().circle(0, 0, 12).fill(0xff0000)) / s.position.set(st.x, st.y) / s.destroy() |
| Babylon.js | scene.onBeforeRenderObservable.add(...) |
BABYLON.MeshBuilder.CreateBox(id, {}, scene) / s.position.set(st.x, st.y, st.z); s.rotationQuaternion = new BABYLON.Quaternion(st.qx, st.qy, st.qz, st.qw) / s.dispose() |
| Kaplay | onUpdate(...) |
add([circle(12), pos(0, 0), color(255, 0, 0)]) / s.pos = vec2(st.x, st.y) / destroy(s) |
On the tickcli server, <script src="lib:phaser"></script> and <script src="lib:pixi"></script> load the installed phaser and pixi.js packages’ own global builds (PixiJS with its no-eval add-on, since game pages run without eval), like lib:three.
Examples: own-engine.html (a plain canvas engine, two marked lines of room code), sky-duel.html (Phaser air shooter), neon-paint.html (PixiJS, draw together), arena3d.html (three.js), chess.html (the shell alone). All in examples/games/, played by two browsers in e2e/demos.ts.