基於團隊的持續優化之道
四月份的博客也欠下了,大部分業餘的時間花在準備UWA DAY 2018的分享上面。當時也說了,做這次分享也算實現了我去年的一個「小願望」,借這個機會認識了不少做技術的朋友。回到杭州之後,也有不少人跟我要PPT,討論一些分享中的細節。在準備分享的時候,因為時間關係,也將一些計劃擴展開的內容刪除掉了。所以這次想再根據準備時候的逐字稿整理一篇博客,以作記錄,也為沒有參加UWA DAY 2018的朋友提供參考。
所以這篇博客的主題還是以這次分享的核心內容為主線,順便把PPT的內容也提供出來(以引用塊或者(補)的方式標註,以方便聽過分享的朋友閱讀)。
題目和提綱
這次分享的題目是《Connecting The Dots——基於團隊的持續優化之道》。優化是一件瑣碎而又繁複的事情,它通常是一個又一個的點,而我們如果要讓一個比如說30人甚至50人的大型團隊,花1到2年的時間去開發一款大型的手游項目,不但需要了解和掌握這些優化的細節,而且要藉助團隊的力量在整個遊戲開發的生命周期過程中不斷地進行持續優化,才能夠讓優化效果可以保持下去。
在聊具體的分享內容之前,我想先拋出這樣一個問題——在優化這樣的過程中,程序應該承擔一個什麼樣的角色?
這也是和我剛才所想要聊的持續優化關聯性非常大的問題。在優化過程中,程序可能有兩種角色,一種是像救火員這樣的角色,哪裡冒火了,就去撲滅它。我自己也有過作為救火員去做優化的經歷。另外一種工作方式是像園丁一樣,可能不需要特別緊急地在最後關頭才處理那些性能問題,而是從項目的初期就開始規劃整個項目的性能指標,然後讓整個團隊可以按照指標的要求合理地產出美術資源、編寫代碼,從而讓整個產品一直處於一個比較健康的狀態。
救火員的職責很有挑戰性,同時也比較容易做出成就感,比如我也有過用1到2周時間讓遊戲的性能有非常大提升的經歷。而園丁在做的事情可能看上去比較普通,都是些日常且瑣碎的事情。在遊戲開發中,這兩種角色並不衝突,而是同時存在的。但是在我的理解里,對於性能更友好的狀態,是讓團隊里的成員都在以一個園丁的角色在工作,從根本上來保持遊戲的性能一直處於一個較好的狀態。因為——每一個神級優化的背後,都隱藏著一個2B的bug!
那這次分享我就想從這樣三個方面來聊一下我所理解的以「園丁」的角色來讓團隊進行繼續優化的方式。
首先是美術資源的優化,美術資源是一個會對客戶端運行效率產生非常重要影響元素之一,我想和大家聊一下,怎麼樣讓我們團隊可以在美術資源產出方面一直保持一個比較好的狀態。然後我想回歸到程序的本職工作,從一個更加宏觀的角度來討論一下程序代碼的部分要怎麼樣去做一些優化,對於代碼質量和一些底層模塊的設計,如何去做一些提前的思考和設計。第三部分,團隊開發效率的優化,這塊可能看上去跟性能優化的題主沒有直接的關係,但是在我看來優化不僅僅是遊戲運行效率優化,而且應該包括整個團隊開發效率的優化,這也是在項目開發過程中非常重要的部分。
1. 美術資源優化
首先來看一下美術資源優化的部分。
大家在日常開發中可以感受到,對於美術資源部分,美術和程序,尤其是要進行性能優化的程序,他們的關注點會有不同。美術更多的是關注美術效果是不是足夠的好,而程序可能更多的關注美術資源在設備上的運行效率是否可以達到目標幀率,這其中就有一些目標導向上的衝突。我認為,解決這一衝突的一個非常重要的手段是儘早來建立合理的美術資源製作規範。
這其實是一個美術資源製作的過程,首先通過在立項時製作Demo來確定適合的美術資源規範,然後通過給美術灌輸效率意識、提供輔助工具讓美術在製作資源的過程中可以嚴格按照規範進行,最後是要藉助QA的力量對產出的資源進行檢查,確保達標。
1.1 規範制定
為什麼制定美術規範這麼重要呢?我想借自己之前的一個項目中的真實經歷來聊一下。
《無盡戰區·覺醒》這款手游是我作為主程帶領團隊開發的第一款手游項目。當這個項目進行到中期的時候,收包了一批場景的資源。在使用這些資源的時候發現它們的運行效率明顯偏低,Profile發現面數和DrawCall都超標了。面數很簡單,使用減面工具進行減面即可,DrawCall也使用類似Unity的Static Batching的方式進行優化,但是結果發現一些場景的DrawCall只能從400減少到300多,並不能達到預期。仔細檢查發現很多模型無法合併的原因是使用了端游常用的一種貼圖製作方法——四方連續的貼圖。這種方式雖然可以使用很小的貼圖尺寸通過tiling製作出精細的效果,但是對於DrawCall敏感的手游來說並不合適。最終項目又請了好幾個外派美術進行資源的整合和製作,導致多花費了幾個周時間和幾十萬的美術成本。雖然對於大公司來說幾十萬的美術成本不算什麼,但是我依然覺得這是我作為一個主程的失職——因為沒有提前和美術溝通好製作方法,導致了這樣的問題發生。所以在出來創業的項目中,在項目最初期Demo製作完畢之後,客戶端團隊就和美術一起製作了非常詳細的美術資源製作規範。
制定規範的步驟大致可以整理為如下的幾個部分:
在制定美術規範的第一步是要進行遊戲信息的收集,因為不同的遊戲類型以及鏡頭視角會對美術資源的製作產生非常大的影響,比如2.5D和3D自由視角的遊戲是有不同的製作和優化方式。
在收集了足夠的信息之後,需要和美術敲定一些大的技術方向,比如線性空間、HDR和動態光影等。線性空間是團隊應當提前關注的一個點,我個人的觀點是在寫實的遊戲風格中,能上線性空間還是盡量使用線性空間,對於美術效果的提升還是很有幫助的。在正確的基礎上,才更容易出正確的效果。Unity目前的版本和OpenGL ES 3.0的普及率,個人觀點是完全可以在移動平台上使用線性空間的。當然這也要考慮具體項目的內容以及開發團隊成員的能力和經驗等因素。HDR的開關在Unity中還是比較簡單的,但在性能方面對於帶寬的影響比較大,收益也不小,建議追求效果的團隊提前考慮。動態光影是我們研發團隊的美術特別想追求的一個效果,但是因為我們項目今年要上線,而且要考慮低配的效果,所以程序一直卡著不讓場景使用全實時的動態陰影,而是盡量使用烘焙的方案。
確定目標機型就不多說了,考慮主流的高中低三檔,建議提前購買一些設備方便後續的性能測試。我對於這三檔的基本分類大致如下(這個也可以參考UWA測試中的機型選擇):
高檔:大部分iOS設備,主流的安卓旗艦設備;
中檔:少部分希望支持的iOS設備,比如目前的iPhone 5s、6/6s,ipad mini等,1-3年前的旗艦以及1-2年的1500元左右的安卓設備;
低檔:1-3年前的千元機。
接下來是要針對面數和DrawCall這兩個核心點,對整體的標準進行定義,並將這些標準分配到場景、角色、特效、UI等資源分類上。因為這些不同類型的資源是由不同的美術同學產出的,他們之間通常不會去溝通各自的性能消耗,因此需要針對每部分進行規則的細化。
場景部分強調一下貼圖「像素密度」的概念。在製作場景的時候需要提前定義貼圖使用的像素密度,否則會導致貼圖精度過高或者不夠等問題。所謂的「像素密度」是指在正常的遊戲視角下,一個一立方米的Cube應當使用多少尺寸像素的貼圖。這個在場景製作的初期應當定義好,美術才好去使用正確的貼圖尺寸,否則到後期優化可能發現大量超標的貼圖在使用,如果此時已經進行了貼圖的合併,美術修改的工作量會非常大。
角色部分的規範需要把面數和DrawCall的標準細化到每個角色中,這時候需要和策劃溝通期望的同屏顯示人數,根據不同的遊戲類型會有很大的差異。比如《崩壞3》不會同時有很多戰鬥單元,因此可以給每個角色非常高的精度,而像《御龍在天》這樣的國戰遊戲,追求百人同屏的效果,對於每個角色的消耗限制就會比較大。
骨骼數量的部分最好在前期和美術溝通好,比如手指骨骼的減少和合併,腳部骨骼的簡化,來減少CS骨骼的數量,給飄帶等Bone骨骼留出足夠的空間。因此美術規範制定的過程並不是單純的程序給美術添加製作限制,而是一個和美術一起來討論如何在有限的資源下製作出更好效果的過程。
UI製作規範是我們初期沒有特別關注的部分,也因此踩了一些坑:
UI組件數量較多導致載入頓卡。我們有些複雜界面有上千個GameObject,幾百個UI組件,這是非常恐怖的。我們測試發現UI的Prefab載入的時間消耗在Unity 5.5.6上和UI組件的數量幾乎成線性關係。這裡補充一張我們測試的GameObject數量和載入時長的測試結果圖(橫軸是GameObject個數,縱軸是單純的Prefab載入的時間消耗,單位秒,測試環境:小米Max2):
因此建議其他團隊提前考慮UI部分的非同步載入和分塊載入。(或者升級引擎到5.6.5+或者2017.3+,對於Prefab的載入速度有了很大的提升)
UI中的粒子特效建議提前考慮非同步載入,我們最初直接放在了UI的Prefab中,不但因為Unity不支持Prefab的嵌套導致維護麻煩,而且因為每一個ParticleSystem的初始化都佔用一個幾乎固定的時長(我們自己測試在PC上也大約有3ms左右),導致UI初始化的時候非常卡。使用一個間接的引用,就可以比較容易地做到既方便更新又可以非同步載入。
最後針對標準,要進行真機的壓力測試,並且在組內推廣,形成大家都認可的美術資源製作規範。
1.2 美術資源製作
在確定好美術資源的製作規範之後,就是需要在美術鋪量製作的階段讓美術可以按照規範進行資源的製作,在這個階段程序需要注意去做的事情我覺得有兩點:
幫助美術建立效率意識;
為美術提供便利的製作和檢查工具。
在我們這樣的一個初創團隊里,沒有經驗豐富的TA存在,而且有部分美術同學之前來自外包團隊,對於遊戲運行時的效率意識比較薄弱,因此需要客戶端程序同學不斷和美術溝通來灌輸對於遊戲運行效率的關注。通常的做法包括日常的溝通、規範和技術點的分享等。
而在提供便利的工具方面,除了教會美術使用Unity已經提供的那些性能檢查工具(Batches和面數查看、Overdraw、Mipmaps、Wireframe等渲染模式)之外,我們也為美術提供了非常多的開發和檢查工具。
從截圖中可以看到我們為美術提供了大量的工具,選擇幾個來著重介紹一下:
1.2.1 場景鏡頭同步功能
在場景資源製作的時候,需要美術去關注的除了常規的DrawCall之外,還有Overdraw、Mipmap以及面數。Unity已經在Scene View下提供了這三種渲染方式的檢查工具,但是在我們的使用中發現,由於遊戲運行時鏡頭規則的複雜和多變,導致美術在Scene View下無法準確判斷是否存在資源不合理的情況。因此美術提出希望可以在Game View下查看這些狀態。
最初的時候我們也是想在Game View下實現這些不同的渲染方式,並且已經集成了OverDraw的檢測方式,基於的也是錢康來建議的Unite2017/OverdrawMonitor。但是後來覺得這種方式會影響美術對於遊戲的操作,程序實現起來也要多花一些時間,比如Mipmap的實現效果就不是很滿意。後來就想到另外一個思路——在Scene View下來做鏡頭和Game View下的鏡頭同步。結果就非常簡單,只需要為Game View下的Camera身上添加一個Component就好:
核心的代碼只有LateUpdate中的那一句:"view.LookAt(transform.position, transform.rotation, 0f);",實現的效果就是正常操作遊戲,Scene View下的Camera可以一直跟隨移動,視角和Game View下非常接近。(有需要動態圖自己使用上述代碼進行試驗即可。)
1.2.2 批量烘焙功能(補)
Unity的烘焙在Unity 5.X的版本速度還是比較低,我們雖然為美術專門購買了CPU強勁的烘焙機,但是比如在製作大世界的時候,因為場景拆分得比較細所以需要一次性連續烘焙多個場景,於是為美術提供了批量烘焙的功能。之前在知乎上也有朋友問過類似問題,代碼非常簡單,直接貼一下,需要的自取好了:
使用同步的方式烘焙,會卡住Unity,但是烘焙速度應該會有些提升(沒有對比過……)。
1.2.3 場景合法性檢查(補)
我們為美術添加了場景的合法性檢查工具,因為我們對於場景中的相機設置等有些特別的要求。這塊跟具體項目相關,只羅列一下我們在檢查的內容以及要檢查它們的原因。
頂點格式,對於帶有color等邏輯上不需要的數據的部分給出警告。因為我們的場景是採用靜態合批的,如果有資源不小心導入了color等不需要的頂點數據,會導致整個合併之後的mesh變大很多。
地形數據,我們使用了T2M的工作流程,美術使用Terrain進行地表的製作,然後通過T2M插件導出成mesh,為了保留原始的編輯數據便於以後修改和調整,Terrain會保留在原始場景里,但是在發布場景里不允許帶有這塊數據,即使Disable也不行,會對場景載入帶來額外負擔。
光源參數,不允許設置光源的ShadowType的Resolution屬性,必須為Use Quality Settings。在做UWA的深度優化的時候,我們發現有些情況下ShadowMap的尺寸從8M增加到了32M,檢查後發現是美術為了更好的效果自己調整了這個參數導致的。脫離程序高中低效果控制的配置是不被允許的。
物理碰撞體,在目前的手游上,我們的希望是盡量少地使用物理,因此我們無論是在尋路還是在動態阻擋方面都盡量少地使用物理,同樣在場景里禁止擺放MeshCollider組件,對於其他類型的Collider組件也進行檢查,對於我們項目通常都是不需要的。
攝像機設置,我們遊戲中鏡頭是完全由程序邏輯控制的,因此不允許在場景中遺留用於預覽的Camera組件。
就像之前說的那樣,這部分非常零散,通常是在項目開發中不斷發現的各種問題通過統一的檢查工具來讓美術在上傳之前進行自查,確保資源提交到svn上的時候就是正確的。
1.3 美術資源檢查
在美術大量產出資源的過程中,除了美術自查之外,還需要其他職位的同事同時對美術資源進行檢查。
在我們項目中,對於美術資源的檢查主要有三個部分:
程序檢查。程序會根據發現的性能問題進行針對性的檢查,比如會使用Profile、FrameDebug工具等進行問題的排查。
我們也是UWA產品的深度用戶,購買了專業會員,幾乎每個月都會提交一份安卓版本的包讓UWA團隊幫忙進行在線的性能診斷與優化,頻繁的優化周期內可能會每周提交一份包。去年的年底也邀請UWA團隊來公司針對我們項目進行了深度優化,發現和解決了不少美術資源的問題。
QA每周的性能測試。每周QA團隊會在周版本之後,生成《性能測試報告》,以郵件的方式發送到全公司所有人的郵箱里。
這裡以我們的《性能測試報告》為例來說一下QA團隊進行性能監測的內容。
1.3.1 包體大小監控
我們經歷過在測試上線之前要進行包體大小優化的情況,因此將包體大小的監控放到了每周的性能測試報告里。
我們會統計整體包體大小、資源佔用大小、按照場景、UI、角色、特效等分類之後統計各自的大小,以及如果包體大小有變動,主要原因是什麼。這部分使用的工具主要是基於打包時候產生的中間文件來統計分析。如下圖所示,左側是按照文件夾分類的列表,右側是選中的節點下的所有文件,可以進行排序、關鍵字過濾,以及多選統計等操作。
在進行資源大小統計和分析方面,我們也開發了一套在Unity內的根據資源的引用關係進行分析的工具,統計對象為所有要打包出去的資源,可以查看資源之間的引用關係、被引用關係、資源消耗統計(粒子系統數量、GameObject數量等)。藉助這個工具可以方便地進行一些不再需要的資源篩查等分析工作。
1.3.2 遊戲幀率統計
另外一塊需要持續關注的是遊戲在設備上的運行幀率。
我們會分別在高配和低配機器上對戰鬥外、不同的戰鬥類型進行幀率的統計,並和之前的記錄進行對比。
1.3.3 具體資源的檢查
在具體資源的檢查方面,由程序提供盡量簡便的測試和統計工具,由QA進行自動化的檢測,主要包括場景資源檢查、場景合法性檢查、技能特效檢查、UWA GOT性能數據等部分。
場景部分主要是統計Batch和面數。我們製作了一個工具,按照填寫的檢查點在默認鏡頭下統計四個方向的數據,同時截圖記錄。這個工具也提供了跳轉到指定坐標直接查看檢查的功能。
前文已經介紹了場景的合法性的部分,這裡是由QA每周進行一次合法性檢查,對於不合法的場景反饋給美術進行修改。
特效檢查主要集中在Drawcall、描述以及粒子數量這幾個比較常規的方面,同樣會列出不合格的特效讓美術修改。
對於特效的Overdraw的統計,我們是針對技能進行的,因為技能釋放過程中會有鏡頭的軌跡,因此這種方式更加合理。工具的功能是逐個釋放技能,然後記錄技能釋放過程中最大的那幀Overdraw的數據和截圖,方便美術排查。
我們也讓QA團隊在時間充裕的情況下使用UWA的GOT工具進行性能數據的記錄,方便程序進行對比和檢查。對於內存的統計數據也會從Overview測試中獲取。
PPT截圖的右下角是工具的製作者在我們組內的外號。標註在這裡也說想說明這些工具的開發工作是整個團隊一起來分工協作完成的,而不僅僅是分享者的功勞。
1.4 小結
第一部分的最後,我們做一下小結。經過規範制定、規範執行以及資源檢查這些步驟,美術資源的優化就形成了一個閉環,這個閉環中的各個環節是由不同職位的同事協作來完成的,程序在其中起到了穿針引線的作用。在規範制定階段,是由程序和美術主導,同時注意從策劃和運營等處收集遊戲設計的信息;在美術製作階段的主角是美術,程序則為美術提供更好用的檢查工具輔助美術自查;資源檢查階段由QA主導,程序負責提供便利的自動化檢查工具,美術則需要對發現的問題進行修正,同時這個階段的檢查結果也可能反饋到規範制定的內容上,對一些規範細節可能會進行修正和調整。
2. 程序代碼優化
在聊完美術資源的優化之後,我們回歸到程序的部分,從一個比較宏觀的角度來看一下程序代碼的優化部分。
Donald Knuth在他的一篇文章里說:
We should forget about small efficiencies, say about 97% of the time: premature
optimization is the root of all evil. Yet we should not pass up our opportunities in that critical 3%.
這段話是那句很有名的——「過早優化是萬惡之源」的出處。首先我很贊同這句話,在沒有必要的情況下進行盲目的優化浪費時間而且有可能有負面作用,但是正如這句話後半句所說,在少量的情況下,我們也不應該放過那些會產生嚴重影響的機會,而這些地方,往往是踩了坑之後才知道的,比如前文所說的美術大量使用四方連續的貼圖這樣的例子。所以這部分我想聊的主題內容著眼在「過早優化是萬惡之源」的另一面,在程序中越早關注我覺得收益越高的部分。
底層模塊。這點比較容易理解,即便是在做好層次劃分的情況下,越底層的模塊對於上層的影響越大,因此早期花費較多的時間和精力在重要的底層模塊上,對於後期的優化可能會有意想不到的收益。
代碼質量。我覺得保證團隊的代碼質量,對於優化和開發效率有著潛移默化的重要影響。
全員參與(補)。在一些團隊里,優化主要由那麼1-2個資深的同學主導進行,而在我們的團隊里,優化的工作是一件全員參與的事情。
2.1 底層模塊
首先來看下底層模塊,這一部分的重要性不言而喻,我想舉兩個例子來說明一下它們設計得好壞對於我們項目的影響。
其中一個是我覺得我們做得比較好的地方——Lua與C#的職責劃分,另外一個是我們踩了很多坑的資源管理模塊。
2.1.1 Lua與C#的職責劃分
Lua與C#的職責劃分可以使用這樣一張圖來描述:
這裡涉及到的語言有C#、Lua和C三種,在項目最初期,我們就決定將大部分的業務邏輯放在Lua層來做。這樣做的原因和大部分使用Lua的項目一樣——為了保證大部分的業務邏輯可以被Hotfix以及Patch更新。同時我們客戶端團隊有大量的Python腳本使用經驗,因此對於Lua來說上手也是沒有特別大的問題。
當決定了核心的業務邏輯存放位置的時候,數據的存放也就比較明了了。我們遵循的一個設計理念是——讓數據盡量靠近它的使用者。
數據越靠近最終的使用者,中間需要進行轉換的CPU和內存消耗就越小。因此我們使用Lua這種原生就支持數據存儲的方式,即Lua Table的方式來存儲客戶端使用的數據。
接下來是網路部分。在項目最初的時候我也考慮過是否應當將網路放置在C#層,因為對於Unity引擎來說,C#是更加原生的語言,對於一些庫的支持也特別方便。但是經過一些思考和討論之後,我們決定將網路放在C層,這樣做的原因和數據放置在Lua層是一樣的。因為對於客戶端來說,網路是數據的來源和發送者,相當於一個數據源的角色,因此也應該更加靠近它的使用者。不放在Lua層的原因是網路傳輸中還是有不少計算量的,加密、內存拷貝等,因此放在C層效率更高。同時我們在C層集成了一些Lua中缺少的擴展庫。
選擇放在C#層的部分有這麼幾種:
引擎功能,這個毋庸置疑,Unity本身就是通過C#來提供引擎介面的;
計算密集型邏輯,對於一些可能涉及到CPU消耗的計算密集的邏輯,封裝成C#的介面供Lua調用;
Tick邏輯,即一些需要持續Update的邏輯,放在C#層通過Component的Update函數來實現;
交互頻繁的邏輯,一些需要Lua和C#頻繁交互的邏輯放置在了C#層來實現,同樣封裝成介面供Lua調用。
這樣的設計依據另外一個設計理念——將Unity作為引擎層來使用,讓C#層盡量少地關注具體的業務邏輯。
在這樣的設計下,C#和Lua之間的交互就非常清晰:
C#通過tolua# warp出來的介面讓Lua調用,Lua是邏輯的驅動者;
C#提供每幀一次的Update/LateUpdate調用,具體內部需要的分發由Lua自己來做,大量的間隔邏輯通過Timer模塊來實現,減少每幀的tick邏輯;
C#對於Lua的感知僅僅是一些非同步操作的回調,比如按鍵點擊事件、非同步載入完成的回調邏輯。
在這樣的設計之下,UWA對我們項目進行深度測試的時候給出的測試報告結論如下:
UWA團隊告訴我們在他們測試的重度項目里,這個數據已經是非常好的了,我們項目在這塊也只使用自己開發的調用次數統計工具進行常規的優化,在項目後期並沒有花費太多的時間。
同事增練開發的調用次數統計工具
這裡提一下跨語言編程的時候關於對象/資源生命周期的設計理念:誰創建誰銷毀。
簡單明了,如果一個對象是由Lua創建的,那它一定要由Lua來顯示地負責銷毀;而如果一個對象是由C#邏輯來創建的,那一定由C#來進行銷毀。只有這樣才能夠避免生命周期錯亂導致的泄露或者提前銷毀的錯誤,對於泄露的檢查也更加明了。
2.1.2 資源管理模塊
我們項目中的資源管理模塊是我覺得由於最初設計不夠導致後期踩了很多坑的一個部分,其中一個表現就是每次遊戲要進行測試上線之前都要花時間解決載入模塊的各種奇怪問題。我們熬夜修復的問題有這麼一些:
最初的版本因為一些迭代導致資源的引用計數存在問題,出現了Asset被卸載了但是又被使用的情況,再次嘗試載入資源就報錯了。後來又發現非常嚴重的內存泄露,表現是幾乎所有的資源都殘留在了內存=_=……為了修復泄露問題,我們將底層AssetBundle的管理從原來的Unload(false)修改為了Unload(true),雖然解決了泄露問題,但是需要上層邏輯有一些迭代工作。後續我們還嘗試處理同一個資源在非同步載入過程中有同步載入請求的問題。
現在回頭來看,對於資源管理模塊可以進行反思的內容有如下幾點:
最初的時候由於團隊內對於Unity的經驗不是很足,面對資源管理模塊這個非常重要的部分,想法是藉助比較成熟的開源框架來彌補經驗上的缺失。所以在大致了解了基本原理之後,選擇了KSFramework這套開源框架。它對於資源管理模塊有一套基於Loader設計的封裝,我們又根據自己的需求和發現的問題進行了一些迭代工作。在初期編輯器模式下,這套東西幫助我們快速建立了Demo和推進前期功能的開發,但是也隱藏了很多設備上的問題。這應當說是非常標準的技術債務,只是沒想到需要付出這麼高昂的利息。
在後期的維護中,因為技術團隊的擴張和一些「不可抗力」的原因,這個模塊先後經手三個負責人的維護,在交接以及討論中因為理念不同也產生過一些設計上的誤解,埋下了一些問題。
最後,因為編輯器模式下沒有使用非同步載入的方式,因此運行邏輯和設備上是不同的,導致很多非同步的問題在真正進行大範圍的真機測試的時候才暴露出來,需要在比較高壓的條件下進行修復,帶來了很多挑戰。
最後,想說的一個點是——即使面臨很大的壓力,對於一些奇怪的問題,不要嘗試用一些臨時手段進行掩蓋和容錯的方式進行處理,而是盡量地去找到問題產生的根源,從根本上進行解決。
有時候在沒搞清楚根本原因的情況下貿然通過「補洞」的方式來進行問題的修復,可能會把坑埋得更加深,讓問題更難復現和排查。我在項目中就經歷過這樣的情況,也都是血與淚的教訓。
2.2 代碼質量
代碼質量的重要性我依然想講一個我在《無盡戰區·覺醒》這樣一款手游開發項目中的例子來進行說明。
在項目中後期,我們進行Python層性能優化的時候發現:__dict__這樣的屬性訪問佔用非常高。用過Python的同學可能都知道這是Python中進行屬性訪問的方式。排查調用源發現很多優化的點都是類似上面這樣的局部變數的優化。(圖中的代碼只是示例,並非真實代碼。)
而對於每一個入職的同事,在進入公司的Python課程里,都會學習到在腳本語言中盡量使用局部變數來進行性能優化的方法和原理。然而在真正的項目開發中,還是會有很多人忽略這種優化,這是代碼質量偏低的一種表現。
在接下來的三四天時間裡,我們不斷地Profile、修改,對__dict__性能消耗比較大的地方使用局部變數的方式進行性能優化之後,整個腳本的性能有了大約10%-20%的性能提升。這是非常大的一個優化了,而且完全是無損的優化。如果我們的開發人員可以在日常的開發中就注意維護代碼質量,對於這些優化時間的消耗就可以節省掉不少。
在我看來,在項目開發中可以提升程序團隊的代碼質量的方式包括如下幾個方面:
針對性的培訓和定期的技術分享。技術分享可能會花費挺多的時間,但是在時間相對寬鬆的研發期堅持進行技術分享還是會給團隊帶來有多正向的收益。我們一年多的創業時間內,技術分享大約做了十幾場,雖然和大廠的分享相比不算很多,但是在促進團隊技術進步、提升代碼和設計質量等方面還是起到了很好的作用。
代碼Review。也有不少人和我討論過在團隊內進行大範圍的Code Review的可行性。首先Code Review對於提升代碼質量肯定是有很大幫助的,但是從我個人的項目經驗來說,要在手游這樣一個需要快速開發迭代的團隊里推行嚴格的Code Review代價還是非常大的。比如工作壓力比較大的情況下,我們一個同事可能會在一天產出上千行的Lua代碼,如果想要另外一個同樣有這樣大工作壓力的同事抽出時間來進行完整的Review,幾乎是一件不可能的事情。因此我們選擇只在關鍵節點進行Review,包括核心代碼和線上Bug修復代碼,以及新同事入職的第一個月提交的代碼。我們有過一次集體Review和迭代的過程,對於項目中會由多人共同維護的一段邏輯,大家都花時間進行迭代,然後分享自己迭代的思路。這種方式雖然會花費團隊挺多時間,但是偶爾針對特定代碼進行還是比較有效果的,可以統一大家對於關鍵部分代碼的設計理念和使用方式。
靜態分析工具。這塊我們在使用的有LuaChecker和UnityEngineAnalyzer,針對代碼進行檢查,可以發現一些優化的點。
2.3 全員參與的優化(補)
我們客戶端程序團隊在進行優化的時候和一些團隊不同的做法是大家都針對自己負責的部分進行優化。這樣做和團隊自身的特點有關,我們客戶端團隊對於一個創業團隊來說算是經驗和技術能力都不錯的一個團隊,每個成員都有多年的遊戲開發經驗。因此每一個同事都負責一些比較底層的模塊,也會負責各個玩法系統的開發,是一個縱向的結構。總結起來,讓所有同事都參與優化的好處主要有:
讓團隊中的每個人建立優化意識;
每個人作為自己負責模塊的優化負責人;
組織專門的優化周期,橫向對比,互相學習。
組織專門進行優化周期的Evernote記錄
3. 團隊開發效率優化
終於來到第三部分,也就是之前說的看上去和性能優化並沒有直接關係的團隊開發效率優化。
在聊這部分之前,我想讓讀者思考一個問題——我們為什麼要做優化?
是為了讓遊戲的運行更加流程?讓遊戲更加省流量?更省電?讓遊戲包體更小?這些都是我們進行優化的目標,但歸根結底,我們做這些優化的目標都是——為玩家提供更好的遊戲體驗。
所以在我看來,如果一個優化,無論使用多麼高超的技巧,如果它的優化結果無法直接或者間接地被玩家感受到,那這個優化可能就只是一個程序員的「自嗨」,無法為遊戲提供真正的價值。反過來說,如果我們可以優化團隊的開發效率,讓團隊有更多的時間來開發新的功能、製作更多的遊戲細節,那對於遊戲來說也是一種優化。
因此在我看來,進行團隊效率的優化是一件非常重要的事情,也是程序的職責之一。我主要想從這樣三個方面來聊一下如何進行團隊開發效率的優化:工作流的構建、程序團隊、策劃團隊。
3.1 工作流的構建
我覺得在項目中構建更好、更順暢的工作流可以很大地提升整體團隊的工作效率。我以我們團隊現在一個功能的完成流程為例來分享一下我們團隊使用的工作流。
策劃提前和程序、美術溝通需求的可行性,在可行性確定之後,通過Redmine這樣的管理軟體提單,將需求詳細地描述在任務單里;
我們在Redmine中集成了Webhook的功能,當有任務提出的時候,Redmine會通過釘釘的介面通知到對應的程序;
程序根據自己手頭的工作安排進行排期和功能實現,當任務單完成並進行自測之後,會將代碼提交到svn上,同時將Redmine上的單子修改為「已完成」的狀態,狀態的變更會同樣通知到相應的策劃和QA;
SVN通過SVN hook的方式,自動觸發Jenkins的Lua代碼編譯指令,Jenkins調用我們部署在公司內網的一套分散式打包服務,進行腳本編譯。我們團隊中只有程序有Lua代碼的svn訪問許可權,其他職位統一使用編譯好的Lua bytes code。
當打包完成之後,分散式的打包服務會調用釘釘介面將完成消息通知到特定的群里。
策劃需要進行導表、更新伺服器,或者QA同事需要進行安卓/iOS打包的時候,都是通過Jenkins進行請求,Jenkins繼續調用分散式打包服務進行打包,並將結果通知到群里。
Jenkins上的部分服務
對於Jenkins部分,提醒一下要做好許可權控制,對於其他職位可能需要的,盡量避免參數式的執行方式,而是以多個任務的方式提供。而程序部分則可以盡量靈活地使用參數進行構建。對於發布版本打包、分支創建等功能,通過許可權控制不要讓策劃/美術/QA誤操作點擊到。
我們的分散式打包服務是基於Python構建的,通過簡單的RPC服務進行內網跨機器的互聯,通過argparse模塊進行參數化的提供,方便擴展:
我覺得這樣的工作流的好處主要有:
程序將更多的事情推出去,交給工具,自己可以更加專註在程序開發的工作上;
其他職位擁有更多的自主權,在不需要程序參與的情況下可以完成自己的很多工作;
通過釘釘這樣的IM的通知功能,將輪詢的消息變成通知,不再需要等待和關注Jenkins任務的完成進度,完成之後自然就會收到通知。
3.2 提高程序開發效率
這塊基本都在PPT里了,不贅述了,其中調試工具部分再次推薦一下:Hdg Remote Debug這樣的設備調試工具,關於Lua的部分在3月份的博客中已經說得非常詳細了,也不再重複。
3.3 策劃工作效率優化
策劃工作效率的優化部分想講兩個切身經歷的事情。一個是非常小的一個優化,幫助策劃實現NPC坐標從Unity中拷貝到Excel中。
我們因為開發周期比較緊,而且伺服器需要一些NPC的位置數據做驗證,因此沒有在Unity內部為策劃實現NPC編輯器,而是需要策劃手動去Excel表裡填寫。這裡就有一個填寫坐標的過程,最初的時候策劃手動填寫非常費時間,而且容易出錯,後來幫助他們實現了一個點擊GameObject節點拷貝坐標到粘貼板的功能,策劃使用後表示極大地提升了填寫NPC表格的工作效率。
有時候程序只需要通過很簡單的代碼就可以幫助其他職位的同事解決一些工作中的痛點,提高工作效率。
第二件事情是之前在大公司工作的時候的一個親身經歷。當時在帶新人做mini項目,一個新人策劃就在公司的KM知識分享平台上提了一個問題——他表示現在的策劃填表的工作效率很低,需要經歷這樣幾個複雜的步驟:
在Excel中編輯數據,然後提交到SVN上,通過導表將數據轉換成程序代碼讀取的資源,然後更新伺服器,更新客戶端,啟動客戶端連接伺服器才能查看結果,這些步驟要花費大約10-20分鐘的時間。他問能否編寫完數據之後就可以直接在遊戲內看到結果?
當時的我作為自以為在遊戲行業已經有幾年工作經驗的「過來人」,看到新人策劃有這樣的疑問,心裡其實是有一些嘲諷的。所以去「耐心」地回復他:對於客戶端來說,可以做到本地導表然後不重啟客戶端就可以直接Reload數據查看結果,但是如果你不把數據上傳到svn上,伺服器如何知道你本地修改的結果?這就像那樣一個笑話:
「是這樣的,張總, 您在家裡的電腦上按了ctrl+c,然後在公司的電腦上再按ctrl+v是肯定不行的。即使同一篇文章也不行。不不,多貴的電腦都不行。」
這個笑話後來的結果是自己成了一個笑話,因為雖然時代的發展,網路硬碟等雲服務的普及,也有了跨電腦進行粘貼拷貝的功能……張總不再需要很貴的電腦就可以實現自己的操作。
這個故事的發展和這個笑話有些相似,在大約半年之後,我和工作室的另外一個同事將rpyc這樣一套中間件引入公司並基於它實現了跨進程的外掛式編輯框架。基於這套框架就實現了策劃在編輯器內編輯完數據,只需要點擊重載數據的按鈕,就可以自動更新本地的客戶端和指定ip的一台伺服器中的數據,不再需要提交到svn,甚至不需要重啟客戶端就可以看到修改之後的結果。
經過樣的改進之後,之前需要10-20分鐘左右時間的操作,現在只需要2-5秒就可以實現,極大地提升了策劃的工作效率。我和那位同事也因此拿了當年公司內部的技術分享獎。
這個故事對於我的觸動還蠻大的,因為最初我所嘲諷的一個新人的想法,最終由我和另外的一個同事一起進行了實現,這對於我來說也是一種諷刺。因此在之後我再聽到策劃或者其他職位的一些看上去「異想天開」的想法的時候,不會急於反駁或者指出其中的漏洞,而是先想想是否自己的思路被自己了解的技術所禁錮,是否有別的方式可以真的實現這些想法。
通過這兩個故事我想表達一個觀點,對於程序在團隊效率優化方面應當承擔什麼樣的角色?借用《蜘蛛俠1》里非常有名的那句話來說——能力越大,責任越大。
因為程序是整個團隊中最了解技術和開發的人,也最有能力開發一些工具或者引入一些方法讓整個團隊的工作效率得到提升,因此也應該肩負起相應的責任。
4. 總結
最後,我們聊了這麼多,進行一些總結。
我在遊戲行業里也做了五六年,特別是自己在創業的這一年多的時間,讓我更加深切地感受到遊戲開發非常符合這樣的冰山理論。
浮在冰面上的這一部分是玩家可以感受到的遊戲內容,比如精緻的美術資源、有趣的玩法,而在這之下,有更多無法被玩家直接感受到的內容,比如被迭代掉的玩法。而今天我們所聊的這些優化的內容,比如美術規範、代碼質量、團隊的工作流構建,它們大都是水面以下,無法被玩家切身感受到的部分。但它們又是如此地重要,是整座冰山不可或缺的一部分。
就像我之前所說,通過剛才的分享大家應該也可以感受到,這些優化的內容非常的瑣碎繁雜,就像散布在各個地方的一個又一個點,是團隊的協作讓這些點可以連接成線,形成類似於美術資源的規範制定、規範執行和規範檢查這樣的閉環,而在整個遊戲的開發周期過程中通過團隊持之以恆地去做這些事情,讓這些線連接成一張大網,將水聚攏在周圍凝結成冰,托起了整座冰山,使得海面上可以被玩家感受到的內容越來越多,這就是我眼中基於團隊的持續優化之道。
最後的最後,我還是想把我在上次分享中也說過的一句話送給大家。
這一年多的創業經歷讓我更加深刻地體會到遊戲開發是一件艱難而且辛苦的事情,有一些朋友或者同事也找我聊作為一個程序進行遊戲開發的迷茫,我自己內心也曾有過彷徨和糾結。因為很多事情太過瑣碎,帶給我們的成就感可能也會偏低。但是我也發現,現在做過幾年遊戲行業之後,依然留在遊戲行業中的人心中都有著對於遊戲發自肺腑的熱愛和激情。它們或從小就喜歡遊戲,或曾經被遊戲感動過心中最為柔軟的那個部分,在遊戲行業內堅持做這些著看似平凡的工作。
所以,我想把這句話送給所有依然在堅持的遊戲開發者們——不忘初心,不愧平凡,相信通過團隊的協作和堅持不懈的努力,可以給這份平凡以不凡!
謝謝!
也歡迎大家來積极參与U Sparkle開發者計劃,簡稱"US",代表你和我,代表UWA和開發者在一起!


TAG:侑虎科技 |