• <strike id="fdgpu"><input id="fdgpu"></input></strike>
    <label id="fdgpu"></label>
    <s id="fdgpu"><code id="fdgpu"></code></s>

  • <label id="fdgpu"></label>
  • <span id="fdgpu"><u id="fdgpu"></u></span>

    <s id="fdgpu"><sub id="fdgpu"></sub></s>
    您當前的位置是:  首頁(yè) > 資訊 > 國內 >
     首頁(yè) > 資訊 > 國內 >

    盤(pán)點(diǎn):Unity SDK開(kāi)發(fā)中有哪些坑?

    2021-08-26 10:21:42   作者:   來(lái)源:CTI論壇   評論:0  點(diǎn)擊:


      Unity是基于Microsoft .Net Framework開(kāi)發(fā)的游戲引擎
      Untiy作為游戲引擎和內容開(kāi)發(fā)平臺,吸引了眾多游戲開(kāi)發(fā)者,基于其開(kāi)發(fā)的游戲更是不勝其數。具體請參見(jiàn)1。
      盤(pán)點(diǎn):Unity SDK開(kāi)發(fā)中有哪些坑?
      環(huán)信作為領(lǐng)先的即時(shí)通訊云服務(wù)商,在游戲行業(yè)也進(jìn)行了持續的探索和研發(fā)投入。在產(chǎn)品發(fā)布的早期(2015年)就推出了Unity SDK,幫助游戲開(kāi)發(fā)者快速實(shí)現游戲場(chǎng)景下諸如世界頻道,游戲公會(huì )、組隊群聊,1對1私聊等功能,安全穩定的服務(wù)也為游戲玩家帶來(lái)了極佳的實(shí)時(shí)溝通體驗。
      2021年第二季度,環(huán)信IM Unity SDK進(jìn)行了重構改版,環(huán)信IM Unity SDK 2.0正式發(fā)布,主要改進(jìn)包括如下:
    1. 迭代更新,更加實(shí)用的API接口
    2. IM+Push增強功能的補全
    3. C#語(yǔ)言層面引入了版本7.0 – 9.0之后的一些新語(yǔ)法改進(jìn)
    4. 特別的,增加了PC端Unity Editor環(huán)境下編譯調試支持,大大提升了開(kāi)發(fā)效率
      在過(guò)去的一段時(shí)間里,筆者也參與了相應的研發(fā)工作。在整個(gè)過(guò)程中,為了解決各種問(wèn)題,不僅要到處翻閱資料,還要嘗試各種方法和參數組合。其間也經(jīng)歷了各種程序崩潰甚至系統崩潰,詭異的程序表現一次次讓開(kāi)發(fā)人員束手無(wú)策,四處碰壁,當真像深夜里行走在迷宮之中,手里還拿著(zhù)一個(gè)待破解的魔方。“此路不通,請繞行!”,是在一次次的嘗試后無(wú)奈的慨嘆和難舍的放棄。而一旦問(wèn)題最后得到圓滿(mǎn)解決,又宛如飛入云端,以上帝視角俯瞰一片片迷宮,一切又顯得那么理所當然,繁復瑣細但又絲絲入扣,這樣的苦盡甘來(lái)也算是做程序員能享受到的巨大喜悅和滿(mǎn)足。
      不敢獨享,特記錄下一些心得供大家參考,也歡迎。NET平臺資深玩家批評指正。以下,Enjoy!
      開(kāi)發(fā)概覽:非托管插件開(kāi)發(fā)(Native/Unmanaged Plugin)
      Unity是基于Microsoft .Net Framework開(kāi)發(fā)的游戲引擎2,它采用了開(kāi)源的。NET Platform,并依賴(lài)此框架來(lái)實(shí)現跨硬件設備和運行時(shí)(操作系統)的目標,也是所謂的”Write once, run anywhere”。在語(yǔ)言方面,Unity選擇C#作為主要的腳本編程語(yǔ)言,雖然。NET平臺本身支持的語(yǔ)言有很多種。
      進(jìn)一步,Unity支持Mono和ILC2PP兩種腳本框架(Scripting Backends)。特別的,Unity Editor采用的是Mono腳本框架。
      一般的,游戲類(lèi)庫開(kāi)發(fā)者可以選擇直接用C#語(yǔ)言開(kāi)發(fā),目標類(lèi)庫可以實(shí)現基于。NET Framework基礎功能之上的高級功能,這類(lèi)插件稱(chēng)之為Managed Plugin(托管插件)。由于環(huán)信IM核心SDK已經(jīng)基于C++開(kāi)發(fā),因此我們選擇另一種Native Plugin(本地插件)的方式,正是它把我們引向了迷宮之旅。兩種類(lèi)型的Plugin介紹,參見(jiàn)3。
      不幸的是,Unity網(wǎng)站上關(guān)于Native Plugin的相關(guān)介紹少只又少,想要了解它的具體細節還要去參考Microsoft MSDN文檔。作為中規中矩的文檔介紹,微軟的文檔是合格的,但是,當你真正上手編程時(shí)就會(huì )發(fā)現,這些遠遠不夠:下面記錄的一些坑點(diǎn)就很難在相應的文檔中得到直接的提示;而要通過(guò)Google大法,結合其他程序員留下的蛛絲馬跡,再加上自己不斷的調試來(lái)最終確認。
      在微軟文檔上下文中,Unity Native Plugin有個(gè)另外的名字:Unmanaged Plugin,即非托管插件。簡(jiǎn)單來(lái)講,Managed Plugin生存在。NET Framework的運行時(shí)環(huán)境(類(lèi)似于Java的JVM),而Unmanaged Plugin則生存在這個(gè)運行時(shí)環(huán)境之外,也即和運行時(shí)環(huán)境是兄弟的關(guān)系。如果你原本的類(lèi)庫實(shí)現滿(mǎn)足微軟的COM(Component Object Model)規范,那自然最好是使用COM Interop4的互操作方式;而環(huán)信IM SDK本身是純C++實(shí)現,因此采用了Platform Invoke5(簡(jiǎn)稱(chēng)P/Invoke)方式,本文剩下的內容均是基于P/Invoke。
      下圖則概要描述了Managed和Unmanaged區域代碼之間互相操作的方式:
      盤(pán)點(diǎn):Unity SDK開(kāi)發(fā)中有哪些坑?
      更具體的,為了實(shí)現對于Unmanaged DLL function的調用,只需要簡(jiǎn)單的4步6:
    1. 確認DLL類(lèi)庫中需要被操作的函數;
    2. 創(chuàng )建一個(gè)C#類(lèi)來(lái)關(guān)聯(lián)被操作的這些函數(給函數穿上一個(gè)馬甲,以便集中管理和反復調用);
    3. 使用DllImport標志在受管側(C#)定義函數原型;
    4. 在受管側隨意調用相關(guān)非托管區域函數。
      上圖中,Standard marshalling service即負責將數據在兩個(gè)區域進(jìn)行封裝/解封裝傳送(marshall/unmarshall),它主要定義了數據在兩個(gè)不同內存區域進(jìn)行拷貝(Copy)和引用(Reference)的規則7,而迷宮中的坑主要是和這些具體規則有關(guān)。
      坑王駕到之封送(Marshall/Unmarshall)中的那些坑
      坑一:sizeof(bool) = ?
      絕大多數的基本類(lèi)型屬于Blittable Types8:如System.Byte, System.Single等。System.Boolean雖然不屬于Blittable types,但是Standard Marshalling Service默認將其轉換為1,2,4字節的內存存儲,當其值為true時(shí),其對應的值為1。如果你想當然的直接將System.Boolean映射到Unmanaged側的bool類(lèi)型而不做特別處理的話(huà),你并一定會(huì )理解碰到編譯或者運行時(shí)錯誤,但是如果你嚴格的測試每個(gè)字段是,會(huì )驚訝的發(fā)現這些bool值跟你想象的不盡相同:有時(shí)正確,有時(shí)錯誤。
      經(jīng)過(guò)調試跟蹤,動(dòng)態(tài)打印sizeof(bool)來(lái)確認Unmanaged側bool類(lèi)型數據長(cháng)度后,你會(huì )發(fā)現System.Boolean默認會(huì )被保存為4個(gè)字節長(cháng)度,而在macOS環(huán)境下(對于其它環(huán)境,需要自行認證),C++定義的bool其實(shí)只有一個(gè)字節。因此當你在Unmanaged側取bool值的時(shí)候,其實(shí)只讀取了System.Boolean的1/4個(gè)字節而已。而當你聲明了多個(gè)連續的System.Boolean/bool值時(shí),可能在Unmanaged側讀取的這幾個(gè)bool值僅僅是第一個(gè)System.Boolean值的不同偏移字節而已。
      知道了原因,解決方案自然就出來(lái)了,在Managed側強制聲明System.Boolean字段封送到Unmanaged側時(shí)僅使用一個(gè)字節:
      [MarshallAs(UnmanagedType.U1)]public bool TrueOrFalse;
      坑二:字節對齊
      對于C++開(kāi)發(fā)者來(lái)說(shuō),可能知道當一個(gè)數據結構(class or struct)中的各字段在內存中進(jìn)行排列時(shí),會(huì )按照一個(gè)設定的裝箱長(cháng)度進(jìn)行字節對齊,例如:
      struct MyStruct {
      int one;
      short two;
      int three;
      bool four;
      }
      假設在我們的平臺上,sizeof(int)=4, sizeof(short)=2, sizeof(bool)=1, 如果問(wèn)你sizeof(MyStruct)=?,你可能會(huì )馬上做個(gè)加法得到答案,但是答案不一定對。It depends! 假設我們是按照4個(gè)字節對齊,這上面的結構體在內存中實(shí)際排列如下圖:
      盤(pán)點(diǎn):Unity SDK開(kāi)發(fā)中有哪些坑?
      了解這個(gè)對于我們編碼有兩個(gè)意義:
    1. 通過(guò)合理排列字段聲明順序來(lái)優(yōu)化存儲效率,內存布局中不留空洞;
    2. MarshalAsAttribute支持Layout.Explicit來(lái)進(jìn)行絕對定位,懂得了字節對齊可以配合Unmanaged側的內存排列規則以保證字段長(cháng)度映射正確,不然同樣會(huì )發(fā)生字段長(cháng)度不一致帶來(lái)的困擾。
      坑三:如何避免Double Free
      Standard Marshalling Service/Interop marshaller總是試圖釋放Unmanaged側代碼分配的內存9,這會(huì )帶來(lái)Double Free的問(wèn)題,如果碰到這種問(wèn)題,程序就會(huì )直接崩潰。
      引用資料中舉了以下例子:
      BSTR MethodOne (BSTR b) {
      return b;
      }
      如果這段代碼直接從Unmanaged側DLL中直接執行,不會(huì )發(fā)生任何額外的內存釋放;但是當你從Managed側調用這個(gè)方法時(shí),b會(huì )被釋放兩次。
      而更讓人抓狂的是,并沒(méi)有相應的信息提示究竟是哪個(gè)指針,哪個(gè)字段被Double Free了,你唯一能做的就是一點(diǎn)點(diǎn)加代碼來(lái)驗證自己猜測。所以,嚴格來(lái)說(shuō),并沒(méi)有一個(gè)萬(wàn)無(wú)一失的方案來(lái)避免Double Free,你唯一能做的就是通過(guò)測試來(lái)驗證結果(有點(diǎn)盲擰魔方的味道了)。
      有兩個(gè)基本的方法來(lái)解決Double Free的問(wèn)題:
    1. 按照官方文檔建議,在Unmanaged側通過(guò)使用CoTaskMemAlloc來(lái)分配內存,通過(guò)此種方法分配的內存,除非顯式調用了CoTaskMemFree方法(在Unmanaged側或者M(jìn)anaged側均可以調用),Interop Marshaller會(huì )嚴格保證不去釋放該內存。使用這種方法可以靈活的在任意一側分配內存,并在合適的時(shí)候在另一側釋放內存。
    2. 但上面這種方法貌似僅適用于Windows平臺,在macOS下沒(méi)有辦法使用(需要引用win32base.dll相關(guān)實(shí)現)。在macOS下僅能通過(guò)在Mananged側調用Marshal.AllocCoTaskMem()方法分配內存,并通過(guò)Marshal.FreeCoTaskMem()來(lái)在同一側進(jìn)行釋放(按照此方法分配的內存指針傳入Unmanaged側后,不要進(jìn)行任何釋放即可)。另外有一個(gè)不太可靠的workaround是:在Unmanaged一側創(chuàng )建的內存指針盡量通過(guò)IntPtr傳遞,并在可能的時(shí)候將對象中一些指針類(lèi)型的屬性值置空,以避免Double Free的發(fā)生。
      坑四:virtual函數帶來(lái)的內存布局變化
      vptr和vtable是C++的一個(gè)概念:當你定義的類(lèi)型中有虛函數存在時(shí),內存對象的第一個(gè)位置會(huì )存放一個(gè)vptr指針,該指針指向vtable(虛函數表)。因此當你開(kāi)始創(chuàng )建的自定義類(lèi)型一開(kāi)始沒(méi)有虛函數時(shí)(包括虛析構函數virtual ~MyClass()),一切運行正常。有一天你重構此類(lèi)型,增加了一些虛函數:DUANG,一切都崩塌了!原因就在于Unmanaged側內存對象的排列規則變了,原有的對象字段都被新加入的vptr往后面移位了。此時(shí)可能你唯一能做的就是通過(guò)Layout.Explicit來(lái)手工對齊每一個(gè)字段新的位置。
      其它坑
      坑一:針對M1芯片編譯
      對于M1芯片的macOS系統,編譯環(huán)信IM Unity SDK時(shí)候需要注意幾個(gè)問(wèn)題:
      1、XCode編譯時(shí)需要Excluded Architecture中排除arm64架構(很奇葩的設置,不是應該排除x86嗎?)
      2、類(lèi)庫的依賴(lài)解決:通過(guò)otool -L命令來(lái)確認相應的plugin依賴(lài)的類(lèi)庫位置都正確(文件路徑下文件確實(shí)存在),如果相應文件不存在要手工拷貝文件到指定目錄:而新的macOS安全架構限制了往系統目錄下(如/usr/lib)進(jìn)行任何改動(dòng),一個(gè)臨時(shí)的解決方法是通過(guò)install_name_tool工具主動(dòng)修改類(lèi)庫依賴(lài)路徑到另一個(gè)可以放置新文件的位置(如home目錄)。
      坑二:Delegate的正確使用姿勢
      如果Managed側的編程語(yǔ)言是C#,則Delegate是實(shí)現回調的重要手段。在Unmanaged側完成期望工作時(shí)回調一個(gè)FunctionPtr即可實(shí)現通用的回調模式,而此FunctionPtr正是對應到Managed側的Delegate。當你的Delegate綁定到一個(gè)類(lèi)對象上時(shí),你有兩種選擇:
      namespace ChatSDK {
      //delegate definition
      public void delegate OnMessageReceived(EMMessage message);
      public class MyDelegate {
      //Option 1: field
      public OnMessageReceived MyMessageReceived;
      //Option 2: instance method
      public void OnMessageReceived(EMMessage message)
      {
      …
      }
      }
      //send delegate method to unmanaged side
      MyDelegate md = new();
      NativeMethods.SetOnMessageReceivedCallback(md.MyMessageReceived); //option 1
      NativeMethods.SetOnMessageReceivedCallback(md.OnMessageReceived); //option 2
      }
      看起來(lái)兩個(gè)方式都沒(méi)有問(wèn)題,并且第二個(gè)方式看起來(lái)更順眼。但是這里隱藏著(zhù)一個(gè)很深的坑,就是你選擇第二個(gè)方式的時(shí)候,如果你在回調方法實(shí)現中采用this.xxx方式引用時(shí),你會(huì )發(fā)現this = null!這是因為當你使用這種方式傳遞一個(gè)對象的方法作為回調方法指針時(shí),其實(shí)已經(jīng)丟失了Delegate.Target(也就是this)屬性。而通過(guò)第一種方式傳遞的是一個(gè)對象的屬性/字段,它和對象本身的綁定是不會(huì )在傳遞過(guò)程中丟失的。
      至于該Delegate字段的定義可以在此類(lèi)的構造函數中通過(guò)以下方式實(shí)現:
      …
      public MyDelegate() {
      MyMessageReceived = (EMMessage message) => { … }
      }
      …
      參考資料
    1. List of Unity Games: https://en.wikipedia.org/wiki/List_of_Unity_games
    2. Unity and .NET: https://docs.unity3d.com/Manual/overview-of-dot-net-in-unity.html
    3. Unity Scripting-Plugins: https://docs.unity3d.com/Manual/Plugins.html
    4. COM Interop: https://docs.microsoft.com/en-us/dotnet/standard/native-interop/cominterop
    5. Platform Invoke: https://docs.microsoft.com/en-us/dotnet/standard/native-interop/pinvoke
    6. 如何調用Unmanaged DLL Functions:https://docs.microsoft.com/en-us/dotnet/framework/interop/consuming-unmanaged-dll-functions
    7. Interop Marshalling:https://docs.microsoft.com/en-us/dotnet/framework/interop/interop-marshaling
    8. Blittable Types: https://docs.microsoft.com/en-us/dotnet/framework/interop/blittable-and-non-blittable-types
    9. Double Free: https://docs.microsoft.com/en-us/dotnet/framework/interop/default-marshaling-behavior
     


    【免責聲明】本文僅代表作者本人觀(guān)點(diǎn),與CTI論壇無(wú)關(guān)。CTI論壇對文中陳述、觀(guān)點(diǎn)判斷保持中立,不對所包含內容的準確性、可靠性或完整性提供任何明示或暗示的保證。請讀者僅作參考,并請自行承擔全部責任。

    相關(guān)熱詞搜索: Unity SDK

    上一篇:[ 呼叫易案例] 北京齊心辦公呼叫中心系統

    下一篇:最后一頁(yè)

    專(zhuān)題

    CTI論壇會(huì )員企業(yè)

    亚洲精品网站在线观看不卡无广告,国产a不卡片精品免费观看,欧美亚洲一区二区三区在线,国产一区二区三区日韩 巨野县| 安庆市| 黑龙江省| 双辽市| 福建省| 剑阁县| 阳原县| 新乡县| 板桥市| 进贤县| 拉萨市| 灌云县| 南雄市| 正安县| 营山县| 永宁县| 宣武区| 浮山县| 临桂县| 富顺县| 同心县| 迭部县| 罗甸县| 云龙县| 南投市| 宁远县| 赣州市| 榆中县| 沾益县| 正镶白旗| 江源县| 资中县| 沁源县| 陇川县| 昭苏县| 海阳市| 玛纳斯县| 梁山县| 久治县| 张家港市| 临桂县| http://444 http://444 http://444 http://444 http://444 http://444