Back to the start page

Devlog

The complete development log, written along the way by Claude. 31 entries, 59 screenshots, three days.

A modern, low-poly neon remake of Blobby Volley: side view, 3D, local + network multiplayer, smash serve, deterministic physics and a training interface for a neural-network opponent.

Team: Leonhard, Claude, Godot 4.7, Blender 5.2, Gemini (image + TTS), Mixamo animations.


Day 1 - 2026-09-29

1. Kick-off and decisions

  • Engine: Godot 4.7.2 (Forward+), assets built in Blender 5.2 via Python scripts (reproducible, headless).
  • Characters: own low-poly blobs (classic two-ball shape), no Kenney figures.
  • Network: LAN + direct IP (ENet), rules switchable (Classic 15 with side-out, Rally 25).
  • Arenas: four stages as a loose homage to classic fighting-game stages (own names, original designs): Spike Bridge, Temple Court, Whispering Woods, Acid Crypt.

2. Style exploration (Gemini 3 Pro Image)

12 concept frames + 3 character sheets in three low-poly directions, generated with tools/gemini/gen_concepts.py: A Neon Grid (Tron-like), B Dark Tournament, C Stylized Toon.

Style sheet

Decision: A - Neon Grid, with the Spike Bridge frame as the exact target look for scenery, blobs and effects:

Target look

Also considered:

Dark Tournament Stylized Toon
Concept Dark Temple Concept Toon Forest

3. Physics core (game/core/sim.gd)

The whole game logic is a small, engine-independent, deterministic 2D simulation:

  • Fixed 120 Hz tick, no engine physics, no trigonometry at runtime.
  • Geometry derived from Blobby Volley 2 (1 m = 40 px): blob = union of two circles (body r 0.825, head r 0.625), ball r 0.78, net = capsule, height 5.4 m.
  • Exact continuous collision detection: time-of-impact against walls, net sides, net cap and both moving blob circles, several events per tick - nothing can tunnel, regardless of speed.
  • Classic Blobby hit response (ball leaves along the contact normal with fixed speed) plus a guard so a rising blob never "catches up" with the ball it just hit.
  • Smash: airborne + action held; direction steep / mid / flat depending on stick input. A smash can only drive the ball down if it is in front of the blob - the direction bends until the ball separates cleanly (no ball through the body). The smasher's upward momentum is killed (hang time).
  • Smash serve: press action on the ground while serving to toss the ball, jump, smash (extra power).
  • Pinned ball (between blob and wall/net/ground): static geometry wins, the blob yields.
  • Full state = 37 doubles -> save_state()/load_state() for rollback netcode, replays and training.

Test suite game/tests/sim_tests.gd (headless):

determinism                  OK   (30k ticks, 2 sims, identical checksums)
rollback                     OK   (save, run 600 ticks, load, replay -> identical)
penetration fuzz (200k ticks) OK  hits=1995 smashes=404 rallies=356 worst_penetration=0.0000003
high speed shots (45 m/s)    OK
rules classic / rally        OK
smash serve                  OK
perf: ~120,000 ticks/s  (~1000x realtime)  -> fast enough for NN training
RESULT: ALL PASSED

Bugs found by the fuzz test on the way: resting ball stopped CCD early (ball sank into blobs), a smash from on top of the head drove the ball through the body, and the pinned-ball fallback resolved the two blob circles one after another instead of jointly.

4. The blob (Blender + Mixamo)

tools/blender/make_blob.py / make_blob_rigged.py:

  • Lathe body whose profile follows the union of the two collision circles - every vertex is asserted to lie inside them, so what you see is what collides. Flat-shaded facets, staggered rings.
  • Small stubby arms and glowing eyes (like the ghost blobs in the concept).
  • A 15-bone Mixamo skeleton (exact mixamorig:* names) fitted into the blob, so Mixamo clips retarget.
  • Animations: idle_happy, idle_sad, idle_dwarf, jump_joyful (retargeted from Leonhard's Mixamo FBX, rotations only, softened so the two-ball body doesn't fold), plus procedural run, jump, jump_air, smash, win, lose.

Blob rig preview

5. Arenas (Blender, tools/blender/make_arenas.py)

Built procedurally in Blender instead of assembling Kenney kits, so everything shares one faceted style. Contract with the game: the court volume stays empty, materials are identified by name (neon_solid, neon_glass, spike, emit_white, emit_accent, acid, banner) and replaced by Godot shaders; Light_* empties carry colour/energy for real lights; Arena_Info carries the palette.

Spike Bridge Temple Court
Arena Bridge Arena Temple
Whispering Woods Acid Crypt
Arena Forest Arena Crypt

(Blender preview renders - the neon edge lines are drawn in-game by a post-process shader.)

6. Neon rendering in Godot

  • Edge post-process (assets/shaders/neon_edges.gdshader): a full-screen pass reads Godot's depth and normal-roughness buffers and draws glowing lines wherever normals or depth jump - i.e. on every face edge of the flat-shaded low-poly scenery. Colour splits cyan (left court) / magenta (right court) via global shader parameters set per arena.
  • Materials encode how strongly their edges glow in the roughness channel (glass = bright, rock = faint), so the post-process can treat structures differently without extra buffers.
  • Blender material names are swapped for shaders at load time (view/arena_view.gd); Light_* empties become OmniLights; Arena_Info sets fog/sky/edge palette.

First in-engine frame - it worked, but far too bright (bloom everywhere, near net post blocking the view):

First render

After tuning bloom, ambient, camera (closer, lower) and removing the near net post:

Spike Bridge Temple Court Whispering Woods
Tuned Bridge Tuned Temple Tuned Forest

7. Feedback round: "snowman" blobs, busy backgrounds, unclear ball

