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