Unity で.NET 4.x を使用する

Unity のスクリプトの基になっている C# と .NET は、Microsoft が 2002 年に最初にそれをリリースして以来、更新され続けています。 しかし、Unity の開発者は、C# 言語と .NET Framework に次々と追加される新機能を認識していない可能性があります。Unity 2017.1 以前は、.NET 3.5 と同等のスクリプト ランタイムを使用しており、何年もの更新プログラムが不足していたためです。

Unity には、Unity 2017.1 のリリースで、.NET 4.6 にアップグレードされ、C# 6.0 と互換性のある試験段階のバージョンのスクリプティング ランタイムが実装されました。 Unity 2018.1 では .NET 4.x と同等のランタイムは試験段階とは見なされなくなり、一方、古い .NET 3.5 と同等のランタイムはレガシ バージョンと見なされるようになりました。 Unity 2018.3 のリリースでは、Unity はアップグレードされたスクリプティング ランタイムを既定の選択として、さらに C# 7 まで更新することを計画しています。 このロード マップの詳細および最新の更新については、Unity の ブログ記事をお読みになるか、試験段階のスクリプティング プレビューのフォーラムを参照してください。 それまでは、以下のセクションで、.NET 4.x スクリプティング ランタイムで現在使用できる新機能を確認し学習してください。

前提条件

Unity で .NET 4.x スクリプティング ランタイムを有効にする

.NET 4.x スクリプティング ランタイムを有効にするには、次の手順を実行します。

  1. [編集] > [プロジェクトの設定] > [プレーヤー] > [その他の設定]の順に選択し、Unity Inspector で [プレーヤー設定] を開きます。

  2. [構成] 見出しで、[Api 互換性レベル] ドロップダウンをクリックし、[.NET Framework] を選択します。 Unity の再起動を促すダイアログが表示されます。

Screenshot showing the Select .NET 4.x equivalent.

.NET 4.x プロファイルと .NET Standard 2.1 プロファイルの選択

.NET 4.x と同等のスクリプティング ランタイムに切り替えたら、[PlayerSettings]\(プレーヤー設定\) ([Edit]\(編集\) > [Project Settings]\(プロジェクトの設定\) > [Player]\(プレーヤー\)) のドロップダウン メニューを使用して、[Api Compatibility Level]\(API の互換性レベル\) を指定することができます。 次の 2 つのオプションがあります。

  • .NET Standard 2.1。 このプロファイルは、.NET Foundation により発行されている .NET Standard 2.1 プロファイルと一致します。 Unity では、新しいプロジェクトに .NET Standard 2.1 を推奨しています。 これは .NET 4.x よりも小規模で、サイズに制限のあるプラットフォームで好都合です。 また、Unity では、Unity がサポートしているすべてのプラットフォームで、このプロファイルをサポートすることをコミットしています。

  • .NET Framework。 このプロファイルでは、最新の .NET 4 API にアクセスできます。 これには、.NET Framework クラス ライブラリで利用できるすべてのコードを含み、また .NET Standard 2.1 のプロファイルも同様にサポートしています。 .NET Standard 2.0 のプロファイルに含まれていない一部の API がプロジェクトで必要な場合は、.NET 4.x のプロファイルを使用します。 ただし、この API の一部は Unity のすべてのプラットフォームでサポートされていない場合があります。

これらのオプションの詳細については、Unity のブログ投稿を参照してください。

.NET 4.x API 互換性レベルの使用時にアセンブリ参照を追加する

[Api 互換性レベル] ドロップダウンの .NET Standard 2.1 設定を使用する場合、API プロファイルのすべてのアセンブリが参照され利用可能です。 ただし、より大きな .NET 4.x プロファイルを使用する場合、Unity に付属するアセンブリの一部は既定では参照できません。 これらの API を使用するには、手動でアセンブリ参照を追加する必要があります。 Unity に付属するアセンブリは、Unity エディターのインストールの MonoBleedingEdge/lib/mono ディレクトリを参照してください。

Screenshot showing the MonoBleedingEdge directory.

たとえば、.NET 4.x プロファイルを使用していて、HttpClient を使用したい場合、System.Net.Http.dll に対するアセンブリ参照を追加する必要があります。 これがない場合、アセンブリ参照がないことがコンパイラーによって通知されます。

Screenshot showing the missing assembly reference.

