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.
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.”
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.”
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.”
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.”
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!”
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.”