在 Unity 中使用 .NET 4.x

C# 和 .NET (以 Unity 指令碼為基礎的技術) 持續接收到更新,因為 Microsoft 一開始是在 2002 年發行它們。 但 Unity 開發人員可能不知道新增至 C# 語言和.NET Framework的新功能穩定串流,因為在 Unity 2017.1 之前,Unity 已經使用 .NET 3.5 對等腳本執行時間,遺漏了更新的年份。

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 指令碼執行階段

若要在 Unity 中啟用 .NET 4.x 指令碼執行階段,請採取下列步驟:

  1. 選取 [ 編輯 > 專案設定 > ] [播放程式其他設定],以在 Unity Inspector 中開啟 PlayerSettings >

  2. 在 [組態] 標題下,按一下[Api 相容性層級] 下拉式清單,然後選取[.NET Framework]。 系統會提示您重新開機 Unity。

顯示 [選取 .NET 4.x 對等專案] 的螢幕擷取畫面。

選擇 .NET 4.x 和 .NET Standard 2.1 設定檔

切換至 .NET 4.x 對等腳本執行時間之後,您可以使用PlayerSettings ([編輯 > 專案設定 > 播放機]) 中的下拉式功能表來指定Api 相容性層級。 有兩個選項:

  • .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 設定檔。 不過,Unity 的所有平台上可能都不支援此 API 的某些部分。

您可以在 Unity 的部落格文章中深入了解這些選項。

在使用 .NET 4.x API 相容性層級時新增組件參考

[Api 相容性層級 ] 下拉式清單中使用 .NET Standard 2.1 設定時,會參考並使用 API 設定檔中的所有元件。 不過,使用較大的 .NET 4.x 設定檔時,預設不會參考隨附于 Unity 的某些元件。 若要使用這些 API,您必須手動新增組件參考。 您可以在 Unity 編輯器安裝的 MonoBleedingEdge/lib/mono 目錄中檢視 Unity 隨附的組件:

顯示 MonoBleedingEdge 目錄的螢幕擷取畫面。

例如,如果您使用 .NET 4.x 設定檔,並且想要使用 HttpClient,則必須新增 System.Net.Http.dll 的組件參考。 否則,編譯器會通知您遺漏組件參考:

顯示遺漏元件參考的螢幕擷取畫面。

Visual Studio 會在每次開啟時重新產生 Unity 專案的 .csproj.sln 檔案。 因此,您無法直接在 Visual Studio 中新增元件參考,因為它們會在重新開啟專案時遺失。 相反地,必須使用名為 csc.rsp 的特殊文字檔:

  1. 在 Unity 專案的根資產目錄中,建立名為csc.rsp的新文字檔。

  2. 在空文字檔中的第一行,輸入:-r:System.Net.Http.dll,然後儲存檔案。 您可以將 "System.Net.Http.dll" 取代為任何可能遺漏參考的內含組件。

  3. 重新啟動 Unity 編輯器。

利用 .NET 相容性

除了新 C# 語法和語言功能之外,.NET 4.x 指令碼執行階段還可讓 Unity 使用者存取與舊版 .NET 3.5 指令碼執行階段不相容之 .NET 套件的超大型程式庫。

將套件從 NuGet 新增至 Unity 專案

NuGet 是 .NET 的套件管理員。 NuGet 整合到 Visual Studio。 不過,Unity 專案需要特殊程式才能新增 NuGet 套件,因為當您在 Unity 中開啟專案時,會重新產生其 Visual Studio 專案檔,並復原必要的組態。 若要將套件從 NuGet 新增至 Unity 專案:

  1. 瀏覽 NuGet 以找到您要新增的相容套件 (.NET Standard 2.0 或 .NET 4.x)。 此範例示範如何將 Json.NET (使用 JSON 的熱門套件) 新增至 .NET Standard 2.0 專案。

  2. 按一下 [ 下載] 按鈕:

    顯示下載按鈕的螢幕擷取畫面。

  3. 找到下載的檔案,並將副檔名從 .nupkg 變更為 .zip

  4. 在 ZIP 檔案內,瀏覽至 lib/netstandard2.0 目錄,並複製 Newtonsoft.Json.dll 檔案。

  5. 在 Unity 專案的根 Assets 資料夾中,建立名為 Plugins 的新資料夾。 Plugins 是 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,以及一系列新的語言功能和語法。

Auto 屬性初始設定式

在 Unity 的 .NET 3.5 指令碼執行階段中,auto-property 語法可讓您輕鬆地快速定義未初始化的屬性,但必須在指令碼的其他位置初始化。 現在使用 .NET 4.x 執行階段,就可以初始化同一行的 auto-properties:

// .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# 語法,Lambda 運算式可以取代函數主體,讓它們更為簡潔:

// .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 開發中慣用的非同步程式設計方法已是搭配使用 asyncawait 關鍵字與 System.Threading.Task工作非同步模式 (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.");
    }
}

TAP 是一個複雜主題,而開發人員應該考慮其 Unity 特定細微差別。 因此,TAP 不是 Unity 中協同程式的通用取代專案;不過,這是另一個要使用的工具。 此功能的範圍不在本文範圍內,但在下面提供一些一般最佳做法和秘訣。

利用 Unity 開始使用 TAP 的參考

這些秘訣可協助您在 Unity 中開始使用 TAP:

  • 要等待的非同步函數應該有傳回型別 TaskTask<TResult>
  • 傳回工作的非同步函數應該在其名稱附加尾碼 "Async"。 "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 語句中,使協同程式處理錯誤變得困難。 不過,try-catch 可與 TAP 搭配運作。
  • 在未衍生自 MonoBehaviour 的類別中,無法使用 Unity 的協同程式功能。 TAP 很適合在這類類別中執行非同步程式設計。
  • 此時,Unity 不建議使用 TAP 完全取代協同程式。 分析是知道其中一種方法與任何指定專案另一種方法之特定結果的唯一方式。

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++ 程式碼,然後使用目標平臺的原生編譯器進行編譯。 在此案例中,不支援數個 .NET 功能,例如反映的一部分,以及關鍵字的使用方式 dynamic 。 雖然您可以在自己的程式碼中使用這些功能來控制,但您可能會遇到使用未以 Unity 和 IL2CPP 撰寫的協力廠商 DLL 和 SDK 的問題。 如需本文的詳細資訊,請參閱 Unity 網站上的 腳本限制 檔。

此外,如上述 Json.NET 範例所述,Unity 將嘗試在 IL2CPP 匯出程序期間去除未使用的程式碼。 雖然此程式通常不是問題,但使用反映的程式庫可能會意外移除在執行時間呼叫的屬性或方法,而無法在匯出時間決定。 若要修正這些問題,請將 link.xml 檔案新增至包含元件和命名空間清單的專案,以不執行等量程式。 如需詳細資訊,請參閱 Unity 的位元組程式碼等量檔

.NET 4.x 範例 Unity 專案

此範例包含數個 .NET 4.x 功能範例。 您可以在 GitHub 下載專案或檢視原始程式碼。

其他資源