A 2D submarine dodge-and-collect game built with SFML 2.x and C++17.
Navigate your submarine through increasingly chaotic obstacle patterns across 3 levels.
bash run.shThat's it. The script compiles everything and launches the game.
Dependencies: libsfml-graphics, libsfml-window, libsfml-system, libsfml-audio
Compiler: g++ with -std=c++17
| Key | Action |
|---|---|
↑ ↓ ← → |
Move submarine |
P |
Pause / Resume |
Escape |
Return to menu from any level |
Enter |
Select menu option |
hydronaut/
├── assets/ Media files (fonts, music, sprites)
├── include/ Header files (one per class)
├── src/ Source files (one per class)
├── run.sh Build + run script
└── hydronaut Compiled binary
Global compile-time constants:
DEFAULT_WINDOW_WIDTH / HEIGHT— initial window size (900×900)MIN_WINDOW_WIDTH / HEIGHT— resize floor (640×480), enforced at runtimePLAYER_SPEED,INITIAL_OBSTACLE_SPEED, etc. — gameplay tuning knobsASSET_*— all asset paths in one place
Singleton that loads every texture and the font once on startup.
AssetManager::instance().loadAll()— throwsstd::runtime_errorif any asset is missing (fast-fail)texture("submarine")/font()— O(1) lookup viaunordered_map- All SFML objects are kept alive for the entire process lifetime — no dangling texture pointers
The player submarine.
- Scaled to 7% of window height, starts at the left-center
handleInput(windowSize)— reads arrow keys, clamps position to window boundsreset(windowSize)— repositions for a new level without reallocating- Position clamping is fully relative to the live
window.getSize()
Abstract base class for all enemies. Defines the interface:
virtual void update(sf::Vector2u windowSize) = 0;
virtual void draw(sf::RenderWindow& window) = 0;
virtual sf::FloatRect getBounds() const = 0;
virtual void reset(sf::Vector2u windowSize) = 0;All implementations receive the live window size each frame — the math functions normalize
x and y to fractions of windowSize, so every level adapts automatically to any resolution.
Orange rotating triangles.
ConvexObstacle— individual spinning triangle; size = 10% ofmin(width, height)ConvexObstaclePool— manages astd::vectorpre-reserved to 30 slots to avoid mid-frame reallocations; spawns with ~1-in-50 random chance per frame; removes off-screen obstacles- Speed starts at
INITIAL_OBSTACLE_SPEEDand grows asspeed = base × 1.3^(score/100)up to score 500
Sea-urchin that traces a sine wave.
Math (window-relative):
half = windowHeight / 2
if x ≤ W/2: y = half × (1 - sin(π/W × x))
else: y = half + |sin(π/W × (x - W/2))| × half
Moves left at 3 px/frame; respawns in the right 50% of the screen.
Crab that bounces along a parabolic arc.
Math (window-relative):
norm = x / windowWidth (0..1)
para = 4 × norm × (1 - norm) (peaks at 1 when centered)
y = half ± half × √para
Direction flips at x=0 (bounce back right) and at x=windowWidth (resets).
Octopus following a secant curve.
Math (window-relative):
angle = (π / windowWidth) × 3 × x
y = sec(angle) / 6 × windowHeight/2 (clamped to avoid asymptotes)
Moves left at 1 px/frame (slowest — hardest to predict). Asymptote guard: if |cos| < 0.05, clamp to 0.05.
Lanternfish on an exponentially damped sine path.
Math (window-relative):
amplitude = windowHeight × 0.15
y = windowHeight/2 + amplitude × sin(0.01×x) × exp(-0.001×x)
Oscillations shrink as x increases; resets to right side when it reaches the left edge.
Treasure chest that appears in Levels 2 and 3.
- Spawns within the inner 70% of the window (15–85% margin on each axis)
respawn(windowSize)— repositions to a new random location and rescales to window- Guard against inverted spawn ranges on very small windows
Abstract base class for all 3 levels. Owns the shared game loop:
run() loop
├─ pollEvent → resize enforcement, P pause, Escape to menu
├─ drawBackground() — deep-sea gradient quad, no image required
├─ update() [pure virtual] → obstacle logic, collision detection
├─ draw() [pure virtual] → draw enemies, player, treasure
└─ drawHUD() → score text top-left
showGameOver() → overlay, 2-second display
- Resize events clamp window to
MIN_WINDOW_WIDTH × MIN_WINDOW_HEIGHT - Pause draws a dim overlay + "PAUSED" text and keeps the scene visible beneath
- Uses
ConvexObstaclePool - Score increments every frame (survival-based)
- Obstacle speed recalculates each frame from
getScore()
SineObstacle(urchin) +ParabolicObstacle(crab)- Score += 10 for each treasure collected
- Touch either obstacle → game over
SecObstacle(octopus) +ExpSineObstacle(lanternfish)- Score += 10 per treasure
- Hardest movement patterns
- Deep-sea gradient background drawn in code (navy → teal-blue) via
sf::VertexArray - Title "HYDRONAUT", subtitle, and controls hint drawn proportionally to window size
- Items and highlight rescale on every frame — correct after any resize
- Returns selected level (1/2/3) or -1 on close
Entry point:
- Creates an
sf::RenderWindow(900×900, resizable) - Calls
AssetManager::instance().loadAll()— throws on missing file - Opens music (non-fatal if missing)
- Main loop:
Menu::run()→ instantiates the correctLevelsubclass asunique_ptr<Level>→level->run() - Two-level exception handling:
- Inner catch around each level → crash returns to menu
- Outer catch around everything → fatal error with message, clean exit
main()
└─ AssetManager::loadAll() ←── throws on missing asset
└─ [music loop]
└─ while(window.isOpen())
├─ Menu::run() ←── returns 1/2/3 or -1
└─ unique_ptr<Level> level
├─ Level1 / Level2 / Level3 (constructor)
│ └─ Player(windowSize)
│ └─ [Obstacle subclasses](windowSize)
│ └─ Treasure(windowSize) [L2/L3 only]
└─ level->run() ← base Level loop
├─ sf::Event polling
├─ drawBackground()
├─ update() ← derived class: move + collide
├─ draw() ← derived class: render enemies
└─ drawHUD()
└─ showGameOver()
└─ [back to menu]
| Scenario | Behaviour |
|---|---|
| Missing font or texture | AssetManager::loadAll() throws → fatal exit with message |
| Missing music | Warning printed; game continues without audio |
| Level throws at runtime | Caught by per-level guard; returns to menu |
| Any other crash | Outer catch(...) prints message and exits cleanly |
| Zero-size window | All obstacle reset() / update() methods early-return |
| Modulo-zero in rand | All rand() % n guarded so n > 0 before call |
- All SFML resources (textures, font) are owned by
AssetManager(static singleton) — no manualdelete ConvexObstaclePoolpre-reserves 30 slots on first use — no mid-frame vector reallocation- Level objects are
unique_ptr<Level>— automatically destroyed when returning to menu - Sprites hold a
const sf::Texture*pointer intoAssetManager(which lives for the process) — no dangling pointers