Batch 404: Solo-Developed Action Roguelike
Batch 404: Solo-Developed Action Roguelike
Batch 404 is a top-down sci-fi co-op roguelike shooter that blends the horde destruction of classic games like Alien Shooter and Crimsonland with modern survivors-like progression.
My Role
Solo developer responsible for gameplay systems, technical art, VFX, UI implementation, optimization, tools, and overall Unity production.
Key Features
Active abilities, local co-op, enemy waves, boss encounters, vehicles, POIs, run-based progression, VFX-heavy combat.
Spatial Hash Grid - O(1) neighbor queries for 4 096 enemies
Naïve distance checks against every enemy would cost O(N²) per frame. I built a custom spatial hash where each enemy hashes into a grid cell; neighbor lookups scan only the surrounding cells, not the whole scene. A frame-stamp integer "clears" the entire table in O(1) - no memset, no full iteration. With 4 096 enemies, separation steering went from a profiler hotspot to negligible.
Persistent Blood Decals on an Infinite Map
A single 1024×1024 RenderTexture covers a 100 m window around the camera and rebuilds when the player moves too far. All stamps are stored in world space in a spatial grid, so when the tile recenters it queries only the relevant cells and redraws from history - backtracking always shows the same stains. Texel-aligned snapping prevents sub-pixel jitter on every recentering. Zero per-frame allocations: CommandBuffers are pooled, stamps are plain structs.
Zero-Allocation Event Bus with Re-Entry Safety
Events are dispatched by generic type - no string keys, no boxing, no LINQ. Structs are passed as in parameters (zero copy). Subscribe/Unsubscribe calls that arrive while publishing are queued and applied after the current dispatch finishes, preventing mid-iteration list mutation. Destroyed subscribers clean themselves up automatically via an owner reference check.
VFX graph Batching - One VisualEffect Per Asset Type
Instead of one VisualEffect component per spawn, all requests for the same asset are queued as plain structs and flushed once per frame in a single SendEvent loop. For persistent emitters (flames, trails) a flat slot array with a free-stack gives O(1) alloc/free. N concurrent effects -> 1 GPU batch per asset type, regardless of N.
Stat Resolver - Modifier Stack That Compiles Away
Power-up bonuses stack as (base + Add) × (1 + Mul). Every method is [AggressiveInlining] and takes modifiers as in structs (no copy). The JIT collapses the entire multi-source damage formula to two float ops at the call site - called thousands of times per frame across all armed enemies, it shows nothing in the profiler.
Generic Object Pool - Swap-Remove & Deferred Despawn
Enemies, bullets, and VFX are pooled to avoid Instantiate/Destroy spikes. Despawn uses swap-remove - the dead object swaps with the last in the list, count decrements - O(1) regardless of pool size. The actual return to pool is deferred one frame so in-flight callbacks (damage events, VFX) can still fire safely. Prewarming calls IPoolable.OnDespawned() directly, skipping the Start() cycle that would require scene services to be ready.
Bullet Penetration - Static Hit Buffer + No-Alloc Sort
Penetrating bullets use SphereCastNonAlloc into a static 16-element RaycastHit[] shared across all projectiles - zero heap allocation per frame. Hits are sorted by distance with insertion sort: at ≤16 entries it beats Array.Sort with no delegate and no GC. Already-hit targets are tracked in a HashSet<IDamageable> per projectile, blocking double-damage across overlapping frames. Position integration uses semi-implicit Euler for stable gravity without extra evaluations.