Managing Game-Loading Memory in WebGL with Unity
Galvanic Games is a small company with few resources. As our primary programmer on the team, it’s my job to figure out how we can deliver content as quickly as possible. We had a goal to release our game, Questmore, after just a few months of development. This makes picking our tools one of the most valuable steps in the process, and Unity is a phenomenal tool for rapid development. However, it isn’t without problems.
One of the biggest issues right now with using WebGL and Unity is memory management. Unity needs to allocate a certain amount of memory up front, and as developers we hope the browser grants it. Sometimes this is a little unpredictable, as it really depends on what’s happening with the browser at the time (how much memory has it already given to its tabs?). On top of that, the architecture of the browser may give different results.
Early on we saw a much higher failure rate on 32-bit browsers than we did on 64-bit. Which makes sense! But still frustrating. There is really only one solution to this problem. Ask for as little memory as possible. Easy!
Of course after having the browser accept the desired amount of memory you then have to make sure you never go up over this amount or you’ll nab yourself an out of memory exception. We ended up settling on an allocation block of 256 MB for Questmore and we pretty easily sit under that amount when the game is playing. So problem solved? Not quite.
A quirk of Unity’s memory management in WebGL is that the garbage collector can only run in between frames instead of during a frame. If you allocate too much memory (even if it becomes garbage) in the middle of a frame, you’ll hit that game-ending out of memory exception.
“Be pro! Don’t generate garbage! The collector causes hitches anyways!” And that’s good! We keep our frame-by-frame garbage generation as low as we can to avoid those nasty hitches, but that’s not where the issue is. When setting up the scene, populating our object pool, or otherwise just doing the expensive initialization needed to run the game, it turns out a ton of garbage is created. This isn’t really surprising and usually isn’t much of an issue since we’re pretty okay with doing a lot of expensive stuff up front if that means the actual play experience for the player is super smooth. But we’re never getting that smooth experience if we run out of memory initializing the game. We could play the game at 256 MB, but we couldn’t launch the game at 256 MB.
The memory when our game idles is manageable but requires far too much when initializing. Since we don’t have threading in WebGL (Unity doesn’t play nicely with multi-threading anyways), the solution we came up with was to initialize as much as we can in a frame before yielding to the next frame. This allowed us to avoid spiking up over the memory limit as well as spin with enough frames that we could show a loading animation or progress bar so it doesn’t look like the game has locked up while loading.
The solution isn’t too complicated, but we enjoy giving back to the game development community, so we open-sourced the loader we created for Unity. While it was created to solve our WebGL memory issue, it’s built to be functional for any platform when it’s desired to initialize a scene while showing progress.
The remainder of this article will be a simple tutorial on how to use the Unity Game Loader. Underneath the hood the loader works by taking an enumerator and continuously advancing it as long as more can be loaded in the frame. Once we need to yield to the next frame (due to memory or time) the loader will pause loading until a rendered frame has occurred. All that’s needed to use the loader is to set it up, register some enumerators, and tell it to load.
Creating the Unity Game Loader
To set up the loader, add the Load Manager component to any GameObject that exists in the scene. The Load Manager has a couple of properties that will need to be set before loading can occur.
Seconds Allowed Per Frame - The amount of time, in seconds, that may pass before the loader will yield to the next frame. This can be thought of as a target framerate so animations can play while loading. So for 30 fps then a value 0.0333 would be used. However, this framerate is in no way guaranteed. If a registered enumerator takes more time than the allowed seconds, then that enumerator will finish before the yield will occur.
Memory Threshold For Yield - This is specific to WebGL and is ignored for other platforms. This specifies memory threshold before the loader will yield to the next frame (to allow the garbage collector to kick in). The value queried to check against this is returned by System.GetTotalMemory() which, according to Unity, is “the total managed memory currently used.” For Questmore a value of 128MB worked out pretty well, but experimentation for each game will be needed to find a value that allows as much to be loaded in a frame as possible (important!) without going over the limit and crashing the program.
Now that the loader is set up, there are two primary ways of loading content. Directly registering enumerators and registering GameObjects/classes that implement the IAssetLoader interface. When told to load, the loader will step through the registered enumerators and IAssetLoaders to load them in order.
Registering Enumerators
To register an enumerator, call the RegisterEnumerator method on LoadManager with the enumerator that will need to load.
LoadManager.instance.RegisterEnumerator(EnumeratorToLoad());
Registering Game Objects
Components that implement the IAssetLoader interface may also be registered to load. Invoke the method RegisterObject, and all the components that implement the interface will be registered.
LoadManager.instance.RegisterObject(gameObject);
Loading
Once registration is complete, then all that’s left is to inform the Load Manager to begin loading.
LoadManager.instance.LoadRegistered(OnLoadComplete);
And we’re done! The registered enumerators and GameObjects have been loaded and the frames will yield when appropriate. Below is a full example of how this could look.
public class Example : MonoBehaviour, IAssetLoader
{
public GameObject prefabToCreate;
public int numberToCreate;
public GameObject secondPrefabToCreate;
public int secondNumberToCreate;
// Use this for initialization
void Start()
{
LoadManager.instance.RegisterEnumerator(EnumeratorToLoad());
LoadManager.instance.RegisterObject(gameObject);
LoadManager.instance.LoadRegistered(OnLoadComplete);
}
private void OnLoadComplete()
{
Debug.Log("Load Complete!");
}
private IEnumerator EnumeratorToLoad()
{
for (int i = 0; i < numberToCreate; i++)
{
Instantiate(prefabToCreate);
yield return null;
}
// Can call into other enumerators
yield return LoadOtherEnumerator();
// Can continue to load here, will be invoked when LoadOtherEnumerator finishes
}
private IEnumerator LoadOtherEnumerator()
{
// Can do whatever loading here too
yield return null;
}
//
// IAssetLoader Implementation
//
public IEnumerator LoadAssets()
{
// Can also have registered the object and had this
// enumerator called
for (int i = 0; i < secondNumberToCreate; i++)
{
Instantiate(secondPrefabToCreate);
yield return null;
}
// Can also yield to other enumerators
yield return LoadOtherEnumerator();
}
}
That’s the system we came up with for Questmore to handle loading resources to avoid the WebGL memory limitations. To learn about more advanced topics (such as loading control, forcing yields, step progressions, and multiple phases) check out the readme on the github page.