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.
2D Shooter Looter
A C++/raylib top-down extraction shooter prototype with room clears, weapons, grenades, loot pressure, extraction zones, and stash decisions.
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.
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.
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.
- 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.
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.
- Input
- Player and pickups
- Weapons and enemies
- Projectiles and collisions
- Room/extract state
- Render
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.
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.
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.
const auto& current_room = get_room_catalog()[state.room.current_room_index];update_player(state.player, input, delta_time, current_room.bounds);const int pickup_events_before = state.pickup_events;update_weapon_pickups(state);update_loot_pickups(state);update_ammo_pickups(state);update_health_pickups(state);update_plate_pickups(state);update_stim_pickups(state);update_grenade_pickups(state);if (state.pickup_events > pickup_events_before)play_sound_if_ready(audio, audio.pickup);const bool reload_requested = input.reload_pressed && !state.player.weapon.is_reloading &&state.player.weapon.ammo < state.player.weapon.magazine_size;update_weapon(state.player.weapon, input.reload_pressed, delta_time);const WeaponFireResult fire_result =try_fire_player_weapon(state, input, muzzle_position, aim_direction);const int grenade_throw_events_before = state.grenade_throw_events;try_throw_grenade(state, input, aim_direction);if (fire_result.fired)play_sound_if_ready(audio, audio.shoot);if (fire_result.started_reload || reload_requested)play_sound_if_ready(audio, audio.reload);if (state.grenade_throw_events > grenade_throw_events_before)play_sound_if_ready(audio, audio.grenade_throw);update_enemy_attacks(state, delta_time);for (auto& enemy : state.enemies){if (!enemy.active)continue;move_enemy(enemy, state.player, delta_time);}update_bullets(state.bullets, delta_time, current_room.bounds);update_bullets(state.enemy_bullets, delta_time, current_room.bounds);const int grenade_explosion_events_before = state.grenade_explosion_events;update_grenades(state, delta_time, current_room.bounds);if (state.grenade_explosion_events > grenade_explosion_events_before)play_sound_if_ready(audio, audio.grenade_explosion);resolve_collisions(state);update_room_state(state);update_room_transition(state);update_extraction(state, delta_time);update_feedback_timers(state, delta_time);render_game(state, assets, profile);
Combat, collisions, room state, extraction, feedback, and rendering happen in a readable sequence instead of being hidden in one opaque update.