A playground for experimenting and prototyping real-time 3D graphics in Three.js. Mainly based on a top-down rogue-like game, developed for the "Interactive Graphics" course in Sapienza University of Rome, a.y. 2024-2025. The game was build from scratch, mixing arcade action with light RPG mechanics (XP, levels, power-ups) while showcasing several real-time graphics techniques and gameplay systems.
Here, or clone the repo and launch index.html.
.
├── assets/ # GLTFs, textures, UI icons, fonts ...
├── src/
│ ├── Bullet.js
│ ├── constants.js
│ ├── Enemy.js
│ ├── EnemySpawner.js
│ ├── Game.js # main game loop & orchestration
│ ├── getParticles.js # GPU sprite system
│ ├── GridPathFinder.js # A* on a binary grid
│ ├── HeartPickup.js
│ ├── LaserTextures.js
│ ├── LoadingMgr.js
│ ├── LoadingScreen.js
│ ├── main.js # entry point
│ ├── Minimap.js
│ ├── Molotov.js
│ ├── Player.js
│ ├── Turret.js
│ └── UI.js # DOM & CSS-only HUD
└── index.html
Two modes, toggled with C:
-
Follow – camera chases the player by simulating a mass–spring–damper:
$$ \mathbf a = -k(\mathbf x - \mathbf x_t);-;c\mathbf v \quad\Longrightarrow\quad \mathbf v \leftarrow \mathbf v + \mathbf a,\Delta t,; \mathbf x \leftarrow \mathbf x + \mathbf v,\Delta t $$ k = 12→ stiffness;c = 8→ damping.- Results in critically-damped motion – no camera “jitter” yet no parachute-like lag.
-
Fixed – user pans with RMB drags; the camera orbits a frozen centre.
Keyboard shortcuts
| Key | Action |
|---|---|
Q / E |
Rotate camera ± about Y |
| Mouse wheel | Zoom (orthographic scale) |
| RMB drag | Pan in Fixed mode |
Game.loadStaticRocks() shows how to control RNG with statistics:
const t = Math.random() ** 3; // cubic bias → many small rocks
const scale = lerp(min, max, t);Because
A similar approach is used for fences & trees – each tries up to maxAttempts times before giving up to avoid overlaps (simple Poisson disk).
Each projectile is a 3‑part composite (tiny cylinder core + glow sprite + pooled trail) yet costs <1 ms per frame for hundreds of rounds.
const E = 0.5 * m * v.lengthSq(); // kinetic energy (no costly sqrt)
const dmg = 0.02 * E; // linear damage scale| Property | Value / behaviour |
|---|---|
| Muzzle speed | 300 m s⁻¹ |
| Mass | 0.05 kg |
| Gravity | Applied every frame → true parabolic drop (g = 9.81 m s⁻²). |
| Drag | None (commented out) – horizontal speed stays constant. |
| Cull distance | Deleted after travelling ≈ 200 m (squared radius = 40000). |
| Collision | Sphere‑sphere (full 3‑D distance). |
| Damage | Proportional to kinetic energy ⇒ quadratic boost if bullet speed is buffed. |
| Tracer | 64‑sprite additive pool, stamped every 0.015 s and faded out. |
Gameplay implications
- Rounds exhibit visible bullet‑drop over long distances.
- Air air implementation is commented ⇒ range is essentially unlimited until the 200 m kill‑box reaps the entity, keeping GPU load bounded. To implement air drag, it would be a simple
this.velocity.multiplyScalar(1 - 0.01 * dt). - Cheap maths: two vector adds for motion, one dot‑product for energy – ideal for large waves.
Knife hits and explosions impart a horizontal impulse
Vertical components are nulled for simplicity (enemies aren’t true rigid bodies).
A small exponential air drag keeps velocity bounded:
this.velocity.multiplyScalar(Math.exp(-4 * dt)); // τ ≈ 0.25 sGridPathFinder.js is a minimalist 8-neighbour A* on a Uint8Array.
-
Convex hull of every static prop is rasterised once.
-
Heuristic: octile distance
$$h = (\sqrt2-1)\min(|dx|,|dz|) + \max(|dx|,|dz|)$$ -
Distance-based adaptive repath timer
const distFactor = clamp(dist² / 100, 0.5, 2.0); this.repathTimer = baseTime * distFactor * rand(0.8,1.2);
Enemies far away waste fewer cycles on path refreshes.
getParticles() is a GPU billboarding engine with custom shaders:
- Fire, smoke, electric aura presets
- Per-particle size, spin & colour driven by linear spline key-frames
- Single
THREE.Pointsdraw call – thousands of quads at <1 ms
Used by:
| Effect | Preset | Notes |
|---|---|---|
| Molotov flames | fire |
120 pps, additive blend |
| Potion buff aura | aura |
Cyan orbits around the player |
| Molotov smoke | smoke |
Normal blend, depth-tested |
Each bottle spawns a burn pool (radius = 30 m, lifetime = 8 s). Every frame we iterate over enemies inside the ring and apply a quadratic heat fall‑off:
const dist = enemy.pos.distanceTo(centre);
const t = 1 - dist / radius; // 0 … 1
const dmg = damagePerSec * t * t * dt; // quadratic drop‑off- Centre (t = 1) → full 150 DPS
- Edge (t ≈ 0) → zero damage – gives players a buffer to escape
- Multiplied by
dt→ frame‑rate independent
Tweakable knobs:
| Parameter | Purpose |
|---|---|
radius |
Footprint of the fire puddle |
damagePerSec |
Peak DPS at the epicentre |
power in t^n |
1 = linear, 2 = quadratic (default), 3+ steeper |
A scorch decal plus GPU‑particle fire & smoke presets sell the effect visually.
- Custom WebGL renderer draws glassy MISO font + dash-offset outline.
LoadingMgrmirrorsTHREE.LoadingManagerprogress → animated “xxx %”.- Asset load + first rendered frame gate the fade-out – no white flash.
I started from @bobbyroe/animated-text-effect for the outline animation effect.
Pure <canvas> overlay:
- Player is always centre; world points are rotated by −(camAngle+90°) so camera-forward = “up”.
drawPlayerArrow()draws a little isosceles triangle oriented with the projected facing vector.- Enemy & pickup dots are 1-pixel squares for a crisp retro vibe (
imageRendering: pixelated).
Mouse & keyboard listeners are registered once in Game.registerEventListeners().
Flags live in this.input → tight update() loops have zero DOM queries.
- Drag-and-drop build mode emits synthetic
pointermoveto reuse the same logic paths. - Global
first-render-completeallows LoadingMgr to finish only after the very firstrenderer.render()– reliable even on low-end hardware.
- Heavy GLTFs are streamed with per-folder
setPath()to cut URL spam.
Browsers parallelise fetches – main thread never stalls.
| Key / Mouse | Action |
|---|---|
| W A S D | Move / run (always sprint) |
| Mouse L | Knife attack |
| Mouse R | Drag to move camera in fixed mode |
| 1 | Place Turret (drag & release) |
| 2 | Throw Molotov (drag) |
| 3 | Drink Potion |
| Q / E | Rotate camera |
| C | Toggle follow / fixed camera |
| R | Restart after death |
| P | Pause |
Enjoy hacking and extend as you please :)

