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

9 comments:

  1. Nice tip! Thanks for taking the time to write this up.

    ReplyDelete
  2. If this is out of place here, I'm sorry, but in trying to wrap my head around this, I've got to ask you about some examples.

    Fighting game: 2 or 3 CMs
    1. Character select menu[s]
    2. players' chosen characters & hud
    3. [optional] Environments (allows for faster level changes with same characters in multi-player)

    Sandbox game: 4 CMs
    1. Character and HUD
    2. Lo-LOD full world/region
    3. Hi-LOD current neighborhood/region
    4. Background Characters and vehicles

    General examples, of course -I don't even think XNA can support sandboxes- but would you say these are vaguely practical implementations of the local/global within these genres or structures?

    ReplyDelete
  3. Not at all. It's ok.

    First I'm going to answer what I usually do and then will try to give you my 2 cents for your examples :)

    What I usually do (for the type of games I create):
    1) Simple games: two global CMs, one for the whole game (for shared assets) plus a secondary CM (for handling temporary assets) for the whole game,
    2) Middle projects: just one global CM for the whole game and one local CM for each fullscreen instance, and
    3) Big projects: just one gobal CM for the whole game and one local CM for each screen instance (either fullscreen, pop-up screen, etc.).

    The global CM is used only for those assets that are shared across all screens/levels, like say spritefonts.

    Now, let's try to answer your questions: take into account that it depends on your implementation of the classes for characters, weapons, world, region, etc. Also, the solution you find should avoid generating "unwanted/unnecessary" garbage.

    (I) Your Fighting game: assuming your game can live with two environments in memory for a brief period of time, I'd use a sort of swap-chain concept for the environment. So I'd create an environment class with a local CM. Then, in the gameplay screen I'd instantiate two environments (say, "current" and "next"). When you need the next environment, just async-load it while displaying the old one (gives a nice dynamic look & feel, you know, things moving while a small loading text is being displayed). When the loading task is done, swap environments and unload the content of the old one. Whether you decide to keep the old environment instance as a cache or dipose it, depends on the room your game has for keeping these both instances in memory (you could define them as weak references but I do not know how performing are these on the Compact Framework).

    Otherwise, I mean if it's not mandatory to display the "old" level while the new one is being loaded, simply create two local CM for the gameplay screen (one for the selected characters and the second for the current environment) and display a loding fullscreen to change from one environment to the next.

    (II) Your Sandbox game: quite interesting example, btw. Even though I'm more focused on casual gaming rigth now, I do believe it can be done but I also believe it would requiere a lot of time, "trial and error" and optimization for mantaining a fluent experience navigating the open world (without impacts on memory/performance).

    Again, for the gameplay/level screen I'd keep a "pseudo-global" CM for the hud and character (because they shall remain in memory during the whole level). Also, this can be used for all the static things that the player will never reach: a skybox.

    Now, let's think loud about the rest of the items: I'd use one CM for each "dynamic" region. Whether they go from low to hi depends on the proximity to the player. If you impose the restriction that only one region at a time can be in hi-LOD, then the trick is swaping the LOD of the current and next region only. Again, I'd use asyn-loading plus caching.

    If your characters and vehicles are bound to one region, you could simplify things by considering them "as if" they were part of the region's scenery (so you handle them in the same CM for that region).

    If not, I mean if they can freely navigate/move from region to region: (a) if the amount of backgorund "entities" is reasonable you could have one CM for them or include them in the CM for the main character and HUD. But (b) if you're thinking in a world with "infinite" number of secondary characters wandering around (including other players), then "Houston ..." :) Maybe a possible solution is to handle them with the region and only change them from CM when the two involved region change (form hi to low-LOD, and viceversa).

    Fro more thoughts on this you should post this question on forums.xna.com (with or w/o a link to these comments).

    ReplyDelete
  4. I forgot to tell you: I hope my comments can help you :)

    ReplyDelete
  5. Nice article! In a MMO like SecondLife, WoW, etc. - objects are 'rezzed' in and out of the player's area. There are no levels.

    If we create a CM for what is in the player's immediate area, then objects come and go. It would not be feasible to unload all objects.

    Do you have a recommendation for this style of game? Much appreciated in advanced.

    ReplyDelete
  6. Hi, thanks for your kind comments.

    Please read my previous response for the sandbox-type of game.

    ReplyDelete
  7. I know its been a while, but thank you for your incredibly helpful response.

    ReplyDelete

Any thoughts? Post them here ...