{"slug": "i-built-a-game-with-zero-asset-files-everything-is-generated-in-code", "title": "I built a game with zero asset files - everything is generated in code", "summary": "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.", "body_md": "This is the first game I've ever made.\n\nI'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.\n\nBut I gave myself one hard rule that ended up shaping the entire project:\n\n**Zero external assets.**\n\nNo textures. No sprite sheets. No audio files. No music files.\n\nThe whole repository contains none of them.\n\nEverything 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.\n\nHere's how I did it, including the parts that went badly wrong.\n\nTwo reasons.\n\nFirst, I can't draw or compose, so \"make it all procedural\" was weirdly *easier* than sourcing, creating, and licensing art assets.\n\nSecond, and this is the part I didn't expect, when everything is code, everything can **react** to the game state for free.\n\nMore on that later.\n\nAll of the 2D art is rendered using Godot's `_draw()`\n\nfunction.\n\nThe most involved piece is the containment dome. It isn't a sprite at all - it's shaded per cell like a tiny software renderer.\n\nFor 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.\n\n``` js\n# Hemisphere surface normal\nvar sx := (mid_x - center_x) * inv_half_w\nvar sz := sqrt(maxf(0.0, 1.0 - sx * sx - sy_sq))\nvar norm := Vector3(-sx, sy, sz).normalized()\n\n# Lambertian diffuse\nvar ndotl := maxf(0.0, norm.dot(light3))\nvar light_val := 0.1 + ndotl * 0.9\n\n# Fresnel rim darkening (surface curving away from viewer goes dark)\nlight_val *= lerpf(0.4, 1.0, clampf(sz * 1.8, 0.0, 1.0))\n\n# Quantise into discrete shade bands -> reads as pixel art\nvar band := clampi(int(round(light_val * max_band_f)), 0, num_bands - 1)\nvar col: Color = shades[band]\n\ndraw_rect(Rect2(px_f, row_y_f, x_end - px_f, y_bot - row_y_f), col)\n```\n\nThat final `draw_rect()`\n\ncall is important because it was the result of a mistake.\n\nMy first implementation shaded the dome per pixel using `draw_line()`\n\nfor essentially every pixel.\n\nOn desktop, it worked fine.\n\nOn a Samsung Galaxy S25 Ultra, it hard-crashed the GPU.\n\nMore than 10,000 draw calls per frame was simply too much.\n\nThe fix was to stop thinking per pixel and start thinking in small grid cells (roughly 8×6 pixels) using `draw_rect()`\n\ninstead.\n\nThe lighting calculations stayed exactly the same, but they were evaluated once per cell rather than once per pixel.\n\nThat reduced draw calls from more than 10,000 per frame to around 500.\n\nThe dithered band quantisation actually helped visually too. The chunkier cells looked intentional, like pixel art, rather than a low-resolution gradient.\n\n**Lesson learned:** on mobile devices, draw-call count matters far more than I expected.\n\nThe 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.\n\nThat's the one place where I let the GPU do the per-pixel work it is actually good at.\n\nI followed the same philosophy for sound.\n\nAll ~15 sound effects are generated from raw waveform mathematics at runtime.\n\nHere's the entire generator for a simple sweep sound - a sine wave that glides between two frequencies with a linear fade-out.\n\n``` php\nfunc _gen_sweep(duration, freq_start, freq_end, volume) -> AudioStreamWAV:\n    var count = int(duration * SAMPLE_RATE)\n    var buf = PackedByteArray()\n\n    buf.resize(count * 2)\n\n    for i in range(count):\n        var t = float(i) / float(count)\n        var freq = lerpf(freq_start, freq_end, t)\n        var envelope = 1.0 - t\n\n        var sample = sin(TAU * freq * float(i) / SAMPLE_RATE) * envelope * volume\n\n        _put_sample(buf, i, sample)\n\n    return _make_stream(buf)\n```\n\nSwap the envelope for:\n\n```\nexp(-t * 8.0)\n```\n\nand you get a percussive blip.\n\nLayer in noise and a low rumble and you get an explosion.\n\nThe background music is generated the same way, including a tension layer that ramps up as the reactor approaches meltdown.\n\nGenerating 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.\n\nThe fix was simple:\n\nGenerate the audio after the first frame renders.\n\nThat way the app appears responsive immediately before doing the heavier processing work.\n\nHere's the upside I didn't see coming.\n\nBecause 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.\n\nNo separate damaged assets.\n\nNo alternate sprites.\n\nIt simply falls out of the same maths.\n\nThe music becomes more tense.\n\nThe CRT distortion intensifies.\n\nThe gauges climb.\n\nOne value drives the mood of the entire screen.\n\nThat's the thing procedural generation gave me that pre-made assets wouldn't have:\n\n**The game isn't decorated - it's driven.**\n\n**Built with:** Godot 4.6 / GDScript\n\n**Play in your browser (no install):**\n\n[https://forcesensitivesaiyan.itch.io/reactor-panic](https://forcesensitivesaiyan.itch.io/reactor-panic)\n\n**Available on Android and iOS:**\n\nGoogle Play: [https://play.google.com/store/apps/details?id=com.aidoo.reactorpanic](https://play.google.com/store/apps/details?id=com.aidoo.reactorpanic)\n\nApp Store: [https://apps.apple.com/us/app/reactor-panic/id6773935208](https://apps.apple.com/us/app/reactor-panic/id6773935208)\n\nHappy to go deeper on any part of the implementation in the comments.\n\nThe dome shading and the audio synthesis were by far the most enjoyable parts to figure out.", "url": "https://wpnews.pro/news/i-built-a-game-with-zero-asset-files-everything-is-generated-in-code", "canonical_source": "https://dev.to/forcesensitivesaiyan/i-built-a-game-with-zero-asset-files-everything-is-generated-in-code-48ae", "published_at": "2026-06-15 21:18:07+00:00", "updated_at": "2026-06-15 21:47:48.853001+00:00", "lang": "en", "topics": ["developer-tools", "generative-ai"], "entities": ["Godot", "Reactor Panic", "Samsung Galaxy S25 Ultra"], "alternates": {"html": "https://wpnews.pro/news/i-built-a-game-with-zero-asset-files-everything-is-generated-in-code", "markdown": "https://wpnews.pro/news/i-built-a-game-with-zero-asset-files-everything-is-generated-in-code.md", "text": "https://wpnews.pro/news/i-built-a-game-with-zero-asset-files-everything-is-generated-in-code.txt", "jsonld": "https://wpnews.pro/news/i-built-a-game-with-zero-asset-files-everything-is-generated-in-code.jsonld"}}