Search Unity

The hand-drawn, 2D indie darling Cuphead just released to Nintendo Switch. We talked with Studio MDHR engineer Adam Winkels to learn about the team’s approach to optimizing for the platform and he shares his advice for developers in today’s blog.

“Bringing Cuphead to Nintendo Switch was a very smooth endeavor. Much of the game worked right out of the box, allowing us to focus on making sure the experience and performance was up to our standards,” – Adam Winkels, Engineer at Studio MDHR.

Pre-plan for Nintendo Switch

While it may be obvious to some, Studio MDHR encourages teams to have the foresight to target the Nintendo Switch hardware from the onset.

“When working on a new game that you’d like to publish (or eventually bring) to Nintendo Switch, always design and performance test it with the platform’s minimum spec target in mind. This avoids a situation where you only test your game on a high powered development PC only to realize that the game performs unexpectedly on the target hardware.”

Profile early and often

The team advocates the use of profilers to optimize for the Nintendo Switch hardware, helping create the smooth and responsive gameplay that Cuphead is known for.

“Be very cognizant of performance bottlenecks in your game. We used Unity’s built-in profiler and Nintendo’s own CPU profiler to analyze our code, clean up any performance spikes, and do our best to make our baseline processor usage as low as possible. Don’t be afraid to use these tools early and often to address problem areas before the technical debt of re-architecting inefficient systems becomes too large.”

Free up RAM with SpriteAtlas

In its initial release, Cuphead’s 45,000+ hand-drawn animation frames were individually packaged, but that approach isn’t super efficient. The answer: Sprite Atlases.

“We were running low on RAM for some of our larger levels (looking at you, Djimmi the Great!), so we chose to use Unity’s Sprite Atlas feature. After trimming transparencies and allowing for in-memory compression using ASTC, Sprite Atlases significantly reduced RAM usage.”

The benefits of AssetBundles

Unity’s AssetBundles helped Studio MDHR shrink not only the total size of the game but also set them up for success for when they release future updates.  

“Use AssetBundles for as much of your game as you can. For Cuphead, we took the SpriteAtlases and split them into compressed AssetBundles. This significantly helped reduce the size of the game (given hard drive space is a premium) and made it much easier to adhere to Nintendo’s patch size requirements. AssetBundles are also an effective way to ensure that your game’s data layout does not change too much between builds.”

Adjust shader loading behavior

The team saw benefit from preloading shaders to avoid performance hiccups when new or rarely used sprites loaded into a level for the first time.

“We ran into a slight performance hitch when first instantiating certain enemies in our Run ‘n Gun levels. After some digging, we discovered that the shader loading was the culprit. Thankfully, Unity provides the ability to preload shaders using Shader Variant Collections, so although the problem was tricky to identify, it was easy to fix!”

Consider garbage collection tweaks

Unity does a great job of automatic garbage collection, but the team opted to create manual calls to realize additional performance gains.

“Although there isn’t a direct way to control the size of the heap in Unity, you can force it to expand by manually allocating memory when you launch your game. Luckily, we had the RAM budget to increase the heap so that it collected once every 15-20 minutes. Given we also trigger garbage collection on every pause, load, or restart (when invisible to the player) and because, well, Cuphead is a very difficult game, it is extraordinarily unlikely that players will be in a level long enough to experience garbage collection during gameplay.”

That’s all folks!

If you’d like to learn more about Studio MDHR and Cuphead (now available on Nintendo Switch!), check out the official Made with Unity page. To learn more about creating games for the Nintendo Switch platform, visit the Nintendo Developer Portal.

Leave a reply

You may use these HTML tags and attributes: <a href=""> <b> <code> <pre>

  1. Why Sprite Atlas is more memory-efficient than any existing sprite sheet?

  2. it’s so difficult to publish your game to nintendo. first of all, because you cant get a way to download it’s installation package… and of course i just write an email, get no reply!

    1. Check out the Nintendo Developer Portal where you can register to be a Nintendo developer: https://developer.nintendo.com/register

  3. I was told there would be content…

  4. Can we get much more information about not only the memory allocation tricks in @AtomicJoe’s comment above, but also the specifics of how they use SpriteAtlas, AssetBundles? Just casually mentioning these things, as though they’re feature adverts, isn’t nearly as helpful as insight into the hows.

  5. “Although there isn’t a direct way to control the size of the heap in Unity, you can force it to expand by manually allocating memory when you launch your game.”
    Could you elaborate on that? I suppose you are talking about manually filling the memory so the heap increases and then free it so the heap has become large but you still have plenty of free memory.
    The problem is the GC will shrink the heap size anyway if it’s not fully used within an arbitrary amount of time.
    To remedy that, I thought of allocating lots of memory and then freeing it EXCEPT for the last little bit. This way, since the GC can’t defragment the heap, it would have a large chunk of free memory ended by a little bit of allocated memory, and thus the heap could not be shrinked.
    I haven’t tested it.
    Is this what you did and does it work? Is there anything more to keep in mind?
    Thanks :P

    1. As far as I remember, Unity is unable to reduce heap size (to be precise: return allocated memory back to the OS). Probably the reason is Unity managed memory system and GC do not move objects ie there are no heap defragmentation or GC generations presented, the GC is extremely simple. So you don’t have to invent workarounds with small object at the end of the memory holding it from shrinkage :)