← Back to blog
Our Games
Granny's RampageRELEASED
Granny's GambitRELEASED
Granny's Gambit
DeckbuilderRoguelike
ChoostApril 15, 2026by Choost Games
Topic:Bullet Heaven & Bullet Hell · Roguelikes & Roguelites · Deckbuilders · Dev Logs & Game Dev

Building a Bullet Heaven in Phaser: What We Learned

Lessons from building a bullet heaven game in Phaser 3 as a solo developer, sprite management, performance, and what nobody warns you about.

We built Granny's Rampage, a five-stage bullet heaven where you play as a grandmother with a minigun fighting through increasingly hellish landscapes, in Phaser 3 with React, TypeScript, and Vite. It took a few months as a solo developer using AI-assisted tools, and along the way we learned a bunch of things that would've saved significant time if we'd known them upfront.

This isn't a tutorial. There are plenty of "how to make a Phaser game" guides out there. This is the stuff those guides don't cover, the problems that show up specifically when you're building a bullet heaven, where hundreds of entities are on screen simultaneously and performance stops being theoretical.

Why use Phaser for a bullet heaven game?

Phaser 3 is a strong choice for bullet heaven development because it offers rapid JavaScript iteration, a mature 2D rendering pipeline that handles hundreds of simultaneous sprites, and browser-based output that runs on virtually any hardware. Built-in physics, animation, and particle systems save months of custom engine work.

The choice was practical. Phaser 3 is JavaScript, which means rapid iteration on a small team, change a number, hit reload, see it. Its 2D rendering pipeline handles sprite-heavy scenes well enough for a bullet heaven where the screen routinely fills with hundreds of entities. And because the runtime is just a browser engine wrapped in Electron for desktop, the resulting Windows build runs on practically anything, no dedicated GPU required, no driver weirdness, integrated graphics handle it fine.

Phaser also has mature sprite handling, animation systems, physics, and particle effects built in. For a genre where the screen fills with hundreds of projectiles and enemies simultaneously, having those systems battle-tested by thousands of other developers matters more than raw performance benchmarks.

How should you set up a sprite sheet pipeline for a bullet heaven?

Automate your sprite sheet pipeline early using a scripting tool like Python/Pillow to assemble frames into sheets and generate JSON metadata for Phaser. Standardize grid dimensions and naming conventions across every entity type so adding new enemies or projectiles is a drag-and-drop process, not a manual headache.

This is where we spent more time than expected. A bullet heaven needs a lot of sprites, enemies, projectiles, pickups, obstacles, bosses, the player character with multiple weapon states. Each one needs idle animations at minimum, and ideally death animations, damage flashes, and directional variants.

We used a Python/Pillow pipeline to process sprite sheets, taking individual frames, assembling them into sheets with consistent cell sizes, and generating the JSON metadata that Phaser's animation system expects. The alternative is doing this manually in TexturePacker or similar tools, but automating it paid off quickly when we needed to iterate on enemy designs.

The lesson: standardize your sprite sheet format early. Every enemy, every projectile, every pickup should follow the same grid dimensions and naming conventions. When you inevitably need to add a new enemy type at 2 AM, you want to drop a PNG into the pipeline and have it just work, not spend twenty minutes remembering how the other sheets were set up.

What are common Phaser animation bugs in bullet heavens?

The most common Phaser animation pitfall in bullet heavens is playing an animation before it has been registered. Always check this.anims.exists(key) and this.textures.exists(key) before calling play() on dynamically spawned sprites. For video backgrounds, use raw HTML5 video elements behind the canvas instead of Phaser's built-in video loader.

Phaser's animation system is solid, but it has a specific quirk that cost us hours of debugging. Animations must be created in the create() method before any game objects try to use them. This sounds obvious, but in a bullet heaven where enemies spawn dynamically based on stage progression and wave timers, it's easy to accidentally spawn an enemy before its animation has been registered.

The fix is simple, always check this.anims.exists(key) and this.textures.exists(key) before calling play() on any sprite. Defensive coding that takes ten seconds to add and saves you from a crash that only happens on stage 4 when a specific enemy type spawns for the first time during a boss phase.

