Hello all,
I'm quite new to XNA Framework 2.0 (at least new to trying to do anything serious with it). It's taken me a while to grasp the ins and outs of content loading in the framework, and what impact that has on designing a non-trivial game engine. It's something of a liberty, but I was hoping that if I posted my understanding here, then wiser minds than mine might be able to point out where I'm on the right track, and flame appropriately where I'm not. Also, if by some freak of fate it turns out I'm not talking absolute nonsense (ha!), then this post might be a potential reference for others searching for the same information.
Apologies for the length of this post. I'm really only scratching the surface of a complex topic, and perhaps with an inappropriate lack of brevity...
XNA 1.0
In XNA 1.0, it was necessary to unload and reload content
when the graphics device is reset (for example, if the user switches from
fullscreen to windowed mode). By “content” we mean anything which internally
referenced graphics device resources such as textures, vertex buffers, etc.
This includes Models, Effects, VertexBuffers, etc. Game.LoadGraphicsContent()
and Game.UnloadGraphicsContent() (and the GameComponent equivalents) were the
requisite methods which all classes owning such content were required to
implement.. Moreover any class which referenced such content, even if it didn’t
own it, used to have to be aware that its previous references to a Model etc.
could become invalid afer a graphics device reset; the objects would still
exist, but be rendered useless and likely to throw exceptions. This lead to fun
and excitement for the game developer.
XNA 2.0
In XNA 2.0, the GraphicsDevice is “virtualized”. This means
that we no longer have to worry about graphics device resources, or the GraphicsDevice
itself, disappearing on us at any point during the running of the game,
including during a graphics device reset. Model instances, VertexBuffer
instances, etc. will now always be valid and useful. In most cases we can
simply ignore the idea of losing our content at any point during the running of
the game. In some cases, such as DynamicVertexBuffer, the object (and thus
references to it) will remain entirely valid, but the contents of the object
may disappear during a device reset. (Why? The long answer is, well, long, but
the short answer is “that’s just the way it is”. In Windows, XNA is built on
top of Direct3D; Direct3D developers have to deal with it, and thus so do we.
On the bright side, so many other things are easier for us by an order of magnitude.)
In such cases this is clearly stated in the documentation, as is the way to
handle it. (For example, DynamicVertexBuffer.ContentLost and .IsContentLost).
LoadGraphicsContent and UnloadGraphicsContent, on Game and
GameComponent, are obsolete as a result. They still work, but new and simpler methods
on Game and GameComponent called LoadContent() and UnloadContent() have
superceded them. Where I refer to LoadContent() and UnloadContent() below, read
“…and LoadGameContent()/UnloadGameContent() for backward compatibility”.
The upshot of the above is that Game.LoadContent() is now only called by the XNA Framework once
during the running of the game: on startup. Game.UnloadContent() is now only called once by the XNA Framework as
well: on shutdown. For GameComponents, LoadContent() is called when that
component is added to the Game’s Component collection (often on startup, in
Game.BeginRun()), and UnloadContent() is called when that component is removed
from the Game’s Component collection, or when the Game object is Dispose()’d on
shutdown.
Handling Content the
Simple Way
What does this mean? This means that the use of
LoadContent() and UnloadContent() for managing the loading and unloading of
resources is purely optional. I’ll explain this below.
In simple games, LoadContent() is a convenient place to load
everything. Your game is starting up and the game loop is not running yet. UnloadContent()
is a good place to unload your content; though in practical terms, not a
necessary one unless you make it so yourself. More on this later.
Handling Content the
Complex Way
In complex games, bear in mind that you’re perfectly able to
load and unload content whenever you like. Doing it on Draw() is a recipe for
pain and suffering, but anywhere else is absolutely fine. Just be aware that
this is most likely going to cause disk access and temporarily hurt your
framerate. So if you can, load things at well defined points, such as on
startup (in LoadContent()) or during a transition between game states or
levels.
Of course, if you’re loading content all the time and never
unloading anything, something has to give eventually. Ideally you’ll set things
up so before you load a bunch of new content, you unload corresponding existing
content. You can have multiple ContentManagers, if you wish, in order to
“partition” your content into sections which you load and unload at different
points during the game. However many ContentManagers you have, you can unload a
ContentManager’s managed content, such as Models or Effects, by calling
ContentManager.Unload() manually.
Two things to remember here: this will not
cause your UnloadContent() methods to be called by the XNA framework (there
wouldn’t be much point, since in XNA 2.0 the point of UnloadContent() is that
it doesn’t have to deal with content managed by a ContentManager); and if parts
of your game code are hanging onto references to objects you obtained from that
ContentManager, those references are now going to be pretty useless.
If you try
to use an Effect which has been unloaded in this way you’ll just end up with an
ObjectDisposedException. If you try to use a Model, that model likely contains
Effects, so again you’ll get an exception. Therefore, if you’re going to call
ContentManager.Unload() you need to make sure all references to unloaded
objects in your code have been tidied up, and that your code expects this to
happen. The easiest way is to set such references to null, and check them
before use, taking other appropriate action as required.
UnloadContent() as
Optional
As previously stated, the only time the XNA Framework calls
Game.UnloadContent() is when your game is closing down. The only time it calls
GameComponent.UnloadContent() is if you manually remove that GameComponent from
the Game yourself, or when the game is closing down. You do not need to unload
content managed by a ContentManager (such as Models and Effects) here: that’s
all handled for you.
In fact, you don’t actually need to do anything here. Note that there is a difference between
the need to do something, and best practice for doing something.
The documentation, and the internet, states that in
UnloadContent() one should release all content not managed by a ContentManager.
An example might be calling Dispose() on a DynamicVertexBuffer.
This is in fact optional because, unless you’re manually
removing GameComponents from the Game, XNA
is not going to call UnloadContent() unless your game is shutting down; and
all XNA objects which hold onto external
resources - such as content tied up with the graphics device - implement
finalizers. When your game exits and the .NET framework shuts down, all
finalizers will be called, and all such resources will be tidily released.
I did say though that there is a difference between need and
best practice. In .NET in general, and thus in XNA, it is best practice to
Dispose() of IDisposable objects as soon as possible. Even in the simplest
case, UnloadContent() is going to be called prior to finalization. In a simple
game this is completely academic, but is tidy and good practice. In a complex
game, management of content may well be an issue, and with a lot of content,
you’re probably going to need to make sure that content gets unloaded as soon
as it isn’t needed, so you can replace it with other exciting content. In this
case, LoadContent() and UnloadContent() may form part of a useful pattern which
you can follow to help you manage content and other resources: see below.
LoadContent() and
UnloadContent() as a Design Pattern
One thing you will see with relation to content management
is that some sample code (and I’m talking about good quality sample code and
tutorials here; as always, take random tutorials and samples you find on the
internet with a pinch of salt, as they may not be doing the “right thing”) will
explicitly call LoadContent() and UnloadContent(). Some samples even declare
these methods on classes other than Game and GameComponent.
An example would be the “game state management” sample. (I’m
going to assume, for the purposes of this example, that you’re at least
slightly familiar with this sample.) Here, a ScreenManager (which derives from
(Drawable)GameComponent) owns a collection of GameScreen objects which each
implement methods called LoadContent() and UnloadContent(). The ScreenManager
calls each GameScreen’s implementations of LoadContent() and UnloadContent()
from its own methods of the same name. This is because: it is convenient for
each GameScreen to load its own resources; the method may as well be called
LoadContent() as anything else; and for completeness it makes sense to ask any
existing GameScreen to load its content when ScreenManager.LoadContent().
Now this sample is rather more clever, and is using
LoadContent() and UnloadContent() as a pattern. When a screen is displayed via
ScreenManager.AddScreen(), the ScreenManager calls GameScreen.LoadContent().
When a screen transitions off via ExitScreen(), or is removed the hard way via
RemoveScreen(), the ScreenManager calls GameScreen.UnloadContent(). This is
clearly outside the normal way in which LoadContent() and UnloadContent() are
called by XNA, but follows the same pattern. It ensures that resources specific
to that GameScreen can be handled by the screen in a consistent way regardless
of the exact lifetime of the screen, and that they are tied to the lifetime of
that screen.
So unless your content lifetimes are very simple (load all
at startup, destroy on shutdown), it may be worth your while to follow the same
pattern. You can design your game so that resources are owned either by a
content manager, or by a class which implements LoadContent() and
UnloadContent(). You can ensure that whatever parent class owns that class also
implements LoadContent() and UnloadContent() and calls its children in turn;
and so on. You can also ensure that ultimately one of these ancestral classes is
a GameComponent (and is part of the Game’s Components collection), or is the
Game itself. If you have multiple content managers, you can consider who owns
them, and ensure that their lifetime is properly controlled.
Building on these basics, judicious use of LoadContent() and
UnloadContent() will allow you to load resources when they are required, and
dispose of them as soon as they are not.
A further useful step is to also follow the Dispose pattern.
This is a familiar one to seasoned .NET developers. The game state management
sample does not do this, probably because it would additionally complicate the
sample for developers new to .NET, and arguably the sample suffices well without
it. But it may be worth your while to read up on this pattern; the XNA
framework itself follows it closely.
A simple way to follow it is to ensure that objects which
manage content implement IDisposable, and have Finalizers, as and where appropriate
according to the Dispose pattern. A detailed discussion of the pattern is
beyond the scope of this article, but for those familiar with the pattern, a
trivial suggestion is to implement all your content unloading in
UnloadContent() as above, and when Dispose(bool disposing) is called with a
disposing value of true, call UnloadContent(). Then you ensure as usual that
all IDisposable objects are disposed of as soon as possible, which will cause
their content to be unloaded as soon as possible. You then only need call
UnloadContent() on a class explicitly from within its parent class’
UnloadContent() implementation, and can otherwise stick with Dispose(). If a
content-owning object is never disposed, finalizers on the underlying XNA
objects will cause them to be cleaned up nicely anyway.
Conclusion
Above all else, bear in mind that there is no magic to
LoadContent() and UnloadContent(). These methods do nothing special in the XNA
Framework, are there for your convenience and are entirely optional. You can
load and unload content pretty much whenever you feel like it, according to the
needs of your game. But these methods provide a useful pattern, which alongside
the Dispose() pattern may make your life easier as your game grows in
complexity.