CONCEPTUALIZATIONAfter years of learning and developing using Unity, Unreal Engine, GameMaker, and Godot, I've found myself wanting to undersatnd past the abstraction layer and work more directly with the engine. I craved a hightened control over what code is running in what order, what data is curently loaded in memory, and building games over top an optimized windows-based platform built around tighter, more stylized 2D experiences.
INITIAL DEVELOPMENTI began development by researching open-source libraries for graphics, audio, and gamepad input APIs. I decided on an combination of SFML 3.0+ and Microsoft's XInput. I chose SFML due to strong but simple rendering, audio, and keyboard input polling capabilities. And Microsoft XInput for its stronger compatibility and analog support for sticks and triggers with Xbox gamepads. From there I sketeched out the program's structure before moving onto full diagrams using Draw.io.
ENGINE STRUCTUREGhost Engine is organized around a central GameManager that owns and orchestrates a set of subsystem managers: Render, Audio, Input, PAK, and Debug ( As of now ). Each manager is the single source of truth for its domain, and GameManager is the only object that owns them outright. Any other system that needs manager access receives it as a const&, never ownership or a mutable reference, so state can only be mutated through the owning manager. This keeps data flow one-directional and makes it obvious where any given piece of the state actually lives.
DEBUGGERThe Debugger is the one deliberate exception to my minimal access philosophy. It holds const references to every manager, since its job is to allow me to inspect and test. It can enumerate all currently loaded PAK assets ( textures, SFX, fonts, text files ) and can trigger test calls into Render, Audio, and Input to validate systems live, without ever being able to mutate them directly.
ASSET PAKKER TOOLI wrote a custom binary asset-packing tool to bundle raw assets ( sprites, audio, fonts, dialogue text ) into .pak files for runtime loading, rather than shipping loose files. The packer reads a list of source files, filters out any that don't resolve on disk, and writes a simple length-prefixed binary format: a file count header followed by per-entry blocks of name length, filename, data length, and raw bytes. The tool also allows the developer to split assets into separate paks by scope, global, stage-common, and per-stage, allowing for greater control over what content is streamed in memory during runtime.
ASSET PAKKERA standalone command line tool that serializes a list of asset files into a single custom binary pak. It writes a [count][nameLen][name][dataLen][data] layout per entry, reading each file's raw bytes via istreambuf_iterator and using the filename as the lookup key so Paks stay portable across directories. It gracefully skips missing files, adjusting the written count beforehand so the header stays accurate, and logs each packed file's size for quick sanity checks. This tool is the counterpart to AssetPakManager, forming the write side of the engine's asset pipeline.
// Comments removed for readability
// Code available on my GitHub page
// PAK format:
// [4 bytes] file count
// per file:
// [4 bytes] name length
// [N bytes] name (used as the lookup key e.g. "assets/tile.png")
// [8 bytes] data length
// [N bytes] raw data
void packFiles( const std::string& outPath, const std::vector & files ) {
std::ofstream out( outPath, std::ios::binary );
if ( !out ) {
std::cerr << "Failed to create: " << outPath << "\n";
return;
}
uint32_t count = static_cast ( files.size() );
for ( const auto& path : files ) {
std::ifstream in ( path, std::ios::binary );
if ( !in ) {
count --;
}
}
out.write( reinterpret_cast( &count ), sizeof( count ) );
for ( const auto& path : files ) {
std::ifstream in( path, std::ios::binary );
if ( !in ) {
std::cerr << "Skipping (not found): " << path << "\n";
continue;
}
std::vector data {
std::istreambuf_iterator ( in ),
std::istreambuf_iterator ()
};
std::filesystem::path fsPath ( path );
std::string fileName = fsPath.filename().string();
uint32_t nameLen = static_cast ( fileName.size() );
out.write( reinterpret_cast ( &nameLen ), sizeof ( nameLen ) );
out.write( fileName.data(), nameLen );
uint64_t dataLen = static_cast( data.size() );
out.write( reinterpret_cast ( &dataLen ) , sizeof ( dataLen ) );
out.write( data.data(), dataLen );
std::cout << "Packed: " << path << " ( " << dataLen << " bytes )\n";
}
std::cout << "\nDone -> " << outPath << "\nCount: " << count;
}
// Example Pakker program : creating 3 pak files
// Comments removed for readability
// Code available on my GitHub page
int main() {
packFiles("../../build/global-test.pak",
{
"../../assets-test/graphics/sprite-sheets/test.png",
"../../assets-test/audio/sfx/test.wav",
"../../assets-test/fonts/test.otf",
"../../assets-test/dialogues/test.txt",
"../../assets-test/nonExistant.png",
"../../assets-test/graphics/sprite-sheets/sprite-sheet.png",
"../../assets-test/audio/sfx/sfx.wav",
"../../assets-test/dialogues/intro.txt",
"../../assets-test/dialogues/stage1.txt"
} );
packFiles("../../build/stage-common-test.pak",
{
"../../assets-test/graphics/sprite-sheets/test.png",
"../../assets-test/audio/sfx/test.wav",
"../../assets-test/fonts/test.otf",
"../../assets-test/dialogues/test.txt",
"../../assets-test/nonExistant.png"
} );
packFiles("../../build/stage-0-test.pak",
{
"../../assets-test/graphics/sprite-sheets/test.png",
"../../assets-test/audio/sfx/test.wav",
"../../assets-test/fonts/test.otf",
"../../assets-test/dialogues/test.txt",
"../../assets-test/dialogues/intro.txt",
"../../assets-test/dialogues/stage1.txt",
"../../assets-test/graphics/stamp.jpg",
"../../assets-test/graphics/cher.jpg",
"../../assets-test/graphics/wifi.jpg",
"../../assets-test/graphics/folder.jpg"
} );
std::cout << "\nPress enter to close\n";
std::cin.get();
}
ASSET PAK MANAGER ( LOADER )A runtime asset pipeline that loads packed binary files into memory and rebuilds them into typed engine objects. The load() function streams a custom binary pak format into a raw byte buffer keyed by filename, while the buildObjects() function sorts those entries by extension and constructs SFML textures, sound buffers, fonts, and parsed dialogue arrays directly from the in-memory data. Fonts get special handling, keeping a persistent raw buffer alive in the Pak struct since sf::Font doesn't own its source data. The result is a clean separation between raw pak I/O and typed asset construction, letting the engine load and unload entire paks at runtime without touching the filesystem again.
// Comments removed for readability
// Code available on my GitHub page
AssetPakManager::AssetPakManager()
{}
/* --- [ Overview ] ---------------------------------
[ void load ] takes in a string path, if pak file -> load all byte data into entries unordered map
[ void buildObjects ] takes in a pak struct -> writes the data in entries as C++ and SFML
objects organized into the pak struct's unordered maps
[ entries ] unordered map holding all currently loaded pak file byte data
--------------------------------------------------*/
void AssetPakManager::load( const std::string& pakPath ) {
std::cout << "CWD: " << std::filesystem::current_path() << std::flush;
std::cout << "Opening pak: " << pakPath << std::flush;
std::ifstream in( pakPath, std::ios::binary );
if ( !in ) {
std::cout << " -> FAILED TO OPEN" << std::flush;
throw std::runtime_error( "Cannot open pak: " + pakPath );
}
uint32_t count = 0;
in.read( reinterpret_cast( &count ), sizeof( count ) );
for ( uint32_t i = 0; i < count; i++ ) {
uint32_t nameLen = 0;
in.read( reinterpret_cast( &nameLen ), sizeof( nameLen ) );
std::string name(nameLen, '\0');
in.read(name.data(), nameLen);
uint64_t dataLen = 0;
in.read( reinterpret_cast( &dataLen ), sizeof( dataLen ) );
std::vector data( dataLen );
in.read( data.data(), dataLen );
entries[name] = { std::move( data ) };
}
}
void AssetPakManager::buildObjects( Pak& pak ) {
pak.textures.clear();
pak.sounds.clear();
pak.fonts.clear();
pak.dialogues.clear();
auto endsWith = []( const std::string& str, const std::string& suffix ) {
return str.size() >= suffix.size() &&
str.compare( str.size() - suffix.size(), suffix.size(), suffix ) == 0;
};
for ( const auto& [name, entry] : entries ) {
const void* ptr = entry.data.data();
size_t size = entry.data.size();
if ( endsWith( name, ".png" ) || endsWith( name, ".jpg" ) ) {
sf::Texture tex( ptr, size );
pak.textures[ name ] = std::move( tex );
std::cout << "Added " << name << " to textures map." << std::endl;
} else if ( endsWith( name, ".wav" ) || endsWith( name, ".ogg" ) ) {
sf::SoundBuffer buf( ptr, size );
pak.sounds[ name ] = std::move( buf );
} else if ( endsWith( name, ".ttf" ) || endsWith( name, ".otf" ) ) {
pak.rawFontBuffers[ name ] = entry.data;
const void* ptr = pak.rawFontBuffers[ name ].data();
size_t size = pak.rawFontBuffers[ name ].size();
sf::Font font( ptr, size );
font.setSmooth( false );
pak.fonts[ name ] = std::move( font );
std::cout << "Added " << name << " to fonts map." << std::endl;
} else if ( endsWith( name, ".txt" ) ) {
std::vector dialogueEntry;
std::string rawText( entry.data.begin(), entry.data.end() );
std::istringstream stream( rawText );
std::string line;
while ( std::getline( stream, line ) ) {
if ( !line.empty() && line.back() == '\r' ) {
line.pop_back();
}
if ( !line.empty() ) {
dialogueEntry.push_back( std::move( line ) );
}
}
pak.dialogues[ name ] = std::move( dialogueEntry );
std::cout << "Added " << name << " to dialogue map." << std::endl;
}
}
entries.clear();
}
const AssetPakManager::Entry& AssetPakManager::get( const std::string& name ) const {
auto entry = entries.find( name );
if ( entry == entries.end() ) {
throw std::runtime_error( "Asset not found in pak: " + name );
}
return entry -> second;
}
bool AssetPakManager::has( const std::string& name ) const {
return entries.count( name ) > 0;
}
POOL BASED AUDIO SYSTEMI built a pool-based spatial audio system on top of SFML 3 that avoids runtime allocation entirely. A fixed pool of SoundContainer objects gets reused for every sound played, with self-return to the pool once playback finishes, so there's no manual lifecycle management and no allocation spikes during high-frequency gameplay audio like footsteps or pickup chimes. Sound identity is resolved through a two-stage lookup: filenames from the pak system map to a stable SoundID enum at compile time, and that ID separately maps to whatever buffer and tuning data is currently loaded.
SoundContainer wraps a single sf::Sound instance and manages its own readiness state. It tracks whether it's currently playing, whether it's in 3D or 2D mode, and whether it's looping, and only pushes state changes down into SFML when the requested state actually differs from what's already set, avoiding redundant calls into the audio backend. Once a sound finishes playing, the container flags itself as ready again on the next update, making reuse automatic rather than something the caller has to manage.
AUDIO MANAGER CLASS
// Comments removed for readability
// Code available on my GitHub page
AudioManager::AudioManager( const PakData& pakData, const float& dt, const Input& input )
: dt ( dt ),
pakData ( pakData ),
input ( input )
{}
void AudioManager::init() {
stringToSoundIDMap.insert ( { "test.wav", SoundID::TEST } );
SoundProperties testSoundProperties;
testSoundProperties.name = "Test Sound";
soundIdToPropertiesMap.insert ( { SoundID::TEST, testSoundProperties } );
loadSoundData( pakData.global );
sf::Listener::setUpVector( { 0, 0, -1 } );
sf::Listener::setDirection( { 0, 1, 0 } );
const sf::SoundBuffer* tempBufferPtr = findMapObject ( pakData.global.sounds, "test.wav" );
if ( !tempBufferPtr ) { return; }
poolSize = soundArray.size();
for ( int i = 0; i < poolSize; i++ ) {
SoundContainer& soundContainer = soundArray[ i ];
soundContainer.init( tempBufferPtr, i );
}
}
SoundContainer* AudioManager::setupAndRetriveNewSound ( SoundID soundId ) {
for ( int i = 0; i < poolSize; i++ ) {
SoundContainer* soundContainerPtr = &soundArray[ i ];
if ( !soundContainerPtr ) { return nullptr; }
if ( soundContainerPtr -> getIsReady() ) {
const sf::SoundBuffer* newBufferPtr = *findMapObject ( soundIdToBufferPtrMap, soundId );
if ( newBufferPtr ) { soundContainerPtr -> setBuffer ( newBufferPtr ); }
else { return nullptr; }
const SoundProperties* newPropertiesPtr = findMapObject ( soundIdToPropertiesMap,
soundId );
if ( newPropertiesPtr ) { soundContainerPtr -> setProperties ( newPropertiesPtr ); }
else { return nullptr; }
return soundContainerPtr;
}
}
return nullptr;
}
void AudioManager::playSound2D( SoundID soundId, bool isLooping ) {
SoundContainer* soundContainerPtr = setupAndRetriveNewSound ( soundId );
if ( !soundContainerPtr ) { return; }
soundContainerPtr -> setAudio3D ( false );
soundContainerPtr -> setLooping ( isLooping );
soundContainerPtr -> playSound ();
}
void AudioManager::update(){
for ( int i = 0; i < poolSize; i++ )
{
soundArray[ i ].update();
}
}
void AudioManager::loadSoundData ( const Pak& pak ) {
for ( const auto &pair : pak.sounds )
{
const std::string name = pair.first;
const sf::SoundBuffer* bufferPtr = &pair.second;
SoundID* soundIDPtr = findMapObject ( stringToSoundIDMap, name );
if ( !soundIDPtr ) { continue; }
soundIdToBufferPtrMap.insert ( { *soundIDPtr, bufferPtr } );
}
}
void AudioManager::removeSoundData ( const Pak& pak ) {
for ( const auto &pair : pak.sounds ) {
std::string name = pair.first;
SoundID* soundIDPtr = findMapObject ( stringToSoundIDMap, name );
if ( !soundIDPtr ) { continue; }
soundIdToBufferPtrMap.erase ( *soundIDPtr );
}
}
SOUND CONTAINER CLASS
// Comments removed for readability
// Code available on my GitHub page
SoundContainer::SoundContainer() : isReady ( false ),
is3D ( false ),
isMoving ( false ),
isLooping ( false ),
isDoppler ( false ),
position ( nullptr ),
speed ( nullptr ),
name ( "Null Sound" )
{}
void SoundContainer::init( const sf::SoundBuffer* tempBufferPtr, int containerId ) {
if ( tempBufferPtr ) {
sound.emplace( *tempBufferPtr );
this -> containerId = containerId;
}
}
void SoundContainer::update() {
if ( sound -> getStatus() == sf::Sound::Status::Stopped && !isReady ) {
isReady = true;
}
}
void SoundContainer::setBuffer ( const sf::SoundBuffer* bufferPtr ) {
sound -> setBuffer( *bufferPtr );
}
void SoundContainer::setProperties ( const SoundProperties* propertiesPtr ) {
name = propertiesPtr -> name;
sound -> setVolume( propertiesPtr -> volume );
sound -> setPitch( propertiesPtr -> pitch );
sound -> setDopplerFactor( propertiesPtr -> dopplerFactor );
sound -> setMinDistance( propertiesPtr -> minDistance );
sound -> setMaxDistance( propertiesPtr -> maxDistance );
sound -> setAttenuation( propertiesPtr -> attenuation );
}
void SoundContainer::setLooping ( bool isTrue ) {
if ( sound ) {
if ( sound -> isLooping() != isTrue ) {
isLooping = isTrue;
sound -> setLooping ( isLooping );
}
}
}
void SoundContainer::setAudio3D( bool isTrue ){
if ( sound ){
if ( sound -> isSpatializationEnabled() != isTrue ){
is3D = isTrue;
sound -> setSpatializationEnabled ( is3D );
}
}
}
void SoundContainer::playSound(){
if ( isReady ) {
sound -> play();
isReady = false;
}
}
const bool SoundContainer::getIsReady() { return isReady; }
const std::string SoundContainer::getName() { return name; }
ADVANCED INPUT POLLINGI built a pool-based spatial audio system on top of SFML 3 that avoids runtime allocation entirely. A fixed pool of SoundContainer objects gets reused for every sound played, with self-return to the pool once playback finishes, so there's no manual lifecycle management and no allocation spikes during high-frequency gameplay audio like footsteps or pickup chimes. Sound identity is resolved through a two-stage lookup: filenames from the pak system map to a stable SoundID enum at compile time, and that ID separately maps to whatever buffer and tuning data is currently loaded.
INPUT MANAGER CLASS
// Comments removed for readability
// Code available on my GitHub page
InputManager::InputManager( Input& input, const float& dt ) : dt( dt ),
input( input ),
isActive ( false )
{
inputNames = {
{ &input.dirPadLeft, "Left" },
{ &input.dirPadUp, "Up" },
{ &input.dirPadRight, "Right" },
{ &input.dirPadDown, "Down" },
{ &input.actionA, "Action A" },
{ &input.actionB, "Action B" },
{ &input.actionX, "Action X" },
{ &input.actionY, "Action Y" },
{ &input.actionLB, "Action LB" },
{ &input.actionRB, "Action RB" },
{ &input.actionLT, "Action LT" },
{ &input.actionRT, "Action RT" },
{ &input.actionStart, "Start" },
{ &input.actionSelect, "Select" }
};
std::cout << "\nInputMgr Constructed." << std::flush;
}
void InputManager::init() {
isActive = true;
}
void InputManager::update() {
if ( !isActive ) return;
xInputConnected = XInputGetState( 0, &xInputState ) == ERROR_SUCCESS;
if ( !xInputConnected ) {
mapGp = {};
}
else {
updateGamepadInputMap();
}
updateKeyboardInputMap();
processInputs();
calculateInputDir( input );
}
void InputManager::resetInputBuffer( InputState& inputState ) {
inputState.alarmBuffer.current = inputState.alarmBuffer.base;
inputState.bufferActive = false;
}
void InputManager::updateInputState( InputState& inputState, bool isDownNow ) {
inputState.prevHeld = inputState.held;
inputState.held = isDownNow;
inputState.pressed = inputState.held && !inputState.prevHeld;
inputState.released = !inputState.held && inputState.prevHeld;
if ( inputState.pressed ) {
inputState.alarmBuffer.current = inputState.alarmBuffer.base;
inputState.bufferActive = true;
}
if ( inputState.bufferActive ) {
inputState.alarmBuffer.current -= dt;
if ( inputState.alarmBuffer.current <= 0 ) {
inputState.alarmBuffer.current = 0.0f;
inputState.bufferActive = false;
}
}
}
void InputManager::calculateInputDir( Input& input ) {
if ( input.isJoystickUsed ) {
input.direction = std::atan2( input.joystickAxisY, input.joystickAxisX );
}
else {
int dirInputX = ( input.dirPadRight.held ) - ( input.dirPadLeft.held );
int dirInputY = ( input.dirPadDown.held ) - ( input.dirPadUp.held );
if ( dirInputX != 0 || dirInputY != 0 ) {
input.direction = std::atan2( dirInputY, dirInputX );
}
}
}
const std::string InputManager::getInputName( InputState* key ) {
auto name = inputNames.find( key );
if ( name != inputNames.end() ) {
return name -> second;
}
return "Null Input";
}
void InputManager::updateGamepadInputMap() {
mapGp.actionA = ( xInputState.Gamepad.wButtons & XINPUT_GAMEPAD_A );
mapGp.actionB = ( xInputState.Gamepad.wButtons & XINPUT_GAMEPAD_B );
mapGp.actionX = ( xInputState.Gamepad.wButtons & XINPUT_GAMEPAD_X );
mapGp.actionY = ( xInputState.Gamepad.wButtons & XINPUT_GAMEPAD_Y );
mapGp.actionLB = ( xInputState.Gamepad.wButtons & XINPUT_GAMEPAD_LEFT_SHOULDER );
mapGp.actionRB = ( xInputState.Gamepad.wButtons & XINPUT_GAMEPAD_RIGHT_SHOULDER );
float ltThreshold = xInputState.Gamepad.bLeftTrigger / 255.0f;
float rtThreshold = xInputState.Gamepad.bRightTrigger / 255.0f;
mapGp.actionLT = ( ltThreshold > input.deadzoneTrigger );
mapGp.actionRT = ( rtThreshold > input.deadzoneTrigger );
mapGp.dirPadLeft = ( xInputState.Gamepad.wButtons & XINPUT_GAMEPAD_DPAD_LEFT );
mapGp.dirPadUp = ( xInputState.Gamepad.wButtons & XINPUT_GAMEPAD_DPAD_UP );
mapGp.dirPadRight = ( xInputState.Gamepad.wButtons & XINPUT_GAMEPAD_DPAD_RIGHT );
mapGp.dirPadDown = ( xInputState.Gamepad.wButtons & XINPUT_GAMEPAD_DPAD_DOWN );
mapGp.actionStart = ( xInputState.Gamepad.wButtons & XINPUT_GAMEPAD_START );
mapGp.actionSelect = ( xInputState.Gamepad.wButtons & XINPUT_GAMEPAD_BACK );
}
void InputManager::updateKeyboardInputMap() {
mapKb.actionA = sf::Keyboard::isKeyPressed( sf::Keyboard::Key::Space );
mapKb.actionB = sf::Keyboard::isKeyPressed( sf::Keyboard::Key::L );
mapKb.actionX = sf::Keyboard::isKeyPressed( sf::Keyboard::Key::J );
mapKb.actionY = sf::Keyboard::isKeyPressed( sf::Keyboard::Key::I );
mapKb.actionLB = sf::Keyboard::isKeyPressed( sf::Keyboard::Key::Q );
mapKb.actionRB = sf::Keyboard::isKeyPressed( sf::Keyboard::Key::E );
mapKb.actionLT = sf::Keyboard::isKeyPressed( sf::Keyboard::Key::LControl );
mapKb.actionRT = sf::Keyboard::isKeyPressed( sf::Keyboard::Key::LShift );
mapKb.dirPadLeft = sf::Keyboard::isKeyPressed( sf::Keyboard::Key::A );
mapKb.dirPadUp = sf::Keyboard::isKeyPressed( sf::Keyboard::Key::W );
mapKb.dirPadRight = sf::Keyboard::isKeyPressed( sf::Keyboard::Key::D );
mapKb.dirPadDown = sf::Keyboard::isKeyPressed( sf::Keyboard::Key::S );
mapKb.actionStart = sf::Keyboard::isKeyPressed( sf::Keyboard::Key::Escape )
|| sf::Keyboard::isKeyPressed( sf::Keyboard::Key::Enter );
mapKb.actionSelect = sf::Keyboard::isKeyPressed( sf::Keyboard::Key::Tab );
}
void InputManager::processInputs() {
input.joystickAxisX = xInputState.Gamepad.sThumbLX / 32767.0f;
input.joystickAxisY = xInputState.Gamepad.sThumbLY / 32767.0f;
input.isJoystickUsed = std::abs(input.joystickAxisX) > input.deadzoneStick
|| std::abs( input.joystickAxisY ) > input.deadzoneStick;
updateInputState( input.actionA, mapGp.actionA || mapKb.actionA );
updateInputState( input.actionB, mapGp.actionB || mapKb.actionB );
updateInputState( input.actionX, mapGp.actionX || mapKb.actionX );
updateInputState( input.actionY, mapGp.actionY || mapKb.actionY );
updateInputState( input.actionLB, mapGp.actionLB || mapKb.actionLB );
updateInputState( input.actionRB, mapGp.actionRB || mapKb.actionRB );
updateInputState( input.actionLT, mapGp.actionLT || mapKb.actionLT );
updateInputState( input.actionRT, mapGp.actionRT || mapKb.actionRT );
updateInputState( input.dirPadLeft, mapGp.dirPadLeft || mapKb.dirPadLeft );
updateInputState( input.dirPadUp, mapGp.dirPadUp || mapKb.dirPadUp );
updateInputState( input.dirPadRight, mapGp.dirPadRight || mapKb.dirPadRight );
updateInputState( input.dirPadDown, mapGp.dirPadDown || mapKb.dirPadDown );
updateInputState( input.actionStart, mapGp.actionStart || mapKb.actionStart );
updateInputState( input.actionSelect, mapGp.actionSelect || mapKb.actionSelect );
}
DEBUGGINGI began development by researching open-source libraries for graphics, audio, and gamepad input APIs. I decided on an combination of SFML 3.0+ and Microsoft's XInput. I chose SFML due to strong but simple rendering, audio, and keyboard input polling capabilities. And Microsoft XInput for its stronger compatibility and analog support for sticks and triggers with Xbox gamepads. From there I sketeched out the program's structure before moving onto full diagrams using Draw.io.
// Comments removed for readability
// Code available on my GitHub page
#include "debugger.h"
#include "commonTypes.h"
Debugger::Debugger( Input& input, PakData& pakData, GameData& gameData, const float& dt,
const float& fps, InputManager& inputMgr )
: isActive ( false ),
debugState( DebugState::ENUM_START ),
input ( input ),
pakData ( pakData ),
selectedPak ( nullptr ),
isPakSwapped ( false ),
gameData ( gameData ),
dt ( dt ),
fps ( fps ),
inputMgr ( inputMgr ) {
std::cout << "\nDebugMgr Constructed." << std::flush;
}
void Debugger::init() {
vecIndex = 0;
debugStateNames = {
{ DebugState::ENUM_START, "Enum Start" },
{ DebugState::INPUT_TEST, "Input Test" },
{ DebugState::SOUND_TEST, "Sound Test" },
{ DebugState::GAMEDATA_VIEW, "Game Data View" },
{ DebugState::TEXTURES_VIEW, "Textures View" },
{ DebugState::STAGEDATA_VIEW, "Stage Data View" },
{ DebugState::DIALOGUES_VIEW, "Dialogues View" },
{ DebugState::ENUM_END, "Enum End" }
};
selectPakStruct ( pakData.global );
posJoystickDraw = { 180.0f, 180.0f };
joystickEmbedRadius = 24.0f;
joystickRadius = 3.0f;
deadzoneRadius = input.deadzoneStick * joystickEmbedRadius;
colorJoystickActive = sf::Color ( 180, 180, 180 );
colorJoystickInactive = sf::Color ( 160, 30, 30 );
circleJoystickEmbed.emplace( joystickEmbedRadius, 24 );
circleDeadzone.emplace( joystickEmbedRadius, 24 );
circleJoystick.emplace( joystickRadius, 8 );
circleJoystickEmbed -> setFillColor ( sf::Color ( 60, 60, 60 ) );
circleJoystickEmbed -> setOrigin ( { joystickEmbedRadius, joystickEmbedRadius } );
circleJoystickEmbed -> setPosition ( posJoystickDraw );
circleJoystick -> setFillColor ( sf::Color ( 255, 255, 255 ) );
circleJoystick -> setOrigin ( { joystickRadius, joystickRadius } );
circleJoystick -> setPosition ( posJoystickDraw );
circleDeadzone -> setRadius ( deadzoneRadius );
circleDeadzone -> setFillColor ( sf::Color ( 30, 30, 30 ) );
circleDeadzone -> setOrigin ( { deadzoneRadius , deadzoneRadius } );
circleDeadzone -> setPosition ( posJoystickDraw );
sf::Font* tempFontPtr = findMapObject( selectedPak -> fonts, "test.otf" );
if (tempFontPtr) {
bodyText.emplace( *tempFontPtr, "...");
fpsText.emplace( *tempFontPtr, "...");
debugMenuText.emplace( *tempFontPtr, "...");
popUpText.emplace( *tempFontPtr, "...");
bodyText -> setFillColor( sf::Color ( 0, 255, 0 ) );
bodyText -> setPosition( { 12.0f, 12.0f } );
bodyText -> setScale( { 0.2f, 0.2f } );
fpsText -> setFillColor( sf::Color ( 0, 255, 0 ) );
fpsText -> setPosition( { 12.0f, 340.0f } );
fpsText -> setScale( { 0.2f, 0.2f } );
debugMenuText -> setFillColor( sf::Color ( 0, 255, 0 ) );
debugMenuText -> setPosition( { 128.0f, 55.0f } );
debugMenuText -> setScale( { 0.2f, 0.2f } );
popUpText -> setFillColor( sf::Color ( 255, 0, 255 ) );
popUpText -> setPosition( { 450.0f, 40.0f } );
popUpText -> setScale( { 0.2f, 0.2f } );
}
else {
std::cout << "tempFontPtr is Null" << "\n";
}
sf::SoundBuffer* tempSoundPtr = findMapObject( selectedPak -> sounds, "test.wav" );
if ( tempSoundPtr ) {
debugSound.emplace( *tempSoundPtr );
}
else {
std::cout << "tempSountPtr is Null" << "\n";
}
sf::Texture* tempTexturePtr = findMapObject ( selectedPak -> textures, "test.png" );
if ( tempTexturePtr ) {
debugSprite.emplace( *tempTexturePtr );
debugSprite -> setPosition ( { 160.0f, 70.0f} );
textureSize = sf::Vector2i( tempTexturePtr -> getSize() );
}
else {
std::cout << "tempTexturePtr is Null" << "\n";
}
keyBindString = "[ Enable/Disable ] \\ [ Accpet ] R-Shift [ Next / Prev ] Arrows: Left / Right [ Traverse Menu ] Arrows: Up / Down";
keyBindString += "\n[ Pak Global ] P + 1 [ Pak Stage Common ] P + 2 [ Pak Stage Current ] P + 3";
timerPopUpText = { 1.5, 1.5 };
setState ( DebugState::INPUT_TEST );
std::cout << "\nDebugMgr Initialized." << std::flush;
}
void Debugger::update(){
updateDebugInputState( inputActivate, sf::Keyboard::isKeyPressed( sf::Keyboard::Key::Backslash ) );
if ( inputActivate.pressed ) {
isActive = !isActive;
}
if ( !isActive ) {
return;
}
updateDebugInput();
if ( inputNextState.pressed ) {
setState ( ( DebugState )( enumToInt( debugState ) + 1 ) );
}
else if ( inputPrevState.pressed ) {
setState ( ( DebugState )( enumToInt( debugState ) - 1 ) );
}
if ( !isPakLoaded ) {
return;
}
if ( isPakSwapped ) {
timerUpdate ( timerPopUpText, dt );
if ( timerIsDone ( timerPopUpText ) ) {
isPakSwapped = false;
}
}
switch(debugState) {
case DebugState::ENUM_START: {
setState( ( DebugState ) ( enumToInt( DebugState::ENUM_END ) - 1 ) );
break; }
case DebugState::INPUT_TEST: {
if ( input.isJoystickUsed && circleJoystick -> getFillColor() != colorJoystickActive ) {
circleJoystick -> setFillColor ( colorJoystickActive );
}
else if ( !input.isJoystickUsed && circleJoystick -> getFillColor() != colorJoystickInactive ) {
circleJoystick -> setFillColor ( colorJoystickInactive );
}
circleJoystick -> setPosition ( { posJoystickDraw.x + input.joystickAxisX * 24.0f
, posJoystickDraw.y - input.joystickAxisY * 24.0f } );
buildString ();
break; }
case DebugState::SOUND_TEST: {
if ( soundsNames.empty() ) { break; }
allowVectorControls( soundsNames.size() - 1 );
sf::SoundBuffer* tempSoundPtr = findMapObject ( selectedPak -> sounds, soundsNames [ vecIndex ] );
if (tempSoundPtr) {
if ( inputDown.pressed || inputUp.pressed || isStateChanged ) {
debugSound -> setBuffer ( *tempSoundPtr );
buildString ();
}
else if ( inputAccept.pressed ) {
debugSound -> play();
}
}
break; }
case DebugState::GAMEDATA_VIEW: {
break; }
case DebugState::TEXTURES_VIEW: {
if ( texturesNames.empty() ) break;
allowVectorControls( texturesNames.size() - 1 );
if ( inputDown.pressed || inputUp.pressed || isStateChanged ) {
sf::Texture* tempTexturePtr = findMapObject ( selectedPak -> textures, texturesNames [ vecIndex ] );
if (tempTexturePtr) {
textureSize = sf::Vector2i( tempTexturePtr -> getSize() );
debugSprite -> setTexture ( *tempTexturePtr );
debugSprite -> setTextureRect( sf::IntRect( { 0, 0 }, textureSize ) );
buildString ();
}
}
break; }
case DebugState::STAGEDATA_VIEW: {
if ( gameData.stages.empty() ) break;
allowVectorControls( gameData.stages.size() - 1 );
if ( inputDown.pressed || inputUp.pressed || isStateChanged ) {
buildString ();
}
break; }
case DebugState::DIALOGUES_VIEW: {
if ( dialoguesNames.empty() ) break;
allowVectorControls( dialoguesNames.size() - 1 );
if ( inputDown.pressed || inputUp.pressed || isStateChanged ) {
std::vector< std::string >* tempStringVecPtr = findMapObject ( selectedPak -> dialogues, dialoguesNames [ vecIndex ] );
if (tempStringVecPtr) {
dialogueEntry = *tempStringVecPtr;
buildString ();
}
}
break; }
case DebugState::ENUM_END: {
setState( ( DebugState ) ( enumToInt( DebugState::ENUM_START ) + 1 ) );
break; }
}
fpsText -> setString ( std::to_string( fps ) );
if ( isStateChanged ) {
isStateChanged = false;
}
//switch selected pak struct, after state changes check so string rebuild triggers
if ( sf::Keyboard::isKeyPressed( sf::Keyboard::Key::P ) ) {
if ( inputNum1.pressed ) {
selectPakStruct( pakData.global );
isPakSwapped = true;
timerReset ( timerPopUpText );
popUpText -> setString( "NOTICE -> Loaded Pak : GLOBAL" );
}
else if ( inputNum2.pressed ) {
selectPakStruct( pakData.stageCommon );
isPakSwapped = true;
timerReset ( timerPopUpText );
popUpText -> setString( "NOTICE -> Loaded Pak : STAGE COMMON" );
}
else if ( inputNum3.pressed ) {
selectPakStruct( pakData.stageCurrent );
isPakSwapped = true;
timerReset ( timerPopUpText );
popUpText -> setString( "NOTICE -> Loaded Pak : STAGE CURRENT" );
}
}
}
// DRAW
void Debugger::draw( sf::RenderWindow& window ) {
if ( !isActive ) return;
window.draw( *bodyText );
window.draw( *fpsText );
if ( isPakSwapped ) {
window.draw( *popUpText );
}
switch(debugState) {
case DebugState::ENUM_START:
break;
case DebugState::INPUT_TEST:
window.draw ( *circleJoystickEmbed );
window.draw ( *circleDeadzone );
window.draw ( *circleJoystick );
break;
case DebugState::SOUND_TEST:
break;
case DebugState::TEXTURES_VIEW:
window.draw ( *debugMenuText );
window.draw ( *debugSprite );
break;
case DebugState::GAMEDATA_VIEW:
break;
case DebugState::STAGEDATA_VIEW:
window.draw ( *debugMenuText );
break;
case DebugState::DIALOGUES_VIEW:
window.draw ( *debugMenuText );
break;
case DebugState::ENUM_END:
break;
}
}
// UPDATE INPUT STATE
void Debugger::updateDebugInputState( InputState& inputState, bool isDownNow )
{
inputState.prevHeld = inputState.held;
inputState.held = isDownNow;
inputState.pressed = inputState.held && !inputState.prevHeld;
inputState.released = !inputState.held && inputState.prevHeld;
}
// UPDATE ALL DEBUG MENU INPUTS
void Debugger::updateDebugInput() {
updateDebugInputState( inputAccept, sf::Keyboard::isKeyPressed( sf::Keyboard::Key::RShift ) );
updateDebugInputState( inputBack, sf::Keyboard::isKeyPressed( sf::Keyboard::Key::RControl ) );
updateDebugInputState( inputNextState, sf::Keyboard::isKeyPressed( sf::Keyboard::Key::Right ) );
updateDebugInputState( inputPrevState, sf::Keyboard::isKeyPressed( sf::Keyboard::Key::Left ) );
updateDebugInputState( inputUp, sf::Keyboard::isKeyPressed( sf::Keyboard::Key::Up ) );
updateDebugInputState( inputDown, sf::Keyboard::isKeyPressed( sf::Keyboard::Key::Down ) );
updateDebugInputState( inputNum1, sf::Keyboard::isKeyPressed( sf::Keyboard::Key::Num1 ) );
updateDebugInputState( inputNum2, sf::Keyboard::isKeyPressed( sf::Keyboard::Key::Num2 ) );
updateDebugInputState( inputNum3, sf::Keyboard::isKeyPressed( sf::Keyboard::Key::Num3 ) );
}
// CREATE INPUT STRING
std::string Debugger::createInputString ( InputState& inputState )
{
return "\n[ "+ inputMgr.getInputName( &inputState ) + " ]"
+ " Held:" + std::to_string(inputState.held)
+ ", Pressed:" + std::to_string(inputState.pressed)
+ ", Released:" + std::to_string(inputState.released)
+ ", Buffer_Active:" + std::to_string(inputState.bufferActive);
}
// ENABLE MENU CONTROLS
void Debugger::allowVectorControls( int endVal )
{
if (inputDown.pressed) vecIndex ++;
if (inputUp.pressed) vecIndex --;
if ( vecIndex < 0 ) vecIndex = endVal;
if ( vecIndex > endVal ) vecIndex = 0;
}
// SELECT PAK STRUCT
void Debugger::selectPakStruct( Pak& pak )
{
isPakLoaded = false;
std::cout << "\nSelecting new pak." << std::flush;
selectedPak = &pak;
std::cout << "\nNew pak selected" << std::flush;
std::cout << "\nClearing all names vectors" << std::flush;
//claer all vectors
texturesNames.clear();
soundsNames.clear();
fontsNames.clear();
dialoguesNames.clear();
std::cout << "\nName vectors cleared." << std::flush;
std::cout << "\nLoad pak data names into names vectors." << std::flush;
if ( !pak.textures.empty() ) for ( const auto& pair : pak.textures ) {
texturesNames.push_back( pair.first );
}
else {
std::cout << "\npak.textures empty." << std::flush;
}
if ( !pak.sounds.empty() ) for ( const auto& pair : pak.sounds ) {
soundsNames.push_back( pair.first );
}
else {
std::cout << "\npak.sounds empty." << std::flush;
}
if ( !pak.fonts.empty() ) for ( const auto& pair : pak.fonts ) {
fontsNames.push_back( pair.first );
}
else {
std::cout << "\npak.fonts empty." << std::flush;
}
if ( !pak.dialogues.empty() ) {
for ( const auto& pair : pak.dialogues ) {
dialoguesNames.push_back( pair.first );
}
}
else {
std::cout << "\npak.dialogues empty." << std::flush;
}
std::cout << "\nPak data names loaded into names vectors" << std::flush;
std::cout << "\nCompleting pak loading." << std::flush;
isPakLoaded = true;
std::cout << "\nPak loading complete." << std::flush;
isStateChanged = true;
}
void Debugger::setState( DebugState debugState )
{
this -> debugState = debugState;
vecIndex = 0;
isStateChanged = true;
buildString ();
}
void Debugger::buildString()
{
//setup body string
bodyString = keyBindString + "\n\n"
+ "Menu: " + debugStateNames.find( debugState ) -> second + "\n";
switch( debugState )
{
case DebugState::INPUT_TEST: {
bodyString +=
createInputString( input.actionA )
+ createInputString( input.actionB )
+ createInputString( input.actionX )
+ createInputString( input.actionY )
+ createInputString( input.actionLB )
+ createInputString( input.actionRB )
+ createInputString( input.actionLT )
+ createInputString( input.actionRT )
+ createInputString( input.dirPadLeft )
+ createInputString( input.dirPadUp )
+ createInputString( input.dirPadRight )
+ createInputString( input.dirPadDown )
+ "\n\n\n Joystick Active: " + std::to_string( input.isJoystickUsed )
+ "\n[ Left-Stick ]\nX:"
+ std::to_string( input.joystickAxisX )
+ "\nY:" + std::to_string( input.joystickAxisY )
+ "\nInputDirection: " + std::to_string( input.direction );
break; }
case DebugState::SOUND_TEST:{
if ( soundsNames.empty() ) {
bodyString += "\nNo Sounds Data.";
break;
}
for ( int i = 0; i < soundsNames.size(); i++ ) {
if ( i == vecIndex ) bodyString += "\n->" + soundsNames[i];
else bodyString += "\n" + soundsNames[i];
}
break;}
case DebugState::TEXTURES_VIEW:{
if ( texturesNames.empty() ){
bodyString += "\nNo Textures Data.";
break;
}
for ( int i = 0; i < texturesNames.size(); i++ ) {
if (i == vecIndex) bodyString += "\n->" + texturesNames[i];
else bodyString += "\n" + texturesNames[i];
}
debugMenuString = "Texture Size: { " + std::to_string( textureSize.x ) + ", " + std::to_string( textureSize.y ) + " }";
break;}
case DebugState::GAMEDATA_VIEW: {
bodyString +=
"\n [firstLaunch] " + std::to_string( gameData.firstLaunch )
+ "\n [totalHFuel] " + std::to_string( gameData.totalHFuel)
+ "\n\n [resolutionX] " + std::to_string( gameData.settings.display.x )
+ "\n [resolutionY] " + std::to_string( gameData.settings.display.y )
+ "\n [windowTitle] " + gameData.settings.display.windowTitle
+ "\n\n [volumeMaster] " + std::to_string( gameData.settings.volume.master )
+ "\n [volumeMusic] " + std::to_string( gameData.settings.volume.music )
+ "\n [volumeSfx] " + std::to_string( gameData.settings.volume.sfx )
;
break;}
case DebugState::STAGEDATA_VIEW: {
if ( gameData.stages.empty() ) {
bodyString += "\nNo Stage Data.";
break;
}
for ( int i = 0; i < gameData.stages.size(); i++ ) {
if (i == vecIndex) {
bodyString += "\n -> " + gameData.stages[i].name;
}
else bodyString += "\n" + gameData.stages[i].name;
}
StageData stage = gameData.stages[ vecIndex ];
debugMenuString =
"[ Unlocked ] " + std::to_string ( stage.unlocked )
+ "\n[ Best Time ] " + std::to_string( stage.bestTime )
+ "\n[ Best Checkpoint Times ] ";
if ( stage.bestCheckPointTimes.empty() ) {
for ( int i = 0; i < stage.bestCheckPointTimes.size(); i++ ) {
debugMenuString += "\n\t" + std::to_string( stage.bestCheckPointTimes[ i ] );
}
}
else debugMenuString += "\n\tN/A";
debugMenuString += "\n[ Highest Rank ] " + std::to_string( static_cast( stage.rank ) )
+ "\n[ Rank Time Requirements ] ";
stageRanksCount = sizeof ( stage.rankTimeRequirements )
/ sizeof ( stage.rankTimeRequirements[0] );
for ( int i = 0; i < stageRanksCount; i++ ) {
debugMenuString += "\n\tRank " + std::to_string( i )
+ " - " + std::to_string( stage.rankTimeRequirements[i] );
}
break;}
case DebugState::DIALOGUES_VIEW:{
// SAFETY GUARD
if ( dialoguesNames.empty() ) {
bodyString += "\nNo Dialogues Data.";
break;
}
for ( int i = 0; i < dialoguesNames.size(); i++ ) {
if (i == vecIndex) bodyString += "\n -> " + dialoguesNames[i];
else bodyString += "\n" + dialoguesNames[i];
}
debugMenuString = "";
for ( int i = 0; i < dialogueEntry.size(); i++ ) {
debugMenuString += "\n" + std::to_string(i) + ". " + dialogueEntry[ i ];
}
break;}
}
if ( bodyText ) {
bodyText -> setString( bodyString );
}
if ( debugMenuText ) {
debugMenuText -> setString ( debugMenuString );
}
std::string* tempStateString = &debugStateNames.find( debugState ) -> second;
if ( tempStateString ) {
std::cout << "body string updated: " << *tempStateString << std::endl;
}
}
Ghost Engine is a custom C++ game engine built from scratch, using SFML for low-level rendering, audio, and input primitives while
every higher-level system, asset packing, audio management, input handling, and debugging tools, is designed and implemented
independently. Each system was built to solve a specific problem encountered during development, with an emphasis on clarity
and maintainability alongside performance.
Developing an engine from the ground up requires taking direct ownership of problems that are normally abstracted away: memory
layout, timing-sensitive input, and asset lifecycle management. The project is under active development, with new systems added
regularly. The full source is publicly available on GitHub, including a working in-engine debugger that can be used to inspect
these systems directly.
GAME OBJECT FAMILY HIERARCHY
ADDITIONAL CLASS DIAGRAMS
Render Manager Structure
Menu Structure & Example Code Workflow
Menu Structure & Example Code Workflow