历史
- 在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。即使用 async
和 await
关键字,以及使用 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.Run
和Task.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
参考资料
- https://docs.microsoft.com/en-us/visualstudio/cross-platform/unity-scripting-upgrade?view=vs-2019
- https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-version-history
- https://docs.microsoft.com/en-us/archive/blogs/appconsult/unity-coroutine-tap-en-us
- https://docs.microsoft.com/en-us/dotnet/standard/asynchronous-programming-patterns/task-based-asynchronous-pattern-tap
- http://www.stevevermeulen.com/index.php/2017/09/using-async-await-in-unity3d-2017/