.NET エコシステムは、数多くの有益な方向でダイナミックに進化をしており、私たちはその進化において生まれた改善点を、できるだけ早く皆さんにお届けしたいと考えています。Unity の .NET Tech Group は、新しい C# 機能や .NET Standard 2.1 など、Unity の .NET 統合の継続的な改善に取り組んでいます。しかし、最近になって、皆さんからのフィードバックをもとに開発者の体験を全面的に向上させるべく、さらにギアを上げました。
このブログ記事では、私たちが取り組んでいる課題を紹介します。GDC 2022 の Unity Dev Summit でもこのテーマで議論しています。セッションの全容はこちらでご覧いただけます。
.NET の進化と Unity
物語は 17 年前、Unity の CTO が C# で Mono .NET ランタイムの利用を始めたことから始まります。Unity が C# を採用したのは、そのシンプルさと、C# を比較的効率の良いネイティブコードに変換する JIT(ジャストインタイム)コンパイラーが組み合わさっていたためです。Unity エンジンの残りの大部分は、バランスよく制御されたパフォーマンスを提供するために、C++ を使用して開発されています。
長年にわたり、Unity は Mono .NET ランタイムと C# 言語の特定のフォーク(2.0)を実行していました。この間、対応プラットフォームを追加しています。また、Unity 独自のコンパイラーとランタイムである IL2CPP を開発し、iOS や一部のコンソールプラットフォームをターゲットにできるようにしました。
その間、Microsoft .NET のエコシステム全体も進化し、新しいライセンスや Windows 以外のプラットフォームへの対応など、さまざまな変化がありました。この進化のおかげで、2018 年には Unity .NET Mono ランタイムをアップグレードし、よりモダンな C# 言語バージョン(7.0 以降)を取り込むことができました。また同じ年に、C# 言語のサブセットを対象とした高速ネイティブコード生成の先駆けとなる Burst コンパイラーの最初のバージョンをリリースしました。このブレークスルーにより、Unity はエンジンの他の重要なセグメントを C++ で開発する必要がなくなり、C# の使用を拡張できる世界を実現することができました。これが、DOTS ランタイムの開発につながっていきました。
Unity 2020 LTS および Unity 2021 LTS では、より新しい C# 言語バージョンと Span のような .NET API が導入されました。これと並行して、.NET エコシステムにおいて驚異的なパフォーマンスの向上が実現し、SDK スタイルの csproj の導入や NuGet エコシステムの隆盛により、より使いやすい開発環境が提供されました。
私たちがするべきこと
この長い進化の結果、Unity プラットフォームには、Mono .NET ランタイムから継承した特定の前提条件を使用して .NET オブジェクトと直接対話する非常に大規模な C++ コードベースが含まれています。これらは、.NET(コア)ランタイムにとって、もはや有効でも効率的でもありません。
さらに、Unity エディターには、MSBuild に依存しない複雑なカスタムコンパイルパイプラインがあり、すべての標準機能の恩恵を容易に受けることができません。
また、過去数年にわたり、インタビューや Unity フォーラムなどを通じて皆さんとの話し合いを重ね、皆さんがより確実に成功できるようにするために、何を改善することができるかを考えてきました。 皆さんから聞かれた意見は、最新の C# 言語、.NET ランタイム技術、および NuGet から提供されるサードパーティの C# コードを使いたい、というものでした。Unity プラットフォームの利用に関しては、高品質の C# テスト、デバッグ、プロファイリングツール、標準的な .NET API と Unity API 間の優れた統合によって、ターゲットハードウェアを最大限に活用したいという要望を受けました。 C# を使う Unity プログラマーは、他のツール群とシームレスにつながり、最高レベルのランタイムパフォーマンスを達成するために迅速なイテレーションを可能にする Unity ツールを求めています。
私たちがそこに至るまでには、数年かかると思います。その過程で私たちが遭遇する技術的な課題について、ブログやフォーラムで頻繁に更新していきますので、ご期待ください。
どのように作業を進めるのか
この取り組みの第一歩は、Unity の C# や .NET について高い関心を持つ社内関係者を集め、C#/.NET 技術グループを結成し、この取り組みを推進することでした。
私たちは、カスタムソリューションを開発するのではなく、.NET エコシステムを基礎として製品を構築したいと考えています。皆さんが最新の .NET SDK/ランタイム、および MSBuild によるパフォーマンスと生産性の向上の恩恵を受けられるように、Mono .NET ランタイムからモダンな .NET(コア)ランタイムである CoreCLR に移行したいと考えています。
この取り組みは、C# スクリプトの .NET イテレーションサイクルの高速化を目標に、既存の .NET の体系を超えるイノベーションをもたらすものです。JIT と AOT(先行処理)ソリューションである IL2CPP と Burst を融合させる取り組みを行い、コンパイル時の効率とコード生成品質の最適なバランスを実現しようとしています。
社外では、Microsoft や JetBrains といった業界のパートナーと協力し、Unity クリエイターが最新の .NET テクノロジーを使用できるように努めています。また、オープンソースのコミュニティへの参加も活発化しています。 この試みはいくつかのステップに分けて取り組んでいく予定です。この次に何が来るのか、見てみましょう。
2022 年の取り組みについて
今年、各チームは以下の分野に取り組む予定です。
C# 開発ワークフロー
イテレーション時間は依然として私たちの最優先事項であり続けています。皆さんが時間をより有効に使いたいと考えていることを私たちは知っているからです。その改善のために行っていることの例をいくつかご紹介します。
MSBuild への移行では、まずコンパイルパイプラインを Unity エディターから切り離し、別プロセスに移行します。これは複雑な仕事です。なぜなら、何千行もの C++ や C# のレガシーコードが何年も前から存在しており、それを紐解いて後方互換性を保ちつつ、移行を実現する必要があるからです。皆さんからはどこが変化したかわからないように進めますが、この仕事により、MSBuild への道を開き、メンテナンスを簡素化することができるのです。
また、Burst で実行中のコードパスにブレークポイントが設定されると、デバッガーが自動的にマネージドデバッグに切り替わるモードを導入し、Burst を使っているときの C# IDE のデバッグ体験を改善する予定です 。これにより、デバッグ対象のコードパスの [BurstCompile] 属性を手動で削除する必要がなくなります。
.NET ランタイムの近代化
.NET CoreCLR ランタイムへの移行に関わる作業はすでに始まっていますが、これは非常にチャレンジングな取り組みです。この移行を成功させるために、少しずつ問題に取り組み、既存の Unity プロジェクトの安定性を維持した形で少しずつ成果物をリリースできるようにしたいと思います。
そこで、この移行を複数の段階に分けて提供する予定です。
Unity ランタイムの近代化
Unity 2021 LTS で .NET Standard 2.1 をサポートしたことで、さまざまな方法で Unity ランタイムの近代化を開始することができるようになりました。現在、2 つの改善に取り組んでいます。
async/await プログラミングモデルの改善。async/await は、エンジンのメインループをブロックすることなく非同期処理の完了を待つ必要のあるゲームプレイコードを書くための基本的なプログラミングアプローチです。
2011 年、async/await が .NET で主流になる前に、Unity はイテレーターベースの非同期操作であるコルーチンを導入しましたが、このアプローチは async/await と互換性がなく、効率が悪くなることがあります。一方、.NET Standard 2.1 では、ValueTask による、より効率的な async/await 操作の処理の導入や、AsyncMethodBuilder による独自のタスクライクシステムの実現により、C# や .NET における async/await のサポートが改善されています。
これらの改善点を活用できるようになったので、Unity の既存の非同期操作(次のフレームを待つ、UnityWebRequest の完了を待つなど)で async/await を使用できるようにするための作業を行っているところです。最初のステップとして、MonoBehavior が破壊されるときや再生モードを終了するときに、保留中の非同期タスクをキャンセルするためのサポートを、キャンセルトークンを使って改善する作業を進めています。また、UniTask の作者など、私たちのコミュニティにおける最大の貢献者がこれらの新機能を活用できるよう、密接に協力しています。
Span の活用による、メモリの割り当てやコピーの削減。Unity は C++ のエンジンに C# のスクリプトレイヤーを加えたもので、両者の間で多くのデータがやり取りされています。この場合、データコピーの往復や新しいマネージドオブジェクトの割り当てが必要になることが多いため、非効率になることがあります。
このようなシナリオにおいて、状況を改善するために Span が C# 7.2 で導入され、.NET Standard 2.1 ではデフォルトで利用可能になっています。近年、Span のおかげで .NET Runtime のパフォーマンスが大幅に向上したことを耳にしたり、読んだりしたことがあるかもしれません(各バージョンでの改善の詳細については、以下のリンク先の文書でご確認ください:.NET Core 2.1、.NET Core 3.0、.NET 5、.NET 6)。これは、多くの API について全体的なパフォーマンスを向上させながら、メモリ割り当てと、その結果として起きるガベージコレクションによる処理の一時停止を減らすのに役立つため、Unity 内部で活用したいと考えています。
この取り組みに参加しよう
これらの変更と機能に対して、私たちと同じように皆さんが楽しみに思ってくれることを願っています。
私たちの計画について、フォーラムでご意見をお聞かせください。また、Unity プラットフォームロードマップのエンジニアリングに関するセクションを定期的に更新していきますので、機能の要望や優先順位の提案をそちらでお伝えいただければと思います。