Leonhard: the concept blob's head flows into the body; ours looked like two stacked balls. Backgrounds should be calmer and more mystical, blobs less transparent, the ball easier to read, blobs more organic, and they need little feet.

  • Physics shape changed from the union of two circles to their convex hull (a 2D round cone): exact signed distance (cone_sdf), exact continuous collision against both arcs and the two tangent segments. The Blender body follows the same profile, so the waist is gone and collisions still match the visible surface. Fuzz test after the change: worst penetration 0.0000009 m.
  • Blender rebuild: organic gumdrop body, softer head-to-body weights, two little feet with their own bones (steps in run, dangling in the jump clips, taps in idle_happy/win).
  • Blob shader: far less transparent (alpha 0.86), inner glow, inward-only jelly wobble in the vertex shader and a "drag" that lets the head lag behind fast movement.
  • Ball: opaque, bright faceted core with a glowing rim; longer trail.
  • Backgrounds: rock edges much fainter (roughness-encoded edge weight), darker skies, stronger depth fog plus low height fog (mist in pits/pools), calmer acid.
Before After
Tuned Bridge After Bridge
Tuned Temple After Temple

8. Netcode (net/net_session.gd)

GGPO-style rollback: both peers simulate; local input is delayed 2 ticks and sent every frame with redundancy (last 32 inputs, unreliable channel); missing remote input is predicted and the simulation is rolled back and re-simulated when the real input differs. Peers ahead of the other slow down slightly (time sync). Safety net: the host sends checksums of fully confirmed ticks, and on a mismatch the client adopts the host state (covers float differences between CPU architectures). LAN discovery via UDP broadcast, direct IP for internet play.

Soak test (tests/net_peer.gd, two headless instances, bots playing, 50 ms latency + 30 ms jitter + 10 % packet loss, 40 s):

host    tick=4464 rollbacks=742 stalls=1 desyncs=0   crc@4320=1184976549
client  tick=4459 rollbacks=517 stalls=6 desyncs=0   crc@4320=1184976549

9. Sound

  • SFX are synthesised at startup (autoload/sfx.gd): hit blip, smash boom+zap, wall tick, net twang, ground thud, arpeggios for point / side-out / win - no files needed, fits the synthwave look.
  • Announcer generated with Gemini 3.8 Flash TTS (tools/gemini/gen_announcer.py, voice "Algenib", gravelly tournament-announcer direction): start, smash, smash serve, point, side out, match point, winner - 2-3 takes each, picked at random.
  • Bug found by Leonhard: the first batch read the style direction aloud ("You are the announcer of an underground tournament..."). Per the current docs, Gemini 3.8 TTS treats the text strictly as a verbatim transcript; the delivery goes into a speech_metadata.style annotation on the new /v1beta/interactions endpoint. Regenerated, then verified by transcribing every take back with Gemini ("Smash", "Let the match begin", "Change of serve", "Victory!").

10. AI preparation

  • ai/agent_io.gd: 24-float mirrored observation, 12 discrete actions (move x jump x smash).
  • ai/heuristic_bot.gd: rule-based CPU (Easy / Normal / Hard) - ballistic landing prediction, smash near the net, smash serves; also the sparring partner for training.
  • ai/mlp.gd: MLP inference in GDScript from JSON; models in ai/models/ appear in the menu automatically.
  • training/bridge.gd: headless Godot TCP server running N simulations (vectorised env, auto-reset, reward per rally, optional shaping, self-play mode, replay re-simulation for imitation learning).
  • tools/training/: blobby_env.py (Python client), train_ppo.py (PyTorch PPO + self-play pool), bc_from_replays.py (behavioural cloning from recorded human matches - every local match is saved).

First PPO run (32 envs vs. the Easy bot, frame-skip 4) - the Python <-> Godot pipeline works end to end:

it 10  0.08M decisions  mean rally reward -0.020
it 30  0.25M            +0.005
it 60  0.49M            +0.073   (~5,500 decisions/s on the M1)

The bridge first served one request per frame (~90/s); switching to a tight request loop made it ~7x faster.

11. Menu and HUD

Main menu runs a live CPU-vs-CPU match in a random arena behind the UI (attract mode).

Menu

In game


Day 2 - 2026-09-30

12. Feedback & fixes on v1

  • The Volley CPU froze while serving: it re-decided only every few ticks and overshot a 12 cm target window forever. The tolerance now scales with how far the bot moves between decisions; a new test plays 4 minutes per difficulty and checks every serve happens within 4 s.
  • Moon toned down (faceted, pale, no bloom), the ball became a glowing ring (dark core, bright rim) so it reads in front of the moon, darker backgrounds with more contrast and a softer, wider glow.
  • v1.0 committed and tagged (local git repository, API key and raw sources ignored).

13. Party games framework (couch first, online optional)

Principle from Leonhard: every mini-game is couch multiplayer first, with an online option.

  • input/device_input.gd: up to 4 local players on keyboard halves (WASD / arrows) and gamepads.
  • core/game_sim.gd: base for deterministic N-player sims (own xorshift RNG in the saved state).
  • core/platformer.gd: shared deterministic platformer physics - solid / one-way / hazard / bounce / crumbling rectangles, moving platforms as a pure function of the tick, a character controller with coyote time, jump buffering, variable jump height, air jumps, drop-through and knockback.
  • net/party_net_session.gd: rollback for up to 4 players on several machines; the host relays inputs, CPU players belong to the host, checksums + state resync as in Volley.
  • autoload/party.gd + ui/party_lobby.gd: lobby with press-to-join per device, skins, ready-up countdown, CPU players, game and arena selection, host/join online (LAN discovery). Joined players stand on the court as blobs and dance when ready.

