Take the 2-minute tour ×
Game Development Stack Exchange is a question and answer site for professional and independent game developers. It's 100% free, no registration required.

I was running the Visual Studio memory profiler on a project and noticed that an enormous amount of strings were being instantiated. It turns out that ContentManager.Load appears to create a new string instance each time it's called. This might not be a big deal on a PC, but on other platforms like the Xbox, it quickly forces a garbage collection.

Maybe I'm just a bit naive, but I assumed that ContentManager would manage my game content, given the name and all. This led to calling the Load method every time I needed a texture (like during Draw), but it seems like this approach apparently can't work since the Load method will allocate strings, even if it's not allocating extra memory for already-loaded textures.

Is there a good solution to this? The obvious answer seems to be to maintain my own references to the textures and not call Load if they've already been loaded...but that seems like precisely what the ContentManager should be doing.

share|improve this question
3  
You shouldn't be loading things on every draw call. Of course that's going to slow things down even if ContentManager caches them (which it does), because as you said, it allocates strings and has to look up the resources in the cache. You should load everything in the Load method that gets called once when your game screen/level/whatever is initialized and assigns the newly loaded objects to member variables. You would then reference those variables in your Draw call. –  Richard Marskell - Drackir Apr 20 '12 at 15:18
add comment

2 Answers

up vote 10 down vote accepted

Your solution to the problem is correct - hold your own references.

In my games I take it a step further with class Asset<T>. Basically:

  • The Asset<T> type can exist in a not-yet-loaded state (a public field).
    • Asset<T> has some helper methods that make skipping unloaded assets easier.
  • Asset<T> objects are allocated by a class that implements AssetManager<T> (one implementation for each content type I care about). AssetManager<T> doesn't care about whether or not it's re-allocating duplicates. It is thread-safe.
  • AssetManager keeps track of already allocated objects and delegates calls to the various AssetManager<T> implementations. It is thread-safe. It is a static class (yeah, yeah, I know it shouldn't be).
  • When the GraphicsDevice is ready I spin up a secondary thread that goes and loads content from Asset<T> objects that are not yet loaded (basically just pop off a queue and load).
  • The AssetManager has a DoneLoading property that checks if the load queue is empty.
  • One implementation even had asset groups. This was used to e.g. keep universal assets in memory between levels.

The raw interface of these types would be:

public class Asset : IDisposable
{
  internal void Load(ContentManager mgr); // Call loader and assign it to Asset then set IsLoaded to true.
}

public class Asset<T> : Asset
{
  public string Name { get; } // Optional, I took it out for release builds.
  public T Asset { get; } // Throws exception if not loaded.
  public bool IsLoaded { get; }

  public Asset(Func<ContentManager, T> loader); // Store loader in a private field.
  public bool Use(Action<T> usage)
  {
    if (IsLoaded) { usage(Asset); return true; }
    return false;
  }
  /* E.g.
    foo.Use(tex =>
    {
      spriteBatch.Draw(tex, ...);
    });
  */
}

public class AssetManager<T>
{
  public Asset<T> Allocate(string name);
}

public static class AssetManager
{
  public static bool DoneLoading { get { return _notYetLoaded.Count == 0; } }
  private static Queue<Asset> _notYetLoaded;
  public static Asset<T> Get<T>(string name);
}

Advantages of this system:

  • You can ask for content before the GraphicsDevice is ready and they will be loaded when it is.
  • Load screens 'know' when to progress onto the game by simply checking the DoneLoading property on the AssetManager.
  • ContentManager is only asked for resources once - you won't get the string allocation problem.
  • You can animate your load screens.
  • By using Asset<T>.Use correctly assets would automagically appear in your game at runtime (e.g. if they are loaded inside the actual game state).

With this system I saw no significant problems with grabbing assets when graphical objects were constructed (and storing them in fields), but grabbing them each frame is just wasteful and lazy.

Also be careful about your ordering when it comes to adding and loading assets. You should always add the assets to the dictionary before they are loaded - this fixes cyclic dependency problems.

share|improve this answer
2  
You asked for a rope to hang yourself with and I gave you a bazooka to shoot yourself with ;). –  Jonathan Dickinson Apr 20 '12 at 13:13
    
Out of curiosity, is calling .Use with an anonymous method every Draw less efficient than, say, creating an OnDraw event and subscribing to it once on Load? –  Richard Marskell - Drackir Apr 21 '12 at 16:43
    
@RichardMarskell-Drackir you have the wrong idea with the event, but yes Use is marginally less efficient. Another way would be to write a implicit conversion (public static implicit operator T (Asset<T> asset)) that either throws an exception or returns the asset and manually use and if (asset.IsLoaded) everywhere. –  Jonathan Dickinson Apr 22 '12 at 9:33
add comment

ContentManager is for loading content, not keeping a reference to it*. Your "solution" as you call it is actually the way it should be done.

A good rule of thumb is only call ContentManager.Load<T>() when you need to load the content into memory. Once it is there keep your own reference to it.

Oh, and don't forget to call ContentManager.Unload() when you are done with everything in that scene.

Yes, internally it will do that, that is more for shared resources than anything else

share|improve this answer
    
Downvoter care to explain? –  Joe Apr 21 '12 at 21:46
add comment

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.