Skip to main content

Asset Management System

This document describes how R-Type manages game assets (textures, sounds, fonts) using an embedded resource system.


🎯 Overview

R-Type uses an embedded asset system where all game resources are compiled directly into the executable as binary data. This eliminates external file dependencies and simplifies distribution.

Benefits:

  • Single executable - No asset folders to distribute
  • No missing files - Assets always available
  • Fast loading - No disk I/O required
  • Easy deployment - Just ship the binary
  • Tamper-proof - Assets can't be modified externally

Trade-offs:

  • ⚠️ Larger executable size (~50-100MB)
  • ⚠️ Requires recompilation to update assets
  • ⚠️ All assets loaded into memory

🏗️ Architecture

┌──────────────────────────────────────────────────┐
│ Game Code (Client) │
├──────────────────────────────────────────────────┤
│ TextureManager │ SoundManager │ FontManager │
│ (Singleton) │ (Singleton) │ (Future) │
├──────────────────────────────────────────────────┤
│ SFML Wrapper (SpriteSFML, etc.) │
├──────────────────────────────────────────────────┤
│ EmbeddedResources (Binary Data) │
│ Generated by CMake from assets/ folder │
└──────────────────────────────────────────────────┘

📦 Embedded Resources System

How It Works

  1. Build-time: CMake processes assets/ directory
  2. Conversion: Binary files converted to C++ byte arrays
  3. Compilation: Arrays compiled into executable
  4. Runtime: Access via std::span<const std::byte>

Generated Header Structure

// wrapper/resources/EmbeddedResources.hpp (auto-generated)
namespace rtype::embedded {

// Sprites
extern const std::byte player_1_data[];
extern const size_t player_1_size;

extern const std::byte boss_1_data[];
extern const size_t boss_1_size;

// Sounds
extern const std::byte shot_sound_data[];
extern const size_t shot_sound_size;

extern const std::byte explosion_sound_data[];
extern const size_t explosion_sound_size;

// Fonts
extern const std::byte main_font_data[];
extern const size_t main_font_size;

} // namespace rtype::embedded

Usage Macro

// Helper macro for creating spans
#define ASSET_SPAN(data) std::span<const std::byte>(data, data##_size)

// Example usage
std::span<const std::byte> playerTexture = ASSET_SPAN(rtype::embedded::player_1_data);

🖼️ TextureManager

Singleton class for managing sprite textures.

Class Definition

class TextureManager {
public:
static TextureManager& getInstance();

// Load a sprite from embedded binary data
bool loadSprite(const std::string& id,
std::span<const std::byte> textureData,
bool smooth = false);

// Retrieve a loaded sprite
rtype::ISprite* getSprite(const std::string& id);

// Load all game sprites at startup
void loadAll();

TextureManager(const TextureManager&) = delete;
TextureManager& operator=(const TextureManager&) = delete;

private:
TextureManager() = default;
std::unordered_map<std::string, std::unique_ptr<rtype::SpriteSFML>> _sprites;
};

Implementation

bool TextureManager::loadSprite(const std::string& id,
std::span<const std::byte> textureData,
bool smooth)
{
// Check if already loaded
if (_sprites.find(id) != _sprites.end()) {
return true;
}

// Create new sprite
auto sprite = std::make_unique<rtype::SpriteSFML>();

// Load texture from memory
if (!sprite->loadTexture(textureData)) {
std::cerr << "Failed to load texture for id: " << id << std::endl;
return false;
}

// Set filtering mode
sprite->setSmooth(smooth);

// Store in cache
_sprites[id] = std::move(sprite);

std::cout << "Loaded texture: " << id << std::endl;
return true;
}

rtype::ISprite* TextureManager::getSprite(const std::string& id)
{
auto it = _sprites.find(id);
if (it != _sprites.end()) {
return it->second.get();
}
return nullptr;
}

Loading All Assets

