Unity .NET 4.x runtime

历史

  • 在Unity 2017.1之前,Unity使用的是一个相当于.NET 3.5的runtime,多年未更新。
  • Unity 2017.1中,Unity引入了一个实验性的runtime版本,升级到.NET 4.6, 兼容C# 6。
  • Unity 2018.1中,.NET 4.x不再是实验性的了,同时老的.NET 3.5 runtime被标记为legacy。
  • Unity 2018.3中,.NET 4.x runtime被设置为默认。同时支持了C# 7。

现状

Unity 2019中已经不能再切换到老的.NET 3.5 runtime了,唯一的runtime就是.NET 4.x

但是可以选择Api Compatibility Level

  • .NET Standard 2.0: 这个profile匹配.NET组织发布的.NET Standard 2.0 profile
    Unity建议新项目使用.NET Standard 2.0。.NET Standard 2.0比.NET 4.x小一些。
    且Unity承诺在所有的平台上支持这个profile。
  • .NET 4.x: 这个profile提供了最新的.NET 4 API接口,包含了.NET Framework class libraries中的所有代码,当然也支持.NET Standard 2.0 profiles。如果你的项目使用了没有包含在.NET Standard 2.0 profile中的API时,使用.NET 4.x profile。但是某些API可能不是所有的平台都支持。

当使用.NET 4.x profile时,添加assembly引用

  • 当使用NET Standard 2.0时,API profile中的所有assemblies都是默认被引用且可用的。但是当使用.NET 4.x profile时,某些assemblies没有被Unity默认引用。此时需要手动添加assembly引用
  • 如果缺少assembly的引用,在Visual Studio中就会报错。
  • 因为Visual Studio打开Unity工程时会重新生成.csproj和.sln文件,因为不能直接在Visual Studio中添加assembly引用,否则再次开启工程时会丢失这些修改。
  • 正确的做法是在Unity工程的Assets根目录创建一个文本文件,命名为mcs.rsp。其中填入 -r:System.Net.Http.dll这样的行来添加引用。然后重启Unity。

使用NuGet为Unity工程添加包

NuGet是.NET的包管理器,Visual Studio中集成了NuGet。但是因为Unity工程打开时会刷新工程文件,所以Unity使用NuGet需要特殊的步骤。
以下以Json.NET包为例(https://www.nuget.org/packages/Newtonsoft.Json/):

  • 浏览NuGet找到你要使用的兼容的包(兼容.NET Standard 2.0或.NET 4.x)
  • 点击下载
  • 将下载下来的文件的后缀从.nupkg改为.zip
  • 将.zip中的dll文件(例如lib/netstandard2.0/Newtonsoft.Json.dll) 拷贝到Unity工程的 Assets/Plugins目录中。
  • 在Unity工程的Assets目录中创建一个link.xml文件。这个文件中的内容确保导出代码到IL2CPP平台时不会去掉必须的数据。
<linker>
  <assembly fullname="System.Core">
    <type fullname="System.Linq.Expressions.Interpreter.LightLambda" preserve="all" />
  </assembly>
</linker>
  • 完成上述步骤后就可以在C#中使用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.
    }
}
  • 这个例子的Json.NET包不依赖于其他包。如果安装的NuGet包依赖于其他包需要下载这些依赖包并用同样的方法手动添加到工程中。

.NET 4.x runtime新增的语法特性

自动属性初始化

// .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
Debug.Log(String.Format("Player health: {0}", Health)); // or
Debug.Log("Player health: " + Health);

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

lambda表达式成员函数(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}";

基于Task的异步模式(TAP: Task-based Asynchronous Pattern)

在Unity中,异步编程以前都是通过coroutines实现的。从C# 5开始,.NET中首选的异步编程方式是使用TAP。即使用 asyncawait 关键字,以及使用 System.Threading.Task。简而言之,在一个 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要点:

  • 需要被await的异步函数应该返回Task或Task<TResult>类型。
  • 返回Task的异步函数需要使用在函数名中增加后缀"Async"。"Async"后缀表示这个函数总是需要被awaited。
  • 如果一个函数是从同步代码中开始触发出异步调用,这个函数需要使用async void返回类型,且不需要使用"Async"后缀。该函数本身不能被awaited。
  • Unity使用UnitySynchronizationContext来保证异步函数在主线程中运行。主线程之外是不能访问Unity API的。
  • 可以使用Task.RunTask.ConfigureAwait(false)来在后台线程中执行方法。
  • WebGL版本不支持使用线程的Tasks。

协程和TAP的区别

  • Coroutines不能返回值,但是Task<TResult>可以。
  • try-catch语句中不能使用 yield,但是try-catch可以和TAP一起工作。
  • Unity的协程不能在非MonoBehaviour子类中使用,但是TAP可以。
  • 目前Unity不建议使用TAP全面代替coroutines。要知道哪种方式更好,Profiling是唯一的方法。

nameof 操作符

nameof操作符可获取变量,类型和成员的字符串名字,当log错误时很好用。
这个例子中获取了枚举值的名字以及获取了函数形参变量的名字。

// 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));
}

Caller info attributes

提供函数调用信息,用法如下:

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 类名; 之后,就可以在调用类的静态方法时省略类名。

// .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关键字。如果是你自己的代码,你可以避免使用这些特性,但是第三方库有可能并没有考虑到IL2CPP。参考文档:https://docs.unity3d.com/Manual/ScriptingRestrictions.html
另外在IL2CPP导出时,Unity会试图去除没有使用到的代码。如果使用到了反射,就有可能去除运行时调用的代码,这些代码在导出时并不能确定会用到。为了处理这个问题,需要在 link.xml 中添加不进行strip的assembly和namespace。参考文档:https://docs.unity3d.com/Manual/ManagedCodeStripping.html

参考资料

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342