tools-unity-addressables
SKILL.md
Unity Addressables
Overview
Addressables provides a system for loading assets by address, with automatic dependency management, memory tracking, and remote content delivery support.
When to Use
- Loading assets dynamically at runtime
- Managing asset memory lifecycle
- Implementing remote asset downloads
- Reducing build size with asset bundles
- Loading scenes asynchronously
Core Concepts
Asset References
// In inspector - reference to addressable asset
[SerializeField] private AssetReference _prefabReference;
[SerializeField] private AssetReferenceT<AudioClip> _audioReference;
[SerializeField] private AssetReferenceSprite _spriteReference;
[SerializeField] private AssetReferenceGameObject _gameObjectReference;
Loading by Address (String)
// Load by address string
var handle = Addressables.LoadAssetAsync<GameObject>("Prefabs/Enemy");
var prefab = await handle.Task;
// Or with UniTask
var prefab = await Addressables.LoadAssetAsync<GameObject>("Prefabs/Enemy").ToUniTask();
Loading by AssetReference
// Load from serialized reference
var handle = _prefabReference.LoadAssetAsync<GameObject>();
var prefab = await handle.ToUniTask();
Loading Patterns
Basic Asset Loading
public class AssetLoader
{
private AsyncOperationHandle<GameObject> _handle;
public async UniTask<GameObject> LoadAsync(string address, CancellationToken ct)
{
_handle = Addressables.LoadAssetAsync<GameObject>(address);
try
{
return await _handle.ToUniTask(cancellationToken: ct);
}
catch (OperationCanceledException)
{
// Release on cancellation
if (_handle.IsValid())
Addressables.Release(_handle);
throw;
}
}
public void Unload()
{
if (_handle.IsValid())
{
Addressables.Release(_handle);
_handle = default;
}
}
}
Instantiate with Auto-Release
// Instantiate and track for automatic release
var handle = Addressables.InstantiateAsync(address, parent);
var instance = await handle.ToUniTask();
// Later: Release when done (also destroys GameObject)
Addressables.ReleaseInstance(instance);
// OR release handle directly
Addressables.Release(handle);
Load Multiple Assets
public async UniTask<IList<T>> LoadAllAsync<T>(string label, CancellationToken ct)
{
var handle = Addressables.LoadAssetsAsync<T>(
label,
obj => Debug.Log($"Loaded: {obj}") // Per-asset callback
);
return await handle.ToUniTask(cancellationToken: ct);
}
// Usage
var allEnemies = await LoadAllAsync<GameObject>("Enemies", ct);
Load with Dependencies
public async UniTask<T> LoadWithDependenciesAsync<T>(string address, CancellationToken ct)
{
// Get download size first
var sizeHandle = Addressables.GetDownloadSizeAsync(address);
var size = await sizeHandle.ToUniTask();
if (size > 0)
{
// Download dependencies
var downloadHandle = Addressables.DownloadDependenciesAsync(address);
await downloadHandle.ToUniTask(cancellationToken: ct);
Addressables.Release(downloadHandle);
}
// Load asset
var loadHandle = Addressables.LoadAssetAsync<T>(address);
return await loadHandle.ToUniTask(cancellationToken: ct);
}
Reference Counting
Understanding Reference Counts
// Each Load increments ref count
var handle1 = Addressables.LoadAssetAsync<T>(address); // RefCount = 1
var handle2 = Addressables.LoadAssetAsync<T>(address); // RefCount = 2 (same asset)
// Each Release decrements
Addressables.Release(handle1); // RefCount = 1
Addressables.Release(handle2); // RefCount = 0 (asset unloaded)
Safe Resource Loader Pattern
public class ResourceLoader : IDisposable
{
private readonly Dictionary<string, AsyncOperationHandle> _loadedAssets = new();
private readonly object _lock = new();
public async UniTask<T> LoadAssetAsync<T>(string key, CancellationToken ct = default)
{
lock (_lock)
{
if (_loadedAssets.TryGetValue(key, out var existingHandle))
{
return (T)existingHandle.Result;
}
}
var handle = Addressables.LoadAssetAsync<T>(key);
try
{
var result = await handle.ToUniTask(cancellationToken: ct);
lock (_lock)
{
_loadedAssets[key] = handle;
}
return result;
}
catch
{
if (handle.IsValid())
Addressables.Release(handle);
throw;
}
}
public async UniTask ReleaseAll(bool immediate = false)
{
List<AsyncOperationHandle> handles;
lock (_lock)
{
handles = _loadedAssets.Values.ToList();
_loadedAssets.Clear();
}
foreach (var handle in handles)
{
if (handle.IsValid())
Addressables.Release(handle);
}
if (!immediate)
await UniTask.Yield(); // Allow cleanup
}
public void Dispose()
{
ReleaseAll(true).Forget();
}
}
Scene Loading
Load Scene Additively
public async UniTask<SceneInstance> LoadSceneAsync(
string sceneAddress,
LoadSceneMode mode = LoadSceneMode.Additive,
CancellationToken ct = default)
{
var handle = Addressables.LoadSceneAsync(sceneAddress, mode);
return await handle.ToUniTask(cancellationToken: ct);
}
// Unload scene
public async UniTask UnloadSceneAsync(SceneInstance scene)
{
var handle = Addressables.UnloadSceneAsync(scene);
await handle.ToUniTask();
}
Scene with Activation Control
public async UniTask<SceneInstance> LoadSceneWithDelayedActivation(string address)
{
var handle = Addressables.LoadSceneAsync(
address,
LoadSceneMode.Additive,
activateOnLoad: false // Don't activate immediately
);
var scene = await handle.ToUniTask();
// Do preparation work...
await PrepareSceneAsync();
// Now activate
await scene.ActivateAsync().ToUniTask();
return scene;
}
Remote Content Delivery
Check for Updates
public async UniTask<bool> CheckForUpdatesAsync()
{
var checkHandle = Addressables.CheckForCatalogUpdates();
var catalogs = await checkHandle.ToUniTask();
Addressables.Release(checkHandle);
return catalogs != null && catalogs.Count > 0;
}
Update Catalogs
public async UniTask UpdateCatalogsAsync(IProgress<float> progress = null)
{
var checkHandle = Addressables.CheckForCatalogUpdates();
var catalogs = await checkHandle.ToUniTask();
if (catalogs != null && catalogs.Count > 0)
{
var updateHandle = Addressables.UpdateCatalogs(catalogs);
await updateHandle.ToUniTask();
Addressables.Release(updateHandle);
}
Addressables.Release(checkHandle);
}
Download Content
public async UniTask DownloadContentAsync(
string label,
IProgress<float> progress,
CancellationToken ct)
{
// Get download size
var sizeHandle = Addressables.GetDownloadSizeAsync(label);
var totalSize = await sizeHandle.ToUniTask();
Addressables.Release(sizeHandle);
if (totalSize <= 0)
{
Debug.Log("All content already downloaded");
return;
}
Debug.Log($"Downloading {totalSize / 1_000_000f:F2} MB");
// Download
var downloadHandle = Addressables.DownloadDependenciesAsync(label);
// Report progress
while (!downloadHandle.IsDone)
{
progress?.Report(downloadHandle.PercentComplete);
if (ct.IsCancellationRequested)
{
Addressables.Release(downloadHandle);
throw new OperationCanceledException(ct);
}
await UniTask.Yield();
}
if (downloadHandle.Status != AsyncOperationStatus.Succeeded)
{
var error = downloadHandle.OperationException?.Message ?? "Unknown error";
Addressables.Release(downloadHandle);
throw new Exception($"Download failed: {error}");
}
Addressables.Release(downloadHandle);
}
Clear Cache
public async UniTask ClearCacheAsync()
{
// Clear all cached bundles
Caching.ClearCache();
// Re-initialize Addressables
await Addressables.InitializeAsync().ToUniTask();
}
public bool ClearCacheForKey(string key)
{
var locator = Addressables.ResourceLocators.FirstOrDefault();
if (locator != null && locator.Locate(key, typeof(object), out var locations))
{
foreach (var location in locations)
{
Caching.ClearAllCachedVersions(location.InternalId);
}
return true;
}
return false;
}
Progress Reporting
Download Progress
public struct DownloadProgress
{
public long TotalBytes;
public long DownloadedBytes;
public float Percent;
public string CurrentFile;
}
public async UniTask DownloadWithDetailedProgress(
string label,
IProgress<DownloadProgress> progress,
CancellationToken ct)
{
var sizeHandle = Addressables.GetDownloadSizeAsync(label);
var totalBytes = await sizeHandle.ToUniTask();
Addressables.Release(sizeHandle);
var downloadHandle = Addressables.DownloadDependenciesAsync(label);
while (!downloadHandle.IsDone)
{
ct.ThrowIfCancellationRequested();
var status = downloadHandle.GetDownloadStatus();
progress?.Report(new DownloadProgress
{
TotalBytes = totalBytes,
DownloadedBytes = status.DownloadedBytes,
Percent = status.Percent,
CurrentFile = status.DownloadedBytes.ToString()
});
await UniTask.Yield();
}
Addressables.Release(downloadHandle);
}
Memory Management
Preloading Assets
public class AssetPreloader
{
private readonly List<AsyncOperationHandle> _preloadedHandles = new();
public async UniTask PreloadAsync(IEnumerable<string> addresses, CancellationToken ct)
{
var tasks = addresses.Select(async addr =>
{
var handle = Addressables.LoadAssetAsync<object>(addr);
await handle.ToUniTask(cancellationToken: ct);
_preloadedHandles.Add(handle);
});
await UniTask.WhenAll(tasks);
}
public void ReleaseAll()
{
foreach (var handle in _preloadedHandles)
{
if (handle.IsValid())
Addressables.Release(handle);
}
_preloadedHandles.Clear();
}
}
Memory Budget Tracking
public class AddressableMemoryTracker
{
public long GetEstimatedMemoryUsage()
{
long total = 0;
// This is approximate - actual tracking requires custom implementation
foreach (var locator in Addressables.ResourceLocators)
{
// Track loaded bundles
}
return total;
}
public void LogLoadedAssets()
{
// Use Addressables Event Viewer in Editor
// For runtime, implement custom tracking
}
}
Error Handling
Robust Loading Pattern
public async UniTask<T> LoadWithRetryAsync<T>(
string address,
int maxRetries = 3,
CancellationToken ct = default)
{
Exception lastException = null;
for (int attempt = 0; attempt < maxRetries; attempt++)
{
try
{
var handle = Addressables.LoadAssetAsync<T>(address);
return await handle.ToUniTask(cancellationToken: ct);
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
lastException = ex;
Debug.LogWarning($"Load attempt {attempt + 1} failed: {ex.Message}");
if (attempt < maxRetries - 1)
{
await UniTask.Delay(1000 * (attempt + 1), cancellationToken: ct);
}
}
}
throw new Exception($"Failed to load {address} after {maxRetries} attempts", lastException);
}
Handle Invalid Operations
public void SafeRelease<T>(ref AsyncOperationHandle<T> handle)
{
if (handle.IsValid())
{
try
{
Addressables.Release(handle);
}
catch (Exception ex)
{
Debug.LogWarning($"Error releasing handle: {ex.Message}");
}
finally
{
handle = default;
}
}
}
Best Practices
- Always release loaded assets when done
- Use labels for grouping related assets
- Preload critical assets during loading screens
- Check download size before downloading
- Handle cancellation properly
- Use typed AssetReferences in inspector
- Profile memory with Addressables Event Viewer
- Test remote loading early in development
- Implement retry logic for network operations
- Clear cache when updating content significantly
Troubleshooting
| Issue | Solution |
|---|---|
| Asset not found | Check address spelling, rebuild addressables |
| Memory leak | Ensure all handles are released |
| Slow loading | Use preloading, check bundle sizes |
| Download fails | Check network, implement retry |
| Duplicate loading | Track loaded assets, use singleton loader |
| Cache issues | Clear cache, check catalog version |
Weekly Installs
1
Repository
tjboudreaux/cc-…-gamedevGitHub Stars
1
First Seen
2 days ago
Security Audits
Installed on
amp1
cline1
opencode1
cursor1
kimi-cli1
codex1