void TextureManager::loadAll()
{
// Backgrounds
loadSprite("bg_back", ASSET_SPAN(rtype::embedded::background_base_data), false);
loadSprite("bg_stars", ASSET_SPAN(rtype::embedded::background_stars_data), false);
loadSprite("bg_planet", ASSET_SPAN(rtype::embedded::background_planet_data), false);

// Player sprites
loadSprite("player_static", ASSET_SPAN(rtype::embedded::player_1_data), false);
loadSprite("player_down", ASSET_SPAN(rtype::embedded::player_2_data), false);
loadSprite("player_up", ASSET_SPAN(rtype::embedded::player_3_data), false);

// Projectiles
loadSprite("projectile", ASSET_SPAN(rtype::embedded::projectile_player_1_data), false);
loadSprite("projectile_enemy", ASSET_SPAN(rtype::embedded::projectile_enemy_1_data), false);

// Bosses
loadSprite("boss", ASSET_SPAN(rtype::embedded::boss_1_data), false);
loadSprite("boss_2", ASSET_SPAN(rtype::embedded::boss_2_data), false);
loadSprite("boss_3", ASSET_SPAN(rtype::embedded::boss_3_data), false);
loadSprite("boss_4", ASSET_SPAN(rtype::embedded::boss_4_data), false);

// Enemies
loadSprite("turret", ASSET_SPAN(rtype::embedded::turret_data), false);

// Effects
loadSprite("explosion_1", ASSET_SPAN(rtype::embedded::blowup_1_data), false);
loadSprite("explosion_2", ASSET_SPAN(rtype::embedded::blowup_2_data), false);

// Power-ups
loadSprite("player_shield", ASSET_SPAN(rtype::embedded::shield_data), false);
loadSprite("shield_item", ASSET_SPAN(rtype::embedded::shield_item_data), false);
loadSprite("search_missile", ASSET_SPAN(rtype::embedded::search_missile_data), false);
loadSprite("search_missile_item", ASSET_SPAN(rtype::embedded::search_missile_item_data), false);
}

Usage in Game Code

// Initialization (once at startup)
TextureManager::getInstance().loadAll();

// Usage in entity classes
class Player {
public:
Player() {
_sprite = TextureManager::getInstance().getSprite("player_static");
}

void update(float deltaTime) {
// Change sprite based on movement
if (movingUp) {
_sprite = TextureManager::getInstance().getSprite("player_up");
} else if (movingDown) {
_sprite = TextureManager::getInstance().getSprite("player_down");
} else {
_sprite = TextureManager::getInstance().getSprite("player_static");
}
}

void render(rtype::IGraphics& graphics) {
if (_sprite) {
_sprite->setPosition(_x, _y);
graphics.draw(*_sprite);
}
}

private:
rtype::ISprite* _sprite = nullptr;
float _x, _y;
};

🔊 SoundManager

Singleton class for managing audio playback.

Class Definition

class SoundManager {
public:
static SoundManager& getInstance();

// Load sound from embedded binary data
void loadSound(const std::string& name, std::span<const std::byte> data);

// Play a sound effect
void playSound(const std::string& name);

// Volume control (0.0 - 100.0)
void setVolume(float volume);
float getVolume() const;

// Music control
void setMusicVolume(float volume);
float getMusicVolume() const;

// Initialize all sound effects
void initialize();

SoundManager(const SoundManager&) = delete;
SoundManager& operator=(const SoundManager&) = delete;

private:
SoundManager() = default;

std::unordered_map<std::string, std::unique_ptr<rtype::ISoundBuffer>> _soundBuffers;
std::unordered_map<std::string, std::unique_ptr<rtype::ISound>> _sounds;

float _sfxVolume = 100.0f;
float _musicVolume = 100.0f;
};

Implementation

void SoundManager::loadSound(const std::string& name,
std::span<const std::byte> data)
{
// Create sound buffer from memory
auto buffer = std::make_unique<rtype::SoundBufferSFML>();

if (!buffer->loadFromMemory(
reinterpret_cast<const void*>(data.data()),
data.size()))
{
std::cerr << "Failed to load sound: " << name << std::endl;
return;
}

// Create sound object
auto sound = std::make_unique<rtype::SoundSFML>();
sound->setBuffer(*buffer);
sound->setVolume(_sfxVolume);

// Store both buffer and sound
_soundBuffers[name] = std::move(buffer);
_sounds[name] = std::move(sound);

std::cout << "Loaded sound: " << name << std::endl;
}

void SoundManager::playSound(const std::string& name)
{
auto it = _sounds.find(name);
if (it != _sounds.end()) {
it->second->play();
} else {
std::cerr << "Sound not found: " << name << std::endl;
}
}

void SoundManager::setVolume(float volume)
{
_sfxVolume = std::clamp(volume, 0.0f, 100.0f);

// Update all loaded sounds
for (auto& [name, sound] : _sounds) {
sound->setVolume(_sfxVolume);
}
}