Visual Studio では Unity のプロジェクトが開かれるたびに、.csproj.sln のファイルが再生成されます。 そのため、プロジェクトを開くときに失われてしまうので、Visual Studio に直接アセンブリ参照を追加できません。 代わりに、csc.rsp という特別なテキスト ファイルを使用する必要があります。

  1. ご使用の Unity のプロジェクトのルートの Assets ディレクトリに csc.rsp という名前の新しいテキスト ファイルを作成します。

  2. 空のテキスト ファイルの最初の行に、-r:System.Net.Http.dll と入力しファイルを保存します。 "System.Net.Http.dll" は、参照が見つからない含まれているすべてのアセンブリに置き換えることができます。

  3. Unity エディターを再起動します。

.NET との互換性を利用する

Unity のユーザーは、NET 4.x スクリプティング ランタイムで、新しい C# の構文と言語機能に加え、従来の .NET 3.5 スクリプティング ランタイムとは互換性がない .NET パッケージの大規模なライブラリにアクセスできるようになります。

NuGet から Unity プロジェクトにパッケージを追加する

NuGet は、.NET のパケット マネージャーです。 NuGet は Visual Studio に統合されています。 ただし、Unity プロジェクトでは、プロジェクトを Unity で開くと、その Visual Studio プロジェクト ファイルが再生成され、必要な構成が元に戻されるため、NuGet パッケージを追加するには特別なプロセスが必要です。 NuGet から Unity のプロジェクトにパッケージを追加するには、次を実行します。

  1. NuGet を参照し、追加する互換性パッケージを探します (.NET Standard 2.0 または .NET 4.x)。 この例では、JSON を使用する場合の一般的なパッケージである Json.NET を .NET Standard 2.0 プロジェクトに追加する例を示します。

  2. [ダウンロード] ボタンをクリックします。

    Screenshot showing the download button.

  3. ダウンロードしたファイルを探し、拡張子を .nupkg から .zip に変更します。

  4. zip ファイル内の lib/netstandard2.0 ディレクトリに移動し、Newtonsoft.Json.dll ファイルをコピーします。

  5. Unity プロジェクトのルートの Assets フォルダーに、Plugins という名前の新しいフォルダーを作成します。 Plugins は Unity の特別なフォルダーの名前です。 詳細については、Unity のドキュメントを参照してください。

  6. Newtonsoft.Json.dll ファイルを Unity のプロジェクトの Plugins ディレクトリに貼り付けます。

  7. ご自分の Unity プロジェクトの Assets ディレクトリに link.xml という名前のファイルを作成し、次の XML を追加します。Unity のバイトコード除去プロセスで、IL2CPP プラットフォームへのエクスポート時に必要なデータが削除されないようにします。 この手順はこのライブラリに特有のものですが、リフレクションを同様な方法で使用するその他のライブラリで問題が発生する可能性があります。 詳細については、この記事について Unity のドキュメントを参照してください。

    <linker>
      <assembly fullname="System.Core">
        <type fullname="System.Linq.Expressions.Interpreter.LightLambda" preserve="all" />
      </assembly>
    </linker>
    

すべてを配置したら、Json.NET パッケージを使用できるようになります。

using Newtonsoft.Json;
using UnityEngine;

public class JSONTest : MonoBehaviour
{
    class Enemy
    {
        public string Name { get; set; }
        public int AttackDamage { get; set; }
        public int MaxHealth { get; set; }
    }
    private void Start()
    {
        string json = @"{
            'Name': 'Ninja',
            'AttackDamage': '40'
            }";

        var enemy = JsonConvert.DeserializeObject<Enemy>(json);

        Debug.Log($"{enemy.Name} deals {enemy.AttackDamage} damage.");
        // Output:
        // Ninja deals 40 damage.
    }
}

これは、依存関係がないライブラリを使用するシンプルな例です。 NuGet パッケージが他の NuGet パッケージに依存する場合、これらの依存関係を手動でダウンロードし、同じ方法でプロジェクトに追加する必要があります。

構文と言語の新機能

Unity の開発者は、更新されたスクリプトのランタイムを使用して、C# 8 と、多くの新しい言語機能と構文を使用できます。

自動プロパティ初期化子

Unity の .NET 3.5 スクリプティング ランタイムの自動プロパティ初期化子では、初期化されていないプロパティをすばやく簡単に定義できますが、初期化はスクリプトの他の場所で起こる必要があります。 .NET 4.x ランタイムでは、同じ行で自動プロパティを初期化できるようになりました。

