2019 年的收穫與成長
今年發生了很多事情,部落格也因此從七月停更到了現在,實在慚愧...現在趁著年終,趕緊抓住 2019 年的尾巴了,來總結下我的這一年。
本文真的會很囉嗦,但是希望能幫到希望用 Unity 恰飯或者其他技術恰飯的同學。
畢業
今年的上半年完成了研究生的學業,結束了留學生涯。
我的專業課程比較鬆,總共兩年要修 16 節課的學分,但是必修課只有四節,因此我可以儘可能的選擇有實戰的課程。這邊課程的大作業大多需要團隊協作,但是有些 IT 研究生同學是跨專業過來的,不是很能寫程式碼,所以有時候挺考驗自身能力的(笑。 我組完隊一般都希望能把大作業的規模做大些,一方面是作業能拿比較好的分數,另一方面是求職的時候可以拿出能往簡歷上放的專案經驗。
畢業後我發現這種選擇是對的,我的隊友在畢業後還問我們的線上專案怎麼開不起來了,他們也到了找工作的時間,還讓我幫忙看了看簡歷。澳洲工作是很休閒的,大部分下午五六點就能下班。身邊同學也在努力地留下來,考 PTE、CCL 考試湊分拿 PR。自己因為還是想做遊戲,澳洲環境不太好,就回了國。
從面試到工作
由於課程結束三個月後才能參加畢業典禮拿到畢業證,當時我對求職還不太上心,還想等著春招。但是又不想在家裡混吃混喝,就開始每天刷刷面試題,學學感興趣的,同時也開始在某直聘找工作,打算每週面試一次,接觸下當前的就業形勢,同時查漏補缺。
第一週
第一週面試了家一百多人的遊戲公司,一上來要求三十分鐘解一道 Leetcode hard 的題...好不容易解出來了,又要求遞迴改迭代,又問有沒有能優化的點。之後還問了些邏輯題,這時候挺慶幸自己複習了《程式設計師面試經典(第5版)》(第六版剛出噢),基本還能 hold 住場面,但是後來分析演算法的時間複雜度分析的十分糟糕,於是就沒有然後了...
當天十分沮喪,恰巧面試的地方和一個主美朋友工作的地方很接近,就約了個飯。他開導我說:”拿美術來說,不同公司也會需要不同的美術:古風遊戲自然需求專精畫古風的,科幻遊戲需求的美術風格很明顯也不一樣。再面試幾家就好,今天面試只代表公司不適合你。“我聽了很有道理!於是繼續不務正業學了喜歡的東西,簡單複習複習演算法,刷了刷題。
第二週
第二週又接到另一個遊戲公司 HR 的面試邀請,面試時直接來了三個面試官,兩個程式大佬一個製作人。很明顯的,面試風格都不一樣。他們事先看了我的簡歷,看了我的部落格。剛好第一週的時候更新了一篇 DOTS 的博文,於是他們一開始就讓我介紹下 Unity 的 DOTS 技術棧是什麼,還有一些概念細節。後來的其他問題很明顯能感覺他們在考察我知識的廣度,例如圖形學,我簡歷上提到的 C# 熱更新等。
剛好那段時間”不務正業“地跟著《自己動手實現Lua》寫了一半的 Lua 虛擬機器,於是問到對 Lua 是否熟悉的時候,我就提了一嘴最近在學的東西,接著又展開新的問答。整個過程中,我覺得面試官的風格和第一週公司的面試風格完全不一樣,但是有些地方還是答得不夠好,於是又在家瞎學。
一週後,我拿到第二家公司的 Offer,成為了公司工具人。
我覺得從面試就能看出公司關注的是開發人員的哪些方面,如主美朋友所說,如果不願意改變自己學習的風格,那就找到需求這種風格的公司,接下來的工作也印證了這一點。
上面提到的內容:
- 《程式設計師面試經典(第6版)》(第5版)
- DOTS 相關博文《洞明 Unity ECS 基礎概念》
- 《自己動手實現Lua:虛擬機器、編譯器和標準庫》,用Go語言實現 Lua 虛擬機器、編譯器和標準庫。
瞭解程式碼的另一面
入職後,才發現公司寫了一套自己的 C# 熱更新,這種熱更新是和 xLua 一樣的注入式熱更,跟 ET 框架分兩個專案跑的還不一樣(下文會解釋)。有意思的是,在我入職過了幾個月後,xLua 作者也開源了C# 注入式的熱更新專案:InjectFix,作者還配套寫了一套 IL 的執行時,聽說效能還比 ILRuntime 更好些。
感興趣的可以先看看 xLua 作者的講解:
先前基於 ILRuntime 的熱更新,例如 ET 框架,大多是分兩個專案,主專案和熱更專案,熱更專案放一些需要經常修改的戰鬥邏輯、UI 介面等。這樣可以直接把熱更專案都放在 ILRuntime 上跑,整個專案都能熱更,十分方便,但是這樣十分依賴 ILRuntime 的效能。
那麼注入式的熱更有什麼區別呢?我們給每個函式前加 if 判斷,如果 ILRuntime 上跑的(能熱更的)DLL裡面有對應的方法,就執行熱更的方法,這樣 ILRuntime 的效能問題也能避免開來,因為我們可能只有需要熱更的函式在 ILRuntime 上面跑,而不是整個專案。
那麼,古爾丹,代價是什麼呢?
——格羅瑪什·地獄咆哮
代價就是能熱更的東西極其侷限,只能熱更函式、和新增的類等。
在瞭解原因之前,我們先來看看例子,假設我們遊戲就這麼多程式碼:
// Unity 2019.2 之前,Scripting Runtime Version: .Net 3.5 Equivalent(Deprecated)
public class TestIL : MonoBehaviour
{
void Start()
{
int[] arr = {1, 2, 3, 4};
Action action = () => Debug.Log("Hello IL");
action();
}
}
上面是看上去如古天樂平平無奇的程式碼,當我們用 dnSpy 反編譯 Library\ScriptAssemblies\Assembly-CSharp.dll
後會發現:
public class TestIL : MonoBehaviour
{
...
private void Start()
{
int[] array = new int[]
{
1, 2, 3, 4
};
Action action = delegate()
{
Debug.Log("Hello IL");
};
action();
}
// 編譯器生成的匿名函式
[CompilerGenerated]
private static void <Start>m__0()
{
Debug.Log("Hello IL");
}
[CompilerGenerated]
private static Action <>f__am$cache0;
}
編譯器為我們的 Action 生成了匿名函式,那也就是說如果我需要更改 Debug.Log 中列印的字串,我只需在熱更 DLL 中提供:修改後的函式 + 編譯器生成的匿名函式就 okay 了?實際上沒那麼簡單,因為編譯器又作妖了。
void Start()
{
int[] arr = {1, 2, 3, 4};
Action action = () => Debug.Log("Hello " + arr[0]); // 修改列印
action();
}
再次檢視反編譯後的 DLL:
private void Start()
{
int[] arr = new int[]
{
1, 2, 3, 4
};
Action action = delegate()
{
Debug.Log("Hello " + arr[0]);
};
action();
}
[CompilerGenerated]
private sealed class <Start>c__AnonStorey0
{
public <Start>c__AnonStorey0()
{
}
internal void <>m__0()
{
Debug.Log("Hello " + this.arr[0]);
}
internal int[] arr;
}
由於 action 中引用了區域性變數,mono 編譯器將本該生成的匿名方法生成了匿名類,並在呼叫的時候傳入 arr int 陣列。
現在我們調整下我們的熱更新策略:如果我們檢測到編譯器生成的匿名函式,將其轉換成匿名類,再把這個新增的類複製到熱更 DLL 中。
還是有問題!
這時候就需要認識 C# 的中間語言—— MSIL(又稱 IL),每句 C# 程式碼都可以轉換成可讀性較好的類似於機器程式碼的 IL 程式碼。
當我們檢視 Start 函式的 IL 程式碼時:
.method private hidebysig
instance void Start () cil managed
{
.maxstack 4
.locals init (
[0] class TestIL/'<Start>c__AnonStorey0',
[1] class [System.Core]System.Action
)
IL_0000: newobj instance void TestIL/'<Start>c__AnonStorey0'::.ctor()
IL_0005: stloc.0
IL_0006: nop
IL_0007: ldloc.0
IL_0008: ldc.i4.4
IL_0009: newarr [mscorlib]System.Int32
IL_000E: dup // 下面這串是什麼?怎麼又引用了另外一個類?
IL_000F: ldtoken field valuetype '<PrivateImplementationDetails>'/'$ArrayType=16' '<PrivateImplementationDetails>'::'$field-1456763F890A84558F99AFA687C36B9037697848'
IL_0014: call void [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::InitializeArray(class [mscorlib]System.Array, valuetype [mscorlib]System.RuntimeFieldHandle)
IL_0019: stfld int32[] TestIL/'<Start>c__AnonStorey0'::arr
IL_001E: ldloc.0
IL_001F: ldftn instance void TestIL/'<Start>c__AnonStorey0'::'<>m__0'()
IL_0025: newobj instance void [System.Core]System.Action::.ctor(object, native int)
IL_002A: stloc.1
IL_002B: ldloc.1
IL_002C: callvirt instance void [System.Core]System.Action::Invoke()
IL_0031: ret
} // end of method TestIL::Start
在 dnSpy 中找了找,發現了 PrivateImplementationDetails
類:
這看起來應該是這個陣列被存到了某個地方,這個類只是提供了 id 告訴 IL 這個陣列應該在哪找?通過查詢 Roslyn 編譯器的文件,發現了這個類的註釋:”The main purpose of this class so far is to contain mapped fields and their types.“ 所以我們要熱更的話,還需要將 PrivateImplementationDetails
類複製到熱更 DLL 中。
我們怎麼分析程式碼是否是匿名函式呢?Mono.Cecil 就是一套基於 IL 分析程式集的庫,我們可以通過這個庫來判斷哪些方法不能熱更等,這又是另外一個話題,略過不提。
以上只是注入熱更的一個小插曲,但是涉及的東西就已經與一開始 Start 方法中的三行程式碼相去甚遠了。如果我們還要支援過載函式的熱更,泛型類中函式的熱更,就更是讓人掉頭髮的話題,涉及的 IL 十分複雜。
現代的高階語言為我們封裝了太多東西,提供了方便程式設計的語法糖,但也為我們知根知底的學習方式設立了門檻。
但是當我們瞭解了 IL 中間語言的話,我們以後面對”在匿名函式引用 for 迴圈中變數的行為會詭異“等問題的時候,我們可以直接反編譯 DLL 來看程式碼真正的面目是怎麼樣的。
不小心寫了足以充當一篇文章的內容,但是我想表達的是:
- 對於遊戲開發者,我們有必要對自己的程式碼做了什麼有充分的瞭解,謹慎運用語法糖,這樣才能充分掌握遊戲的效能。
- 雖說我們遠遠沒達到造 InjectFix 輪子的程度,但是瞭解該技術的根基——IL,再嘗試根據其他文件來分析,能讓我們更好的瞭解這個框架的背後,瞭解這種熱更新的優缺點。
上面提到的內容:
- InjectFix C# 熱更新
- dnSpy .Net 反編譯編輯器
- Mono.Cecil 基於 IL 分析程式集的庫,dnSpy 也是基於這個庫來分析的。
自頂向下,再自底向上
遊戲開發很多技術都倚重於硬體的能力,因此我們有必要對這些硬體的實現和原理有所瞭解。但這方面也是我的弱點,我個人喜歡按照興趣來學,因此我的總結的方法是:自頂向下,再自底向上。
就如上面熱更新的例子一般,自頂向下就是揭開抽象的面紗,從感興趣的框架或庫的應用入手,逐步通過各種方式來了解底層的原理。
拿 DOTS 技術棧做例子,ECS 的程式設計模式保證資料線性地排列在記憶體中,恰好能記憶體 cache 快取命中率,從而得到更高效的資料讀取。更多細枝末節可以先放一旁,例如 Shared components 這種與理念不相符的共享元件是怎麼實現的。
知道了記憶體 cache 快取命中率在其中發揮巨大作用後,我刷知乎還發現,Disruptor 庫為了對齊 128 個位元組方便 CPU cache line 用,選擇浪費空間提高緩衝命中率。
到底記憶體的結構是怎麼樣的?快取命中率又是什麼?位元組對齊又是什麼?為什麼這樣的做法能提高效能?帶著種種疑問,我們開始自底向上地瞭解記憶體結構。
從感興趣的應用入手,瞭解硬體底層,這樣不至於太枯燥。學到一定程度,當我們把知識網中重要的幾個概念都瞭解之後,我們可以再閱讀相關作業系統的書,將周邊的知識點與之前比較大的概念相連線,互相印證,從而結成結實的知識圖譜。
一開始我想重新撿起《編碼:隱匿在計算機軟硬體背後的語言》這本書,這本書從手電筒聊天開始,依次講了摩斯電碼、二進位制、再到繼電器電路、組合成 CPU 等等,能解答我的疑惑,但是後來發現節奏偏慢。於是又看了 Crash Course Computer Science(電腦科學速成課) 中講 CPU 的一集,覺得節奏非常好,通過 12 分鐘視訊講解除法電路、快取、流水線設計、並行等概念,揭開計算機一層一層的抽象,我認為這是自底向上最好的教材。另外在知乎刷到一篇文章:《帶你深入理解記憶體對齊最底層原理》也是很好的佐料。
瞭解了硬體原理之後,這種優化能帶到實踐中嗎?一維陣列的順序訪問比逆序訪問要快,那麼二維陣列呢?
public class TestCache : MonoBehaviour
{
private const int LINE = 10240;
private const int COLUMN = 10240;
void Start()
{
int[,] array = new int[LINE, COLUMN];
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
for (int i = 0; i < LINE; i++) // 344 ms
{
for (int j = 0; j < COLUMN; j++)
{
array[i, j] = i + j; // 一行一行訪問
}
}
stopwatch.Stop();
Debug.Log("用時1:" + stopwatch.ElapsedMilliseconds + " ms");
stopwatch.Restart();
for (int i = 0; i < LINE; i++) // 1274 ms
{
for (int j = 0; j < COLUMN; j++)
{
array[j, i] = i + j; // 一列一列訪問
}
}
stopwatch.Stop();
Debug.Log("用時2:" + stopwatch.ElapsedMilliseconds + " ms");
}
}
一行一行訪問和一列一列訪問的效率很明顯不一樣,自此,我們自底向上地把對硬體的瞭解反映到了實際編碼中。
由於自己就是興趣導向的學習,所以學東西難免有所偏科,但是我認為只要感興趣的夠多,就不怕偏科(笑。每個人有自己學習的節奏,在這裡只是提供一種思路。
上面提到的內容:
- 《Unity DOTS 走馬觀花》
- 你在開源專案裡看到過哪些精髓的程式碼片段?
- 《編碼:隱匿在計算機軟硬體背後的語言》
- Crash Course Computer Science(電腦科學速成課)第九集:高階 CPU 設計
- 建議空閒的同學從第一集開始看起,內容十分詳實有趣。
- 《帶你深入理解記憶體對齊最底層原理》,其專欄也是個寶藏之地。
找到適合自己的學習方式
說起來容易,但是要運用之前,我首先需要知道學習這種知識的途徑有哪些,才能從中選擇最適合自己的學習方式。
拿 Unity 中的渲染來說,我能通過相關官方文件來學,能通過看博文來了解,也能通過 Direct3D、OpenGL 相關教程來學,或者重新拿起圖形學的書本,因為都會涉及到渲染流水線的知識。
那麼怎麼樣的才算是適合自己的學習方式呢?有的同學可能喜歡先把理論吃一遍再來實戰,我喜歡通過動手來了解。我嘗試跟著 LearnOpenGL 教程和一些 Shader 教程來學習渲染管線的知識,也初識 Batch(批次)、GPU Instancing 等名詞,但是不同教程都有不同側重:Shader 教程可能著重教你如何實現各種效果,而圖形學課程可能對這些名詞的背後實現語焉不詳。
之後一直壓著疑惑使用著這些技術,我後來又發現了一些選擇:要不自己寫一套軟渲染器?又或者我可以通過 Unity 新出的 Scriptable Render Pipeline(可程式設計渲染管線)來自定義一套自己的渲染管線?這似乎已經很靠近我想學的東西了,再考慮自己的時間和興趣,我決定跟著 catlikecoding 的 SRP 教程寫一個可程式設計渲染管線,嘗試以一種較底層的角度來了解渲染管線相關的實現。
上面提到的內容:
- LearnOpenGL 英文版 中文版
- 《Unity Shader 入門精要》
- 圖形學:Scratchapixel
- Scriptable Render Pipeline 可程式設計渲染管線(目前在學 catlikecoding 的教程):
- Custom SRP (Unity 2019 and later)
- Scriptable Render Pipeline(Unity 2018)
囉嗦了這麼多,其實只是想分享下我學習的經驗:正所謂曲高和寡,越是進階的知識點,教程風格越五花八門,當然也會越少人寫。每個人有自己的學習風格,儘量多擴充自己的知識來源,不要將自己限制在國內教程和書籍中。
如何擴充套件知識來源呢?
- 國內外出版社書籍(此處推薦英子姐寫的《程式設計師最喜歡的技術書大都出自這 20 家出版社》)
- O'Relly Learning 的書籍分類做的比較好,可以拿來關注新書的出版。
- 關注國內出版社的書訊
- 關注 Youtube 博主、部落格博主、知乎專欄等(我通過 RSS 訂閱)
- Github 看有沒有人上傳自己的 Demo,例如一個小型的光線追逐實現、或者一個神奇寶貝 Unity 復刻版等。
- 線下 Meetup 技術分享。
有了選擇,就可以根據自己興趣愛好來跟著學習,想辦法把他們學到腦子裡!
先寫,再優化
工作後的這段時間我一直在思考,我距離獨立造輪子還差多少能力。先寫框架劃分模組?還是先實現功能?如何寫出高內聚低耦合的程式碼?拋去程式碼可讀性和模組的劃分不談,寫出一個輪子最基本的功能對於我可能都是一個難題。
我的工位在寫出熱更新的大佬的旁邊,一開始大佬說招我的原因,是希望我能接受他的熱更新框架,繼續維護。可惜自己能力不夠,看懂了實現,但是卻無從下手,之後也就不了了之。不過有段時間,我問大佬問題問到什麼程度呢?我一轉頭瞄下他,他就會划著電腦椅過來等我提問...
大佬在工作室中負責戰鬥以外的技術攻堅,有需求的話就會主動去學,去實現,C# 熱更新框架就是他的作品。後來團隊覺得可能要通過幀同步來防外掛,他又開始看相關的論文,文章來從頭寫,雖然到現在團隊還沒用上。耳濡目染之下,我也會跟著大佬去看幀同步相關的內容,有時候噹噹小黃鴨,幫他查漏補缺。
對於造輪子,有需求,有思路,就先開始寫!這是我這段時間從大佬身上學到的一點。例如他認為我們 UI 的資料不好管理,於是參考了《Flux 架構》和其他文章,準備將這種理念與 Unity 結合,於是他又開新坑了。
他給我的評價是:知識面較廣,好學,就是不肯開始寫。
說的沒錯,太多考慮反而會束手束腳,適得其反。先把功能寫出來,設計模式按照經驗來劃分,能用了再優化,這是功利的做法,但也是能讓人顧慮少地造輪子的做法。
新的開始
工作的節奏和上學完全不一樣,雖說是 9.5 6.5 5,但是兩個小時的通勤仍然讓我措手不及。公司下班吃完飯,回到家九點,隨便看點啥就十一點了,早上還得八點多起…
最後嘗試更換了出行方式,從地鐵改成坐巴士上班,這樣有位置坐,才能靜下心看看書。儘量把閱讀安排在通勤時間上,這樣才能勉強保持學的進度。
工作方面壓力也還行,平常有時間的話能關注下業務邏輯以外的東西。例如有一次運營拿來一個我們遊戲的指令碼樣本,我花了點時間解包,學了學反編譯,再讓負責戰鬥的大佬針對性地做了點預防。這個過程對我而言也是新的經驗,其中應對 iOS 外掛時借鑑了圖靈的《九陰真經:iOS黑客攻防祕籍》的思路。
前不久轉了正,算是給學生時代交了份答卷。隨著見識的增長,自己對博文和程式碼又有了更高的要求。見識了“好”的文章後,自己怎麼寫才能組織好文章,才能更好地講述一些知識點?
我們需要放下書本,去實踐,去體驗,去觀察,去琢磨,去嘗試,去創造,去設計,去stay hungry, stay foolish。
號稱終極快速學習法的費曼技巧,究竟是什麼樣的學習方法?
學習、實踐、總結、再清楚地解說一件事,並將其寫成博文,這是我對我下一階段的要求。其中學習總結的速度也得跟上,我已經看到有讀者抱怨我部落格斷更了……
今年讀過的書:
- 《Unity3D網路遊戲實戰(第2版)》
- 《遊戲架構:核心技術與面試精粹》
- 《深入理解C#(第3版)》
- 《利用Python進行資料分析 原書第2版》
- 《白話機器學習演算法》
- 《遊戲安全——手遊安全技術入門》
- 《九陰真經:iOS黑客攻防祕籍》
- 《網路多人遊戲架構與程式設計》
- 《自己動手實現Lua : 虛擬機器、編譯器和標準庫》
- 《C#從現象到本質》
明年想更深入多執行緒、記憶體佈局、和渲染相關的話題。
加油吧,感謝 2019 年幫助過我的所有人。
相關文章
- 這幾年來我建站的3大收穫
- 每次的挫敗,都是收穫
- 關於2021年的一些收穫和思考
- 有點收穫了。
- 7 年 700 篇技術文章,收穫的 7 個心得
- 【績效季】遇到一個好領導有多重要,從被打差績效到收穫成長
- Totoro 在自動化測試領域的深耕與收穫
- 2019年營收超170億,陌陌增長的秘密何在營收
- 如何讓網站收穫好的排名?網站
- 使用 ClojureScript 開發瀏覽器外掛的過程與收穫瀏覽器
- 《鬼泣 5》開發團隊訪談:四年創作歷程的收穫與總結
- 2019年完美世界營收80.38億元 同比增長0.05%營收
- 攜程:2019年營收357億元 同比增長15%營收
- 談談WhatsApp一年設計經歷和收穫APP
- 做小遊戲要趟那些坑?手遊團隊轉型一年來的收穫與思考遊戲
- 20241101 資料結構與演算法期中機試收穫資料結構演算法
- 海軍的 2021年終總結, 跳槽後,我收穫了什麼
- 張一鳴:我的大學四年收穫及工作感悟
- TiDB 社群成長足跡與小紅花 | TiDB DevCon 2019TiDBdev
- 百度星辰計劃的一年躬耕與收穫:造一座人性溫暖與理性科技的橋樑
- 帶屏智慧音響如何成為2019年新的增長點?
- 乾貨分享:Totoro 在自動化測試領域的深耕與收穫
- 2019-2021年TWITCH收視時長(附原資料表)
- 58同城:2019年全年營收155.8億元 同比增長18.6%營收
- 拼多多:2019年全年營收301.4億元 同比增長130%營收
- 美團:2019年營收同比增長49.5%至975億元營收
- 趣頭條:2019年營收55.7億元 同比增長84.3%營收
- Sensor Tower:2019年Q1 TikTok營收同比增長222%營收
- 做遊戲伺服器端開發的一些收穫與總結遊戲伺服器
- 售罄率超過九成 業績同比增長156% 阿里雲資料中臺數智賦能企業收穫驚喜阿里
- 2020年的成長印記
- 貓眼娛樂:2019年營收42.675億元 同比增長13.6%營收
- Omdia:2019年線性電視佔美國電視收視的63% 長視訊的收視率佔16%
- Adobe 的瘋狂星期四:全家桶收穫新成員Figma,設計的盡頭還是Adobe
- 2017-07-13今天研究jquery原始碼的收穫jQuery原始碼
- 工作間隙整理學習內容的意外收穫
- 徵文 | 收穫,不止GBase 8a——GBase 8a培訓總結與感受
- Apptopia:2019年4季度TikTok應用內購營收增長超300%APP營收