Bug of the day: a PackedInt32Array taken out of an Array is a copy in Godot 4 - the online session appended local inputs to a temporary and every peer stalled at tick 22. Found with packet counters.

14. New animations

Mixamo clips retargeted onto the blob rig (table-driven in make_blob_rigged.py, now 21 clips): defeated, throw, carry_walk, carry_idle, swing, spin_attack, slam, dance, victory, struggle (while being carried) and splat (faceplant after a big hit), plus a ball_socket bone.

15. Blob Brawl

Platform fighter for 2-4 blobs over the arena backdrops: damage %, knockback that grows with damage, 3 lives, double jump, swing / spin attack / slam, the ball as a weapon (grab, throw, or smack a free ball with an attack - a "hot" ball hits everyone but the thrower), and grabbing other blobs: carry them overhead while they struggle (mash to escape) and throw them off the stage. Stage layouts per arena, including moving platforms. Camera frames every fighter.

Spike Bridge Temple Court
Brawl Bridge Brawl Temple

16. Blob Race

Elimination race: shared camera, the left screen edge is a deadly wall that speeds up; fall behind, into a pit or onto spikes and you are out. Last blob standing wins the round, first to 3 wins. Shove, stomp on heads, grab-and-throw, wall jumps, crumbling and moving platforms, bounce pads, item boxes (ball, boost, mine, shockwave). Course generated from the match seed.

Whispering Woods Acid Crypt
Race Forest Race Crypt

17. Tests

party tests: platformer (landing, one-way, drop-through, walls at 80 m/s), per game: determinism,
             rollback (also into a fresh sim), 2- and 4-bot matches must finish, performance
brawl: 4 bots -> finished after 95 s, 13,600 ticks/s     race: 4 bots -> finished after 161 s, 35,000 ticks/s
online brawl, 2 machines x 2 blobs, 50 ms + 30 ms jitter + 10 % loss, 40 s:
  host rollbacks=610 desyncs=0 crc@4320=608424414 / client rollbacks=652 desyncs=0 crc@4320=608424414
online race, 3 machines (host relays), 40 ms + 30 ms jitter + 8 % loss, 40 s:
  all three peers desyncs=0, crc@4320=3813285776

18. Blob Tactics Soccer (v1.2)

Turn-based football from above, 4 vs 4 blobs, designed as a couch game: everyone plans at the same time, then all orders play out together. Leonhard's choices: real-time planning with a timer, 4 vs 4, moves = dash, hold/block, tackle/foul, kick/pass.

  • games/soccer/soccer_sim.gd: deterministic disc physics at 120 Hz (2 substeps), no engine physics. Per blob an order: DASH (direction + power), TACKLE (harder dash, knocks opponents over), HOLD (heavy, traps the ball) plus a KICK arrow that fires on the first ball contact of the turn. A tackle on a blob that is not near the ball (and did not just touch it) is a foul -> free kick at that spot, opponents pushed back 4.5 m. Goals with posts, nets and goal boxes, power-ups on the pitch (Boost, Shield, Super Kick). First to 3 goals, max 16 turns, tie -> golden goal.
  • The arrows do not lie: the damping is a fixed geometric series, so the travel distance of a dash or kick is exactly (v - v_still) / K - the planning arrows show where a blob will stop if nobody is in the way (a test checks 9.33 m predicted vs 9.33 m simulated).
  • soccer_planner.gd: one planner per local device - keyboard left (A/D aim, W/S power, F mode, Space confirm, Shift back), keyboard right (arrows, Ctrl, Enter, Backspace) and gamepads (stick = direction and power, X mode, A confirm, B back, RB skip). With 2-4 players the team's blobs are split between the team mates (slots alternate teams).
  • soccer_session.gd: plan timer (6 s + 3.5 s per blob you control), the turn starts early when everyone is READY. Online = turn lockstep: the host owns the clock, peers send their orders, the host broadcasts {state, orders} and every machine executes the same turn; the next plan message carries the host state again, so nothing can drift. 2 machines, 16 turns: identical checksums, 0 desyncs.
  • soccer_bot.gd: CPU coach with roles (keeper, 2 defenders on the ball-goal line, forward), shoots through the most open lane of the goal mouth or passes to a free team mate, tackles sometimes.
  • Balancing with a stats script (tests/soccer_stats.gd, CPU vs CPU): first version 0.02 goals per turn
    • shots died on the way or hit the defensive wall. Faster, longer-rolling ball (26 m/s, less friction), 3.5 s turns, wider goals and a fixed defender bug (the two defenders crossed paths and collided) -> about 0.1 goals per turn for CPU vs CPU; humans find more gaps.
  • View: neon pitch shader in the two team colours, glass boards, glowing goal frames with nets, top-down blobs (new yaw mode in PartyBlobView, rolling ball in BallView), additive planning arrows drawn with an ImmediateMesh, referee whistle synthesised, new announcer lines (goal, foul, save) with Gemini 3.8 Flash TTS.
Planning (1 human + 3 CPUs) Playing out, power-ups on the pitch
Soccer Plan Soccer Play
soccer tests: slot->blob split, determinism, save/load mid-turn, exact arrows, kick+goal, fair tackle vs
foul + free kick, 60 random-order turns (no NaN, nothing leaves the pitch, no overlap), keyboard planner,
6 CPU matches (2-4 players) finish, offline session plays a whole match
online soccer, 2 machines x 2 coaches: 16 turns, score 2:0 on both, crc 4087779947 both, desyncs 0

19. Feedback round on v1.2 (v1.3)