// .NET 3.5
public int Health { get; set; } // Health has to be initialized somewhere else, like Start()

// .NET 4.x
public int Health { get; set; } = 100;

文字列の補間

以前の .NET 3.5 ランタイムでは、文字列の連結に面倒な構文が必要でした。 .NET 4.x ランタイムでは、$ 文字列補間機能によって、より直接的で読み取り可能な構文の文字列に式を挿入することができるようになりました。

// .NET 3.5
Debug.Log(String.Format("Player health: {0}", Health)); // or
Debug.Log("Player health: " + Health);

// .NET 4.x
Debug.Log($"Player health: {Health}");

式形式のメンバー

.NET 4.x ランタイムでより新しい C# 構文が利用可能になったことにより、関数の本文をラムダ式で置き換え、より簡潔にすることができます。

// .NET 3.5
private int TakeDamage(int amount)
{
    return Health -= amount;
}

// .NET 4.x
private int TakeDamage(int amount) => Health -= amount;

また、読み取り専用のプロパティに、式形式のメンバーを使用することもできます。

// .NET 4.x
public string PlayerHealthUiText => $"Player health: {Health}";

タスク ベースの非同期パターン (TAP)

非同期プログラミングでは、アプリケーションが応答しなくならないようにしながら、時間のかかる操作を行うことができます。 この機能では、時間のかかる操作の結果に依存するコードが続行される前に、その操作が終わるのを待つこともできます。 たとえば、ファイルの読み込みやネットワーク操作の完了を待つことができます。

Unity では、非同期プログラミングは通常コルーチンで実行されます。 ただし、C# 5 以降の .NET 開発で推奨されるのは、System.Threading.Taskasyncawait のキーワードを使用するタスクベースの非同期パターン (TAP) の方法となりました。 つまり、async 関数では、残りのアプリケーションのアップデートをブロックせずに、タスクの完了を await できます。

// Unity coroutine
using UnityEngine;
public class UnityCoroutineExample : MonoBehaviour
{
    private void Start()
    {
        StartCoroutine(WaitOneSecond());
        DoMoreStuff(); // This executes without waiting for WaitOneSecond
    }
    private IEnumerator WaitOneSecond()
    {
        yield return new WaitForSeconds(1.0f);
        Debug.Log("Finished waiting.");
    }
}
// .NET 4.x async-await
using UnityEngine;
using System.Threading.Tasks;
public class AsyncAwaitExample : MonoBehaviour
{
    private async void Start()
    {
        Debug.Log("Wait.");
        await WaitOneSecondAsync();
        DoMoreStuff(); // Will not execute until WaitOneSecond has completed
    }
    private async Task WaitOneSecondAsync()
    {
        await Task.Delay(TimeSpan.FromSeconds(1));
        Debug.Log("Finished waiting.");
    }
}

Unity 固有のニュアンスでは、TAP は開発者が考慮すべき複雑なテーマです。 そのため、TAP は Unity のコルーチンの代わりになるものとして普遍的ではありませんが、別のツールとして使用することはできます。 この機能の範囲はこの記事では説明しませんが、一般的なベスト プラクティスおよびヒントを以下に示します。

Unity で TAP を使用開始する

次に、Unity で TAP の使用を開始する場合のヒントを示します。

  • 待機することが期待されている非同期関数には、Task または Task<TResult> の戻り値の型が必要です。
  • タスクを戻す非同期関数には、その名前に "Async" のサフィックスが設定されている必要があります。 "Async" のサフィックスは、関数が常に待機している必要があることを示します。
  • 従来の同期コードから非同期関数を実行する関数には、async void の戻り値の型のみを使用します。 このような関数は、それだけでは待機できず、名前に "Async" のサフィックスがあるべきではありません。
  • Unity は UnitySynchronizationContext を使用し、既定でメインのスレッドで async 関数が実行されるのを保証します。 Unity の API には、メインのスレッド外ではアクセスできません。
  • Task.RunTask.ConfigureAwait(false) などのメソッドを使用し、バックグラウンド スレッドでタスクを実行することが可能です。 この手法は、パフォーマンスの向上のためにメインのスレッドからコストの高い操作をオフロードする場合に便利です。 ただし、バックグラウンド スレッドを使用すると、競合状態など、デバッグが困難な問題につながる可能性があります。
  • Unity の API には、メインのスレッド外ではアクセスできません。
  • スレッドを使用するタスクは、Unity WebGL のビルドではサポートされていません。

