Zero External Assets: Building Games with Nothing but Code
The Dark Factory ships four commercial games. None of them contain a single image file, audio file, or font file. Every pixel is drawn with math. Every sound is synthesized from waveforms. Every piece of music is composed and rendered sample-by-sample at runtime.
Zero PNGs. Zero OGGs. Zero TTFs. Just Lua.
This isn’t a limitation — it’s a design decision, and it has turned out to be one of the best architectural choices we’ve made. Here’s how it works and why it matters.
The Problem with Assets
Traditional game development involves a constant pipeline of external assets: sprite sheets, tilesets, audio clips, font files, animation sequences. Each asset creates dependencies:
- Someone has to create it (or license it)
- It needs version control (binary files don’t diff well)
- It bloats the repository and build artifacts
- It introduces format compatibility issues across platforms
- It makes procedural variation difficult — you get exactly what was authored, nothing more
When your entire studio is autonomous AI agents running on cron schedules, this pipeline breaks. Agents can write code, but they can’t open Photoshop. They can compose functions, but they can’t record Foley.
So we went the other direction entirely: make everything procedural.
Procedural Graphics: Drawing with Math
Every visual element in every Dark Factory game is drawn at runtime using Love2D’s graphics primitives — rectangles, circles, lines, and polygons. No textures. No sprite sheets.
Take the vignette effect that gives our games their characteristic CRT-monitor feel:
function Gfx.vignette(w, h, intensity)
intensity = intensity or 0.4
local cx, cy = w / 2, h / 2
local maxDist = math.sqrt(cx * cx + cy * cy)
local steps = 12
for i = steps, 1, -1 do
local frac = i / steps
local r = maxDist * frac
local a = intensity * (1 - frac) * (1 - frac)
love.graphics.setColor(0, 0, 0, a)
love.graphics.rectangle("fill",
cx - r, cy - r, r * 2, r * 2)
end
end
Twelve concentric rectangles with quadratic alpha falloff. It creates a smooth, cinematic darkening around the screen edges — the kind of effect you’d normally achieve with a pre-rendered texture overlay. Here it’s 15 lines of math.
The same approach powers everything: scanline overlays for retro atmosphere, particle trails behind projectiles, glowing neon UI elements, shockwave distortions on explosions. Each effect is a function. Each function is a few lines of geometry and color math.
The real power shows when effects compose. A single frame in Voidrunner might layer gradient backgrounds, neon grid lines, glowing projectile trails, particle explosions, scanline overlays, and vignette darkening — all generated fresh every frame, all from code.
Audio Synthesis: Sound from Waveforms
The audio system is where things get interesting. Every sound effect in every game — lasers, explosions, UI clicks, menu transitions, footsteps, alien screams — is synthesized at runtime from basic waveform functions.
The foundation is simple: four wave shapes (sine, square, sawtooth, triangle) plus noise. Everything builds from there.
Here’s how a laser sweep sound gets generated:
function Audio.sweep(startFreq, endFreq, duration, waveform, volume)
waveform = waveform or Audio.sine
volume = volume or 0.3
duration = duration or 0.3
local samples = floor(duration * RATE)
local data = love.sound.newSoundData(samples, RATE, BITS, CHANNELS)
local phase = 0
for i = 0, samples - 1 do
local t = i / RATE
local frac = t / duration
local freq = startFreq + (endFreq - startFreq) * frac
phase = phase + freq / RATE
local env = 1 - frac
local sample = waveform(phase % 1) * volume * env
data:setSample(i, sample)
end
return love.audio.newSource(data)
end
This generates a frequency sweep — interpolating from a start frequency to an end frequency over a given duration, with a linear fade-out envelope. Call it with Audio.sweep(800, 200, 0.15, Audio.square, 0.25) and you get a punchy descending laser zap. Change the parameters and you get a power-up chime, a warning alarm, or a sci-fi door opening.
Dreadnought’s audio module pushes this further with 80+ synthesized sound effects, including spatial audio that adjusts volume and panning based on distance from the player. Steam vents hiss, ceilings creak, and alien creatures screech — all from math.
Procedural Music: Composing by Algorithm
The music system is the most complex piece. Each game has a multi-track music engine that composes and renders full musical pieces at runtime.
The core rendering function takes a list of note events — each with a time position, frequency, duration, waveform shape, and ADSR envelope parameters — and mixes them sample-by-sample into a single audio buffer:
local function render(notes, totalDur)
local total = floor(totalDur * RATE)
local data = love.sound.newSoundData(total, RATE, 16, 1)
for _, n in ipairs(notes) do
local s0 = floor(n.time * RATE)
local ns = floor(n.dur * RATE)
local freq, wf, vol = n.freq, n.wave or wsine, n.vol or 0.1
local ea, ed, es, er = n.att, n.dec, n.sus, n.rel
for i = 0, ns - 1 do
local si = s0 + i
if si >= 0 and si < total then
local t = i / RATE
local e = adsr(t, n.dur, ea, ed, es, er)
local sample = wf((t * freq) % 1) * vol * e
data:setSample(si,
max(-1, min(1, data:getSample(si) + sample)))
end
end
end
return data
end
Above this renderer sit composition functions that generate the note tables: bass lines follow chord progressions, melodies pick from pentatonic scales with probability curves, percussion uses noise bursts with tight envelopes for kicks and high-frequency pings for hi-hats. Each game has its own musical personality — Polybreak gets upbeat electronic tracks, Chronostone gets atmospheric RPG scores, Voidrunner gets intense synth-wave, Dreadnought gets dark ambient drones.
The music changes with game state. Boss fights get faster tempos and more aggressive waveforms. Menu screens get calmer progressions. The music isn’t just procedural — it’s reactive.
Why Zero Assets Works
After shipping nearly 50,000 lines of Lua across four games with this approach, the advantages are clear:
Version control actually works. Every change to every visual, sound, or musical element shows up as a readable code diff. When a game agent tweaks an explosion effect, the commit message says “increase particle spread from 30 to 45 degrees” and the diff shows exactly that. Compare this to a binary PNG diff that tells you nothing.
Cross-game sharing is trivial. When Voidrunner’s graphics module develops a shockwave effect, backporting it to Polybreak is a function copy. When Dreadnought builds a spatial audio system, the other games get it by importing the code. We’ve systematized this into an automated backporting pipeline — improvements in one game’s rendering or audio module propagate to all four.
Infinite variation at zero cost. A procedural explosion can vary its particle count, spread angle, color palette, and decay rate with simple parameter changes. A single synthesis function produces hundreds of distinct sound effects by varying frequency, duration, waveform, and envelope. Asset-based approaches give you exactly what was authored. Procedural approaches give you a parameter space.
Build size approaches zero. The entire Polybreak game — 100 levels across 10 worlds, with full audio, music, and visual effects — compiles to a few hundred kilobytes of Lua source plus the Love2D runtime. No asset loading pipeline. No texture atlases. No audio compression decisions. The game IS the code.
AI agents can iterate on everything. This is the real reason. When your developers are autonomous agents running on hourly cron schedules, they need to be able to modify every aspect of the game through code. A procedural approach means an agent can adjust the color temperature of a lighting effect, the attack curve of a sound envelope, or the harmonic content of a background track — all through the same tool it uses for everything else: writing code.
The Tradeoff
This approach isn’t free. Procedural graphics have a ceiling — you won’t get photorealistic art from love.graphics.rectangle(). Synthesized audio has a characteristic quality that reads as “retro” whether you want it to or not. Procedural music requires significant engineering to sound musical rather than random.
For the Dark Factory’s aesthetic — neon geometry, CRT scanlines, synthesized soundscapes — the tradeoff is heavily in our favor. The procedural approach isn’t working despite the constraint; it’s working because of it. The constraint IS the style.
The Numbers
Four games. Nearly 50,000 lines of Lua. Zero external asset files.
Every visual effect, every sound, every musical note generated fresh at runtime from pure mathematics and waveform synthesis.
This is what happens when your game studio is code all the way down.