Search Unity

『Cuphead』開発者が明かす、Nintendo Switch向けに最適化するためのヒント

, 4月 18, 2019

可愛い手描き風のグラフィックが特徴的な インディー 2D ゲーム『Cuphead』が Nintendo Switch 向けにリリースされました。Unity は Studio MDHR のエンジニア Adam Winkels 氏にインタビューして、彼らのチームがプラットフォームに合わせて最適化するために取ったアプローチについて話しました。この記事は、Adam 氏からのアドバイスをまとめたものです。

「Nintendo Switch への『Cuphead 』の移植は非常にスムーズに行えました。ゲームコードの大部分を再利用できたので、あとはユーザー体験とパフォーマンスが私たちの設ける基準を満たしているかを確認するだけで十分でした。」 – Adam Winkels 氏、Studio MDHR エンジニア

Nintendo Switch に移植するための事前準備

一部の方には当たり前のように感じられるかもしれませんが、Studio MDHR では開発当初からチーム全体に向けて、将来的に Nintendo Switch にリリースするのだという意識付けをしていたといいます。

「(将来的な予定である場合も含めて)Nintendo Switch でリリースすることを見据えて新しいゲームの開発を始めるときは、Nintendo Switch の最小スペックを意識して設計し、パフォーマンスをテストしてください。こうしておけば、高性能な開発 PC 上だけで作業して、いざ実機に持っていったらまったく期待通りに動かないという事態を回避することができます。」

早くから頻繁にプロファイルする

Cuphead』はその滑らかな動きや、プレイヤーからの入力への反応が良いことで評判です。Cuphead チームは、そうしたゲームプレイを実現したいならプロファイラーを使って Nintendo Switch のハードウェアに最適化していくことを推奨しています。

「ゲームのパフォーマンスのボトルネックがどこか、意識してください。コードの分析、パフォーマンスのスパイクの解消、およびプロセッサの使用率のベースラインをできるだけ低くするために、Unity のビルトインプロファイラーと、Nintendo 独自の CPU プロファイラーを使用しました。非効率的なシステムの再設計を迫られるほどに技術的負債が大きくなってしまうことを防ぐために、これらのツールを早期から頻繁に使用して問題にきちんと対処していくようにしてください。」

スプライトアトラスを使って RAM を解放する

Cuphead』の初期リリースでは、45,000 を超える手描きのアニメーションフレームが個別にパッケージ化されていましたが、この方法では満足なパフォーマンスが得られませんでした。それを解決したのがスプライトアトラスです

「ちょっと大きめのレベルを作ってしまうと、RAM が使い果たされてしまったのです(Djimmi the Great、あんたのことだよ!)。そこで Unity のスプライトアトラス機能を活用することにしました。透過部分をトリミングして、ASTC によるメモリ内圧縮を適用したスプライトアトラスを作った結果、RAM 使用量を大幅に削減できたのです。」

アセットバンドルの恩恵

Unity のアセットバンドルの活用は、ゲーム全体のサイズを縮小するというだけでなく、将来のアップデートのリリースもスムーズにできるというメリットを Studio MDHR にもたらしました。

「どんなゲームを作るときも、できるだけアセットバンドルを使うようにしましょう。『Cuphead』では、スプライトアトラスを圧縮したアセットバンドルに分割して格納しました。これにより、ゲームのサイズを大幅に縮小でき(ハードドライブの空き容量は貴重なのです!)、また、任天堂のかかげるパッチサイズの要件への対応もぐっと簡単になりました。アセットバンドルの活用は、ゲームのデータレイアウトがビルド間で大きく変更されないことを保証する効果的な方法でもあります。」

シェーダーのロード動作の調整

チームは、新規またはほとんど使用されていないスプライトが初めてレベルにロードされたときにパフォーマンスの問題が発生しないように、シェーダを事前ロードしておくメリットについても言及しました。

「ランアンドガンスタイルのレベルで特定の敵を最初にインスタンス化したときに、ちょっとしたパフォーマンス上の不具合が発生しました。少し調査したところ、それはシェーダーのロードが原因であることがわかりました。問題の特定には苦労しましたが、幸いなことに Unity にはシェーダーバリアントのコレクションを使ってシェーダーを事前に読み込む機能があったので、修正は簡単でした。」

ガベージコレクションの調整を検討する

Unity には優秀な自動ガベージコレクションが搭載されていますが、さらにパフォーマンスを向上させるために、チームはあえて手動でガベージコレクションを呼び出すことにしました。

「Unity はヒープのサイズを直接制御する方法を提供していませんが、ゲーム起動時に手動でメモリを割り当てることで強制的にヒープサイズを拡張することができます。幸い、15 分から 20 分に一度ガベージコレクションをかければ十分なくらいの RAM 予算が確保できました。これによって、ポーズ、ロード、リスタートのときに(プレイヤーに気づかれないように)ガベージコレクションをかけるようにすればいいということになりました。『Cuphead』は非常に難しいゲームなので、プレイヤーがゲームをプレイしている間にガベージコレクションがかかるということはほぼないのです!」

これでおしまい!

Studio MDHR と『Cuphead(Nintendo Switch 向けに発売中)』についてもっと知りたい場合は、Made with Unity 公式ページをチェックしてください。Nintendo Switch 向けゲームの開発についての詳細は、Nintendo Developer Portalをご覧ください。

コメント受付を終了しました。

  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. Dustin Burg

      4月 22, 2019 7:01 am

      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 :)