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
- Build-time: CMake processes
assets/directory - Conversion: Binary files converted to C++ byte arrays
- Compilation: Arrays compiled into executable
- 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
-
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();
} -
Texture Atlases: Combine multiple small sprites into one texture
-
Compressed Audio: Use OGG instead of WAV for music
-
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:
- Check asset file exists in
assets/directory - Verify CMake regenerated EmbeddedResources
- Ensure image format is supported (PNG, JPG, BMP)
- Check image is not corrupted
- Rebuild project:
cmake --build --preset build-debug
Issue: No sound playback
Error: Sound not found: shot
Solutions:
- Verify
SoundManager::initialize()was called - Check sound file format (WAV preferred)
- Ensure volume > 0
- Test audio device is working
- Check for audio driver issues
Issue: Large executable size
Symptom: Executable is 100+ MB
Solutions:
- Use compressed formats (PNG instead of BMP, OGG instead of WAV)
- Reduce texture resolution
- Remove unused assets
- Use texture atlases
- Consider external asset loading for large assets
📚 Related Documentation
🎯 Best Practices
- Naming Conventions: Use snake_case for asset IDs
- Consistent Formats: PNG for sprites, WAV for SFX, OGG for music
- Optimize Assets: Compress images, trim audio silence
- Centralized Loading: Use manager singletons
- Error Handling: Always check load return values
- Resource Cleanup: Use RAII and smart pointers
- Cache Lookups: Store sprite/sound pointers in entities
- Version Control: Track assets in Git (use LFS for large files)