Asset Registry
The Popcorning Guinea Pig engine provides a centralized asset management system through the IAssetRegistry, enabling efficient loading, caching, and management of game assets.
Overview
The Asset Registry allows you to:
- Load and manage textures, sounds, fonts, and other assets
- Cache assets to improve performance
- Organize assets with logical naming
- Support multiple file formats
- Handle asset dependencies
- Implement asset hot-reloading during development
- Manage memory usage with asset disposal
IAssetRegistry Interface
public interface IAssetRegistry
{
T LoadAsset<T>(string assetPath) where T : class;
T GetAsset<T>(string assetPath) where T : class;
bool IsAssetLoaded(string assetPath);
void UnloadAsset(string assetPath);
void UnloadAllAssets();
void PreloadAssets(string[] assetPaths);
string[] GetLoadedAssets();
void SetAssetSearchPath(string path);
void RegisterAssetLoader<T>(IAssetLoader<T> loader) where T : class;
}
Asset Types
Supported Asset Types
The engine supports various asset types out of the box:
// Textures
var texture = _assetRegistry.LoadAsset<Texture2D>("textures/player.png");
var spriteSheet = _assetRegistry.LoadAsset<Texture2D>("sprites/animations.png");
// Audio
var music = _assetRegistry.LoadAsset<AudioClip>("audio/background_music.ogg");
var soundEffect = _assetRegistry.LoadAsset<AudioClip>("audio/explosion.wav");
// Fonts
var font = _assetRegistry.LoadAsset<Font>("fonts/arial.ttf");
var customFont = _assetRegistry.LoadAsset<Font>("fonts/game_font.otf");
// Data files
var gameData = _assetRegistry.LoadAsset<GameData>("data/levels.json");
var config = _assetRegistry.LoadAsset<Configuration>("config/settings.xml");
// Scenes
var scene = _assetRegistry.LoadAsset<SceneData>("scenes/level1.pgpscn");
Getting Started
Initialize Asset Registry
public class MyGame : Game
{
private readonly IAssetRegistry _assetRegistry;
public MyGame()
{
_assetRegistry = GetService<IAssetRegistry>();
// Set the base path for assets
_assetRegistry.SetAssetSearchPath("assets/");
// Preload critical assets
PreloadCriticalAssets();
}
private void PreloadCriticalAssets()
{
var criticalAssets = new[]
{
"textures/ui/button.png",
"textures/ui/background.png",
"audio/ui/click.wav",
"fonts/main_font.ttf"
};
_assetRegistry.PreloadAssets(criticalAssets);
}
}
Loading Assets
public class PlayerSprite
{
private readonly IAssetRegistry _assetRegistry;
private Texture2D _playerTexture;
public PlayerSprite()
{
_assetRegistry = GetService<IAssetRegistry>();
LoadAssets();
}
private void LoadAssets()
{
// Load player texture
_playerTexture = _assetRegistry.LoadAsset<Texture2D>("textures/player.png");
// Load animation frames
var walkAnimation = _assetRegistry.LoadAsset<Texture2D>("animations/player_walk.png");
var jumpAnimation = _assetRegistry.LoadAsset<Texture2D>("animations/player_jump.png");
}
}
Asset Organization
Directory Structure
Organize your assets with a clear directory structure:
assets/
├── textures/
│ ├── ui/ # User interface textures
│ ├── characters/ # Character sprites and animations
│ ├── environments/ # Background and environment art
│ ├── effects/ # Visual effects and particles
│ └── icons/ # Game icons and symbols
├── audio/
│ ├── music/ # Background music tracks
│ ├── sfx/ # Sound effects
│ └── voice/ # Voice acting and dialogue
├── fonts/
│ ├── ui/ # UI fonts
│ └── display/ # Display and title fonts
├── data/
│ ├── levels/ # Level data files
│ ├── configs/ # Configuration files
│ └── localization/ # Translation files
└── scenes/
├── menus/ # Menu scenes
└── gameplay/ # Gameplay scenes
Naming Conventions
Use consistent naming conventions for your assets:
// Good naming examples
_assetRegistry.LoadAsset<Texture2D>("textures/characters/player_idle.png");
_assetRegistry.LoadAsset<Texture2D>("textures/ui/button_play_normal.png");
_assetRegistry.LoadAsset<AudioClip>("audio/sfx/explosion_large.ogg");
// Asset variants
var buttonNormal = _assetRegistry.LoadAsset<Texture2D>("textures/ui/button_normal.png");
var buttonHover = _assetRegistry.LoadAsset<Texture2D>("textures/ui/button_hover.png");
var buttonPressed = _assetRegistry.LoadAsset<Texture2D>("textures/ui/button_pressed.png");
Advanced Features
Asset Caching
The Asset Registry automatically caches loaded assets:
public class AssetCacheExample
{
private readonly IAssetRegistry _assetRegistry;
public void DemonstrateCache()
{
// First load - loads from disk and caches
var texture1 = _assetRegistry.LoadAsset<Texture2D>("textures/player.png");
// Second load - returns cached asset (fast)
var texture2 = _assetRegistry.LoadAsset<Texture2D>("textures/player.png");
// texture1 and texture2 reference the same object
Debug.Assert(ReferenceEquals(texture1, texture2));
// Check if asset is already loaded
if (_assetRegistry.IsAssetLoaded("textures/player.png"))
{
var cachedTexture = _assetRegistry.GetAsset<Texture2D>("textures/player.png");
}
}
}
Custom Asset Loaders
Create custom loaders for specialized asset types:
// Define a custom asset type
public class LevelData
{
public string Name { get; set; }
public Vector2 PlayerStartPosition { get; set; }
public List<EnemySpawn> Enemies { get; set; }
public List<ItemSpawn> Items { get; set; }
}
// Implement a custom asset loader
public class LevelDataLoader : IAssetLoader<LevelData>
{
public LevelData LoadAsset(string path)
{
var jsonContent = File.ReadAllText(path);
return JsonSerializer.Deserialize<LevelData>(jsonContent);
}
public bool CanLoad(string path)
{
return Path.GetExtension(path).ToLower() == ".level";
}
}
// Register the custom loader
public class MyGame : Game
{
public MyGame()
{
var assetRegistry = GetService<IAssetRegistry>();
assetRegistry.RegisterAssetLoader(new LevelDataLoader());
// Now you can load custom asset types
var level1 = assetRegistry.LoadAsset<LevelData>("levels/level1.level");
}
}
Asset Dependencies
Handle complex assets with dependencies:
public class MaterialAsset
{
public Texture2D DiffuseTexture { get; set; }
public Texture2D NormalTexture { get; set; }
public Texture2D SpecularTexture { get; set; }
public Color Tint { get; set; }
}
public class MaterialLoader : IAssetLoader<MaterialAsset>
{
private readonly IAssetRegistry _assetRegistry;
public MaterialLoader(IAssetRegistry assetRegistry)
{
_assetRegistry = assetRegistry;
}
public MaterialAsset LoadAsset(string path)
{
var materialData = LoadMaterialData(path);
return new MaterialAsset
{
DiffuseTexture = _assetRegistry.LoadAsset<Texture2D>(materialData.DiffusePath),
NormalTexture = _assetRegistry.LoadAsset<Texture2D>(materialData.NormalPath),
SpecularTexture = _assetRegistry.LoadAsset<Texture2D>(materialData.SpecularPath),
Tint = materialData.Tint
};
}
}
Asset Hot-Reloading
Enable hot-reloading for development:
public class DevelopmentAssetRegistry : IAssetRegistry
{
private readonly FileSystemWatcher _watcher;
private readonly Dictionary<string, object> _loadedAssets;
public DevelopmentAssetRegistry()
{
_loadedAssets = new Dictionary<string, object>();
if (IsDevelopmentMode())
{
SetupFileWatcher();
}
}
private void SetupFileWatcher()
{
_watcher = new FileSystemWatcher("assets/")
{
IncludeSubdirectories = true,
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName
};
_watcher.Changed += OnAssetChanged;
_watcher.Created += OnAssetChanged;
_watcher.EnableRaisingEvents = true;
}
private void OnAssetChanged(object sender, FileSystemEventArgs e)
{
var assetPath = Path.GetRelativePath("assets/", e.FullPath);
if (_loadedAssets.ContainsKey(assetPath))
{
// Reload the asset
_loadedAssets.Remove(assetPath);
LoadAsset<object>(assetPath); // Reload based on original type
// Notify subscribers
AssetReloaded?.Invoke(assetPath);
}
}
public event Action<string> AssetReloaded;
}
Memory Management
Asset Disposal
Properly manage memory by unloading unused assets:
public class LevelManager
{
private readonly IAssetRegistry _assetRegistry;
private readonly List<string> _currentLevelAssets;
public void LoadLevel(string levelName)
{
// Unload previous level assets
UnloadCurrentLevelAssets();
// Load new level assets
_currentLevelAssets.Clear();
LoadLevelAssets(levelName);
}
private void UnloadCurrentLevelAssets()
{
foreach (var assetPath in _currentLevelAssets)
{
_assetRegistry.UnloadAsset(assetPath);
}
}
private void LoadLevelAssets(string levelName)
{
var levelAssets = GetRequiredAssetsForLevel(levelName);
foreach (var assetPath in levelAssets)
{
_assetRegistry.LoadAsset<object>(assetPath);
_currentLevelAssets.Add(assetPath);
}
}
}
Asset Streaming
Implement asset streaming for large games:
public class StreamingAssetManager
{
private readonly IAssetRegistry _assetRegistry;
private readonly Queue<string> _streamingQueue;
private readonly int _maxConcurrentLoads;
public StreamingAssetManager(IAssetRegistry assetRegistry)
{
_assetRegistry = assetRegistry;
_streamingQueue = new Queue<string>();
_maxConcurrentLoads = 3;
}
public void RequestAsset(string assetPath, Action<object> onLoaded)
{
if (_assetRegistry.IsAssetLoaded(assetPath))
{
onLoaded?.Invoke(_assetRegistry.GetAsset<object>(assetPath));
return;
}
_streamingQueue.Enqueue(assetPath);
ProcessStreamingQueue();
}
private async void ProcessStreamingQueue()
{
var activeTasks = new List<Task>();
while (_streamingQueue.Count > 0 && activeTasks.Count < _maxConcurrentLoads)
{
var assetPath = _streamingQueue.Dequeue();
var task = Task.Run(() => LoadAssetAsync(assetPath));
activeTasks.Add(task);
}
await Task.WhenAll(activeTasks);
}
private async Task LoadAssetAsync(string assetPath)
{
// Perform asset loading on background thread
await Task.Run(() =>
{
_assetRegistry.LoadAsset<object>(assetPath);
});
}
}
Performance Optimization
Asset Bundling
Group related assets for efficient loading:
public class AssetBundle
{
public string Name { get; set; }
public List<string> Assets { get; set; }
}
public class AssetBundleManager
{
private readonly IAssetRegistry _assetRegistry;
public void LoadBundle(AssetBundle bundle)
{
// Load all assets in the bundle
_assetRegistry.PreloadAssets(bundle.Assets.ToArray());
}
public void UnloadBundle(AssetBundle bundle)
{
// Unload all assets in the bundle
foreach (var assetPath in bundle.Assets)
{
_assetRegistry.UnloadAsset(assetPath);
}
}
}
// Usage
var uiBundle = new AssetBundle
{
Name = "UI",
Assets = new List<string>
{
"textures/ui/button.png",
"textures/ui/panel.png",
"textures/ui/slider.png",
"audio/ui/click.wav"
}
};
bundleManager.LoadBundle(uiBundle);
Asset Variants
Handle different quality levels and platforms:
public class VariantAssetRegistry : IAssetRegistry
{
private readonly string _platform;
private readonly QualityLevel _quality;
public VariantAssetRegistry()
{
_platform = DetectPlatform();
_quality = GetQualitySettings();
}
public T LoadAsset<T>(string assetPath) where T : class
{
// Try platform-specific version first
var platformPath = GetPlatformVariant(assetPath);
if (File.Exists(platformPath))
{
return LoadAssetInternal<T>(platformPath);
}
// Try quality-specific version
var qualityPath = GetQualityVariant(assetPath);
if (File.Exists(qualityPath))
{
return LoadAssetInternal<T>(qualityPath);
}
// Fall back to original asset
return LoadAssetInternal<T>(assetPath);
}
private string GetPlatformVariant(string assetPath)
{
var directory = Path.GetDirectoryName(assetPath);
var fileName = Path.GetFileNameWithoutExtension(assetPath);
var extension = Path.GetExtension(assetPath);
return Path.Combine(directory, $"{fileName}_{_platform}{extension}");
}
private string GetQualityVariant(string assetPath)
{
var directory = Path.GetDirectoryName(assetPath);
var fileName = Path.GetFileNameWithoutExtension(assetPath);
var extension = Path.GetExtension(assetPath);
return Path.Combine(directory, $"{fileName}_{_quality.ToString().ToLower()}{extension}");
}
}
Best Practices
Asset Organization
- Consistent structure: Use a clear, consistent directory hierarchy
- Meaningful names: Use descriptive names that indicate content and purpose
- Size variants: Include multiple resolutions for different screen sizes
- Format optimization: Choose appropriate formats for each asset type
Performance
- Preload critical assets: Load essential assets at startup
- Lazy loading: Load non-critical assets on-demand
- Memory management: Unload unused assets to free memory
- Asset streaming: Stream large assets for better performance
Development
- Hot-reloading: Enable asset hot-reloading during development
- Asset validation: Validate asset integrity and dependencies
- Error handling: Handle missing or corrupted assets gracefully
- Debugging tools: Provide tools to inspect loaded assets
Example: Complete Asset Management
public class GameAssetManager
{
private readonly IAssetRegistry _assetRegistry;
private readonly Dictionary<string, List<string>> _sceneBundles;
public GameAssetManager()
{
_assetRegistry = GetService<IAssetRegistry>();
_sceneBundles = new Dictionary<string, List<string>>();
InitializeAssetBundles();
PreloadCommonAssets();
}
private void InitializeAssetBundles()
{
_sceneBundles["MainMenu"] = new List<string>
{
"textures/ui/menu_background.png",
"textures/ui/logo.png",
"audio/music/menu_theme.ogg",
"fonts/menu_font.ttf"
};
_sceneBundles["Gameplay"] = new List<string>
{
"textures/characters/player.png",
"textures/environments/level1_bg.png",
"audio/sfx/jump.wav",
"audio/sfx/collect.wav"
};
}
private void PreloadCommonAssets()
{
var commonAssets = new[]
{
"textures/ui/pixel.png",
"fonts/default_font.ttf",
"audio/ui/click.wav"
};
_assetRegistry.PreloadAssets(commonAssets);
}
public void LoadSceneAssets(string sceneName)
{
if (_sceneBundles.ContainsKey(sceneName))
{
var assets = _sceneBundles[sceneName].ToArray();
_assetRegistry.PreloadAssets(assets);
}
}
public void UnloadSceneAssets(string sceneName)
{
if (_sceneBundles.ContainsKey(sceneName))
{
foreach (var assetPath in _sceneBundles[sceneName])
{
_assetRegistry.UnloadAsset(assetPath);
}
}
}
public T GetAsset<T>(string assetPath) where T : class
{
return _assetRegistry.GetAsset<T>(assetPath);
}
}
The Asset Registry provides a powerful and flexible system for managing all your game assets efficiently and effectively.