Loading All Sounds

void SoundManager::initialize()
{
// Weapon sounds
loadSound("shot", ASSET_SPAN(rtype::embedded::shot_sound_data));
loadSound("shot_2", ASSET_SPAN(rtype::embedded::shot_2_sound_data));

// Impact/damage sounds
loadSound("hit", ASSET_SPAN(rtype::embedded::hit_sound_data));
loadSound("explosion", ASSET_SPAN(rtype::embedded::explosion_sound_data));

// UI sounds
loadSound("click", ASSET_SPAN(rtype::embedded::click_sound_data));

// Game state sounds
loadSound("game_over", ASSET_SPAN(rtype::embedded::game_over_sound_data));
loadSound("level_win", ASSET_SPAN(rtype::embedded::level_win_sound_data));

// Power-up sounds
loadSound("powerup", ASSET_SPAN(rtype::embedded::powerup_sound_data));

// Ambient sounds
loadSound("space_rumble", ASSET_SPAN(rtype::embedded::space_rumble_sound_data));

std::cout << "SoundManager initialized with "
<< _sounds.size() << " sounds" << std::endl;
}

Usage in Game Code

// Initialization (once at startup)
SoundManager::getInstance().initialize();

// Play sounds during gameplay
void Player::shoot() {
// Create projectile...

// Play shoot sound
SoundManager::getInstance().playSound("shot");
}

void Enemy::takeDamage(int damage) {
_health -= damage;

if (_health <= 0) {
// Play explosion sound
SoundManager::getInstance().playSound("explosion");
destroy();
} else {
// Play hit sound
SoundManager::getInstance().playSound("hit");
}
}

// Volume control (from settings menu)
void SettingsMenu::onVolumeChanged(float newVolume) {
SoundManager::getInstance().setVolume(newVolume);
Config::getInstance().setFloat("sfxVolume", newVolume);
Config::getInstance().save();
}

🎨 SFML Wrapper Integration

SpriteSFML::loadTexture()

bool SpriteSFML::loadTexture(std::span<const std::byte> binaryData)
{
// Load from memory into SFML texture
if (!_texture.loadFromMemory(
reinterpret_cast<const void*>(binaryData.data()),
binaryData.size()))
{
return false;
}

// Set texture to sprite
_sprite.setTexture(_texture);

return true;
}

SoundBufferSFML::loadFromMemory()

bool SoundBufferSFML::loadFromMemory(const void* data, std::size_t size)
{
return _buffer.loadFromMemory(data, size);
}

📂 Asset Directory Structure

assets/
├── sprites/
│ ├── player/
│ │ ├── player_1.png # Static
│ │ ├── player_2.png # Moving down
│ │ └── player_3.png # Moving up
│ ├── enemies/
│ │ ├── boss_1.png
│ │ ├── boss_2.png
│ │ ├── turret.png
│ │ └── ...
│ ├── projectiles/
│ │ ├── projectile_player_1.png
│ │ └── projectile_enemy_1.png
│ └── effects/
│ ├── explosion_1.png
│ ├── explosion_2.png
│ └── shield.png
├── sound/
│ ├── shot.wav
│ ├── explosion.wav
│ ├── hit.wav
│ ├── click.wav
│ └── ...
├── background/
│ ├── background_base.png
│ ├── background_stars.png
│ └── background_planet.png
└── fonts/
└── main_font.ttf

🔨 Build System Integration

CMake Asset Embedding

# Function to embed assets as C++ arrays
function(embed_resources TARGET_NAME ASSETS_DIR)
file(GLOB_RECURSE ASSET_FILES "${ASSETS_DIR}/*")

set(GENERATED_HEADER "${CMAKE_CURRENT_BINARY_DIR}/EmbeddedResources.hpp")
set(GENERATED_SOURCE "${CMAKE_CURRENT_BINARY_DIR}/EmbeddedResources.cpp")

# Generate header
file(WRITE ${GENERATED_HEADER} "// Auto-generated - do not edit\n")
file(APPEND ${GENERATED_HEADER} "#pragma once\n")
file(APPEND ${GENERATED_HEADER} "#include <cstddef>\n\n")
file(APPEND ${GENERATED_HEADER} "namespace rtype::embedded {\n\n")

