InvadersBreaker: 72 Hours, A Custom Engine, Zero Planning





The Starting Line: ColumbaEngine vs The Clock
Tuesday morning. Solo Game Jam drops the theme: "Retro." No pre-planning, no design docs, just 72 hours starting now. I had ColumbaEngine - my custom C++ framework with ECS, SDL rendering, TTF text support, and audio already battle-tested. The question wasn't whether the engine could handle it, but what "it" would be.
Staring at the theme, the mashup hit me: Breakout meets Space Invaders. They both scream retro. But here's the twist - what if the aliens shot back while you're trying to line up your shot? Sold. No second-guessing, just commit and build.
The MVP Sprint: Middle of Jam or Bust
The Hard Constraint: Have something playable by Wednesday afternoon. Not pretty, not polished, just mechanically complete. Why? Because the second half of a jam should be polish and playtesting, not debugging nullptr exceptions at 4 AM.
Day 1: Core Loop (Tuesday)
2 PM - First Entity Spawns
ColumbaEngine's ECS made the start trivial. Created a new project, registered the basic systems:
ecs.createSystem<PaddleControlSystem>(); ecs.createSystem<BallPhysicsSystem>(); ecs.createSystem<AlienFormationSystem>();
Here's the beauty of the architecture: each system is a self-contained unit that queries for entities with specific components and transforms them. No inheritance hierarchies, no GameObject base class bullshit.
The PaddleControlSystem looks for any entity with Paddle + Position + Velocity components. Doesn't care what the entity is called, doesn't need to know about rendering. It just reads input, updates velocity, applies boundaries. Clean separation.
class PaddleControlSystem : public System {
void execute() override {
// This queries ALL entities with these exact components
for (auto entity : viewGroup<Position, Velocity, Paddle>()) {
// Transform data, nothing else
if (keyPressed[KEY_A])
entity->get<Velocity>()->dx = -PADDLE_SPEED;
}
}
};First paddle on screen in 30 minutes. Not because I'm fast, but because the engine already handles all the SDL boilerplate. makeSimple2DShape() gives me a rendered rectangle with position and size. No texture loading, no render queues, just data.
6 PM - Physics That Feels Right
Ball physics isn't hard math - it's making it feel good. The secret sauce was the paddle bounce angle:
float hitPos = (ballCenter - paddleCenter) / (paddlePos->width / 2.0f); ballVel->dx = hitPos * 250.0f;
Pure game feel, zero physics accuracy. Players don't want realistic momentum transfer, they want control.
11 PM - The Formation Moves
Instead of individual alien AI, I built one puppet master:
class AlienFormationSystem : public System {
// One brain controls all aliens
void moveFormation() {
// Move all, check edges, drop if needed
}
};Aliens are just dumb data. The formation system moves them all in lockstep. This decision saved hours - no state synchronization, no individual timers, just one system calling the shots.
Day 2 Morning: They Fight Back
Wednesday 2 AM - Bullets Rain Down
Alien shooting was deliberately simple. Pick random front-row aliens, spawn bullets, let gravity handle the rest. No prediction, no targeting, just controlled chaos:
std::map<int, std::pair<float, float>> frontRowPositions; // Store positions, not entity references - learned that the hard way
Early bug: storing entity pointers that got invalidated. Fixed by storing positions instead. When you control the engine, you know exactly why pointers go bad.
Wednesday Noon - MVP ACHIEVED
The game was playable. Ugly as sin, but mechanically complete:
- Paddle moves
- Ball bounces and destroys aliens
- Aliens move and shoot back
- You can win (kill all aliens)
- You can lose (run out of lives)
18 hours from theme announcement to playable game. The engine investment paid off.
How The Hell Does This Actually Work?
Let me break down the ECS magic since that's what made this possible. In ColumbaEngine, systems are just classes that query for entities with specific component combinations. No base GameObject class, no inheritance trees, just data and transformations.
// This creates a system and adds it to the game loop ecs.createSystem<AlienFormationSystem>();
That one line does a lot. The system auto-registers itself, sets up its component queries, and slots into the execution order. Every frame, the engine calls execute() on each system in order.
Here's the crucial bit - systems don't own entities. They just query them:
class AlienFormationSystem : public System {
void init() override {
// Tell the engine what components we care about
registerGroup<Position, Alien>();
}
void execute() override {
// This queries ALL entities that have BOTH components
for (auto entity : viewGroup<Position, Alien>()) {
auto pos = entity->get<Position>();
// Move the alien, check boundaries, whatever
pos->x += FORMATION_SPEED;
}
}
};The beauty? I can slap an Alien component on literally anything with a Positionand it joins the formation. No refactoring, no inheritance changes. Just data composition.
This is why I could add power-ups in 2 hours. New component, new system, done. The paddle doesn't know about power-ups, the power-up system doesn't know about rendering. Everything's decoupled.
The Polish Sprint: Making It Actually Fun
Day 2 Afternoon: Power-Ups Change Everything
With the core loop solid, I could focus on what makes games memorable: unexpected moments. Built six power-ups with weighted drops:
std::map<PowerUpType, float> dropRates = {
{PowerUpType::HEALTH, 0.095f}, // Rare lifeline
{PowerUpType::MULTIBALL, 0.02f}, // Chaos mode
{PowerUpType::BARRIER, 0.06f}, // Panic button
// ... others
};Multi-ball at 2% drop rate was the sweet spot. Common enough to happen, rare enough to feel special. When 8 balls are bouncing around, the game transforms from tactical to beautiful chaos.
Implementation was dead simple: power-ups are just components with timers. No complex state machines, just attach/detach with duration tracking.
Day 2 Night: The Danger Zone
Original design: aliens reaching bottom = instant death. Boring.
Better design: aliens stop at Y=420 and go ballistic. Double fire rate, but you can still play. This turned the bottom screen from a death line into a high-pressure zone. Tactical decision, not binary failure.
Day 3: The Juice (Thursday)
Audio Integration (2 hours)
ColumbaEngine already had SDL_mixer wrapped, so adding sound was just placing playSound() calls:
- Alien hit: quick blip
- Power-up collect: ascending tone
- Paddle hit: dull thud
- BGM: 3-note loop (don't judge, it's hypnotic)
The audio transformed the feel more than any visual effect could.
Visual Effects (3 hours)
Three simple effects that shipped:
- Screen shake: Move all entities by offset on impact. Crude but effective.
- Particles: Six squares with velocity vectors. No particle system, just entities with timers.
- Flash effect: Store color, paint red, restore. 200ms of visual feedback.
void onEvent(const PlayerHitEvent& event) {
// Flash paddle red - immediate feedback
paddle->get<Simple2DObject>()->setColors({255, 0, 0, 255});
}The Background Over-Engineering Incident (2 hours I'll never get back)
Built a 500-line scrolling tile system with object pools and performance metrics. For a checkerboard. That moves diagonally. I'm still angry about this.
The Engine Experience: Dogfooding at 120 MPH
Using ColumbaEngine in a jam was the ultimate stress test. The good: component composition is stupid fast for prototyping. Need balls to have trails? Add Trail component. Need aliens to pulse in danger zone? Add PulseEffect. No refactoring, just composition.
The viewGroup<Components>() pattern was the hero:
for (auto entity : viewGroup<PositionComponent, Velocity, Ball>()) {
// Clean iteration, type-safe, no nulls
}
The bad: when your entity mysteriously disappears and you wrote the entity lifecycle system, there's no StackOverflow to save you. But you also know exactly where to look.
The verdict: The engine handled 8-ball multiball without breaking a sweat. 60 FPS stable. No memory leaks. The months of engine development paid off in 72 hours of productivity.
What Ships vs What Could Be
What Shipped
- Complete game loop with win/lose conditions
- 6 power-ups with distinct gameplay impact
- Danger zone mechanics that change strategy
- Full audio (BGM + effects)
- Particle effects and screen shake
- Score multipliers for risk/reward play
- Wave System: Progressive difficulty with alien variety
What's Next (Post-Jam Reality)
Version 1.1 (Actually Happening)
- Combo System: Track consecutive hits, add visual feedback
- Local High Scores: Simple file I/O, give players a target
- More Alien Types: Shielded (2 hits), Bomber (spread shot), Dodger (independent movement)
Version 1.2 (Maybe)
- Boss Fights: Every 5 waves, something big and pattern-based
- Actual Sprites: The rectangles have charm, but actual art wouldn't hurt
Version Never (Let's Be Real)
- Online leaderboards
- Multiplayer
- Steam release
The Takeaway
72 hours. One developer. Pre-built engine. Zero planning.
The result? A game that's actually fun. Not "fun for a jam game" but genuinely fun. People are competing for high scores. The multi-ball power-up makes players yell at their screen.
What worked:
- Having a stable engine ready to go
- MVP by mid-jam strategy - left entire day for polish
- Power-ups as components - clean, extensible
- Formation controller instead of individual AI
- Simple effects with big impact
What I learned:
- Theme interpretation doesn't need to be clever, just clear
- Sound effects multiply fun more than visual polish
- The best feature is the one that ships
- Engine investment pays off when time is critical
The brutal truth: If I'd used Unity, I'd probably have better graphics. But I wouldn't understand every line of code, and I wouldn't have shipped this exact game. Sometimes owning the stack is worth the pain.
Play it: InvadersBreaker on itch.io
The code: https://github.com/Gallasko/ColumbaEngine/tree/dev/exemples/InvadersBreaker
The engine: https://columbaengine.org/
Files
InvadersBreaker
Breakout meets Space Invaders. They shoot back. You deal with it.
| Status | In development |
| Author | PigeonCodeur |
| Genre | Action |
| Tags | Arcade, Breakout, Fast-Paced, High Score, invaders, Minimalist, Retro, Singleplayer |
| Languages | English |
Leave a comment
Log in with itch.io to leave a comment.