Projects
C++ Game Prototype

2D Shooter Looter

A C++/raylib top-down extraction shooter prototype with room clears, weapons, grenades, loot pressure, extraction zones, and stash decisions.

Problem

A shooter looter only works if moment-to-moment combat, loot pressure, extraction timing, and raid consequences feel coherent; the hard part is keeping those systems readable while the prototype grows.

What I Built

Built a playable C++/raylib extraction shooter loop with WASD movement, mouse aiming, pistol and shotgun behavior, reloads, dash timing, grenades, enemy types, pickups, room transitions, extraction, and an in-memory stash.

Hardest Tradeoff

I kept the architecture intentionally plain instead of introducing an engine, ECS, scripting, or editor tooling too early, because the prototype needed playable feedback before generalized infrastructure.

Result / Proof

The result is concrete proof that I can build and harden C++ gameplay systems, manage stateful loops, test edge cases, and make pragmatic architecture tradeoffs under prototype constraints.

Outcomes
  • Supports a playable three-room extraction loop with weapons, grenades, pickups, enemy fire, room transitions, and extraction decisions.
  • Reduces debugging time by keeping frame order auditable instead of scattering state changes across opaque callbacks.
  • Hardened through validation tests and known-issues tracking for collision, timing, room, and edge-case behavior.
Architecture Sketch

The prototype uses plain game state and small systems so frame order stays explicit: input is read once, gameplay systems update in sequence, collisions resolve centrally, room/extraction state advances, and rendering reads the final state.

  1. Input
  2. Player and pickups
  3. Weapons and enemies
  4. Projectiles and collisions
  5. Room/extract state
  6. Render
State Map

How the loop holds together.

A visual map of the raid lifecycle and per-frame gameplay pipeline: input, state mutation, room/extraction decisions, and read-only rendering.

State graph for the 2D Shooter Looter raid lifecycle and per-frame gameplay pipeline.
Recently Improved

What changed in the proof surface.

  • Added the C++/raylib shooter looter as current systems evidence with stronger gameplay-loop proof.
  • Focused the source excerpt on frame-order discipline so reviewers can see how gameplay state stays auditable.
Proof Points

What this project demonstrates.

  • Implements a playable three-room extraction run with optional early banking and final-room extraction.
  • Supports weapons, reloads, dash, grenades, enemy projectiles, health, ammo, plate, stim, grenade, weapon, and loot pickups.
  • Uses tests and a known-issues log to harden frame-rate behavior, room validation, collision boundaries, and gameplay edge cases.

The result is concrete proof that I can build and harden C++ gameplay systems, manage stateful loops, test edge cases, and make pragmatic architecture tradeoffs under prototype constraints.

Shooter Looter / C++hstephan23/2D_shooter_looter
  1. const auto& current_room = get_room_catalog()[state.room.current_room_index];
  2. update_player(state.player, input, delta_time, current_room.bounds);
  3. const int pickup_events_before = state.pickup_events;
  4. update_weapon_pickups(state);
  5. update_loot_pickups(state);
  6. update_ammo_pickups(state);
  7. update_health_pickups(state);
  8. update_plate_pickups(state);
  9. update_stim_pickups(state);
  10. update_grenade_pickups(state);
  11. if (state.pickup_events > pickup_events_before)
  12. play_sound_if_ready(audio, audio.pickup);
  13. const bool reload_requested = input.reload_pressed && !state.player.weapon.is_reloading &&
  14. state.player.weapon.ammo < state.player.weapon.magazine_size;
  15. update_weapon(state.player.weapon, input.reload_pressed, delta_time);
  16. const WeaponFireResult fire_result =
  17. try_fire_player_weapon(state, input, muzzle_position, aim_direction);
  18. const int grenade_throw_events_before = state.grenade_throw_events;
  19. try_throw_grenade(state, input, aim_direction);
  20. if (fire_result.fired)
  21. play_sound_if_ready(audio, audio.shoot);
  22. if (fire_result.started_reload || reload_requested)
  23. play_sound_if_ready(audio, audio.reload);
  24. if (state.grenade_throw_events > grenade_throw_events_before)
  25. play_sound_if_ready(audio, audio.grenade_throw);
  26. update_enemy_attacks(state, delta_time);
  27. for (auto& enemy : state.enemies)
  28. {
  29. if (!enemy.active)
  30. continue;
  31. move_enemy(enemy, state.player, delta_time);
  32. }
  33. update_bullets(state.bullets, delta_time, current_room.bounds);
  34. update_bullets(state.enemy_bullets, delta_time, current_room.bounds);
  35. const int grenade_explosion_events_before = state.grenade_explosion_events;
  36. update_grenades(state, delta_time, current_room.bounds);
  37. if (state.grenade_explosion_events > grenade_explosion_events_before)
  38. play_sound_if_ready(audio, audio.grenade_explosion);
  39. resolve_collisions(state);
  40. update_room_state(state);
  41. update_room_transition(state);
  42. update_extraction(state, delta_time);
  43. update_feedback_timers(state, delta_time);
  44. render_game(state, assets, profile);
Line 38Frame order stays auditable.

Combat, collisions, room state, extraction, feedback, and rendering happen in a readable sequence instead of being hidden in one opaque update.

2026 · Harrison Stephan · Building Calm, Reliable Software Systems