Also: if you're using video backgrounds (we used Kling-generated MP4 loops for menu screens), set opacity only on the video element itself, never on the container. And use raw HTML5 video elements behind the Phaser canvas rather than Phaser's built-in video loader. The built-in loader fights you on looping and autoplay in ways that the native HTML element handles gracefully.

How do you optimize bullet heaven performance in Phaser?

Object pooling and spatial partitioning are the two essential performance techniques for any Phaser bullet heaven. Use separate Phaser Group pools (with maxSize) for enemies, projectiles, pickups, and particles. Then implement a grid-based spatial hash for collision checks so you only test entities within the same cell, not every entity against every other entity.

The genre-specific performance challenge is this: a typical bullet heaven run starts with maybe 20 enemies on screen and ramps up to 200+ by the midgame, each one checking collision against multiple player projectiles, each projectile potentially generating particles on hit, each kill spawning an experience gem that also needs collision detection against the player.

Object pooling is mandatory, not optional. If you're creating and destroying sprites every frame, the garbage collector will eat you alive and your frame rate will stutter every few seconds. Phaser's Group class with maxSize and recycling handles this, but you need pools for enemies, projectiles, pickups, and particles separately. Mixing them into one pool causes bizarre bugs.

Beyond pooling, the biggest performance win was spatial partitioning for collision checks. Checking every projectile against every enemy every frame is O(n*m) and it gets brutal fast. A simple grid-based spatial hash, dividing the play area into cells and only checking collisions within the same cell, brought our collision checks from "the game freezes when you use the flamethrower" to "buttery smooth at 300+ entities."

How do you structure a stage system for a bullet heaven?

Define each stage as data (enemy roster, spawn rates, obstacle types, music, background) rather than hardcoded logic, and split systems into separate files like ObstacleSystem, ParticleOverlaySystem, and StageSystem from day one. Boss fights need their own class with independent state management for HP-based phase transitions and attack patterns.

Granny's Rampage has five stages, each with different enemy types, obstacles, particle overlays, and boss fights. Managing this without the codebase becoming unmaintainable required splitting systems early.

Our GameScene.ts and EnemySystem.ts both grew too large for comfortable editing, we eventually split things into ObstacleSystem, ParticleOverlaySystem, and StageSystem as separate files. The lesson is to do this from the start rather than after things get unwieldy. Each stage should be defined as data (enemy roster, spawn rates, obstacle types, music track, background) rather than code. When adding stage 5, you want to create a JSON config, not touch the core game loop.

Boss fights need their own state management. A boss that spawns minions, has phase transitions based on HP thresholds, and uses attack patterns on timers is fundamentally different from the wave-based spawning system that handles regular enemies. Trying to make the regular spawn system handle boss behavior leads to spaghetti. Give bosses their own class.

How should you design a weapon system in a bullet heaven?

Abstract your weapon interface from the start so every weapon implements the same methods: fire(), update(), and getCollisionShape(). This keeps the game loop weapon-agnostic and makes adding new weapons painless. Different weapon archetypes (projectile-based vs. area-of-effect) interact with collision and particle systems in fundamentally different ways.

Granny's Rampage has a minigun and a flamethrower, and the way each one interacts with the collision and particle systems is completely different.

FeatureMinigunFlamethrower
Damage typeIndividual projectilesPersistent cone-shaped damage zone
Travel behaviorFires in a direction, despawns on hit or after a distanceStays active, hits continuously
Collision shapePoint/small circle per bulletCone area

If we were starting over, we'd abstract the weapon interface more aggressively from day one. Every weapon should implement the same interface, fire(), update(), getCollisionShape(), and the game loop should be weapon-agnostic. We got there eventually, but retrofitting an abstraction onto two specific implementations is messier than designing for it upfront.

What AI tools actually help with indie game development?

AI tools let a solo developer handle the entire asset and code pipeline for a bullet heaven. Use a reasoning model for architecture and debugging, a code-focused IDE for implementation, and generative tools for sprites, music, and sound effects. The key is giving AI specific, bounded tasks rather than open-ended system prompts.

