DEV Community

Dominic Harmon
Dominic Harmon

Posted on

My first raylib game — Plane War Ultimate (C++17, 2300 LOC, no engine)

I Built a 2D Shoot-'em-Up in C++ Without a Game Engine — Here's What I Learned

Gameplay Demo


After a few weeks of late nights and too much coffee, I shipped my first real game: Plane War Ultimate — a classic arcade shoot-'em-up built from scratch in C++17.

No Unity. No Unreal. No Godot. Just C++, raylib, and a lot of trial and error.


Why Build Without an Engine?

Because I wanted to understand what's actually happening under the hood.

When you use Unity, a lot is done for you — physics, collision, rendering pipeline, object lifecycle. Building from scratch forces you to answer questions like:

  • How do I manage 300 particles without GC stutter?
  • What's the cleanest way to structure a game loop?
  • How do I make a boss fight feel satisfying?

Every feature in this game is something I had to design, implement, and debug myself. That's where the real learning happens.


What the Game Actually Does

Feature Details
Combat Hold-to-shoot, triple-shot power-up, screen-clearing bomb
Enemies 3 types (small/medium/large) with difficulty scaling
Boss 2-phase AI — aimed shots in phase 1, 5-way fan pattern in phase 2
Death FX Screen flash, slow-motion, multi-stage explosion
Visuals Particle explosions, combo popups, screen shake, procedural sprites
Audio Procedural SFX generation (sine/square/saw waves)
Persistence Local high score save/load
Settings Master volume + SFX volume, fullscreen toggle

Architecture: Keep It Simple

The codebase is ~2,300 lines split across 7 modules:

main.cpp           Entry point
game.h / game.cpp  Game loop, state machine, rendering (1,200 lines)
player             Movement, shooting, invincibility frames
enemy              Three enemy types, spawning logic
boss               2-phase AI, attack patterns, death effects
particle           Explosion particle system
powerup            Heal / triple-shot / bomb pickups
pool.h             Generic object pool template (37 lines!)
Enter fullscreen mode Exit fullscreen mode

The Object Pool

Every dynamic object in the game — bullets, enemies, particles, powerups — uses the same 37-line template:

template<typename T, int N>
struct Pool {
    T items[N];

    T* Acquire() {
        for (int i = 0; i < N; i++)
            if (!items[i].active) {
                items[i] = {};
                items[i].active = true;
                return &items[i];
            }
        return nullptr;
    }

    void Clear() {
        for (int i = 0; i < N; i++)
            items[i].active = false;
    }
};
Enter fullscreen mode Exit fullscreen mode

No new. No delete. No heap allocation during gameplay. At 60 FPS with 300 particles, 100 bullets, and 30 enemies on screen, this matters.

State Machine

The game uses a flat enum-based state machine:

enum class GameState { MENU, SETTINGS, PLAYING, PAUSED, GAMEOVER };

void Game::Update() {
    switch (state) {
    case GameState::MENU:     UpdateMenu();     break;
    case GameState::SETTINGS: UpdateSettings(); break;
    case GameState::PLAYING:  UpdatePlaying();  break;
    case GameState::PAUSED:   UpdatePaused();   break;
    case GameState::GAMEOVER: UpdateGameOver(); break;
    }
}
Enter fullscreen mode Exit fullscreen mode

No framework, no middleware — just a switch statement. It's boring code, and boring code is easy to debug.


The Boss Fight

The boss was the most fun to design. 240x140 pixels, all drawn in code:

Phase 1 (HP > 50%):
  - Two wing pods with gun barrels
  - Central hull with panel lines
  - Blue-glowing core
  - Twin engine exhaust with animated flames
  - Single aimed shot at the player

Phase 2 (HP <= 50%):
  - Core turns red with pulsing glow
  - Red border flicker on all modules
  - Sinusoidal vertical movement
  - 5-way fan-pattern attacks
  - Visible hull cracks at low HP
Enter fullscreen mode Exit fullscreen mode

On death: screen flashes white, 3 simultaneous explosions spawn 90 particles, and the shake intensity doubles. It's dumb, it's over-the-top, and it's exactly what a boss fight should feel like.


The Hardest Bug

I shipped a bug where the game froze after 2 seconds. Turns out I'd accidentally renamed a function in a way that created infinite recursion:

// The method was originally named PlaySfx, calling raylib's PlaySound
void Game::PlaySound(Sound& snd) {
    SetSoundVolume(snd, sfxVolume);
    PlaySound(snd);  // Calls itself! Stack overflow -> freeze
}
Enter fullscreen mode Exit fullscreen mode

A replace_all refactor gone wrong. Lesson: never blindly rename functions that shadow library APIs. Name your wrapper something unambiguous: PlayGameSfx, not PlaySound.


What I'd Do Differently

  1. Split game.cpp earlier. 1,200 lines in one file is manageable but pushing it. I'd split into game_menus.cpp, game_combat.cpp, game_render.cpp from the start.

  2. Add unit tests for Pool. It's a 37-line template, but it's the backbone of the entire game. A simple test suite would have caught edge cases early.

  3. Use a fixed-point physics timestep. Right now, everything runs at display rate. If vsync fails or the monitor is 144Hz, gameplay speed changes. A proper accumulator-based loop would fix this.

  4. Pre-render complex shapes to textures. The boss is drawn with ~30 draw calls every frame. Baking that into a render texture would cut that to 1.


Try It Yourself

The project builds on Windows with Visual Studio 2022 and raylib 6.0. Everything you need is in the repo:

git clone https://github.com/dominicharmon-commits/PlaneWarUltimate.git
# Open MyFirstGame.sln in VS2022
# Switch to Release + x64, press F5
Enter fullscreen mode Exit fullscreen mode

Or just download the zip from Releases and run.


Final Thoughts

Building a game without an engine is not the efficient way to make a game. But if your goal is to learn — to actually understand memory management, game loops, collision detection, and audio pipelines — there's no substitute.

The full source is on GitHub. Star it if you find it useful, and I'd love feedback from anyone who's walked this path before.

Tags: #cpp #gamedev #raylib #tutorial #beginners

Top comments (0)