player/ARCHITECTURE.md
МИН 87444ee2c8 Initial public release: Rublox Player v1.0
Open-source web player for Rublox games, dual-licensed under
AGPL-3.0 + Commercial.

Highlights:
- Babylon.js 7 + React 18 + Vite 5 stack
- Self-contained engine (~46k lines): BlockManager, ModelManager,
  PlayerController, ScriptSandboxWorker, MultiplayerSync, 30+ GD
  gamemodes
- Configurable backend via VITE_API_BASE and friends — works against
  staging (dev-api.rublox.pro) out of the box
- Standalone mode (VITE_STANDALONE=true) loads a bundled sample game
  for first-run without any backend
- Full docs: README, ARCHITECTURE, CONTRIBUTING, SECURITY, CHANGELOG
- Lint + format scaffolding (ESLint + Prettier + EditorConfig)
- Legal: LICENSE (AGPL-3.0), LICENSE-COMMERCIAL.md, CLA.md, COPYRIGHT.md
- Issue templates: bug_report, feature_request, security_disclosure

Removed before public release:
- frontend_deploy.py (contained production SSH credentials)
- ~27 admin endpoints (kept in private repo)
- Hard-coded internal URLs and IPs
- All previous git history (clean repo init)
2026-05-27 23:04:04 +03:00

6.5 KiB
Raw Blame History

Architecture — Rublox Player

How a 3D game is loaded, rendered, and synced. ~5 minutes read.

Top-level flow

URL = /<gameId>           e.g. /265
   │
   ▼
PlayerAuthProvider          checks JWT in localStorage["player_jwt"]
   │                        OR exchanges URL #ticket=... for JWT
   ▼
useAuth().isAuthenticated
   │
   ▼
KubikonPlayer.jsx           main container, gets {projectId} from useParams
   │
   ├── GET /api-storys/kubikon3d/projects/{id}    → project_data (JSON)
   │
   ▼
BabylonScene.create()       construct Babylon engine, scene, lights, skybox
   │
   ▼
GameRuntime.loadProject()   parse project_data, instantiate everything:
   │                        - BlockManager.placeBlock() × N (Minecraft-style blocks)
   │                        - ModelManager.spawnModel() × N (Kenney prop GLBs)
   │                        - DecoManager.placeDeco() × N (terrain decorations)
   │                        - PrimitiveManager.add() × N (cubes/spheres/cylinders)
   │                        - PlayerController.spawn() (R15 character + camera)
   │
   ▼
ScriptSandboxWorker         user JS scripts run in a Web Worker sandbox.
   │                        Exposed API: game.player, scene, ui, broadcast.
   │
   ▼
MultiplayerSync             optional. Colyseus room joins, syncs positions.
   │
   ▼
RENDER LOOP (60 fps)        scene.render() each frame

Key modules

engine/BabylonScene.js

Wraps Babylon Engine + Scene. Creates lighting (HemisphericLight + DirectionalLight + shadow generator), skybox, fog. Single source of truth for scene reference.

engine/GameRuntime.js

Orchestrator. Reads project_data (the JSON saved by the studio editor) and dispatches each item to the right manager. Has lifecycle hooks: loadProject(), start(), pause(), dispose().

Also handles "external URL" resolution (_resolveExternalUrl) — game scripts can call game.openUrl('/kubikon/play/12') and it correctly resolves to the player itself or the main Rublox site.

engine/PlayerController.js

R15 character (15-bone Mixamo rig). First-person and third-person cameras. WASD/touch input. Jumping, gravity, collision against block grid + AABB models. Spawning/respawning.

Special GD modes: setAutoRun(true), setShipMode(true) — used by Geometry Dash gamemodes (cube/ship/wave/UFO/ball/spider).

engine/BlockManager.js / TerrainVoxelBuilder.js

Voxel terrain. Blocks are uint16 type IDs in chunked arrays. rebuildChunk(chunkId) does greedy meshing → single mesh per material per chunk (~40-100× fewer draw calls than naive).

