Load localized content from a place the built-in sources can’t reach.
This example serves localized tables from an AssetBundle in StreamingAssets, so localized content can ship and update separately from the Player. For example, you can use this structure to create downloadable content or mods for your application.
A custom source has two halves. The runtime provider loads by address, and a companion class in the Unity Editor records what the provider serves when you assign a collection to it. For details on how sources resolve requests, refer to Content sources.
Before you start, make sure you have the following:
The runtime side implements IAssetProvider plus the loading capabilities it supports: ISynchronousAssetProvider, IAsyncAssetProvider, or both. Requests arrive as an AssetKey that carries an address and an expected type. The provider answers the addresses it recognizes and declines the rest, so the next source in the chain can serve them.
[Serializable]
public class AssetBundleAssetProvider : IAssetProvider, IAsyncAssetProvider, ISynchronousAssetProvider
{
[SerializeField] string m_BundleName = "localization";
// The addresses this provider serves. The editor half fills the list when a collection is assigned.
[SerializeField, HideInInspector] List<string> m_Addresses = new();
AssetBundle m_Bundle;
AssetBundle Bundle
{
get
{
if (m_Bundle == null)
m_Bundle = AssetBundle.LoadFromFile(Path.Combine(Application.streamingAssetsPath, m_BundleName));
return m_Bundle;
}
}
public void AddAddress(string address)
{
if (!string.IsNullOrEmpty(address) && !m_Addresses.Contains(address))
m_Addresses.Add(address);
}
public void RemoveAddress(string address) => m_Addresses.Remove(address);
public void ClearAddresses() => m_Addresses.Clear();
public bool TryLoadAsset<T>(AssetKey key, out T asset) where T : Object
{
asset = m_Addresses.Contains(key.Address) && Bundle != null
? Bundle.LoadAsset(key.Address, key.Type) as T
: null;
return asset != null;
}
public async Awaitable<T> LoadAssetAsync<T>(AssetKey key, CancellationToken cancellationToken) where T : Object
{
if (!m_Addresses.Contains(key.Address) || Bundle == null)
return null;
var request = Bundle.LoadAssetAsync(key.Address, key.Type);
await request;
cancellationToken.ThrowIfCancellationRequested();
return request.asset as T;
}
public void Release(Object asset)
{
// Assets unload with the bundle, so there is nothing to release per asset.
}
}
When you assign a collection to a source, the Editor asks the source’s companion to register the collection’s content. This companion records each table’s address on the provider. The addresses follow the {collectionName}_{localeCode} convention the base class provides.
[AssetProviderEditor(typeof(AssetBundleAssetProvider))]
public class AssetBundleAssetProviderEditor : AssetProviderEditor
{
protected override void RegisterTable(IAssetProvider provider, ResourceTableCollection collection, ResourceTable table)
{
// Record the table's address so the runtime serves it from the bundle.
// The bundle must contain the table asset under the same name.
((AssetBundleAssetProvider)provider).AddAddress(TableAddress(collection, table));
}
protected override void UnregisterTable(IAssetProvider provider, ResourceTableCollection collection, ResourceTable table)
{
((AssetBundleAssetProvider)provider).RemoveAddress(TableAddress(collection, table));
}
public override void ClearRegistrations(IAssetProvider provider)
{
((AssetBundleAssetProvider)provider).ClearAddresses();
}
}
The provider expects the bundle to contain the collection’s table assets, resolvable by the same names it registered. Assign the collection’s table assets to an AssetBundle named localization, then build the AssetBundle into StreamingAssets. Refer to AssetBundles.
To serve a collection from your provider: