在 Unity 中使用 .NET 4.x

C# 和 .NET 作为 Unity 脚本编写的基础技术,自微软于 2002 年首次发布它们以来一直在持续更新。 但 Unity 开发者可能并未意识到,C# 语言和 .NET Framework 一直在持续新增功能,因为在 Unity 2017.1 之前,Unity 一直使用相当于 .NET 3.5 的脚本运行时,因此错过了多年的更新。

随着 Unity 2017.1 的发布,Unity 引入了其脚本运行时的实验版本升级到 .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 检查器中,通过选择 编辑>项目设置>Player>其他设置 打开 Player 设置。

  2. “配置”标题下,单击“Api 兼容性级别”下拉列表,然后选择.NET框架。 系统将提示你重启 Unity。

显示 Select .NET 4.x equivalent 的屏幕截图。

在 .NET 4.x 和 .NET Standard 2.1 配置文件之间进行选择

切换到 .NET 4.x 等效脚本运行时后,可以使用 PlayerSettings(>)中的下拉菜单指定 Api 兼容性级别。 存在两个选项:

  • .NET标准 2.1。 此配置文件与 .NET Foundation 发布的 .NET Standard 2.1 配置文件匹配。 Unity 建议新项目使用 .NET Standard 2.1。 它小于 .NET 4.x,这对受大小约束的平台有利。 此外,Unity 还承诺在 Unity 支持的所有平台上支持此配置文件。

  • .NET框架。 此配置文件提供对最新 .NET 4 API 的访问权限。 它包括 .NET Framework 类库中提供的所有代码,并支持.NET标准 2.1 配置文件。 如果项目需要.NET Standard 2.0 配置文件中不包含的 API 的一部分,请使用 .NET 4.x 配置文件。 但是,此 API 的某些部分可能在所有 Unity 平台上都不受支持。

若要了解有关这些选项的详细信息,请参阅 Unity 的 博客文章

使用 .NET 4.x Api 兼容性级别时添加程序集引用

当在 API Compatibility Level 下拉列表中选择 .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 项目的根 资产 文件夹中,创建名为 Plugins 的新文件夹。 插件是 Unity 中的特殊文件夹名称。 有关详细信息,请参阅 Unity 文档

  6. Newtonsoft.Json.dll 文件粘贴到 Unity 项目的 插件 目录中。

  7. 在 Unity 项目的 Assets 目录中创建一个名为 link.xml 的文件,并添加以下 XML,确保在导出到 IL2CPP 平台时 Unity 的字节码剥离过程不会删除必要的数据。 虽然此步骤特定于此库,但可能会遇到其他库的问题,这些库使用反射的方式类似。 有关详细信息,请参阅本文中的 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# 语法,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 中协同例程的通用替代项;但是,这是另一个要使用的工具。 此功能的范围超出了本文的范围,但下面提供了一些常规最佳做法和提示。

将 TAP 与 Unity 配合使用的入门参考

这些提示可帮助你开始使用 Unity 中的 TAP:

  • 要等待的异步函数应具有返回类型 TaskTask<TResult>
  • 返回任务的异步函数应将后缀 “Async” 追加到其名称中。 “Async”后缀有助于指示应始终等待函数。
  • 仅对那些从传统同步代码中调用异步函数的函数使用 async void 返回类型。 此类函数本身无法等待,不应在其名称中包含“Async”后缀。
  • Unity 使用 UnitySynchronizationContext 来确保异步函数默认在主线程上运行。 在主线程之外无法访问 Unity API。
  • 可以使用类似 Task.RunTask.ConfigureAwait(false)的方法在后台线程上运行任务。 此方法可用于从主线程卸载昂贵的操作以提高性能。 但是,使用后台线程可能会导致难以调试的问题,例如竞态条件
  • 在主线程之外无法访问 Unity API。
  • Unity WebGL 构建版本不支持使用线程的任务。

协同例程与 TAP 之间的差异

协同例程与 TAP/async-await 之间存在一些重要差异:

  • 协程不能返回值,但 Task<TResult> 可以。
  • 不能将 yield 放在 try-catch 语句中,这使得在协程中处理错误变得困难。 不过,try-catch 可与 TAP 配合使用。
  • Unity 的协程功能无法在未派生自 MonoBehaviour 的类中使用。 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

使用静态方式

使用静态 功能,无需键入其类名即可使用静态函数。 如果使用静态函数,如果需要使用同一类中的多个静态函数,则可以节省空间和时间:

// .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 Project

此示例包含多个 .NET 4.x 功能的示例。 可以在GitHub下载项目或查看源代码。

其他资源