コルーチンと TAP 間の違い

コルーチンと TAP / async-await の間には重要な違いがあります。

  • コルーチンは、値を返すことはできませんが、Task<TResult> はできます。
  • yield は、try-catch ステートメントに入れることができないので、コルーチンでのエラー処理が困難になります。 ただし、TAP で try-catch は動作します。
  • Unity のコルーチンの機能は、MonoBehaviour から派生しないクラスでは利用できません。 TAP はそのようなクラスの非同期プログラミングに最適です。
  • 現時点では、Unity で完全にコルーチンに代わるものとして TAP を提案していません。 プロファイリングは、いかなるプロジェクトでも、1 つのアプローチに対する他のアプローチの特定の結果を知ることのできる唯一の方法です。

nameof 演算子

nameof 演算子は、変数、型、メンバーの文字列名を取得します。 nameof は、エラーの記録、列挙型の文字列名の取得などで便利です。

// Get the string name of an enum:
enum Difficulty {Easy, Medium, Hard};
private void Start()
{
    Debug.Log(nameof(Difficulty.Easy));
    RecordHighScore("John");
    // Output:
    // Easy
    // playerName
}
// Validate parameter:
private void RecordHighScore(string playerName)
{
    Debug.Log(nameof(playerName));
    if (playerName == null) throw new ArgumentNullException(nameof(playerName));
}

呼び出し元情報属性

呼び出し元情報属性では、メソッドの呼び出し元の情報を得ることができます。 呼び出し元情報属性では、使用する各パラメーターの既定値を提供する必要があります。

private void Start ()
{
    ShowCallerInfo("Something happened.");
}
public void ShowCallerInfo(string message,
        [System.Runtime.CompilerServices.CallerMemberName] string memberName = "",
        [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "",
        [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
{
    Debug.Log($"message: {message}");
    Debug.Log($"member name: {memberName}");
    Debug.Log($"source file path: {sourceFilePath}");
    Debug.Log($"source line number: {sourceLineNumber}");
}
// Output:
// Something happened
// member name: Start
// source file path: D:\Documents\unity-scripting-upgrade\Unity Project\Assets\CallerInfoTest.cs
// source line number: 10

Using static

Using static では、そのクラス名を入力せずに、静的な関数を使用できます。 using static では、同じクラスからのいくつかの静的な関数を使用する必要がある場合、領域と時間を節約できます。

// .NET 3.5
using UnityEngine;
public class Example : MonoBehaviour
{
    private void Start ()
    {
        Debug.Log(Mathf.RoundToInt(Mathf.PI));
        // Output:
        // 3
    }
}
// .NET 4.x
using UnityEngine;
using static UnityEngine.Mathf;
public class UsingStaticExample: MonoBehaviour
{
    private void Start ()
    {
        Debug.Log(RoundToInt(PI));
        // Output:
        // 3
    }
}

IL2CPP に関する考慮事項

iOS などのプラットフォームにゲームをエクスポートする場合、Unity ではその IL2CPP エンジンを使用して IL を C++ コードに "トランスパイル" します。その後、それはターゲット プラットフォームのネイティブ コンパイラを使用して、コンパイルされます。 このシナリオには、リフレクションの部分や dynamic キーワードの用途など、サポートされていないいくつかの .NET 機能があります。 この機能の使用は、ご自分のコードで制御できますが、サードパーティー製の DLL および SDK が Unity と IL2CPP を考慮して記述されていない場合、問題が発生するおそれがあります。 このトピックの詳細については、Unity のサイトでスクリプティングの制限に関するドキュメントを参照してください。

また、上の Json.NET の例で示したとおり、Unity は IL2CPP のエクスポート処理時に、未使用のコードを除去しようとします。 リフレクションを使用するライブラリでこの処理が問題となることは通常はありませんが、実行時に呼び出されるプロパティまたはメソッドが偶発的に除去される場合があり、これはエクスポート時には判断できません。 これらの問題を解決するには、削除プロセスを実行しないアセンブリおよび名前空間の一覧が記述された link.xml ファイルをプロジェクトに追加します。 詳細については、バイトコードの削除に関する Unity のドキュメントを参照してください。

.NET 4.x の Unity のサンプル プロジェクト

このサンプルには、いくつかの .NET 4.x の機能例が含まれています。 プロジェクトをダウンロードしたり、GitHub でソース コードを参照することができます。

その他のリソース