Tech Blog 1 - Dynamic Content Loading
·Dynamic content loading is an important topic for me. I want to be able to release small updates and add content without users needing to reinstall the app. I had this in mind from the very beginning, but little did I know that I had implemented it incorrectly.
I approached it using Addressables from the very beginning, but I failed to research well enough what Addressables were and how they work in Unity. Addressables in our architecture are buried inside the Persistence layer. Below is a simplified version of the Local adapter:
public class LocalStorageStrategySo : StorageStrategy
{
[SerializeField] private AbilityRepositorySo abilityRepositorySo;
public AbilityRepositorySo GetAbilityRepository()
{
return abilityRepositorySo;
}
}
public class AbilityRepositorySo : ScriptableObject, IAbilityRepository
{
public List<AbilitySo> abilities;
public List<AbilitySo> GetAbilities()
{
return abilities;
}
}
public class AbilitySo : ScriptableObject
{
[Multiline(10)] public string description;
public AssetReferenceSprite icon;
public string name;
}
The idea was that later I can introduce an Http adapter like the one below, pseudo-code:
public class HttpStorageStrategySo : StorageStrategy
{
[SerializeField] private AbilityRepositorySo abilityRepositorySo;
public AbilityRepositorySo GetAbilityRepository()
{
return abilityRepositorySo;
}
}
public class AbilityRepositorySo : ScriptableObject, IAbilityRepository
{
public List GetAbilities()
{
// download all ability related assets and store on user device
// download all AbilitySo.asset files and deserialize them
var abilities;
return abilities;
}
}
I thought would obviously have no problem with deserializing AbilitySo because it uses AssetReferenceSprite instead of Sprite.
I wanted to test this assumption before moving forward, so I bootstrapped a simple nodejs server and found out that serialisation / deserialization was not not that easy. In fact, it was so not easy, that I thought I must be doing something wrong.
Turns out Addressables work much much easier and Unity handles all the adapter staff as well. All I ended up doing was marking AbilityRepositorySo itself as Addressable and referring to it as AssetReference loaded on demand like so:
public class ContentManager: ScriptableObject
{
[SerializeField] private AssetReference abilityRepository;
private async Task GetAbilityRepository()
{
var handle = abilityRepository.LoadAssetAsync();
await handle.Task;
return handle.Result;
}
}
When the game loads on your device, AbilityRepositorySo is loaded from the cloud. Unity Addressables handle all serialisation, deserialization, and adapter concerns automatically. Done deal.
Since now content can be accessed directly, and it didn't belong in Persistence purely anyway, architecture was changed as follows:
It’s a game changer in terms of simplicity. Because now I can refer to content directly from any layer without fearing that doing so would prevent me from loading them dynamically afterwards. I simplified the code base by doing so a lot, and avoided mapping models from layer to layer for matters related to any content in the game that I want to be able to add without users updating the game on their device, plus Persistence now really handles just persisting player state and nothing more.
Bottleneck is still that when I persist content I refer to content by their unique ID’s on a persistence layer, and on Engine layer I just map IDs with actual content. So if ID’s accidentally change then loading previous saves may no longer be possible. This can be handled, but I am not sure if this can be avoided altogether yet.