SNAYKEE
This game is a solo project I developed in 2025. I used C++ for core logic and system architecture--handling game loops, state management, AI behaviors, and physics. SFML was used to manage graphics rendering, input, and audio. This combination allowed for high performance and control over gameplay mechanics while also maintaining a lightweight codebase.
Summary
As the captain of a lone spacecraft hurtling through the cosmos, your mission is simple: STAY ALIVE!!! Dodge deadly obstacles, navigate treacherous celestial terrain, and collect vital star energy to keep your ship fueled and functional.


TRY IT OUT!!!
*** If you want to run the project and test it out for yourself, consider the link(s) below. ***
Project Build & Executable: https://mctigerr17.itch.io/snaykee
​
Github Repository: https://github.com/Mctigerbeast/Snaykee
Highlights
Object Pooling
Managing obstacles (asteroids), player projectiles, and star energy.
Game Context
Managing access to crucial systems.
Procedural Generation
Generation obstacles (asteroids) and star energy pickups.
​
Save/Load System
Saving and loading player/user information and data.
State Management
- Menu/Scene transitions.
- Player/user's life and ship status.
- Handling gameplay states.
OBJECT POOLING
I made use of object pooling for spawning the obstacles (asteroids), player projectiles, and sounds. This allows for "spawning" multiple objects without having to constantly create and destroy them.
Asteroid Pool
The player's main obstacle in this game are the asteroids that are constantly coming from the top of screen. The player must use their piloting skill to maneuver around and dodge the asteroids, thus preventing devasting damage to the spacecraft. Asteroids vary in size, speed, position, and texture.
​
Initially each asteroid was created dynamically; added to a list/container (std::vector) and then removed from the vector once the asteroid exited the screen. Although this was simple and useful during the prototyping stage, this approach quickly led to performance issues as asteroid object creation and destruction ramped up.​​
​
To address this, I implemented object pooling for the asteroid generation system. Instead of constantly allocating and deallocation memory, a fixed pool of asteroids is created at startup. When an asteroid is needed, the game reuses an inactive object form the pool. Once the asteroid exits the screen, it is deactivated and returned to the pool for future use.
​
A struct was created, representing a pooled asteroid object. The struct holds an instance of the asteroid object as well as a Boolean which keeps track of whether the object is currently being used. If an object is needed (for drawing, updating, collision checking, etc.), the game will check to see if that object is currently in use before it executes the necessary operations.
​
To maintain consistency across systems, I extended the pooling approach to other frequently created objects, including player projectiles and sound effects--which both benefit similar performance gains during high-actions sequences.
​
Benefits
-
Eliminates runtime object allocation, reducing CPU spikes.
-
Avoids unnecessary memory churn.
-
Ensure consistent frame rates, even during heavy asteroid waves.
-
Simplified lifecycle management with no repeated creation/destruction logic.
- Obstacle class and struct representing a pooled obstacle (asteroid) object.

- Object pools.

- Creating and initializing object pool on startup.

- Using object pools.




PROCEDURAL GENERATION
I made use of procedural generation for spawning the obstacles (asteroids) and star energy pickups. This allows for a different experience each time the player/user plays the game. This lowers repetitiveness and keeps players engaged.
Generating Asteroids & Stars
After setting up an object pooling system to efficiently manage high frequency objects, I focused on how to introduce variety into gameplay.
To keep gameplay unpredictable and fresh, I implemented procedural generation for spawning asteroids. This controls when, where, and how asteroids appear on screen. Instead of hardcoding spawn points or fixed waves, I wrote a system that generates asteroids at runtime using random values within defined constraints. Asteroids spawn at random horizontal positions across the top of the screen, with randomized sizes and speeds. This gives each asteroid slightly different behavior, forcing the player to stay alert.
​
To manage these calculations, I used a custom math library I developed (MR_Math). The library features interpolation, clamping, and random number generation. This made it easier to write expressive, reusable code for procedural operations. For example, the math library helps calculate randomized but bounded spawn intervals.​​​
I also applied procedural generation to the star pickups, which players collect to restore energy. These are spawned at randomized intervals and positions, adding a layer of unpredictability and encouraging players to move around the screen rather than staying in one spot. This also, helps encourage risk-reward decisions--players often need to maneuver dangerously close to hazards to grab a star. Using the same math utilities and pooling system, the pickups feel well-integrated into the game’s pacing without overwhelming the screen or disrupting flow.
- Generating asteroids.

- Generating Star Energy.

STATE MANAGEMENT
I built a state management system to manage the game's different scenes and modes (ex. menus, gameplay, pause screens). This allowed me to decouple and separate different processes while also keeping the game architecture modular, flexible, and easy to maintain.
Managing/Handling Game State Changes
To manage the different screens and modes in the game (main menu, gameplay, pause screen, game over, controls, and credits, etc.), I built a state management system using a stack-based architecture.
Each state (such as the game itself, or a menu screen) is encapsulated in its own class, implementing a common interface for handling input, updating logic, and rendering. These states are then pushed onto or popped off a stack depending on what the player is doing. For example, when the player pauses the game, the pause screen state is pushed on top of the gameplay state. The gameplay state remains on the stack (and in memory), but no longer updates or renders until the pause state is popped off again.
This stack-based approach made it easy to manage transitions between states while keeping the code modular and clean. I can pause and resume gameplay without reinitializing game data. I can overlay temporary states like the pause or controls screen. I can avoid tightly coupling UI logic with the game logic. It also allowed me to handle input and updates in a layered fashion, where only the top-most state is active/executed, but the underlying states can still render if needed.
The state management system was written to be flexible and extensible, so adding new states like a settings menu or intro cutscene would only require creating a new class and registering it with the manager. It made the overall game architecture more maintainable and kept the update loop straightforward.
- Example: Switching to the game/gameplay state, when "PLAY" button is pressed on main menu screen.

- Example: Switching to the pause-screen state, when the "Esc" key is pressed during gameplay.

- The game's main/core update loop. Notice that only the currently active state is being updated and rendered.

- Example: Game States (Pause, Game Over, Game Controls).



GAME CONTEXT
I created a GameContext struct that holds references to key systems like the asset manager, audio manager, save/load system, and the main game window. A single instance is created at startup and passed by reference to each game state, enabling easy and organized access to these shared resources.
Managing Important Subsystems
To keep my game systems organized and accessible across different states, I created a GameContext struct that holds references to important subsystems like the asset manager, audio manager, save/load system, and the main game window. This GameContext acts as a central hub for anything that needs to be shared globally without relying on singletons.
​
A single instance of this is created when the game starts up. As new game states are created and pushed onto the stack (ex. main menu, gameplay, pause screen), a reference to the context is passed into their constructors. This allows each state to easily access shared systems without needing to manage or duplicate them. For example, the game/gameplay state can load sprites using the asset manager, play sounds through the audio manager, save progress using the save system, and query the current window size for layout or scaling logic.
​
Using a shared context helped keep dependencies clear and made it easier to test or modify individual systems. It also made the state management system cleaner, since each state could simply rely on the context instead of constructing and managing resources on its own.
​
- GameContext struct object.

- GameContext being used when initializing the Main Menu state.

SAVE/LOAD SYSTEM
I implemented a binary save/load system to store player data (ex. high scores and favorite spacecraft) in the system’s LocalAppData directory. Saving in binary keeps file sizes small, improves performance, and helps discourage casual cheating. The system is accessible through the shared GameContext, making it easy to use across different game states.
Saving Player Information/Data
To maintain/persist player progress and preferences between sessions, I implemented a custom save/load system that handles data like the player's high score and favorite spacecraft selection. This system writes data in a compact binary format for faster read/write times and to minimize file size. This also helps discourage casual cheating or tampering, since the data isn't human-readable or easily editable like a plain text file or JSON.
​
Player data is stored in the operating system’s LocalAppData directory, ensuring it's saved in a user-specific location that doesn’t require special permissions. At startup, the game attempts to load the existing save file from that directory. If the file doesn't exist (ex. on the first run), it initializes with default values. During gameplay or from the main menu, the system can update and write new data back to disk--for example, when a new high score is achieved or the player selects a different spacecraft.
​
By using binary serialization, I kept the system fast and straightforward while avoiding the overhead of formats like JSON or XML. The save system integrates cleanly with the GameContext, so any state that needs to read or update player data can easily access it through the shared context.
- Saving player data.

- Player save file location.