Leonhard: the Volley victory animation was hidden behind the results panel, controllers should work for every player, a 4-player Volley with depth as a separate mode (classic Volley must stay exactly as it is), and ten more stages in the spirit of the classic fighting-game arenas.

  • Results panels (Volley and party games) moved to the top of the screen, and Volley shows it 2.2 s after the final point, so the victory / defeat animations are fully visible.
  • Controllers: any standard gamepad (SDL mapping: Xbox, PlayStation, Switch Pro, 8BitDo ...) works for any player. Volley: one human (vs CPU / online) -> every connected pad controls your blob; two humans -> "Auto" gives the first and second connected pad to left / right, or pick Gamepad 1-4 / keyboard only in the setup menu. Hot-plugging re-assigns. Start pauses on every pad. Party games already had one device per player; the pads gained LB as jump and Y as action.

20. Blobby Volley 2v2 - with depth

A new party game (games/volley2v2/), classic core/sim.gd untouched (all Volley tests still pass).

  • Court 20 m x 10 m, blobs move left/right and forward/back (stick or WASD / arrows), jump with a dedicated button (Space / Enter / pad A). The blob is the same round cone as in classic Volley, now revolved around the vertical axis: the 2D signed distance is evaluated at (radial distance, height), so the hit feel (fixed hit speed along the contact normal) is identical.
  • Ball physics in 4 sub-steps per tick with "static geometry wins" ordering and a blob-yields safety pass; a 60 000-tick random-input fuzz finds no ball inside a blob, below the floor or through the net.
  • 3 touches per team, rally scoring to 15 (win by 2), the server rotates within the team.
  • Charge meter (Leonhard's idea): every touch fills your meter; 5 touches = SMASH READY. Progress is lost when your team loses the rally or your touch goes into the net. Only a full meter allows the power smash (airborne + action + ball above your head, aim steep / flat and left / right with the stick).
  • Power-ups (Leonhard's idea): GROW (1.5x size for 20 s - bigger reach, heavier in collisions), TURBO (faster for 15 s), CHARGE (instant full meter). They appear on the floor, walk over them.
  • Friendly fire (Leonhard's idea): team mates are solid for each other; running into your partner shoves both (the grown blob wins), so you can block each other - communication matters.
  • Raised camera that follows high balls, a floor marker + height line under the ball for depth perception, colored rings under the blobs, smash meters above their heads, far net post only.
  • CPU: predicts the landing point (walls + net), the closer team mate plays the ball, the other covers the other half; smashes when charged and aims away from the opponents.
  • Online: rollback netcode as for Brawl/Race. 2 machines x 2 blobs, 50 ms + 30 ms jitter + 10 % loss: 0 desyncs, identical checksum at tick 4320 (3302839690) on both.
2v2 on Temple Court Smash meters full
V2 Temple V2 Smash

21. Ten new arenas

All built by the same Blender script (make_arenas.py, now 14 builders), same contract (court volume empty, clear line of sight - all ten report 0 violations, 0 blocked rays), 5-23 k triangles each, own palette and material tuning (accent / solid / acid / glass / cloth colors per arena in ArenaView.STYLES). Loose homages, original designs and names:

Arena Homage to
Ember Throne the emperor's colosseum: tiered crowd, braziers, a spiked throne under a blood moon
Frozen Sanctum the ice clan's temple: crystal clusters, glass pillars, frozen falls, aurora
Hellforge the underworld: lava river, obsidian spires, hanging cages, skull piles, embers
Hollow Belfry the sorcerer's tower: rooftop court, giant bell, castle roofs and the sea far below
Soul Well the soul chamber: a vortex of captured souls, guardian statues, sarcophagi
Undercity Line the subway stage: tiles, tracks, a dead train, fluorescent tubes
Ashen Wastes the ruined city: broken towers with a few lit windows, wrecks, a hazy sun
Bone Lair the four-armed beast's cave: a colossal ribcage, bone piles, torches
Void Portal the portal stage: a platform adrift in space, a torn gateway, asteroids, a ringed planet
Lotus Garden the paradise realm: blossom trees, lotus pond, waterfalls, floating shrines

Brawl reuses the four stage layouts for the new arenas; Race, Soccer and 2v2 use them as backdrops.

Arenas

22. CPU difficulty: Easy / Normal / Pro / Elite

One setting for every game. Party lobby: V (keyboard) or Y (gamepad) cycles the level for all CPUs (saved, new CPUs inherit it, the cards show "CPU PRO" etc.). Classic Volley: Bot - Easy / Normal / Pro (the former Hard) / Elite in the setup menu. Every bot got a fourth parameter set (reaction time, aim error, aggression). The 2v2 CPU also misjudges some incoming balls on the lower levels, which ends rallies naturally:

Level 2v2 CPU vs CPU: touches per rally seconds per rally
Easy 3.2 4.6
Normal 10.0 11.1
Pro 16.6 17.8
Elite 24.5 28.1

23. One start screen for everything + more power-ups (v1.4)

Leonhard: start directly in the lobby, choose any game there, change its settings, start - and show every game. So the old main menu is gone:

  • The lobby is the start screen. Game carousel with all five games (classic Volley included), the description, player count, and the game settings right below (arena, CPU level, and per game: Volley rules + points, 2v2 points / touches per smash / power-ups, Brawl lives, Race round wins, Soccer goals / turns / power-ups). Edit with Tab / gamepad Start + arrows / d-pad, or click (right click = back). Settings are saved and sent to online guests.
  • Live preview: the selected game plays itself (CPU Pro) in a window top right - rendered at 1280x720 in its own world and scaled down, with sounds muted (Sfx.game_muted).
  • Classic Volley from the lobby: each side is driven by the device its player joined with (keyboard half, any gamepad) or a CPU of the chosen level (InputSources.Device); online it uses the classic rollback session - two processes via the lobby: identical checksum at tick 1200, 0 desyncs.
  • Options panel (Esc): name, volume, announcer, fullscreen, quit. CPUs alone never start a match.
  • Start checks with a clear message ("Blobby Volley is for 2 players - remove 1 (pick 2v2 for more)").

New 2v2 power-ups (all switchable, frequency Off / Rare / Normal / Often):

  • BOMB - the ball becomes a hot potato with a 6 s fuse (countdown above the ball, ticking faster at the end). When it blows up, the team on whose side it is loses the point and every blob nearby is blasted away. Picked up during a serve, the fuse starts with the rally.
  • SHRINK - the other team shrinks to 0.7x for 15 s (less reach, lighter in bumps).

Tests: lobby_flow.gd boots the lobby, starts all five games (classic Volley with keyboard vs CPU), checks they run and returns to the lobby; 2v2 tests cover bomb, shrink, "power-ups off" and "only bombs".

Lobby

24. Race start grid + Blob Micro Racers (v1.5)

Leonhard: Blob Race needs a logic for who starts 1st / 2nd / 3rd / 4th - a button-mash meter. And a new special game: an homage to the old tabletop micro-car racers - tiny cars on a table, in a kids' room, between mustard and jam, falling off, jumps, Mario-Kart-style drifting, power-ups, boost pads.

Blob Race start grid. Every round now opens with a 2.5 s MASH phase: hammer Jump / Action, the meter counts presses; the most presses gets pole position (front of the grid), ties are decided by the seeded RNG. Banner "POLE: name" with the whole order and press counts. CPUs mash at 5 / 7 / 9 / 11 presses per second depending on their level. Lobby option "Start order": Button mash / Rotate.

Blob Micro Racers. First I asked the image model for options, with our neon bridge image as the style reference - breakfast table, workbench, kids' room, pool table. All four became tracks:

Track Special
Breakfast Table toast ramp over the gap to the serving tray, butter-knife bridge back, mustard bottle, jam jar, cereal bowl, sugar (slippery), jam (sticky)
Workbench U-shaped bench around a cut-out: the bent end of a ruler launches you over the hole, the ruler is the bridge on the way back; sawdust, glue, soldering station, pegboard with tools
Kids Room figure eight on a desk - the picture-book ramp jumps you over the crossing traffic; toy blocks, robot, lamp, marbles, play dough, a cable hole to fall into
Pool Table six pockets to fall into, the ball rack in the middle, a thick book and a narrow cue as bridges to the side table

How it plays:

  • One shared screen (the classic): the camera follows the leader; drop out of the view and you are out, the last car on screen scores, first to 3 / 5 / 7 points. Or Lap race (3 / 5 / 7 laps): stragglers are put back behind the pack instead.
  • Drive: steering relative to the car, gas, brake / reverse; lateral grip model, off the road is slower, sugar / chalk / sawdust / marbles are slippery, jam / glue / dough are sticky.
  • Drift: hop + hold while steering = drift; the longer the drift, the bigger the mini-turbo when you let go (blue after 1 s, orange after 2.2 s). Sparks and light trails show the level.
  • Rocket start: hit the gas in the last moment before GO.
  • Ramps launch you over gaps (too slow = you fall), boost pads, falling off any edge or into a pocket (respawn behind, briefly a ghost), bumping rivals off the table.
  • Items from ? boxes (weighted by position): BOOST, OIL (spin-out puddle), MINE, ROCKET (flies straight, knocks the car up), SHOCK (pushes everyone close away).
  • CPU drivers (Easy / Normal / Pro / Elite): look-ahead steering, brake for corners from the track curvature, flat out and centred over gaps and narrow bridges, dodge oil and mines, drift through long corners (Pro / Elite), rocket start timing, item tactics.

Everything is a deterministic 120 Hz sim, so online works with the same rollback netcode as Brawl / Race (soak test 2 + 2 CPUs over the network with 50 ms latency, 30 ms jitter, 5 % loss: identical checksums, 0 desyncs). The view builds the world procedurally: extruded table tops, a neon road ribbon that narrows on bridges and stops over gaps, giant low-poly props, cars with blob drivers, a minimap.

Tests (micro_tests.gd): determinism, save/load rollback, falling + respawn, the toast jump (fast lands on the tray, slow falls), rocket start, CPU races on every track in both modes. CPU lap times (breakfast, 3 laps, 4 CPUs): Easy 14.1 s, Normal 13.0 s, Pro 12.4 s, Elite 12.1 s per lap.

Micro Kidsroom Micro Breakfast Micro Workbench Micro Pool Lobby Micro

25. Micro Racers polish: Blender models, rails, drop markings, race announcer, lobby seats (v1.6)

Leonhard's play-test feedback on v1.5:

  • the cars were not pretty and the style did not match the other games,
  • you could not see where you can fall off and where not,
  • the props needed a lot more love (the fork!), the detail of the concept images is the target,
  • the announcer said "Serve it up!" at the start of a race,
  • many arrows on the road pointed (and ran) the wrong way,
  • in the lobby it was awkward to add / pick CPUs, and classic Volley showed 4 seats for a 2-player game.

What changed:

  • Jelly toy kart built in Blender (tools/blender/make_micro.py): chunky rounded tub with an open cockpit, bumper-car side pods, rear wing, engine, exhausts, headlights, big treaded wheels with glowing rims. The shell uses the same jelly shader as the blobs in the player colour, the trim is dark with neon edges, so the kart looks like it belongs to the blob. Wheels spin with the speed and the front wheels steer.
  • All props rebuilt in Blender (43 models): serrated butter knife, mustard bottle with label, jam jar with glowing jam and a spoon, cereal bowl with milk and loops, mug, cereal box, plates, toast ramp with butter, a proper fork, crumbs, steel ruler with numbers, soldering station with the hot iron, hex nuts, screw, tape measure, circuit boards, vice, the pegboard wall with tools, letter blocks, robot, desk lamp, book stack, picture-book ramp, crayons, cable hole with pencils, glass marbles, numbered pool balls, cue, chalk, the "RULES" book bridge and turned table legs. Materials are assigned by name (prop_ / metal_ / glass_ / emit_ / jelly_ ... + colour) and swapped for the neon shaders in the game.
  • Rails + marked drops: low neon rails (real walls in the sim) run along most table edges; only the deliberate drops stay open - the gap after the toast / ruler / book ramps, the bridges (knife, ruler, cue, book), the pockets, one edge next to the spilled sugar, the desk edge by the marbles. Every open edge gets pulsing red / amber hazard stripes and a red glow falling into the dark; pockets and holes have a red ring.
  • Arrows fixed: the chevrons on the road, boost pads and ramp arrows now point along the driving direction and run forward.
  • Announcer per game: new voice lines (Gemini TTS): "Ready to race!", "Start your engines!", "Final lap!", "Rocket start!", "Wipeout!", "Checkered flag!", "Last car standing!", plus "Fight!" for Brawl and "Kick off!" for Soccer. "Serve it up!" is only used by Volley now.
  • Lobby seats: the lobby shows as many seats as the game has (classic Volley 2). Every seat can be used with the mouse: "+ CPU" on the next empty seat, "<" / ">" for that CPU's level, "Remove" / "Leave". Gamepad: X adds, Y removes a CPU. Switching to a game with fewer seats removes surplus CPUs; a human joining a full 2-seat game replaces a CPU.

Next (once the driving feels right): Autoball - micro-car football in the spirit of Rocket League, without flying.

Kart Close Micro Models Kidsroom Drops Pool Pocket Lobby Seats

26. Play-test round: fair micro racing, new look, readable 2v2, fair mashing, controller-first menu (v1.7)

Leonhard's play-test notes and what changed:

  • "I don't survive 10 seconds in Micro Racers - is the screen rule only for players?" The rule was the same for everyone, but it was brutal: the camera followed only the leader, a car that hit a rail head-on could not turn at standstill (stuck until out), and a mine dropped right after the start spun the whole pack out. Now:
    • the shared camera zooms out (up to 1.8x) to keep every car on screen; only who falls behind even the widest view is out,
    • cars can always turn, even standing still; the CPU backs off when stuck,
    • catch-up: cars behind the leader get up to +30 % top speed (lap race +10 %),
    • no items in the first 3 s, spin-outs are shorter,
    • after a fall you respawn inside the view (never straight into an OUT). An Easy CPU against three Elite CPUs now lasts 8 - 18 s instead of 3 s; rounds last 20 - 35 s.
  • Micro look (camera, cars, tables, road - "all of it"): the camera is lower (50 degrees) and closer, so cars are about 1.5x bigger and the scene has depth like the concept art; the tables float in the dark like islands on faceted rock cliffs; the props are lit, matte low-poly (the neon edge effect is toned down here); the road is no longer a black strip - just the neon lines on the table; the car is now a rounded jelly toy car with the blob in a glass bubble.
  • 2v2 Volley depth: a target ring on the floor shows where the ball will land (coloured for the side, it fills up as the ball comes down), plus a real shadow under the ball and a higher camera.
  • Blob Race mash: the CPUs mashed 9 - 11 times per second. Now 4.5 / 5.5 / 6.5 / 7.5 presses per second (Easy .. Elite), every press a little early or late and slowing down towards the end - like a thumb.
  • Start menu, controller first: the selected game plays itself full-screen behind the menu. The first device that presses A / Space / Enter is player 1 and drives everything (game, settings, CPUs, CPU level, colour, START, online, options, quit) - no mouse needed. Others join with A, pick a colour with left / right, leave with B. No ready step any more: player 1 starts.

Tests: new lobby_menu.gd (gamepad becomes player 1, second player joins by keyboard, CPUs via the menu, game switch, START starts the match); micro tests unchanged and passing.

Menu Micro Breakfast Micro Kidsroom Kart V2 Landing

27. Autoball - car football without flying (v1.8)

Leonhard: "once the driving works, a micro Rocket League without flying - Autoball". Built on what was already there: the car handling (gas, brake, steer with lateral grip) comes from Micro Racers, the jelly kart model and the island look too; the team logic, the kickoff / goal / whistle flow, the goal nets and the team cards follow Tactics Soccer.

  • Rules: blue vs orange, slot 1 / 3 blue, slot 2 / 4 orange (1v1, 2v2, 2v1 with CPUs). A timed match (2 / 3 / 5 min); a draw goes to golden-goal overtime. Kickoff countdown after every goal, the conceding team kicks off.
  • Cars: gas / brake / steer like Micro Racers, plus jump (a hop), dodge (second jump press in the air - a forward flip that hits the ball hard), boost (meter 0 - 100, starts at 34) and powerslide (less grip for tight turns). Nobody flies: a car is never in the air for more than a hop.
  • Ball and pitch: a big ball with gravity, bounces and rolling friction; the pitch is a walled rectangle with chamfered corners, goal mouths with a crossbar and a net pocket behind. A goal counts when the ball is completely behind the line. 8 small boost pads (+12) and 4 big corner pads (+100).
  • Camera: one shared screen, 55 degrees from above. It checks every car and the ball against the real camera frustum each frame and zooms out just enough to keep them all visible (fast out, slow in), leaving room for the score on top and the player cards at the bottom. When the play is spread out it shows the whole pitch; when everyone is around the ball it comes close.
  • CPU: the team mate nearest to the ball attacks (drives to a point behind the ball on the line to the opponent goal, goes around the ball when on the wrong side, boosts on straights, jumps for bouncing balls, Pro / Elite dodge into it), the other one guards the own goal and clears. Easy .. Elite differ in reaction time, top speed, boost and jump use and aim.
  • Controls: keyboard WASD + Space (jump) + F / Shift (boost) + E (slide), arrows + Enter + Ctrl / Num 0
    • . or /; gamepad stick, RT (or Y, or stick up) gas, LT brake, A jump, B boost, X / LB / RB slide. The stick-up gas is a fallback for pads whose triggers do not show up as axes.
  • Lobby: new entry "Autoball" with Match length and Boost on / off; the player cards show TEAM BLUE / TEAM ORANGE; the background demo plays a 1-minute CPU match.

Tests: new autoball_tests.gd - determinism, save / load rollback, goal + kickoff, crossbar, walls, overtime, and CPU matches (1v1 and 2v2, Easy and Elite: every match ends and both levels score). Online soak (4 CPUs, 50 ms latency, 30 ms jitter, 10 % loss): 0 desyncs, same checksum on both peers.

Autoball 1v1 Autoball 2v2 Autoball Close Autoball Menu

28. Classic Volley polish: solid blobs, music, Flawless Victory (v1.9)

Leonhard's play-test of 1v1 Volley: "the blobs are a bit too see-through, sometimes you lose them; there is no music - the sound effects go down really well; Mortal Kombat inspired music? And an easter egg: win to zero, the loser cracks, light beams come out of the cracks, it explodes, 'Flawless Victory', and the winner plays Shake Victory."

  • Blobs easier to follow: the classic blobs are now nearly opaque (97 % instead of 86 %), with a stronger core glow and a less white-hot rim, so they stay saturated in their own colour even in front of bright lamps and neon backgrounds.
  • Music: three original instrumentals made with Lyria 3.5 (Gemini API, tools/gemini/gen_music.py): menu (dark synthwave, temple bells, 100 BPM) in the lobby, fight_1 / fight_2 (90s dark techno, orchestral hits, gongs, 140 - 145 BPM) in every match - random, the next track when one ends. The prompts only describe the style (no franchise or song names), so the tracks are our own. New "Music" volume in the options; the music fades between menu and match and ducks under the Flawless Victory finisher.
  • Flawless Victory (classic Volley, win without conceding a point) - concept art first (tools/gemini/gen_flawless.py, four moments: cracks with beams, burst, aftermath, close-up), then built to match it:
    • the loser freezes; a Voronoi crack pattern (in the blob shader) spreads from its head over the body and glows white-gold, 36 light beams shoot out of the cracks one after the other, the blob trembles and swells, a rising charge sound with crackles, the camera pushes in and the music ducks;
    • after 1.8 s it explodes: white core flash and light, the beams flare out, 40 faceted shards in the blob's colour with glowing broken edges fly off, bounce and settle on the court (cooling from white-gold to the blob colour), glowing dust drifts up, a shockwave ring on the floor, a boom with glass tinkling;
    • "FLAWLESS VICTORY" on screen and the announcer (new Gemini TTS line "Flawless victory!");
    • the winner dances Shake Victory (Leonhard's Mixamo clip, retargeted onto the blob rig like the other clips; a new face_front option damps the dancer's turning so the blob keeps facing the camera) under a spotlight, then the usual Rematch panel appears. It is view-only (the match is already decided), so it looks the same on both machines online.
  • Video capture: tests/record_match.gd records any game CPU vs CPU with Godot's Movie Maker (video + game audio); --flawless_at plays the finisher for a showcase clip.

Tests: new flawless_test.gd (trigger only at 0 points, cracks before the explosion, shards, loser hidden, winner celebrates, rematch resets); all suites pass.

Flawless Concepts Flawless Cracks Flawless Burst Flawless Aftermath Shake Victory

29. Wall impacts, blob glow and a mirrored view for the right side (v1.10)

  • Blobs, second pass: only the middle of the body (the part facing the camera) is slightly see-through again (88 %), the silhouette stays fully solid (center_alpha_mode in the blob shader), and every blob gets a soft halo in its own colour behind the body - like the ball's glow, only weaker - so the contour stays crisp (blob_halo.gdshader).
  • Wall impacts: the floor shows an expanding ring where the ball lands; the side walls now get the same kind of ring, lying on the (invisible) wall plane, a bit more subtle, in that side's neon colour and stronger for faster balls (FxView.wall_hit).
  • Mirrored view (Leonhard: "can the side you play on be shown mirrored, e.g. if someone has a hard time on the right?"): new option "Right side: mirror view". When the only local player is on the right side (online / LAN client, or alone against a CPU from the lobby), the game node gets scale.x = -1 - every 3D child is mirrored and Godot flips the face winding by itself; the camera is top_level and placed in mirrored world coordinates; the HUD swaps the score panels, the court swaps its side flashes and rings, and the player's left / right input is swapped so "left" still moves left on screen. The simulation and the network never see a difference, so the opponent keeps the normal view. New mirror_test.gd.

Normal view, then a mirrored one (orange plays on the right and now sees itself on the left; the score panels swap too):

Normal Mirrored

30. Opens on Blobby Volley, loading screen (v1.10)

  • The game always opens on classic Blobby Volley (it used to reopen the last game, e.g. Micro Racers, whose first start takes a few seconds). Coming back from a match still shows the game you just played.
  • Why it was slow: building a game takes about half a second; the rest is macOS compiling the shaders of a game the first time it is drawn (5 s for Micro Racers on the first run, under 1 s once cached).
  • Loading screen (autoload Loading): a picture rendered from the game itself - tests/splash_shot.gd stages a classic Volley moment in the Temple Court (cyan blob swinging at the ball mid-air, orange jumping to block, lanterns) and takes a 1920x1080 screenshot (assets/ui/loading.jpg). It is shown at startup and for every switch lobby -> match -> lobby, with a slow push-in, the name of the game that is loading, a progress bar and a tip for that game. The next scene loads on a background thread; the screen stays until the new scene runs smoothly (shaders compiled), so there is no frozen half-built picture. In the lobby the background game fades in only once it runs smoothly. (A first version with a hand-drawn 2D blob on a synthwave grid was scrapped - "a rendered scene, not a drawing".)

Loading

31. Blob Brawl trophy mode, Blob Race with depth and a golden ball (v1.12)

Leonhard: "rebuild the Smash version: you get points as if the ball were a trophy, whoever fills the meter first wins, no more falling off, power-ups instead" and "Blob Race needs depth - everyone running behind each other is far too hard, they should start side by side - and maybe chase a ball that gives points". His choices: hold the ball = points; power-ups Shield, Giant, Bomb; free movement in depth; golden ball = +1. Both games were rebuilt in parallel by two sub-agents (separate files), then checked and tuned together.

Blob Brawl - Trophy (new default; the old stocks mode is still there as "Mode: Stocks"):

  • closed arena: neon energy walls at x = +-12 and a ceiling, solid floor - nobody can fall off; blobs that are flung bounce off walls and ceiling;
  • the ball is the trophy: grab it with ACTION, your meter fills while you hold it (15 / 20 / 30 s to win), the holder runs a bit slower; every hit knocks the ball loose (it pops out towards the attacker) and the one who lost it can't re-grab it for 0.5 s; throwing still works (hot ball), and a thrown blob now knocks over whoever it hits - "throw a blob at the holder";
  • damage % stays but is capped at 150 % and heals slowly (no KOs means no reset);
  • power-up boxes (max 2, every 6 - 10 s, at fixed spots): SHIELD 6 s (untouchable, keeps the ball), GIANT 8 s (1.5x size and reach, hits 1.35x harder, takes 0.6x knockback), BOMB (shockwave on pickup, 6 m radius, knocks everyone and the ball away);
  • CPU: chases the loose ball, hunts the holder (swing / slam / spin / grab, throws carried blobs at him), runs away while holding, grabs nearby boxes;
  • view: trophy meter bar above every player card (glows while holding), holder aura, hologram power-up boxes, shield bubble, giant scaling, bomb rings. First Mac render: walls and frame posts glowed far too bright (the neon post-process doubled them) - toned down to a faint energy sheet.
  • 4 CPUs finish a 20 s match in about 1 - 2 minutes; the ball changes hands 18 - 115 times.

Blob Race - depth:

  • every blob has a depth (-2.2 .. 2.2 m), up / down moves across the track at 5.5 m/s; the course is the same 2D profile stretched across the whole depth;
  • start: everyone side by side on one line (lanes 1.2 m apart); the mash now only gives a head start (+1.2 / +0.8 / +0.4 m); "Level start" option without mash;
  • shove, stomp, grab, item ball and mines only hit blobs close in depth (|dz| < 0.8 - 1.0), so you can pass; item boxes come in pairs across the depth;
  • controls: W / S (arrows, stick) = depth, jump moved to Space / Enter / A; fast fall and drop-through are gone, grab = ACTION next to a stunned blob or from behind, lob = ACTION while jumping;
  • golden ball (option, default on): 4 - 8 s after GO it appears ahead of the leader, floats down, then hops along the course and drifts in depth; whoever touches it gets +1 match point at once ("+1" and a banner). Caught in about 95 % of rounds, so "Points to win" now defaults to 5;
  • CPU: picks lanes to pass and to avoid mines, heads for boxes, chases the golden ball;
  • camera looks down at 32 degrees so the depth reads; dashed lane lines, drop shadows.

Tests: new brawl_tests.gd (60 checks) and race_tests.gd; party, lobby and online soak tests pass (0 desyncs for both games).

Brawl Trophy Race Depth

32. macOS app and the website (v1.12)

macOS app: Godot's macOS export templates (4.7.2) installed, a new export_presets.cfg (universal x86_64 + arm64, bundle id at.cloudcompany.blobbyvolley3d, tests excluded). The first export stopped with "ETC2 ASTC texture format is disabled" - Apple Silicon needs import_etc2_astc=true in the project settings. The app is signed ad-hoc (no Apple developer account, so not notarized: first start with right-click -> Open). Results: Blobby Volley 3D.app, a DMG (about 88 MB) and a zip of the Godot project (git archive, about 45 MB). New app icon rendered from the game.

Website (website/): plain PHP 8 + Tailwind CSS v4, German and English, everything self-hosted:

  • no CDN, no cookies, no tracking, no external requests (strict Content-Security-Policy 'self'), fonts as local files - GDPR-friendly for Austria; imprint (Paragraph 25 MedienG / Paragraph 5 ECG) and privacy page;
  • motion: GSAP + ScrollTrigger, Lenis smooth scroll and a three.js hero with the real rigged blob (glTF) and bloom - all vendored from npm into assets/vendor;
  • every game mode as a short CPU-vs-CPU clip from the game itself (Movie Maker);
  • key art rendered in Blender (Cycles): a line-up of four blobs and single cut-outs, posed with the game's own animations, jelly material with rim glow;
  • the full devlog with all screenshots, rendered from this file (tools/sync-devlog.mjs);
  • downloads (DMG and Godot project) served by download.php with file size and SHA-256; MIT license page, THIRD_PARTY.md lists Godot, the font, the Mixamo-derived animations and the AI-generated assets.
  • "Made 100% with AI": code, art, music (Lyria), announcer voice (Gemini TTS), key art and this website were all produced by AI agents; the human part was ideas, play-testing and feedback.

Website