# Generate source
file(WRITE ${GENERATED_SOURCE} "#include \"EmbeddedResources.hpp\"\n\n")
file(APPEND ${GENERATED_SOURCE} "namespace rtype::embedded {\n\n")

foreach(ASSET_FILE ${ASSET_FILES})
# Get filename without extension
get_filename_component(ASSET_NAME ${ASSET_FILE} NAME_WE)

# Convert to binary array
file(READ ${ASSET_FILE} ASSET_CONTENT HEX)

# Declare in header
file(APPEND ${GENERATED_HEADER}
"extern const std::byte ${ASSET_NAME}_data[];\n")
file(APPEND ${GENERATED_HEADER}
"extern const size_t ${ASSET_NAME}_size;\n\n")

# Define in source
# ... (binary data conversion logic)
endforeach()

file(APPEND ${GENERATED_HEADER} "} // namespace rtype::embedded\n")
file(APPEND ${GENERATED_SOURCE} "} // namespace rtype::embedded\n")

target_sources(${TARGET_NAME} PRIVATE ${GENERATED_SOURCE})
endfunction()

# Usage
embed_resources(r-type_client "${CMAKE_SOURCE_DIR}/assets")

🚀 Performance Considerations

Memory Usage

// Estimate memory usage
size_t totalMemory = 0;

// Textures (RGBA format)
for (const auto& [id, sprite] : _sprites) {
// Assuming 1920x1080 max size, RGBA (4 bytes per pixel)
totalMemory += 1920 * 1080 * 4; // ~8MB per texture
}

// Sounds (WAV format)
for (const auto& [name, buffer] : _soundBuffers) {
// Assuming 16-bit stereo at 44100 Hz, ~10s duration
totalMemory += 44100 * 2 * 2 * 10; // ~1.7MB per sound
}

std::cout << "Total asset memory: "
<< (totalMemory / 1024 / 1024) << " MB" << std::endl;

Typical Memory Usage:

  • Textures: 30-50 MB (20-30 sprites @ 2-3MB each)
  • Sounds: 15-25 MB (10-15 sounds @ 1-2MB each)
  • Total: ~50-75 MB

Loading Performance

// Measure load time
auto startTime = std::chrono::high_resolution_clock::now();

TextureManager::getInstance().loadAll();
SoundManager::getInstance().initialize();

auto endTime = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(
endTime - startTime
);

std::cout << "Assets loaded in " << duration.count() << "ms" << std::endl;

Typical Load Times:

  • Debug build: 200-300ms
  • Release build: 50-100ms

Optimization Tips

  1. Lazy Loading: Load assets on-demand instead of all at once

    rtype::ISprite* getSprite(const std::string& id) {
    if (_sprites.find(id) == _sprites.end()) {
    loadSprite(id, ...); // Load on first access
    }
    return _sprites[id].get();
    }
  2. Texture Atlases: Combine multiple small sprites into one texture

  3. Compressed Audio: Use OGG instead of WAV for music

  4. Mipmap Generation: For scaled sprites (not used in 2D)


🐛 Troubleshooting

Issue: Failed to load texture

Error: Failed to load texture for id: player_static

Solutions:

  1. Check asset file exists in assets/ directory
  2. Verify CMake regenerated EmbeddedResources
  3. Ensure image format is supported (PNG, JPG, BMP)
  4. Check image is not corrupted
  5. Rebuild project: cmake --build --preset build-debug

Issue: No sound playback

Error: Sound not found: shot

Solutions:

  1. Verify SoundManager::initialize() was called
  2. Check sound file format (WAV preferred)
  3. Ensure volume > 0
  4. Test audio device is working
  5. Check for audio driver issues

Issue: Large executable size

Symptom: Executable is 100+ MB

Solutions:

  1. Use compressed formats (PNG instead of BMP, OGG instead of WAV)
  2. Reduce texture resolution
  3. Remove unused assets
  4. Use texture atlases
  5. Consider external asset loading for large assets


🎯 Best Practices

  1. Naming Conventions: Use snake_case for asset IDs
  2. Consistent Formats: PNG for sprites, WAV for SFX, OGG for music
  3. Optimize Assets: Compress images, trim audio silence
  4. Centralized Loading: Use manager singletons
  5. Error Handling: Always check load return values
  6. Resource Cleanup: Use RAII and smart pointers
  7. Cache Lookups: Store sprite/sound pointers in entities
  8. Version Control: Track assets in Git (use LFS for large files)