·來源于唐老獅的視頻教學,僅作記錄和感悟記錄,方便日后復習或者查找
一.C#版本與Unity的關系
1.各Unity版本支持的C#版本
更多信息可以在Untiy官網說明查看
https://docs.unity3d.com/2020.3/Documentation/Manual/CSharpCompiler.html(這個好像要科學上網)
2.為什么不同版本Unity支持的C#版本不同
是因為不同版本的Unity使用的 C#編譯器 和 腳本運行時版本不同
3.不同版本C#的意義
主要就是可以使用新功能,來節約代碼量,讓代碼更簡單直觀簡潔
4.Unity中調整.Net API 兼容級別
可以去工程中選擇對應的兼容級別
目前新版本(這里是2022.3.62f1c1)分為了.Net Standard 2.1 和 .Net Framework
.Net Framework(特殊需求時):
- 具備較為完整的.Net API,甚至包含了一些無法跨平臺的API
- 如果你的應用主要針對Windows平臺,并且會使用到.Net Standard 2.0中沒有的功能時會選擇使用它
- 包體更大
.Net Standard 2.1(常規情況下):
- ?是一個.Net標準API集合,相對.Net Framework包含更少的內容,可以減小最終可執行文件大小
- ?它具有更好的跨平臺支持
- .Net Standard 2.1?配置文件大小是.Net Framework配置文件的一半
通常情況下,為了跨平臺和更小的包體,我們都選擇使用默認的.Net Standard 2.1
二.C#1~4
1.Unity最低支持的C#版本
2.C#1~4的功能和語法
這里只要提及一些Unity開發中常用的功能與特性
3.補充內容--命名與可選參數
可以讓我們在調用函數的時候,指定傳入的數據是傳入到哪一個參數上的。例如如下:
public void Test(int i, float f, bool b)
{}public void Test2(int i , bool b = true, string s = "123")
{}//有了命名參數,我們將不用匹配參數在所調用方法中的順序
//每個參數可以按照參數名字進行指定
Test(1, 1.2f, true);
Test(f: 3.3f, i: 5, b: false);
Test(b: false, f: 3.4f, i: 3);//命名參數可以配合可選參數使用,讓我們做到跳過其中的默認參數直接賦值后面的默認參數
Test2(1, true, "234");
Test2(1, s: "234");
這樣的好處是:
①可以跳過一些默認參數去賦值后面的參數
②通過好處一可以讓我們少寫一些代碼與重載函數
4.補充內容--動態類型
關鍵詞:dynamic
作用:能夠接收任意類型的不為NULL的變量,類似于Object,但是不是把他裝箱為Object,而是直接動態地解析成該變量的類型,我們可以在代碼后面直接使用該變量去賦值或者調用里面的成員變量與方法(不過不會有提示,需要我們自己保證拼寫沒有出錯)。更詳細的解釋如下:
注意事項:
- 使用dynamic功能 需要將Unity的.Net API兼容級別切換為.Net Framework
- IL2CPP 不支持 C# dynamic 關鍵字。它需要 JIT 編譯,而 IL2CPP 無法實現
- 動態類型是無法自動補全方法的,我們在書寫時一定要保證方法的拼寫正確性
- 該功能我們只做了解,不建議大家使用
使用例子:
using UnityEngine;public class TestClass
{public string name = "Test Me";public void PrintName(){Debug.Log(name);}
}public class Lesson3 : MonoBehaviour
{// Start is called before the first frame updatevoid Start(){//不確定要賦值的類型的時候使用dynamic i = 1;print(i);//可以接受一個類類型dynamic t = new TestClass();//去調用其中的方法t.PrintName(); print(t.GetType()); }
}
好處:
- 可以讓我不用自己去關心和處理類型轉化相關的事項,節約代碼。
- 當不確定對象類型,但是確定對象成員時,可以使用動態類型通過反射處理某些功能時,也可以考慮使用動態類型來替換它
三.C#5
1.C#5的新功能和新語法
①調用方信息特性(C#進階套課——特性)
using System.Runtime.CompilerServices;public static void Log(string message,[CallerMemberName] string caller = null,[CallerFilePath] string file = null,[CallerLineNumber] int line = 0)
{Console.WriteLine($"【{message}】| 調用者:{caller} | 文件:{file} | 行號:{line}");
}// 調用示例
public class Demo
{public void Test(){Log("發生錯誤"); // 編譯器自動注入調用信息}
}
②異步方法async和await
2.回顧內容--線程
Unity中線程使用的注意事項:
- Unity支持多線程
- Unity中開啟的多線程不能使用主線程中的對象(不能使用Unity的API)
- Unity中開啟多線程后一定記住關閉(一般在一個對象中創建線程后,在OnDestory中關閉線程)
線程的使用例子:
//創建一個新線程對象
t = new Thread(()=> {//線程中進行一個死循環while (true){print("123");//每次運行到這里的時候讓線程休眠1秒Thread.Sleep(1000);}
});
//啟用線程
t.Start();//下面是主線程的代碼
print("主線程執行");
3.補充內容--線程池
3.1.原理與特性
命名空間:System.Threading
類名:ThreadPool(線程池)
目的:
- 在多線程的應用程序開發中,頻繁的創建刪除線程會帶來性能消耗,產生內存垃圾
- 為了避免這種開銷C#推出了 線程池ThreadPool類
大致原理:
- ThreadPool中有若干數量的線程,如果有任務需要處理時,會從線程池中獲取一個空閑的線程來執行任務
- 任務執行完畢后線程不會銷毀,而是被線程池回收以供后續任務使用
- 當線程池中所有的線程都在忙碌時,又有新任務要處理時,線程池才會新建一個線程來處理該任務,
- 如果線程數量達到設置的最大值,任務會排隊,等待其他任務釋放線程后再執行
優點:
- 線程池能減少線程的創建,節省開銷,可以減少GC垃圾回收的觸發
缺點:
- 不能控制線程池中線程的執行順序,也不能獲取線程池內線程取消/異常/完成的通知
3.2.內部的方法使用
//1.獲取可用的工作線程數和I/O線程數
int num1;
int num2;
ThreadPool.GetAvailableThreads(out num1, out num2);
print(num1);
print(num2);//2.獲取線程池中工作線程的最大數目和I/O線程的最大數目ThreadPool.GetMaxThreads(out num1, out num2);print(num1);print(num2);
//3.設置線程池中可以同時處于活動狀態的 工作線程的最大數目和I/O線程的最大數目
// 大于次數的請求將保持排隊狀態,知直到線程池線程變為可用
// 更改成功返回true,失敗返回false
if(ThreadPool.SetMaxThreads(20, 20))
{print("更改成功");
}
//5.設置 工作線程的最小數目和I/O線程的最小數目
if(ThreadPool.SetMinThreads(5, 5))
{print("設置成功");
}//4.獲取線程池中工作線程的最小數目和I/O線程的最小數目
ThreadPool.GetMinThreads(out num1, out num2);
print(num1);
print(num2);
最小線程數是:在初始的時候一直保持至少有這么多個線程是在運行的(即使它沒有執行什么任務,但是一旦有任務可以馬上用它來執行)
最大線程數是:如果當前線程數已經達到了最大線程數,當有新任務的時候,就會先排隊等待線程資源被釋放出來
//6.將方法排入隊列以便執行,當線程池中線程變得可用時執行//ThreadPool.QueueUserWorkItem((obj) =>
//{
// print(obj);
// print("開啟了一個線程");
//}, "123452435345");for (int i = 0; i < 10; i++)
{//第一個參數傳入一個回調函數,第二個參數傳入一個要在回調中使用的參數值(傳給obj了)ThreadPool.QueueUserWorkItem((obj) =>{print("第" + obj + "個任務");}, i);
}print("主線程執行");
4.補充內容----Task任務類
4.1.認識Task
命名空間:System.Threading.Tasks
類名:Task
說明:Task就是任務的意思,它繼承了線程池的優點的同時改進了線程池的缺點,即創建的任務能夠被精確地控制。它是基于線程池的優點的一個封裝類,可以幫助我們更加高效地完成多線程相關的開發。
4.2.創建Task的方式
4.2.1.創建無返回值的Task的三種方式
//1.通過new一個Task對象傳入委托函數并啟動Task t1 = new Task(() => {int i = 0;while (isRuning) {print("方式一:" + i);++i;Thread.Sleep(1000);}});t1.Start();//2.通過Task中的Run靜態方法傳入委托函數Task t2 = Task.Run(() => {int i = 0;while (isRuning) {print("方式二:" + i);++i;Thread.Sleep(1000);}});//3.通過Task.Factory中的StartNew靜態方法傳入委托函數Task t3 = Task.Factory.StartNew(() => {int i = 0;while (isRuning) {print("方式三:" + i);++i;Thread.Sleep(1000);}});
①new一個Task對象,傳入委托函數
②使用Task.Run靜態方法傳入委托函數
③使用Task.Factory.StartNew()傳入委托函數
4.2.2.創建有返回值的Task的三種方式
//1.通過new一個Task對象闖入委托函數并啟動t1 = new Task<int>(() => {int i = 0;while (isRuning) {print("方式一:" + i);++i;Thread.Sleep(1000);}return 1;});t1.Start();//2.通過Task中的Run靜態方法傳入委托函數t2 = Task.Run<string>(() => {int i = 0;while (isRuning) {print("方式二:" + i);++i;Thread.Sleep(1000);}return "1231";});//3.通過Task.Factory中的StartNew靜態方法傳入委托函數t3 = Task.Factory.StartNew<float>(() => {int i = 0;while (isRuning) {print("方式三:" + i);++i;Thread.Sleep(1000);}return 4.5f;});
①方式和無返回值的基本方法差不多:new,Task.Run(),Task.Factory.RunNew()
②不同的在于這些方法后面要多加一個用于告訴返回值是多少的泛型,并且傳入的委托函數中要有相對應的返回值
//獲取返回值
//注意:
//Resut獲取結果時會阻塞線程
//即如果task沒有執行完成
//會等待task執行完成獲取到Result
//然后再執行后邊的代碼,也就是說 執行到這句代碼時 由于我們的Task中是死循環
//所以主線程就會被卡死
print(t1.Result);
print(t2.Result);
print(t3.Result);print("主線程執行");
①通過task變量的值.Result獲取里面返回的值
②要注意一旦使用這個方法的時候,如果該線程中還沒有計算完成返回值的話就會一直阻塞主線程的執行
4.3.同步執行Task
默認情況下各個線程和主線程之間肯定是異步執行的
同步執行Task的意思大概可以粗暴地理解成把Task中的任務再放回主線程中執行。
//如果你希望Task能夠同步執行
//只需要調用Task對象中的RunSynchronously方法
//注意:需要使用 new Task對象的方式,因為Run和StartNew在創建時就會啟動Task t = new Task(() => {Thread.Sleep(1000);print("哈哈哈");
});
//t.Start();
t.RunSynchronously();
print("主線程執行");
//不Start 而是 RunSynchronously
①在開始線程的時候用的不是Start()而是RunSynchronously()
②注意這個只能用在用new一個Task的時候使用,因為另外兩種方法會直接默認Start()線程
4.4.Task中線程阻塞的方式(任務阻塞)
即讓一個線程執行完了之后才能夠繼續執行后面的內容
當我們需要在某幾個任務完成之后再去執行其他任務的時候可以使用這個方法
//1.Wait方法:等待任務執行完畢,再執行后面的內容
Task t1 = Task.Run(() =>
{for (int i = 0; i < 5; i++){print("t1:" + i);}
});Task t2 = Task.Run(() =>
{for (int i = 0; i < 20; i++){print("t2:" + i);}
});
t2.Wait();//2.WaitAny靜態方法:傳入任務中任意一個任務結束就繼續執行
//Task.WaitAny(t1, t2);//3.WaitAll靜態方法:任務列表中所有任務執行結束就繼續執行
//Task.WaitAll(t1, t2);//print("主線程執行");
①可以直接讓t1.wait()
②可以用Task中的靜態方法.WaitAny(),傳入可變參數個任務,等待其中任意一個任務完成之后就繼續執行
③可以用Task中的靜態方法.WaitAll(),傳入可變參數個任務,等待所有這些任務完成之后就繼續執行
4.5.Task中完成后續其他Task(任務延續)
即讓Task按一定順序接續執行
當我們需要讓某幾個任務之間按一定順序完成的時候就要使用這個方法
//1.WhenAll靜態方法 + ContinueWith方法:傳入任務完畢后再執行某任務
Task.WhenAll(t1, t2).ContinueWith((t) => {print("一個新的任務開始了");int i = 0;while (isRuning) {print(i);++i;Thread.Sleep(1000);}
});Task.Factory.ContinueWhenAll(new Task[] { t1, t2 }, (t) => {print("一個新的任務開始了");int i = 0;while (isRuning) {print(i);++i;Thread.Sleep(1000);}
});//2.WhenAny靜態方法 + ContinueWith方法:傳入任務只要有一個執行完畢后再執行某任務
Task.WhenAny(t1, t2).ContinueWith((t) => {print("一個新的任務開始了");int i = 0;while (isRuning) {print(i);++i;Thread.Sleep(1000);}
});Task.Factory.ContinueWhenAny(new Task[] { t1, t2 }, (t) => {print("一個新的任務開始了");int i = 0;while (isRuning) {print(i);++i;Thread.Sleep(1000);}
});
①可以通過Task的靜態類的.WhenAll和.WhenAny中的.ContinueWith方法,或者Task.Factory中的靜態方法.ContinueWhenAll()和.ContinueWhenAny()來指定所有任務完成后或者其中任意一個任務完成之后再去執行的任務
②委托函數中傳入的參數t是一個Task類型的,它是里面包含了前置的那些任務的執行狀態之類的變量。比如可以按如下方式使用:
.ContinueWith(t => {if (t.IsFaulted) {Debug.LogError($"任務失敗: {t.Exception}");return;}// 安全執行后續邏輯...
});
4.6.取消Task執行
方法一:通過加入bool標識 控制線程內死循環的結束
方法二:通過CancellationTokenSource取消標識源類 來控制
CancellationTokenSource對象可以達到延遲取消、取消回調等功能
//聲明一個取消標識源類c = new CancellationTokenSource();//延遲取消c.CancelAfter(5000);//取消回調c.Token.Register(() =>{print("任務取消了");});Task.Run(() =>{int i = 0;//用里面的是否被撤銷的屬性來控制循環while (!c.IsCancellationRequested){print("計時:" + i);++i;Thread.Sleep(1000);}});//延遲取消// Update is called once per frame
void Update()
{if(Input.GetKeyDown(KeyCode.Space)){//調用取消標識源中的取消方法來取消任務c.Cancel();}
}
①可以在一開始的時候設定一個控制取消的延遲時間
②可以在取消的時候觸發一個取消回調事件
③可以用是否撤銷的屬性來控制線程是否被撤銷
5.異步方法
5.1.什么是同步和異步
同步和異步主要用于修飾方法
同步方法:
當一個方法被調用時,調用者需要等待該方法執行完畢后返回才能繼續執行
異步方法:
當一個方法被調用時立即返回,并獲取一個線程執行該方法內部的邏輯,調用者不用等待該方法執行完畢
5.2.什么時候需要異步編程
當我們調用的方法計算量比較大且耗時的時候,我們不希望花太多時間在這上面阻塞主線程。這個時候據需要使用異步方法了。
比如:
- 1.復雜邏輯計算時
- 2.網絡下載、網絡通訊
- 3.資源加載時
等等
5.3.異步方法async和await
async和await一般需要配合Task進行使用
async:負責修飾方法,告訴編譯器這個是異步方法
await:負責在異步方法內部返回一個線程中的執行邏輯,此時會回到函數外部的主線程中,當該線程中的這部分執行邏輯執行結束了之后才會繼續執行函數后面的邏輯
- 在一個async異步函數中可以有多個await等待關鍵字
使用await等待異步內容執行完畢(一般和Task配合使用)
遇到await關鍵字時發生的事情:
- 1.異步方法將被掛起
- 2.將控制權返回給調用者
- 3.當await修飾內容異步執行結束后,繼續通過調用者線程執行后面內容
//異步方法執行流程示意
public async void TestAsync()
{//1print("進入異步方法");//2.這步會在一個線程中執行,此時程序返回到函數外部的主線程中繼續執行await Task.Run(() =>{Thread.Sleep(5000);});//3.當第二步中的線程執行完畢了之后,就會繼續執行這之后的邏輯print("異步方法后面的邏輯");
}
使用async修飾異步方法的注意事項:
- 1.在異步方法中使用await關鍵字(不使用編譯器會給出警告但不報錯),否則異步方法會以同步方式執行
- 2.異步方法名稱建議以Async結尾
- 3.異步方法的返回值只能是void、Task、Task<>
- 4.異步方法中不能聲明使用ref或out關鍵字修飾的變量
5.4.使用例子
5.4.1.復雜邏輯計算
利用Task新開線程進行計算 計算完畢后再使用 比如復雜的尋路算法
CalcPathAsync(this.gameObject, Vector3.zero);
public async void CalcPathAsync(GameObject obj, Vector3 endPos){print("開始處理尋路邏輯");int value = 10;await Task.Run(() =>{//處理復雜邏輯計算 我這是通過 休眠來模擬 計算的復雜性Thread.Sleep(1000);value = 50;//是多線程 意味著我們不能在 多線程里 去訪問 Unity主線程場景中的對象//這樣寫會報錯//print(obj.transform.position);});print("尋路計算完畢 處理邏輯" + value);obj.transform.position = Vector3.zero;}
5.4.2.計時器
顧名思義,就是按時間計時了
Timer();
print("主線程邏輯執行");
public async void Timer(){UnityWebRequest q = UnityWebRequest.Get("");source = new CancellationTokenSource();int i = 0;while (!source.IsCancellationRequested){print(i);await Task.Delay(1000);++i;}}// Update is called once per frame
void Update()
{if (Input.GetKeyDown(KeyCode.Space))source.Cancel();
}
5.4.3.資源加載
Addressables的資源異步加載是可以使用async和await的
Resources.LoadAsync()這些早期的異步加載資源的方式在await這里用不了,只有使用協同程序進行使用。除非搭配一些第三方的插件https://github.com/svermeulen/Unity3dAsyncAwaitUtil(需要科學上網)
用到.Net庫中的一些API的時候可以考慮使用異步方法
用第三方插件實現的Resources.LoadAsync()實例:
using UnityEngine;/// <summary>
/// 用異步函數異步加載一個立方體到場景中
/// </summary>
public class Lesson6 : MonoBehaviour
{// Start is called before the first frame updatevoid Start(){LoadCubeAsync("cube");}/// <summary>/// 異步加載一個資源并實例化到場景中/// </summary>private async void LoadCubeAsync(string resName){print("開始加載一個資源");//獲取異步加載請求ResourceRequest request = Resources.LoadAsync<GameObject>(resName);//等待資源異步加載完成await request;print("加載完畢,開始實例化");GameObject cube = request.asset as GameObject;cube = Instantiate(cube, Vector3.zero, Quaternion.identity);}
}
①用來這插件之后可以讓await支持對ResourceRequest這個類的異步等待
②之后按異步方法的常規套路來處理等待加載完成之后的邏輯
四.總結
①Unity各個版本支持的C#版本是不同的,因為各個版本的Unity使用的腳本運行時和C#編譯器都是更新的,這能夠讓我們使用最新的C#來在我們的代碼中進行編程
②更新版本的C#一般會帶來新的方法和特性,使用這些方法和特性往往能夠讓我們的代碼更加簡潔
③在C#4中,加入了dynamic動態類型,能夠讓我們繞過編譯器檢查去在運行的時候動態地生成相應類型的變量,這在我們無法一開始確定對象的類型的時候還是比較有用的。但是它不支持IL2CPP,且需要把.Net兼容版本設置為.Net Framework,這意味著更大的包體,更低的執行效率,以及放棄了跨平臺的特性。因此我們一般不推薦使用這個
④在C#5中,加入了ThreadPool,Task,和異步方法Async與await關鍵字
⑤ThreadPool能夠幫助我們更高效地利用線程資源,但是無法精確地獲取每個線程的信息與控制
⑥Task是基于ThreadPool的優點的更高一層封裝,它能夠創建無返回值和有返回值的任務類型,并且能夠讓任務同步執行(用new方法創建的),讓關鍵任務進行阻塞,讓幾個任務按一定順序完成,同時可以用一個取消標記源類來控制任務的撤銷以及觸發相應的撤銷回調
⑦Async和await關鍵字合作來完成對一個異步方法的修飾,Async修飾的方法只能夠以void,Task,Task<>作為返回值,await是用于掛起該函數并等待一個線程邏輯執行完畢之后再執行后續的函數邏輯,一個函數中可以包含多個await。
⑧異步方法通常用于復雜計算,計時器,資源加載等場景。其中Unity內置的一些資源加載的異步方法需要加入一些第三方插件才能夠支持運行。不過一般情況下用協同函數也能夠完成這樣的資源加載。