There is going to be a point in the development of many projects for iOS and Android that you might decide your system for loading content at runtime needs a major upgrade. Maybe you’re targeting a 50 MB size for your app so people can download it over a cellular connection. Perhaps your engineers are getting frustrated tracking what content needs to be loaded with what file loading system. You might decide you want to be able to replace content without pushing a full app update. To solve these issues, I recommend writing a custom resource loading system.
A custom resource loading system is going to have multiple parts, working together to give your team a focused pipeline for loading and displaying content at runtime. The solution I recommend would exist as follows. In the middle of the system, the function game code would call should be as simple as “CustomAssetLoader.LoadAssetAsync(assetID)”. First the custom asset loader would check if the asset was already loaded by checking refcounts it keeps track of, and if so it could return a reference to the loaded asset. If the asset was not already loaded, the custom asset loader would then internally have a list of individual asset loaders it could use to load an asset. The custom asset loader would use a hinting system to decide which asset loader to use for this particular asset. These individual asset loaders would implement different Unity file loading methods, Resources.Load, AssetBundles, the WWW class. Even though not all resource loading behavior of Unity supports asynchronous loading, I recommend you write your code as if everything is asynchronous, keeping it so all code that interacts with this system can assume all assets are asynchronously loaded will force strong development patterns.
Deciding what to use for your hinting system for your custom asset loader can be a bit tricky. I recommend abstracting your asset loading system so game code does not know or care about file paths, and instead uses a asset identification system. This would allow the custom asset loader to take in that asset identification, and check the asset database for the path to load. You could then build other useful information for the asset loading system into this, such as an identifier of which file loading system to use for that asset. You can also include dependency information in your asset database, which becomes very useful if you have a texture you want to make sure is always loaded when a certain model is loaded. While building this, you need to be very conscious of the pipeline for adding and changing assets, and try and provide as much automation as you can. For asset bundles, you can automate this as a step in your asset bundle build process, have the asset bundles add their manifest of assets to the asset identification system, as well as marking those assets as included in asset bundles, and which bundles they are in. For Resources.Load, you can write a function that scans through all Resources folders in your project, and adds them to the asset identification system. This function can then be called as a step in the build process. For assets kept on a server, you might add functionality into the system that you use to put those assets on the server to also update the asset database.
With a smart asset manifest system, implementing the custom asset loader becomes much simpler. At run-time, the location of each asset is stored in the asset database, so the custom asset loader’s “LoadAssetAsync” function has a few simple steps. First, check if the asset is already loaded and cached, and if so, just return the cached asset. Second, access the asset database, and check what individual asset loader to use to load that asset. Finally, call that individual asset loader with the information it needs to load the asset. This system is also modular, and you can begin with support for just one custom asset loader, such as Resources.Load. At this point you can upgrade all your game code to work around this system, and then will not have to make any major changes as new asset loading systems come online.
The other big benefit of this system is running the game within the Unity editor. A very common problem with trying to build an asset loading solution, especially for asset bundles, is testing in editor can become very frustrating. Generally, your team will want to be able to iterate quickly in editor, and if they have to wait on asset bundles to build every time they make a change to an asset, you will have a very frustrated team. If your custom asset loader checks if you are running Unity in the editor, then you can have it fall back to loading content outside of asset bundles by using Resources.LoadAssetAtPath, a function that only works when running the game in the Unity Editor.
The next step, after you have a function that can load in a single asset asynchronously, is upgrading your custom asset loader to handle groups of assets. Often times, game code will know it needs to load a bunch of assets at once, such as the texture, model, and animation for a character, and the person writing this game code just wants to pass off this list of assets to the asset loading system to load everything at once. The simplest way to implement this is a coroutine that does not consider itself finished until it has loaded every asset in the list.
A custom asset loader also needs to be able to handle asset loading failure gracefully. Maybe the user has no internet connection and your game is trying to download an asset. Maybe the server is taking a long time to respond, or download speeds are extremely slow. You will probably want to build a timeout into the custom asset loader to bail if an asset fails to load. If you are using the asset fallback system I will describe next, then your game code should not even need to care if an asset fails to load, as it will just display one of the fallbacks. If you are using any sort of event reporting system, such as Flurry, for tracking user behavior, then this is a great spot for an event, you can track what assets fail to load, and why.
Dependencies are very tricky in Unity. Different back end file loading systems for Unity handle the automatic loading of dependencies very differently, and if you have dependencies across systems, by default Unity does not do any system-wide reference tracking, and you can easily accidently load the same asset multiple times, if it had a dependency from an asset bundle, as well as from a Resources asset. I recommend handling dependencies by forcing them through your asset database, try and keep your in-game assets as free of dependencies as possible. These dependencies are things such as any assets referenced or used by a prefab, or the texture and material associated with a model. I think the cleanest way to handle this is to make your game as factory driven as possible, using your asset database and other in-game databases to construct content made of multiple assets at runtime. This is not always possible, and if it is not, you might need to define clean lines, and treat all content loaded with a prefab as a single content chunk, that prefab. If another asset uses the same texture as the prefab, then you might end up just loading that texture twice to avoid dealing with dependencies across different resource loading systems.
This brings up the concept of unloading assets. The way Unity handles dependencies, and unloading content is handled very differently based on how it is loaded. To explain this, think of the basic dependency layer of a model file with an associated material and texture that are loaded with it. With Resources.UnloadAsset, Unity will automatically reload that asset if there is still a loaded dependency. This means if you try and unload that texture without unloading the model file, your game code might assume the texture is unloaded, whereas Unity will reload it. With AssetBundles, this is not the case, and once you unload an asset Unity will not reload it if there is a dependency on it. Unity provides some useful editor functionality for checking asset dependencies, such as EditorUtility.CollectDependencies. You can use these functions to figure out asset dependencies at build time, and use that information at runtime to give your custom asset loader information on dependencies that might exist.
To really give this system a punch, I recommend building a asset fallback system that calls into the custom asset loader. The asset fallback system would take in a core asset ID, and then check a database for a list of fallback assets. This list of fallback assets would be organized so the asset at the front of the list is the desired final asset, and as you near the back of the list you have more generic fallback assets. An example of this for a builder game might be a desired model for a high resolution, specific level of a building, which is normally stored on a server and downloaded when needed. The fallback from there is the default first level of that building, which was included in the initial iOS app download, and stored locally on device. The final fallback is a generic swirling cloud graphic used as a fallback everywhere, that is generally always loaded. This fallback system would start the initial load, requesting all three of those assets to load asynchronously. The swirling cloud graphic is always loaded, and the fallback system would set the viewable, in-game art to the swirling cloud. The default first level of the building would be a relatively quick load, and the fallback system would swap that for the visible art once it finished loading. The first time a request to display this building, the high resolution model would not be downloaded yet, and the default first level building will be displayed a while, until the high resolution building finishes downloading. Once that finishes downloading and is loaded into memory, then the fallback system would swap that in as the visible asset, and the fallback system would be finished for this asset. On subsequent calls to this asset, the high resolution building will be cached locally on device, and will be displayable much more quickly.
Ideally, your user will never see these asset fallbacks. You want your game to start loading assets ahead of time, before they are needed. You can accomplish this by starting asset loads before the asset is immediately needed. An example of this might be the menu flow for a shop for your game. The first menu of the shop probably provides the players some options of what type of thing to shop for, maybe a submenu for weapons, armor, and consumables. Or for a builder type game, maybe resource structures, combat structures, and decorative structures. When the player is at the front end shop menu, the game can begin loading items for sale within the submenus on the next menu. This can even be hinted at based on what the player was doing previously, and the player’s current game state. If the player has recently suffered an attack, they might be headed to the defensive structure menu, and the game can start loading those first, hopefully to have them loaded by the time the player gets there. If the player just leveled up their town and has new buildings available, then they are probably looking for those in the shop, so the game should start loading the new buildings. If there is a lot of content on the server for the game, then whenever possible, the game should be asynchronously loading this data, and caching it locally on device. The priority of these downloads should be based on how soon the content might need to be available. There is no reason to load end-game content if the player is only a few hours into your game, unless everything else is already downloaded and cached on device. The background loading code should prioritize content to be downloaded by how soon the player will need that content for gameplay. For loading content into memory, you have much less room to work with, and will want to keep content loaded that is immediately needed, or may be needed soon. If you have a more traditional gameplay flow, maybe the user selects a level to play, selects some settings for that level such as equipment, and then enters the level, the game can begin loading content for that level when the player is on the equipment select screen. The longer the player takes to set themselves up, the more content that will be loaded and ready to go by the time the player starts the level.
With these systems in place, a custom asset loader, an asset fallback system, and a background downloading and loading system, you will have very fine tuned control over your gameplay experience, and can really focus on things like reducing time spent on loading screens with no player interaction. You will also have separated game code from asset loading code well enough that you can move assets around between asset bundles, an online server, and the Resources folder on device without needing to do any major adjustments to your game code.