Wednesday, October 15, 2008

CONTENT MANAGERS 101

If you have been using XNA GS for some time now, then you will know by heart the benefits of the content pipeline. If not, just to mention a few, here is a brief list for you to check before reading on this post:

  • Improves the process of importing assets to your own XNA games,
  • Faster loading times for pre-built assets (in binary format), and
  • Easy to extend for importing custom content to our XNA-based creations.

Ok, let us go on, now.

I. THE BASICS

On the process of creating your game, having built all your assets at compile time with GS you will have to use a Content Manager to load them at runtime. Fortunately, as you all may know by now, XNA already provides one for you. What is more, the game project template defines a reference and creates and instance of it on the game class.

This CM will take care of both, the loading and unloading process of pre-built assets. This means that behind the scenes, not only will it handle the instantiation of the assets being loaded but also, when properly requested, it will dispose for you the ones that can be disposed.

For loading, you will have to use a sentence like "this.Content.Load<...>(...);" or "this.Game.Content.Load<...>(...);" and the usual place to put them is inside the overriden LoadContent method of your game class and or drawable game components.

And for unloading, all you have to do is call your CM's Unload() method. Again, being a usual place to include this sentence inside the overriden UnloadContent method of your game class and or drawable game component.

II. BEHIND THE SCENES

Now, If you have been reading carefully you will notice that you can load assets individually but there is no way to unload them individually. At first you may think that this is an unpleasant restriction, but as you may conclude after reading this post -or at least, as I expect you will, this is not. In fact, it helps you manage your assets on a very tidy manner.

Generally, for the sake of performance, efficient memory allocation and best user experience, you design your game so that all the needed assets are loaded at "one time", say, at the begining of each level. So, even if you load the assets individually, that behavior from the perspective of "one-shared time bucket" -to call it some way- shows that you programmed the logic of your game to treat all these loaded assets as a cluster.

The above-mentioned rationale can be then extended to the process of unloading assets. There is no need to assume a priori that each asset will be unloaded individually at different times during gameplay. And therefore, they will be treated as a cluster, in this case during the respective level, and unloaded altogether, again, in "one-shared time bucket", when requested.

You may wonder: "Ok, but what if I want to dispose certain assets during runtime and, at the same time, to keep others in memory?". The answer to that is quite simple actually: just create more than one content manager.

We will get back to this topic in a moment, but first let us consider another important aspect of the Content Manager.

III. THINGS NOT TO DO

Avoid to manually dispose your assets!!! The CM does not like that you handle the destruction of assets by using sentences like "this.myAsset.Dispose();". In fact, the CM does NOT monitor if you do, and thus, it could and will get confused when that happens.

Let us see an example. Say you have a Screen1 and Screen2 classes and:

  1. Both share the game's CM,
  2. Both load the same pre-built texture on their respective local Texture2D fields,
  3. Screen1 is created and shown first, and
  4. Screen2 is only created and shown after Screen1 is manually disposed.

If Screen1 manually disposes the texture (either using the Dispose Pattern or by calling Dispose within the UnloadContent method) without calling "this.Game.Content.Unload();" first, when Screen2 tries to draw the texture on screen you will get an exception stating that the texture has being disposed, even if Screen2 loaded that texture. As I told you before, the CM gets confused with situations like this.


Therefore, avoid these implementations:


protected override void
Dispose(bool disposing)
{
if (disposing)
{
if (this._myTexture != null)
{
this._myTexture.Dispose();
this._myTexture = null;
}
}

base.Dispose(disposing);
}


... and ...


protected override void
UnloadContent()
{
if (this._myTexture != null)
{
this._myTexture.Dispose();
this._myTexture = null;
}

base.UnloadContent();
}


Being the proper way to unload assets:


protected override void
UnloadContent()
{
this.Game.Content.Unload(); // Or the CM you use.

base.UnloadContent();
}


Please notice that this is also allowed by the CM:


protected override void
Dispose(bool disposing)
{
base.Dispose(disposing);

if (disposing)
{
if (this._myTexture != null)
{
this._myTexture.Dispose();
this._myTexture = null;
}
}
}

protected override void UnloadContent()
{
this.Game.Content.Unload(); // Or the CM you use.

base.UnloadContent();
}


Comply with the above, and you will do good.

IV. THINGS TO DO

Getting back to the question of how to unload assets at different times at runtime, there is one practice that will help us understand why it is sound and thus, advisable, to have more than one CM in our XNA-based games.

When you create a program you declare global variables and local variables. This common practice incorporated to our daily programming tasks is the key that will then leads us to think of global and local assets.

You may consider an asset "global" if its lifetime lasts across all the game, or at least, most of it. In this sense, assets should be deem as "local" instead if it is expected and intended to unload them once a stage in the game has finished but the game has not and will not, at least for a while more.

Thus, when creating your game, use:
1) A Content Manager for "global" assets: the one instantiated by default within the Game class (which you can access using the Content property), and
2) One (or more) Content Manager(s) for "local" assets: say one content manager per level, per screen, or per whatever category that fits your game's design.

Again, group your assets in clusters based on lifetime, and you will know which one is global and which ones are locals. If all your screens will use one spritefont in common, then there is no reason to load and unload it for every screen; just load it once and hold it in the collection of global assets. If a ship model will be used in only one level, handle it locally to that level.

As you can see, using more than one Content Manager is a sound practice: easier to apply, easier to debug, easier to mantain and easier to extend.

Sometimes there is no need to have more than one CM for all the game, depending on the game of course, but still it is a good practice to know and get accustomed to, since in the end it helps us manage our assets at runtime in a more tidy and convenient manner.

So, my recommendation is: get used to it. The sooner, the better.

V. CONCLUSION

The Content Pipeline is a great tool to import assets to our XNA-based games which is complemented with the proper manager to handle that content at runtime: the ContentManager class.

By using instances of this class wise and properly, we could get (more) efficience in the fields of implementation, debugging, mantainability and extensibility.

In order to achieve this goal certain practices must be applied:

  1. Use one instance of the ContentManager class for global assets,
  2. Use at least one instance of the ContentManager class for local assets, and
  3. Do not manually dispose assets; instead, call the Unload() method of the CM.

I hope you find this post useful. Comments and suggestions are welcome.

Cheers!
~Pete