In an average-sized XNA game, you’ll end up having many levels using many art assets, with most of them sharing textures and models between each other. Using the standard ContentManager class, the basic approach is to load all of a level’s assets into a single ContentManager, and unload it when switching levels : this way there is no possible memory leak and memory usage is kept to a minimum.
But what about load times? Users usually want level transitions to be as seamless as possible, yet we can’t just pre-load everything, you gotta watch the memory budget…
Sharing is caring
One solution is to preserve shared assets : an asset that is loaded for Level #1 and re-used in Level #2 can be kept in memory instead of being destroyed and reloaded. Memory-wise it’s costless because you were about to reload it anyway; keeping it for a longer time has no negative effect.
A simple way to keep track of shared assets is to use reference counting : increment a counter whenever you ask to load an asset, and flush assets that have 0 references when you unload. But even the almighty Shawn Hargreaves thinks it’s a bad idea…
[…] reference counting sucks for all sorts of reasons I can’t be bothered to go into here. It is better than nothing, but falls short of the automatic, rapid development approach .NET developers have rightly come to expect.
Fair enough, but how about making asset disposal transparent by using the same ContentManager containers with the same public interface, yet use reference counting in the background?
I tried doing exactly that, and had great success with it, so I suggest you take a look at the code below and give it a shot!
public class SharedContentManager : ContentManager { static CommonContentManager Common; List<string> loadedAssets; public SharedContentManager(IServiceProvider serviceProvider, string rootDirectory) : base(serviceProvider, rootDirectory) { EnsureSharedInitialized(); loadedAssets = new List<string>(); } static void EnsureSharedInitialized() { if (Common == null) Common = new CommonContentManager(ServiceProvider, RootDirectory); } // This is ripped straight off the ContentManager disassembled source... // Wouldn't have to do that if it were protected! :) internal static string GetCleanPath(string path) { // Ugly, boring code that you'll get if you download the codefile } public override T Load<T>(string assetName) { assetName = GetCleanPath(assetName); loadedAssets.Add(assetName); return Common.Load<T>(assetName); } public override void Unload() { if (loadedAssets == null) throw new ObjectDisposedException(typeof(SharedContentManager).Name); Common.Unload(this); loadedAssets = null; base.Unload(); } class CommonContentManager : ContentManager { readonly Dictionary<string, ReferencedAsset> references; public CommonContentManager(IServiceProvider serviceProvider, string rootDirectory) : base(serviceProvider, rootDirectory) { references = new Dictionary<string, ReferencedAsset>(); } public override T Load<T>(string assetName) { assetName = GetCleanPath(assetName); ReferencedAsset refAsset; if (!references.TryGetValue(assetName, out refAsset)) { refAsset = new ReferencedAsset { Asset = ReadAsset<T>(assetName, null) }; references.Add(assetName, refAsset); } refAsset.References++; return (T) refAsset.Asset; } public void Unload(SharedContentManager container) { foreach (var assetName in container.loadedAssets) { var refAsset = references[assetName]; refAsset.References--; if (refAsset.References == 0) { if (refAsset.Asset is IDisposable) (refAsset.Asset as IDisposable).Dispose(); references.Remove(assetName); } } } class ReferencedAsset { public object Asset; public int References; } } }
Notes
By design, the class assumes that all your content managers will have the same root path and use the same service provider. This version uses the constructor parameters of the first instance for all subsequent instances. It’s kind of redundant to pass those parameters everytime since they aren’t used after the first instance has been created, you can probably simplify and optimize that part (I did otherwise in my project but it’s tied to my engine code).
Content loading is not thread-safe with this method. The version I use in my project again uses a different way to initialize the common content manager and monitors, but I thought it made the implementation too heavy for demonstration… this too would need work if you use threaded loading.
It works if you use forward slashes for paths because of the GetCleanPath method. But fun fact, it treats paths and filenames as case-sensitive so it will reload assets if you change the case between loadings! So be careful with that, or fix it. :P
Usage
Here’s the procedure for level transitions :
// Create a content manager for the next level var nextLevelCM = new SharedContentManager(Game.Services, Game.Content.RootDirectory); // Load the content for this next level var fooTexture = nextLevelCM.Load<Texture>("foo"); var barSound = nextLevelCM.Load<SoundEffect>("bar"); // Unload the current (old) level's content manager currentLevelCM.Unload(); // Cycle currentLevelCM = nextLevelCM;
If you unload the last level’s content manager before you load the next level’s content, all the assets will be reloaded, which renders my code useless. Make sure you follow that order!
The code can be downloaded here : SharedContentManager.cs (4 kB, XNA 3.0 / C#3.5)
And that’s it! Hope it works for you!