I built a game with zero asset files - everything is generated in code A developer built a complete arcade game, Reactor Panic, using zero external assets—all visuals and audio are generated at runtime in code. The 2D art uses Godot's _draw() function with per-cell lighting and quantization, while sound effects are synthesized from waveform mathematics. The project highlights performance lessons, such as reducing draw calls from over 10,000 to around 500 for mobile compatibility. This is the first game I've ever made. I'm not a developer by trade, I'd never touched Godot before, and I leaned on AI to help me get over the learning curve. But I gave myself one hard rule that ended up shaping the entire project: Zero external assets. No textures. No sprite sheets. No audio files. No music files. The whole repository contains none of them. Everything you see and hear in Reactor Panic - a small arcade game where you sort plasma cores before the reactor melts down - is generated at runtime in code. Here's how I did it, including the parts that went badly wrong. Two reasons. First, I can't draw or compose, so "make it all procedural" was weirdly easier than sourcing, creating, and licensing art assets. Second, and this is the part I didn't expect, when everything is code, everything can react to the game state for free. More on that later. All of the 2D art is rendered using Godot's draw function. The most involved piece is the containment dome. It isn't a sprite at all - it's shaded per cell like a tiny software renderer. For each cell, I compute a hemisphere surface normal, perform Lambertian diffuse lighting with a specular hotspot, add Fresnel-style rim darkening, and then quantise the result into a handful of discrete steel bands so it reads as pixel art rather than a smooth gradient. js Hemisphere surface normal var sx := mid x - center x inv half w var sz := sqrt maxf 0.0, 1.0 - sx sx - sy sq var norm := Vector3 -sx, sy, sz .normalized Lambertian diffuse var ndotl := maxf 0.0, norm.dot light3 var light val := 0.1 + ndotl 0.9 Fresnel rim darkening surface curving away from viewer goes dark light val = lerpf 0.4, 1.0, clampf sz 1.8, 0.0, 1.0 Quantise into discrete shade bands - reads as pixel art var band := clampi int round light val max band f , 0, num bands - 1 var col: Color = shades band draw rect Rect2 px f, row y f, x end - px f, y bot - row y f , col That final draw rect call is important because it was the result of a mistake. My first implementation shaded the dome per pixel using draw line for essentially every pixel. On desktop, it worked fine. On a Samsung Galaxy S25 Ultra, it hard-crashed the GPU. More than 10,000 draw calls per frame was simply too much. The fix was to stop thinking per pixel and start thinking in small grid cells roughly 8×6 pixels using draw rect instead. The lighting calculations stayed exactly the same, but they were evaluated once per cell rather than once per pixel. That reduced draw calls from more than 10,000 per frame to around 500. The dithered band quantisation actually helped visually too. The chunkier cells looked intentional, like pixel art, rather than a low-resolution gradient. Lesson learned: on mobile devices, draw-call count matters far more than I expected. The full-screen effects - CRT scanlines, barrel distortion, chromatic aberration, an instability-reactive background with drifting dust motes, and heat shimmer - are handled by three small GLSL shaders. That's the one place where I let the GPU do the per-pixel work it is actually good at. I followed the same philosophy for sound. All ~15 sound effects are generated from raw waveform mathematics at runtime. Here's the entire generator for a simple sweep sound - a sine wave that glides between two frequencies with a linear fade-out. php func gen sweep duration, freq start, freq end, volume - AudioStreamWAV: var count = int duration SAMPLE RATE var buf = PackedByteArray buf.resize count 2 for i in range count : var t = float i / float count var freq = lerpf freq start, freq end, t var envelope = 1.0 - t var sample = sin TAU freq float i / SAMPLE RATE envelope volume put sample buf, i, sample return make stream buf Swap the envelope for: exp -t 8.0 and you get a percussive blip. Layer in noise and a low rumble and you get an explosion. The background music is generated the same way, including a tension layer that ramps up as the reactor approaches meltdown. Generating all of those audio buffers up front around 750,000 samples blocked the main thread long enough for Android to throw an "Application Not Responding" warning on slower devices. The fix was simple: Generate the audio after the first frame renders. That way the app appears responsive immediately before doing the heavier processing work. Here's the upside I didn't see coming. Because the dome colour is derived from a lighting value, I can feed reactor instability directly into that calculation and the entire structure gradually heats towards red as you get closer to failure. No separate damaged assets. No alternate sprites. It simply falls out of the same maths. The music becomes more tense. The CRT distortion intensifies. The gauges climb. One value drives the mood of the entire screen. That's the thing procedural generation gave me that pre-made assets wouldn't have: The game isn't decorated - it's driven. Built with: Godot 4.6 / GDScript Play in your browser no install : https://forcesensitivesaiyan.itch.io/reactor-panic https://forcesensitivesaiyan.itch.io/reactor-panic Available on Android and iOS: Google Play: https://play.google.com/store/apps/details?id=com.aidoo.reactorpanic https://play.google.com/store/apps/details?id=com.aidoo.reactorpanic App Store: https://apps.apple.com/us/app/reactor-panic/id6773935208 https://apps.apple.com/us/app/reactor-panic/id6773935208 Happy to go deeper on any part of the implementation in the comments. The dome shading and the audio synthesis were by far the most enjoyable parts to figure out.