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.
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 theLoad
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 yourDraw
call. \$\endgroup\$