engine/ModelManager.js

Loads .glb Kenney models (or designer-uploaded GLBs from /api-storys/assets/rublox-designer/models/...). Caches AssetContainer per modelTypeId, instantiates clones per spawn.

engine/DecoManager.js

Lightweight decorative props (rocks, plants, signs). Uses ThinInstanceCount for massive performance (thousands of instances → single draw call per type).

engine/scripts/ScriptSandboxWorker.js

User-uploaded JS scripts run in a separate Web Worker. Exposed API surface (read-only):

// In a user script:
game.onTick((dt) => { ... });
game.onKey('space', () => { player.jump(); });
game.broadcast('event', payload);
game.player.setHealth(100);
scene.findOne('Cube1').rotateY(0.1);
ui.set({ score: 42 });

Scripts CANNOT access window, document, fetch, localStorage, network. They run isolated in a Worker with a strict postMessage bridge.

engine/multiplayer/MultiplayerSync.js

Colyseus 0.16 client. Joins a room per gameId if multiplayer is enabled. Syncs player positions/rotations/animations at 20 Hz. Server is at VITE_REALTIME_WS.

engine/gd/* (Geometry Dash modules)

30+ classes implementing GD-style 2D autorunner gamemodes within a 3D world:

  • GdCube.js — basic jump
  • GdShip.js — gravity-flip flight
  • GdWave.js — sine-wave dash
  • GdBall.js — flip-jump ball
  • GdUfo.js — multi-tap jumps
  • GdSpider.js — instant teleport
    • portals, speedpads, spikes, finish lines, trail effects, checkpoint music

Factories for each (GdSpikeFactory, GdPortalFactory, GdMusicFactory, ...) live under AdminPreview/gd*/.

Data flow (single frame)

1. INPUT       keyboard/touch/gamepad → PlayerController.onInput()
2. PHYSICS     gravity + velocity + collision against blocks/models
3. SCRIPTS     user script onTick(dt) callbacks (in worker, asynchronous)
4. MULTIPLAYER read remote positions, lerp other players
5. RENDER      Babylon scene.render() → WebGL2 draw calls
6. UI          React re-renders only if game.ui.set() called (throttled 250ms)

What's NOT in the player

  • Editing — that lives in Studio.
  • Project listing / search / publishing — handled by main site (rublox.pro/app).
  • Authentication UI — players arrive with JWT/ticket. The player only consumes them.
  • Admin / moderation — moved out to private repo (disaster-recovery/ for owner).

Performance hotspots (places to look first when laggy)

  1. game.ui.set() called every frame — React setState 60Hz tanks FPS. Throttle to 250ms with a diff check.
  2. scene.findOne() on initsceneSnapshot arrives via rAF; reference is null. Move into onTick or setTimeout(0).
  3. Babylon's blockMaterialDirtyMechanism=true — DON'T enable. Breaks new mesh rendering (debris, tracers).
  4. createOrUpdateSelectionOctree() — same. Breaks editor preview ghosts.
  5. GLB load via SceneLoader instead of AssetContainer — leaks materials. Use AssetContainer + instantiateModelsToScene().

Where to start a feature

What you want to add Start here
New block type engine/CONST/blockTypes.js + texture in public/kubikon-assets/blocks/
New scripting API engine/scripts/ScriptSandboxAPI.js (add to apiSurface)
New GD gamemode Copy engine/gd/GdBall.js, register in GdGameModeRegistry.js
New HUD widget editor-shared/GameHud.jsx
New preview route for designer AdminPreview/ (e.g. gdSkins/PreviewGdSkins.jsx)
Multiplayer event Colyseus schema in engine/multiplayer/schemas/ + handler in MultiplayerSync.js

License notes for contributors

By contributing you agree to license your changes under AGPL-3.0 AND grant the project maintainer a non-exclusive irrevocable license to sublicense (see CLA.md). This is required so we can offer commercial licenses to enterprises.