We used Claude for planning, architecture decisions, and debugging logic. We used Cursor (always with Opus) for code implementation. The split matters, AI is excellent at tracing through logic, spotting missed edge cases, and suggesting architecture patterns. It's less reliable at writing complete game systems from scratch without introducing subtle bugs that only manifest under specific gameplay conditions.

The most productive pattern was: figure out the design in conversation, get a clear description of what needs to happen, then give Cursor a surgical instruction. Not "build the boss fight system" but "add a phase transition to the Stage 3 boss that triggers at 50% HP, switches its attack pattern from spiral to radial burst, and spawns 4 minions at cardinal positions." Specific, bounded, testable.

ToolRole
ClaudePlanning, architecture decisions, debugging logic
Cursor (with Opus)Code implementation from specific instructions
GeminiSprite art generation
SunoBackground music tracks
ElevenLabsSound effects

Gemini generated our sprite art. Suno created the BGM tracks. ElevenLabs handled sound effects. The entire asset pipeline for a game that would have required a team of specialists five years ago ran through AI tools on a single developer's machine. That's not a flex, it's just the reality of indie development in 2026, and it's why the bullet heaven genre keeps growing. The barrier to making one of these games has never been lower.

What we make at Choost

We're a small indie studio building games starring unconventional protagonists. Our current lineup includes a bullet heaven and a deckbuilder roguelike, both available on itch.io, with the bullet heaven also on Steam and Google Play.

We're a small indie studio. Our games: Granny's Rampage, a bullet heaven where grandma grabs a minigun and fights through hell, and Granny's Gambit, a Victorian deckbuilder roguelike starring a card-slinging nan with a chip on her shoulder. Granny's Rampage is $2.99 on itch (Windows) and free on Google Play (Android), and on Steam since June 22 (also $2.99). Granny's Gambit is pay-what-you-want on itch.

The Takeaway

Phaser 3 is a genuine option for bullet heaven development. It trades raw entity count (where a C++ engine wins) for cross-platform output, a mature ecosystem, and development speed that lets a solo developer ship a polished five-stage game in months. Get your object pooling and sprite pipeline right in week one, then build everything else on that foundation.

Phaser 3 is a genuine option for bullet heaven development. It's not the fastest engine, a C++ engine with custom rendering will always beat a JavaScript framework on raw entity count. But for a game targeting itch.io and Steam via an Electron desktop wrap, the trade-off makes sense. You get cross-platform output, a mature ecosystem, and a development speed that lets a solo developer ship a polished five-stage game in months rather than years.

If you're thinking about building one, the best advice is: get your object pooling and sprite pipeline right in week one, then build everything else on top of that foundation. Everything in a bullet heaven flows downstream from "how many things can I have on screen without the frame rate dying." Solve that first.

Frequently Asked Questions

Is Phaser 3 good for making a bullet heaven game?

Yes. Phaser 3 handles bullet heaven development well thanks to its mature 2D rendering pipeline, built-in physics, animation and particle systems, and fast JavaScript iteration loop. It runs on integrated graphics via Electron and can target itch.io, Steam, and mobile. Object pooling and spatial partitioning are needed to keep performance smooth at 200+ on-screen entities.

How do you handle performance with hundreds of enemies on screen in Phaser?

Use object pooling (separate Phaser Group pools with maxSize for enemies, projectiles, pickups, and particles) to avoid garbage collection stutter. Then implement a grid-based spatial hash for collision detection so you only check entities within the same cell, reducing checks from O(n*m) to manageable levels even at 300+ entities.

What AI tools can a solo indie developer use to build a full game?

Claude handles planning, architecture, and debugging. Cursor (with Opus) handles code implementation from specific instructions. Gemini generates sprite art, Suno creates background music, and ElevenLabs produces sound effects. The key is giving each tool bounded, specific tasks rather than open-ended prompts.

What is the best way to set up sprite sheets for a Phaser bullet heaven?

Automate the pipeline using a scripting tool like Python with Pillow to assemble frames into sheets and generate JSON metadata. Standardize grid dimensions and naming conventions across all entity types early so new enemies or projectiles can be added by dropping a PNG into the pipeline without manual setup.

Granny's Rampage key art
Like roguelites and bullet heavens? Try Granny's Rampage.
A locked-and-loaded grandmother vs. demonic suburbia. Out now on Steam.