知乎鏈接:https://zhuanlan.zhihu.com/p/36930662? (閱讀體驗更好一些。)
四月份的博客也欠下了,大部分業(yè)余的時間花在準備UWA Day 2018的分享上面。當時也說了,做這次分享也算實現(xiàn)了我去年的一個“小愿望”,借這個機會認識了不少做技術(shù)的朋友?;氐胶贾葜?,也有不少人跟我要PPT,討論一些分享中的細節(jié)。在準備分享的時候,因為時間關(guān)系,也將一些計劃擴展開的內(nèi)容刪除掉了。所以這次想再根據(jù)準備時候的逐字稿整理一篇博客,以作記錄,也為沒有參加UWA Day 2018的朋友提供參考。(并不是偷懶不想寫欠下的那些博客,真的!)
所以這篇博客的主題還是以這次分享的核心內(nèi)容為主線,順便把PPT的內(nèi)容也提供出來,可能添加少量當時沒聊的私貨(以引用塊或者(補)的方式標注,以方便聽過分享的朋友閱讀)。
0. 題目和提綱
這次分享的題目是《Connecting The Dots——基于團隊的持續(xù)優(yōu)化之道》。優(yōu)化是一件瑣碎而又繁復(fù)的事情,它通常是一個又一個的點,而我們?nèi)绻屢粋€比如說30人甚至50人的大型團隊,花1到2年的時間去開發(fā)一款大型的手游項目,不但需要了解和掌握這些優(yōu)化的細節(jié),而且要借助團隊的力量在整個游戲開發(fā)的生命周期過程中不斷地進行持續(xù)優(yōu)化,才能夠讓優(yōu)化效果可以保持下去。

在聊具體的分享內(nèi)容之前,我想先拋出這樣一個問題——在優(yōu)化這樣的過程中,程序應(yīng)該承擔一個什么樣的角色?

這也是和我剛才所想要聊的持續(xù)優(yōu)化關(guān)聯(lián)性非常大的問題。在優(yōu)化過程中,程序來可能有兩種角色,一種是像救火員這樣的角色,哪里冒火了,就去撲滅它。我自己也有過作為救火員去做優(yōu)化的經(jīng)歷。另外一種工作方式是像園丁一樣,可能不需要特別緊急地在最后關(guān)頭才處理那些性能問題,而是從項目的初期就開始規(guī)劃整個項目的性能指標,然后讓整個團隊可以按照指標的要求合理地產(chǎn)出美術(shù)資源、編寫代碼,從而讓整個產(chǎn)品一直處于一個比較健康的狀態(tài)。
救火員的職責很有挑戰(zhàn)性,同時也比較容易做出成就感,比如我也過用1到2周時間讓游戲的性能有非常大提升的經(jīng)歷。而園丁在做的事情可能看上去比較普通,都是些日常且瑣碎的事情。在游戲開發(fā)中,這兩種角色并不沖突,而是同時存在的。但是在我的理解里,對于性能更友好的狀態(tài),是讓團隊里的成員都在以一個園丁的角色在工作,從根本上來保持游戲的性能一直處于一個較好的狀態(tài)。因為——
每一個神級優(yōu)化的背后,都隱藏著一個2B的bug!
那這次分享我就想從這樣三個方面來聊一下我所理解的以“園丁”的角色來讓團隊進行繼續(xù)優(yōu)化的方式。

首先是美術(shù)資源的優(yōu)化,美術(shù)資源是一個會對客戶端運行效率產(chǎn)生非常重要影響元素之一,我想和大家聊一下,怎么樣讓我們團隊可以在美術(shù)資源產(chǎn)出方面一直保持一個比較好的狀態(tài)。然后我想回歸到程序的本職工作,從一個更加宏觀的角度來討論一下程序代碼的部分要怎么樣去做一些優(yōu)化,對于代碼質(zhì)量和一些底層模塊的設(shè)計,如何去做一些提前的思考和設(shè)計。第三部分,團隊開發(fā)效率的優(yōu)化,這塊可能看上去跟性能優(yōu)化的題主沒有直接的關(guān)系,但是在我看來優(yōu)化不僅僅是游戲運行效率優(yōu)化,而且應(yīng)該包括整個團隊開發(fā)效率的優(yōu)化,這也是在項目開發(fā)過程中非常重要的部分。
1. 美術(shù)資源優(yōu)化
首先來看一下美術(shù)資源優(yōu)化的部分。

大家在日常開發(fā)中可以感受到,對于美術(shù)資源部分,美術(shù)和程序,尤其是要進行性能優(yōu)化的程序,他們的關(guān)注點會有不同。美術(shù)更多的是關(guān)注美術(shù)效果是不是足夠的好,而程序可能更多的關(guān)注美術(shù)資源在設(shè)備上的運行效率是否可以達到目標幀率,這其中就有一些目標導(dǎo)向上的沖突。我認為,解決這一沖突的一個非常重要的手段是盡早來建立合理的美術(shù)資源制作規(guī)范。

這其實是一個美術(shù)資源制作的過程,首先通過在立項時制作Demo來確定適合的美術(shù)資源規(guī)范,然后通過給美術(shù)灌輸效率意識、提供輔助工具讓美術(shù)在制作資源的過程中可以嚴格按照規(guī)范進行,最后是要借助QA的力量對產(chǎn)出的資源進行檢查,確保達標。
1.1 規(guī)范制定
為什么制定美術(shù)規(guī)范這么重要呢?我想借自己之前的一個項目中的真實經(jīng)歷來聊一下。

《無盡戰(zhàn)區(qū)·覺醒》這款手游是我作為主程帶領(lǐng)團隊開發(fā)的第一款手游項目。當這個項目進行到中期的時候,收包了一批場景的資源。在使用這些資源的時候發(fā)現(xiàn)它們的運行效率明顯偏低,Profile發(fā)現(xiàn)面數(shù)和Drawcall都超標了。面數(shù)很簡單,使用減面工具進行減面即可,Drawcall也使用類似Unity的Static Batching的方式進行優(yōu)化,但是結(jié)果發(fā)現(xiàn)一些場景的Drawcall只能從400減少到300多,并不能達到預(yù)期。仔細檢查發(fā)現(xiàn)很多模型無法合并的原因是使用了端用常用的一種貼圖制作方法——四方連續(xù)的貼圖。這種方式雖然可以使用很小的貼圖尺寸通過tiling制作出精細的效果,但是對于Drawcall敏感的手游來說并不合適。最終項目又請了好幾個外派美術(shù)進行資源的整合和制作,導(dǎo)致多花費了幾個周時間和幾十萬的美術(shù)成本。雖然對于大公司來說幾十萬的美術(shù)成本不算什么,但是我依然覺得這是我作為一個主程的失職——因為沒有提前和美術(shù)溝通好制作方法,導(dǎo)致了這樣的問題發(fā)生。所以在出來創(chuàng)業(yè)的項目中,在項目最初期Demo制作完畢之后,客戶端團隊就和美術(shù)一起制作了非常詳細的美術(shù)資源制作規(guī)范。
制定規(guī)范的步驟大致可以整理為如下的幾個部分:

在制定美術(shù)規(guī)范的第一步是要進行游戲信息的收集,因為不同的游戲類型以及鏡頭視角會對美術(shù)資源的制作產(chǎn)生非常大的影響,比如2.5D和3D自由視角的游戲是有不同的制作和優(yōu)化方式。
在收集了足夠的信息之后,需要和美術(shù)敲定一些大的技術(shù)方向,比如線性空間、HDR和動態(tài)光影等。線性空間是團隊應(yīng)當提前關(guān)注的一個點,我個人的觀點是在寫實的游戲風(fēng)格中,能上線性空間還是盡量使用線性空間,對于美術(shù)效果的提升還是很有幫助的。在正確的基礎(chǔ)上,才更容易出正確的效果。Unity目前的版本和OpenGL ES 3.0的普及率,個人觀點是完全可以在移動平臺上使用線性空間的。當然這也要考慮具體項目的內(nèi)容以及開發(fā)團隊成員的能力和經(jīng)驗等因素。HDR的開關(guān)在Unity中還是比較簡單的,但在性能方面對于帶寬的影響比較大,收益也不小,建議追求效果的團隊提前考慮。動態(tài)光影是我們研發(fā)團隊的美術(shù)特別想追求的一個效果,但是因為我們項目今年要上線,而且要考慮低配的效果,所以程序一直卡著不讓用場景使用全實時的動態(tài)陰影,而是盡量使用烘焙的方案。
確定目標機型就不多說了,考慮主流的高中低三檔,建議提前購買一些設(shè)備方便后續(xù)的性能測試。我對于這三檔的基本分類大致如下:
高檔:大部分iOS設(shè)備,主流的安卓旗艦設(shè)備;
中檔:少部分希望支持的iOS設(shè)備,比如目前的iPhone 5s、6/6s,ipad mini等,1-3年前的旗艦以及1-2年的1500元左右的安卓設(shè)備;
低檔:1-3年前的千元機。
這個也可以參考UWA測試中的機型選擇。

接下來是要針對面數(shù)和drawcall這兩個核心點,對整體的標準進行定義,并將這些標準分配到場景、角色、特效、UI等資源分類上。因為這些不同類型的資源是由不同的美術(shù)同學(xué)產(chǎn)出的,他們之間通常不會去溝通各自的性能消耗,因此需要針對每部分進行規(guī)則的細化。

場景部分強調(diào)一下貼圖“像素密度”的概念。在制作場景的時候需要提前定義貼圖使用的像素密度,否則的會導(dǎo)致貼圖精度過高或者不夠等問題。所謂的“像素密度”是指在正常的游戲視角下,一個一立方米的Cube應(yīng)當使用多少尺寸像素的貼圖。這個在場景制作的初期應(yīng)當定義好,美術(shù)才好去使用正確的貼圖尺寸,否則到后期優(yōu)化可能發(fā)現(xiàn)大量超標的貼圖在使用,如果此時已經(jīng)進行了貼圖的合并,美術(shù)修改的工作量會非常大。

角色部分的規(guī)范需要把面數(shù)和drawcall的標準細化到每個角色中,這時候需要和策劃溝通期望的同屏顯示人數(shù),根據(jù)不同的游戲類型會有很大的差異。比如《崩壞3》不會同時有很多戰(zhàn)斗單元,因此可以給每個角色非常高的精度,而像《御龍在天》這樣的國戰(zhàn)游戲,追求百人同屏的效果,對于每個角色的消耗限制就會比較大。
骨骼數(shù)量的部分最好在前期和美術(shù)溝通好,比如手指骨骼的減少和合并,腳部骨骼的簡化,來減少CS骨骼的數(shù)量,給飄帶等Bone骨骼留出足夠的空間。因此美術(shù)規(guī)范制定的過程并不是單純的程序給美術(shù)添加制作限制,而是一個和美術(shù)一起來討論如何在有限的資源下制作出更好效果的過程。


UI制作規(guī)范是我們初期沒有特別關(guān)注的部分,也因此踩了一些坑:
1. UI組件數(shù)量較多導(dǎo)致加載頓卡。我們有些復(fù)雜界面有上千個GameObject,幾百個UI組件,這是非常恐怖的。我們測試發(fā)現(xiàn)UI的Prefab加載的時間消耗在Unity 5.5.6上和UI組件的數(shù)量幾乎成線性關(guān)系。這里補充一張我們測試的GameObject數(shù)量和加載時長的測試結(jié)果圖(橫軸是GameObject個數(shù),縱軸是單純的Prefab加載的時間消耗,單位秒,測試環(huán)境:小米Max2):

因此建議其他團隊提前考慮UI部分的異步加載和分塊加載。(或者升級引擎到5.6.5+或者2017.3+,對于Prefab的加載速度有了很大的提升)
UI中的粒子特效建議提前考慮異步加載,我們最初直接放在了UI的Prefab中,不但因為Unity不支持Prefab的嵌套導(dǎo)致維護麻煩,而且因為每一個ParticleSystem的初始化都占用一個幾乎固定的時長(我們自己測試在PC上也大約有3ms左右),導(dǎo)致UI初始化的時候非???。使用一個間接的引用,就可以比較容易地做到既方便更新又可以異步加載。

最后針對標準,要進行真機的壓力測試,并且在組內(nèi)推廣,形成大家都認可的美術(shù)資源制作規(guī)范。
1.2 美術(shù)資源制作
在確定好美術(shù)資源的制作規(guī)范之后,就是需要在美術(shù)鋪量制作的階段讓美術(shù)可以按照規(guī)范進行資源的制作,在這個階段程序需要注意去做的事情我覺得有兩點:
幫助美術(shù)建立效率意識;
為美術(shù)提供便利的制作和檢查工具。

在我們這樣的一個初創(chuàng)團隊里,沒有經(jīng)驗豐富的TA存在,而且有部分美術(shù)同學(xué)之前來自外包團隊,對于游戲運行時的效率意識比較薄弱,因此需要客戶端程序同學(xué)不斷和美術(shù)溝通來灌輸對于游戲運行效率的關(guān)注。通常的做法包括日常的溝通、規(guī)范和技術(shù)點的分享等。
而在提供便利的工具方面,除了教會美術(shù)使用Unity已經(jīng)提供的那些性能檢查工具(Batches和面數(shù)查看、Overdraw、Mipmaps、Wireframe等渲染模式)之外,我們也為美術(shù)提供了非常多的開發(fā)和檢查工具。


從截圖中可以看到我們?yōu)槊佬g(shù)提供了大量的工具,選擇幾個來著重介紹一下:
1.2.1 場景鏡頭同步功能
在場景資源制作的時候,需要美術(shù)去關(guān)注的除了常規(guī)的Drawcall之外,還有Overdraw、Mipmap以及面數(shù)。Unity已經(jīng)在Scene View下提供了這三種渲染方式的檢查工具,但是在我們的使用中發(fā)現(xiàn),由于游戲運行時鏡頭規(guī)則的復(fù)雜和多變,導(dǎo)致美術(shù)在Scene View下無法準確判斷是否存在資源不合理的情況。因此美術(shù)提出希望可以在Game View下查看這些狀態(tài)。

最初的時候我們也是想在Game View下實現(xiàn)這些不同的渲染方式,并且已經(jīng)集成了OverDraw的檢測方式,基于的也是錢康來建議的Unite2017/OverdrawMonitor。但是后來覺得這種方式會影響美術(shù)對于游戲的操作,程序?qū)崿F(xiàn)起來也要多花一些時間,比如Mipmap的實現(xiàn)效果就不是很滿意。后來就想到另外一個思路——在Scene View下來做鏡頭和Game View下的鏡頭同步。結(jié)果就非常簡單,只需要為Game View下的Camera身上添加一個Component就好:
namespaceThorProfile{publicclassSyncSceneView:MonoBehaviour{#if UNITY_EDITORprivateSceneViewview=null;// Use this for initializationvoidAwake(){view=SceneView.lastActiveSceneView;}privatevoidLateUpdate(){if(view!=null){view.LookAt(transform.position,transform.rotation,0f);}}privatevoidOnDestroy(){if(view!=null){view.LookAt(transform.position,transform.rotation,5f);}}#endif}}
核心的代碼只有LateUpdate中的那一句:"view.LookAt(transform.position, transform.rotation, 0f);",實現(xiàn)的效果就是正常操作游戲,Scene View下的Camera可以一直跟隨移動,視角和Game View下非常接近。(知乎對于gif上傳支持不太好,放一張靜幀圖感受下,有需要自己使用上述代碼進行試驗即可。)

1.2.2 批量烘焙功能(補)
Unity的烘焙在Unity 5.X的版本速度還是比較低,我們雖然為美術(shù)專門購買了CPU強勁的烘焙機,但是比如在制作大世界的時候,因為場景拆分得比較細所以需要一次性連續(xù)烘焙多個場景,于是為美術(shù)提供了批量烘焙的功能。之前在知乎上也有朋友問過類似問題,代碼非常簡單,直接貼一下,需要的自取好了:
? ? [MenuItem("美術(shù)工具/烘焙選中場景(同步)")]publicstaticvoidBakeSelectedScenes(){Object[]selectedAsset=Selection.GetFiltered(typeof(SceneAsset),SelectionMode.DeepAssets);foreach(ObjectobjinselectedAsset){stringscenePath=AssetDatabase.GetAssetPath(obj);Debug.Log("開始烘焙場景: "+scenePath);EditorSceneManager.OpenScene(scenePath);Lightmapping.Bake();EditorSceneManager.SaveOpenScenes();//如果有更新Prefab的需求,可以放這里。EditorSceneManager.SaveOpenScenes();Debug.Log(scenePath+" 場景烘焙完成!");}}
使用同步的方式烘焙,會卡住Unity,但是烘焙速度應(yīng)該會有些提升(沒有對比過……)。
1.2.3 場景合法性檢查(補)
我們?yōu)槊佬g(shù)添加了場景的合法性檢查工具,因為我們對于場景中的相機設(shè)置等有些特別的要求。這塊跟具體項目相關(guān),只羅列一下我們在檢查的內(nèi)容以及要檢查它們的原因。
頂點格式,對于帶有color等邏輯上不需要的數(shù)據(jù)的部分給出警告。因為我們的場景是采用靜態(tài)合批的,如果有資源不小心導(dǎo)入了color等不需要的頂點數(shù)據(jù),會導(dǎo)致整個合并之后的mesh變大很多。
地形數(shù)據(jù),我們使用了T2M的工作流程,美術(shù)使用Terrain進行地表的制作,然后通過T2M插件導(dǎo)出成mesh,為了保留原始的編輯數(shù)據(jù)便于以后修改和調(diào)整,Terrain會保留在原始場景里,但是在發(fā)布場景里不允許帶有這塊數(shù)據(jù),即使Disable也不行,會對場景加載帶來額外負擔。
光源參數(shù),不允許設(shè)置光源的ShadowType的Resolution屬性,必須為Use Quality Settings。在做UWA的深度優(yōu)化的時候,我們發(fā)現(xiàn)有些情況下ShadowMap的尺寸從8M增加到了32M,檢查后發(fā)現(xiàn)是美術(shù)為了更好的效果自己調(diào)整了這個參數(shù)導(dǎo)致的。脫離程序高中低效果控制的配置是不被允許的。
物理碰撞體,在目前的手游上,我們的希望是盡量少地使用物理,因此我們無論是在尋路還是在動態(tài)阻擋方面都盡量少地使用物理,同樣在場景里禁止擺放MeshCollider組件,對于其他類型的Collider組件也進行檢查,對于我們項目通常都是不需要的。
攝像機設(shè)置,我們游戲中鏡頭是完全由程序邏輯控制的,因此不允許在場景中遺留用于預(yù)覽的Camera組件。
就像之前說的那樣,這部分非常零散,通常是在項目開發(fā)中不斷發(fā)現(xiàn)的各種問題通過統(tǒng)一的檢查工具來讓美術(shù)在上傳之前進行自查,確保資源提交到svn上的時候就是正確的。
1.3 美術(shù)資源檢查
在美術(shù)大量產(chǎn)出資源的過程中,除了美術(shù)自查之外,還需要其他職位的同事同時對美術(shù)資源進行檢查。

在我們項目中,對于美術(shù)資源的檢查主要有三個部分:
程序檢查。程序會根據(jù)發(fā)現(xiàn)的性能問題進行針對性的檢查,比如會使用Profile、FrameDebug工具等進行問題的排查。
我們也是UWA產(chǎn)品的深度用戶,購買了專業(yè)會員,幾乎每個月都會提交一份安卓版本的包讓UWA團隊幫忙進行在線的性能診斷與優(yōu)化,頻繁的優(yōu)化周期內(nèi)可能會每周提交一份包。去年的年底也邀請UWA團隊來公司針對我們項目進行了深度優(yōu)化,發(fā)現(xiàn)和解決了不少美術(shù)資源的問題。
QA每周的性能測試。每周QA團隊會在周版本之后,生成《性能測試報告》,以郵件的方式發(fā)送到全公司所有人的郵箱里。
這里以我們的《性能測試報告》為例來說一下QA團隊進行性能監(jiān)測的內(nèi)容。
1.3.1 包體大小監(jiān)控
我們經(jīng)歷過在測試上線之前要進行包體大小優(yōu)化的情況,因此將包體大小的監(jiān)控放到了每周的性能測試報告里。

我們會統(tǒng)計整體包體大小、資源占用大小、按照場景、UI、角色、特效等分類之后統(tǒng)計各自的大小,以及如果包體大小有變動,主要原因是什么。這部分使用的工具主要是基于打包時候產(chǎn)生的中間文件來統(tǒng)計分析。如下圖所示,左側(cè)是按照文件夾分類的列表,右側(cè)是選中的節(jié)點下的所有文件,可以進行排序、關(guān)鍵字過濾,以及多選統(tǒng)計等操作。

在進行資源大小統(tǒng)計和分析方面,我們也開發(fā)了一套在Unity內(nèi)的根據(jù)資源的引用關(guān)系進行分析的工具,統(tǒng)計對象為所有要打包出去的資源,可以查看資源之間的引用關(guān)系、被引用關(guān)系、資源消耗統(tǒng)計(粒子系統(tǒng)數(shù)量、GameObject數(shù)量等)。借助這個工具可以方便地進行一些不再需要的資源篩查等分析工作。

1.3.2 游戲幀率統(tǒng)計
另外一塊需要持續(xù)關(guān)注的是游戲在設(shè)備上的運行幀率。
我們會分別在高配和低配機器上對戰(zhàn)斗外、不同的戰(zhàn)斗類型進行幀率的統(tǒng)計,并和之前的記錄進行對比。
1.3.3 具體資源的檢查
在具體資源的檢查方面,由程序提供盡量簡便的測試和統(tǒng)計工具,由QA進行自動化的檢測,主要包括場景資源檢查、場景合法性檢查、技能特效檢查、UWA GOT性能數(shù)據(jù)等部分。

場景部分主要是統(tǒng)計Batch和面數(shù)。我們制作了一個工具,按照填寫的檢查點在默認鏡頭下統(tǒng)計四個方向的數(shù)據(jù),同時截圖記錄。這個工具也提供了跳轉(zhuǎn)到指定坐標直接查看檢查的功能。

前文已經(jīng)介紹了場景的合法性的部分,這里是由QA每周進行一次合法性檢查,對于不合法的場景反饋給美術(shù)進行修改。

特效檢查主要集中在Drawcall、描述以及粒子數(shù)量這幾個比較常規(guī)的方面,同樣會列出不合格的特效讓美術(shù)修改。


對于特效的Overdraw的統(tǒng)計,我們是針對技能進行的,因為技能釋放過程中會有鏡頭的軌跡,因此這種方式更加合理。工具的功能是逐個釋放技能,然后記錄技能釋放過程中最大的那幀Overdraw的數(shù)據(jù)和截圖,方便美術(shù)排查。

我們也讓QA團隊在時間充裕的情況下使用UWA的GOT工具進行性能數(shù)據(jù)的記錄,方便程序進行對比和檢查。對于內(nèi)存的統(tǒng)計數(shù)據(jù)也會從Overview測試中獲取。
PPT截圖的右下角是工具的制作者在我們組內(nèi)的外號。標注在這里也說想說明這些工具的開發(fā)工作是整個團隊一起來分工協(xié)作完成的,而不僅僅是分享者的功勞。
1.4 小結(jié)
第一部分的最后,我們做一下小結(jié)。經(jīng)過規(guī)范制定、規(guī)范執(zhí)行以及資源檢查這些步驟,美術(shù)資源的優(yōu)化就形成了一個閉環(huán),這個閉環(huán)中的各個環(huán)節(jié)是由不同職位的同事協(xié)作來完成的,程序在其中起到了穿針引線的作用。在規(guī)范制定階段,是由程序和美術(shù)主導(dǎo),同時注意從策劃和運營等處收集游戲設(shè)計的信息;在美術(shù)制作階段的主角是美術(shù),程序則為美術(shù)提供更好用的檢查工具輔助美術(shù)自查;資源檢查階段由QA主導(dǎo),程序負責提供便利的自動化檢查工具,美術(shù)則需要對發(fā)現(xiàn)的問題進行修正,同時這個階段的檢查結(jié)果也可能反饋到規(guī)范制定的內(nèi)容上,對一些規(guī)范細節(jié)可能會進行修正和調(diào)整。

2. 程序代碼優(yōu)化
在聊完美術(shù)資源的優(yōu)化之后,我們回歸到程序的部分,從一個比較宏觀的角度來看一下程序代碼的優(yōu)化部分。

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%.
這段話是那句很有名的——“過早優(yōu)化是萬惡之源”的出處。首先我很贊同這句話,在沒有必要的情況下進行盲目的優(yōu)化浪費時間而且有可能有負面作用,但是正如這句話后半句所說,在少量的情況下,我們也不應(yīng)該放過那些會產(chǎn)生嚴重影響的機會,而這些地方,往往是踩了坑之后才知道的,比如前文所說的美術(shù)大量使用四方連續(xù)的貼圖這樣的例子。所以這部分我想聊的主題內(nèi)容著眼在“過早優(yōu)化是萬惡之源”的另一面,在程序中越早關(guān)注我覺得收益越高的部分。
底層模塊。這點比較容易理解,即便是在做好層次劃分的情況下,越底層的模塊對于上層的影響越大,因此早期花費較多的時間和精力在重要的底層模塊上,對于后期的優(yōu)化可能會有意想不到的收益。
代碼質(zhì)量。我覺得保證團隊的代碼質(zhì)量,對于優(yōu)化和開發(fā)效率有著潛移默化的重要影響。
全員參與(補)。在一些團隊里,優(yōu)化主要由那么1-2個資深的同學(xué)主導(dǎo)進行,而在我們的團隊里,優(yōu)化的工作是一件全員參與的事情。

2.1 底層模塊
首先來看下底層模塊,這一部分的重要性不言而喻,我想舉兩個例子來說明一下它們設(shè)計得好壞對于我們項目的影響。

其中一個是我覺得我們做得比較好的地方——Lua與C#的職責劃分,另外一個是我們踩了很多坑的資源管理模塊。
2.1.1 Lua與C#的職責劃分
Lua與C#的職責劃分可以使用這樣一張圖來描述:

這里涉及到的語言有C#、Lua和C三種,在項目最初期,我們就決定將大部分的業(yè)務(wù)邏輯放在Lua層來做。這樣做的原因和大部分使用Lua的項目一樣——為了保證大部分的業(yè)務(wù)邏輯可以被Hotfix以及Patch更新。同時我們客戶端團隊有大量的Python腳本使用經(jīng)驗,因此對于Lua來說上手也是沒有特別大的問題。
當決定了核心的業(yè)務(wù)邏輯存放位置的時候,數(shù)據(jù)的存放也就比較明了了。我們遵循的一個設(shè)計理念是——
讓數(shù)據(jù)盡量靠近它的使用者。
數(shù)據(jù)越靠近最終的使用者,中間需要進行轉(zhuǎn)換的CPU和內(nèi)存消耗就越小。因此我們使用Lua這種原生就支持數(shù)據(jù)存儲的方式,即Lua Table的方式來存儲客戶端使用的數(shù)據(jù)。
接下來是網(wǎng)絡(luò)部分。在項目最初的時候我也考慮過是否應(yīng)當將網(wǎng)絡(luò)放置在C#層,因為對于Unity引擎來說,C#是更加原生的語言,對于一些庫的支持也特別方便。但是經(jīng)過一些思考和討論之后,我們決定將網(wǎng)絡(luò)放在C層,這樣做的原因和數(shù)據(jù)放置在Lua層是一樣的。因為對于客戶端來說,網(wǎng)絡(luò)是數(shù)據(jù)的來源和發(fā)送者,相當于一個數(shù)據(jù)源的角色,因此也應(yīng)該更加靠近它的使用者。不放在Lua層的原因是網(wǎng)絡(luò)傳輸中還是有不少計算量的,加密、內(nèi)存拷貝等,因此放在C層效率更高。同時我們在C層集成了一些Lua中缺少的擴展庫。
選擇放在C#層的部分有這么幾種:
引擎功能,這個毋庸置疑,Unity本身就是通過C#來提供引擎接口的;
計算密集型邏輯,對于一些可能涉及到CPU消耗的計算密集的邏輯,封裝成C#的接口供Lua調(diào)用;
Tick邏輯,即一些需要持續(xù)Update的邏輯,放在C#層通過Component的Update函數(shù)來實現(xiàn);
交互頻繁的邏輯,一些需要Lua和C#頻繁交互的邏輯放置在了C#層來實現(xiàn),同樣封裝成接口供Lua調(diào)用。
這樣的設(shè)計依據(jù)另外一個設(shè)計理念——
將Unity作為引擎層來使用,讓C#層盡量少地關(guān)注具體的業(yè)務(wù)邏輯。
在這樣的設(shè)計下,C#和Lua之間的交互就非常清晰:
C#通過tolua# warp出來的接口讓Lua調(diào)用,Lua是邏輯的驅(qū)動者;
C#提供每幀一次的Update/LateUpdate調(diào)用,具體內(nèi)部需要的分發(fā)由Lua自己來做,大量的間隔邏輯通過Timer模塊來實現(xiàn),減少每幀的tick邏輯;
C#對于Lua的感知僅僅是一些異步操作的回調(diào),比如按鍵點擊事件、異步加載完成的回調(diào)邏輯。
在這樣的設(shè)計之下,UWA對我們項目進行深度測試的時候給出的測試報告結(jié)論如下:

UWA團隊告訴我們在他們測試的重度項目里,這個數(shù)據(jù)已經(jīng)是非常好的了,我們項目在這塊也只使用自己開發(fā)的調(diào)用次數(shù)統(tǒng)計工具進行常規(guī)的優(yōu)化,在項目后期并沒有花費太多的時間。

同事增練開發(fā)的調(diào)用次數(shù)統(tǒng)計工具
這里提一下跨語言編程的時候關(guān)于對象/資源生命周期的設(shè)計理念:
誰創(chuàng)建誰銷毀。
簡單明了,如果一個對象是由Lua創(chuàng)建的,那它一定要由Lua來顯示地負責銷毀;而如果一個對象是由C#邏輯來創(chuàng)建的,那一定由C#來進行銷毀。只有這樣才能夠避免生命周期錯亂導(dǎo)致的泄露或者提前銷毀的錯誤,對于泄露的檢查也更加明了。
2.1.2 資源管理模塊
我們項目中的資源管理模塊是我覺得由于最初設(shè)計不夠?qū)е潞笃诓攘撕芏嗫拥囊粋€部分,其中一個表現(xiàn)就是每次游戲要進行測試上線之前都要花時間解決加載模塊的各種奇怪問題。我們熬夜修復(fù)的問題有這么一些:

最初的版本因為一些迭代導(dǎo)致資源的引用計數(shù)存在問題,出現(xiàn)了Asset被卸載了但是又被使用的情況,再次嘗試加載資源就報錯了。后來又發(fā)現(xiàn)非常嚴重的內(nèi)存泄露,表現(xiàn)是幾乎所有的資源都殘留在了內(nèi)存=_=……為了修復(fù)泄露問題,我們將底層AssetBundle的管理從原來的Unload(false)修改為了Unload(true),雖然解決了泄露問題,但是需要上層邏輯有一些迭代工作。后續(xù)我們還嘗試處理同一個資源在異步加載過程中有同步加載請求的問題。
現(xiàn)在回頭來看,對于資源管理模塊可以進行反思的內(nèi)容有如下幾點:

最初的時候由于團隊內(nèi)對于Unity的經(jīng)驗不是很足,面對資源管理模塊這個非常重要的部分,想法是借助比較成熟的開源框架來彌補經(jīng)驗上的缺失。所以在大致了解了基本原理之后,選擇了KSFramework這套開源框架。它對于資源管理模塊有一套基于Loader設(shè)計的封裝,我們又根據(jù)自己的需求和發(fā)現(xiàn)的問題進行了一些迭代工作。在初期編輯器模式下,這套東西幫助我們快速建立了Demo和推進前期功能的開發(fā),但是也隱藏了很多設(shè)備上的問題。這應(yīng)當說是非常標準的技術(shù)債務(wù),只是沒想到需要付出這么高昂的利息。。。
在后期的維護中,因為技術(shù)團隊的擴張和一些“不可抗力”的原因,這個模塊先后經(jīng)手三個負責人的維護,在交接以及討論中因為理念不同也產(chǎn)生過一些設(shè)計上的誤解,埋下了一些問題。
最后,因為編輯器模式下沒有使用異步加載的方式,因此運行邏輯和設(shè)備上是不同的,導(dǎo)致很多異步的問題在真正進行大范圍的真機測試的時候才暴露出來,需要在比較高壓的條件下進行修復(fù),帶來了很多挑戰(zhàn)。
最后,想說的一個點是——
即時面臨很大的壓力,對于一些奇怪的問題,不要嘗試用一些臨時手段進行掩蓋和容錯的方式進行處理,而是盡量地去找到問題產(chǎn)生的根源,從根本上進行解決。
有時候在沒搞清楚根本原因的情況下貿(mào)然通過“補洞”的方式來進行問題的修復(fù),可能會把坑埋得更加深,讓問題更難復(fù)現(xiàn)和排查。我在項目中就經(jīng)歷過這樣的情況,也都是血與淚的教訓(xùn)。
2.2 代碼質(zhì)量
代碼質(zhì)量的重要性我依然想講一個我在《無盡戰(zhàn)區(qū)·覺醒》這樣一款手游開發(fā)項目中的例子來進行說明。

在項目中后期,我們進行Python層性能優(yōu)化的時候發(fā)現(xiàn):__dict__這樣的屬性訪問占用非常高。用過Python的同學(xué)可能都知道這是Python中進行屬性訪問的方式。排查調(diào)用源發(fā)現(xiàn)很多優(yōu)化的點都是類似上面這樣的局部變量的優(yōu)化。(圖中的代碼只是示例,并非真實代碼。)
而對于每一個入職的同事,在進入公司的Python課程里,都會學(xué)習(xí)到在腳本語言中盡量使用局部變量來進行性能優(yōu)化的方法和原理。然而在真正的項目開發(fā)中,還是會有很多人忽略這種優(yōu)化,這是代碼質(zhì)量偏低的一種表現(xiàn)。
在接下來的三四天時間里,我們不斷地profile、修改,對__dict__性能消耗比較大的地方使用局部變量的方式進行性能優(yōu)化之后,整個腳本的性能有了大約10%-20%的性能提升。這是非常大的一個優(yōu)化了,而且完全是無損的優(yōu)化。如果我們的開發(fā)人員可以在日常的開發(fā)中就注意維護代碼質(zhì)量,對于這些優(yōu)化時間的消耗就可以節(jié)省掉不少。
在我看來,在項目開發(fā)中可以提升程序團隊的代碼質(zhì)量的方式包括如下幾個方面:

針對性的培訓(xùn)和定期的技術(shù)分享。技術(shù)分享可能會花費挺多的時間,但是在時間相對寬松的研發(fā)期堅持進行技術(shù)分享還是會給團隊帶來有多正向的收益。我們一年多的創(chuàng)業(yè)時間內(nèi),技術(shù)分享大約做了十幾場,雖然和大廠的分享相比不算很多,但是在促進團隊技術(shù)進步、提升代碼和設(shè)計質(zhì)量等方面還是起到了很好的作用。
代碼Review。也有不少人和我討論過在團隊內(nèi)進行大范圍的Code Review的可行性。首先Code Review對于提升代碼質(zhì)量肯定是有很大幫助的,但是從我個人的項目經(jīng)驗來說,要在手游這樣一個需要快速開發(fā)迭代的團隊里推行嚴格的Code Review代價還是非常大的。比如工作壓力比較大的情況下,我們一個同事可能會在一天產(chǎn)出上千行的Lua代碼,如果想要另外一個同樣有這樣大工作壓力的同事抽出時間來進行完整的Review,幾乎是一件不可能的事情。因此我們選擇只在關(guān)鍵節(jié)點進行Review,包括核心代碼和線上Bug修復(fù)代碼,以及新同事入職的第一個月提交的代碼。我們有過一次集體Review和迭代的過程,對于項目中會由多人共同維護的一段邏輯,大家都花時間進行迭代,然后分享自己迭代的思路。這種方式雖然會花費團隊挺多時間,但是偶爾針對特定代碼進行還是比較有效果的,可以統(tǒng)一大家對于關(guān)鍵部分代碼的設(shè)計理念和使用方式。
靜態(tài)分析工具。這塊我們在使用的有LuaChecker和UnityEngineAnalyzer,針對代碼進行檢查,可以發(fā)現(xiàn)一些優(yōu)化的點。
2.3 全員參與的優(yōu)化(補)
我們客戶端程序團隊在進行優(yōu)化的時候和一些團隊不同的做法是大家都針對自己負責的部分進行優(yōu)化。這樣做和團隊自身的特點有關(guān),我們客戶端團隊對于一個創(chuàng)業(yè)團隊來說算是經(jīng)驗和技術(shù)能力都不錯的一個團隊,每個成員都有多年的游戲開發(fā)經(jīng)驗。因此每一個同事都負責一些比較底層的模塊,也會負責各個玩法系統(tǒng)的開發(fā),是一個縱向的結(jié)構(gòu)。總結(jié)起來,讓所有同事都參與優(yōu)化的好處主要有:
讓團隊中的每個人建立優(yōu)化意識;
每個人作為自己負責模塊的優(yōu)化負責人;
組織專門的優(yōu)化周期,橫向?qū)Ρ?,互相學(xué)習(xí)。


組織專門進行優(yōu)化周期的Evernote記錄
3. 團隊開發(fā)效率優(yōu)化
終于來到第三部分,也就是之前說的看上去和性能優(yōu)化并沒有直接關(guān)系的團隊開發(fā)效率優(yōu)化。

在聊這部分之前,我想讓讀者思考一個問題——我們?yōu)槭裁匆鰞?yōu)化?

是為了讓游戲的運行更加流程?讓游戲更加省流量?更省電?讓游戲包體更小?這些都是我們進行優(yōu)化的目標,但歸根節(jié)點,我們做這些優(yōu)化的目標都是——為玩家提供更好的游戲體驗。

所以在我看來,如果一個優(yōu)化,無論使用多么高超的技巧,如果它的優(yōu)化結(jié)果無法直接或者間接地被玩家感受到,那這個優(yōu)化可能就只是一個程序員的“自嗨”,無法為游戲提供真正的價值。反過來說,如果我們可以優(yōu)化團隊的開發(fā)效率,讓團隊有更多的時間來開發(fā)新的功能、制作更多的游戲細節(jié),那對于游戲來說也是一種優(yōu)化。
因此在我看來,進行團隊效率的優(yōu)化是一件非常重要的事情,也是程序的職責之一。我主要想從這樣三個方面來聊一下如何進行團隊開發(fā)效率的優(yōu)化:工作流的構(gòu)建、程序團隊、策劃團隊。

3.1 工作流的構(gòu)建
我覺得在項目中構(gòu)建更好、更順暢的工作流可以很大地提升整體團隊的工作效率。我以我們團隊現(xiàn)在一個功能的完成流程為例來分享一下我們團隊使用的工作流。

策劃提前和程序、美術(shù)溝通需求的可行性,在可行性確定之后,通過Redmine這樣的管理軟件提單,將需求詳細地描述在任務(wù)單里;
我們在Redmine中集成了Webhook的功能,當有任務(wù)提出的時候,Redmine會通過釘釘?shù)慕涌谕ㄖ綄?yīng)的程序;
程序根據(jù)自己手頭的工作安排進行排期和功能實現(xiàn),當任務(wù)單完成并進行自測之后,會將代碼提交到svn上,同時將Redmine上的單子修改為“已完成”的狀態(tài),狀態(tài)的變更會同樣通知到相應(yīng)的策劃和QA;
SVN通過SVN hook的方式,自動觸發(fā)Jenkins的Lua代碼編譯指令,Jenkins調(diào)用我們部署在公司內(nèi)網(wǎng)的一套分布式打包服務(wù),進行腳本編譯。我們團隊中只有程序有Lua代碼的svn訪問權(quán)限,其他職位統(tǒng)一使用編譯好的Lua bytes code。
當打包完成之后,分布式的打包服務(wù)會調(diào)用釘釘接口將完成消息通知到特定的群里。
策劃需要進行導(dǎo)表、更新服務(wù)器,或者QA同事需要進行安卓/iOS打包的時候,都是通過Jenkins進行請求,Jenkins繼續(xù)調(diào)用分布式打包服務(wù)進行打包,并將結(jié)果通知到群里。

對于Jenkins部分,提醒一下要做好權(quán)限控制,對于其他職位可能需要的,盡量避免參數(shù)式的執(zhí)行方式,而是以多個任務(wù)的方式提供。而程序部分則可以盡量靈活地使用參數(shù)進行構(gòu)建。對于發(fā)布版本打包、分支創(chuàng)建等功能,通過權(quán)限控制不要讓策劃/美術(shù)/QA誤操作點擊到。
我們的分布式打包服務(wù)是基于Python構(gòu)建的,通過簡單的RPC服務(wù)進行內(nèi)網(wǎng)跨機器的互聯(lián),通過argparse模塊進行參數(shù)化的提供,方便擴展:
defParseArgs(args):parser=argparse.ArgumentParser(description='Build App')parser.add_argument('-p','--platform',choices=('android','ios',),required=True)parser.add_argument('-c','--channel')#all, xiaomiparser.add_argument('-b','--build-type',choices=('dev','pub',),default="dev")parser.add_argument('-sp','--spmark')parser.add_argument('-hm','--headmark')parser.add_argument('--non-sdo-server',action='store_true')parser.add_argument('--nopatch',action='store_true')parser.add_argument('--onlyab',action='store_true')parser.add_argument('--uwashipping',action='store_true')#單獨為uwa測試準備的發(fā)布參數(shù),臨時添加parser.add_argument('--make-base',action='store_true')parser.add_argument('--il2cpp',action='store_true')parser.add_argument('-xp','--xcode-profile-type',choices=('development','addhoc','appstore',),default="development")buildArgs=parser.parse_args(args)
我覺得這樣的工作流的好處主要有:
程序?qū)⒏嗟氖虑橥瞥鋈?,交給工具,自己可以更加專注在程序開發(fā)的工作上;
其他職位擁有更多的自主權(quán),在不需要程序參與的情況下可以完成自己的很多工作;
通過釘釘這樣的IM的通知功能,將輪詢的消息變成通知,不再需要等待和關(guān)注Jenkins任務(wù)的完成進度,完成之后自然就會收到通知。
3.2 提高程序開發(fā)效率
這塊基本都在PPT里了,不贅述了,其中調(diào)試工具部分再次推薦一下:Hdg Remote Debug這樣的設(shè)備調(diào)試工具,關(guān)于Lua的部分在3月份的博客中已經(jīng)說得非常詳細了,也不再重復(fù)。

3.3 策劃工作效率優(yōu)化
策劃工作效率的優(yōu)化部分想講兩個切身經(jīng)歷的事情。一個是非常小的一個優(yōu)化,幫助策劃實現(xiàn)NPC坐標從Unity中拷貝到Excel中。

我們因為開發(fā)周期比較緊,而且服務(wù)器需要一些NPC的位置數(shù)據(jù)做驗證,因此沒有在Unity內(nèi)部為策劃實現(xiàn)NPC編輯器,而是需要策劃手動去Excel表里填寫。這里就有一個填寫坐標的過程,最初的時候策劃手動填寫非常費時間,而且容易出錯,后來幫助他們實現(xiàn)了一個點擊GameObject節(jié)點拷貝坐標到粘貼板的功能,策劃使用后表示極大地提升了填寫NPC表格的工作效率。
有時候程序只需要通過很簡單的代碼就可以幫助其他職位的同事解決一些工作中的痛點,提高工作效率。

第二件事情是之前在大公司工作的時候的一個親身經(jīng)歷。當時在帶新人做mini項目,一個新人策劃就在公司的KM知識分享平臺上提了一個問題——他表示現(xiàn)在的策劃填表的工作效率很低,需要經(jīng)歷這樣幾個復(fù)雜的步驟:

在Excel中編輯數(shù)據(jù),然后提交到SVN上,通過導(dǎo)表將數(shù)據(jù)轉(zhuǎn)換成程序代碼讀取的資源,然后更新服務(wù)器,更新客戶端,啟動客戶端連接服務(wù)器才能查看結(jié)果,這些步驟要花費大約10-20分鐘的時間。他問能否編寫完數(shù)據(jù)之后就可以直接在游戲內(nèi)看到結(jié)果?
當時的我作為自以為在游戲行業(yè)已經(jīng)有幾年工作經(jīng)驗的“過來人”,看到新人策劃有這樣的疑問,心里其實是有一些嘲諷的。所以去“耐心”地回復(fù)他:對于客戶端來說,可以做到本地導(dǎo)表然后不重啟客戶端就可以直接Reload數(shù)據(jù)查看結(jié)果,但是如果你不把數(shù)據(jù)上傳到svn上,服務(wù)器如何知道你本地修改的結(jié)果?這就像那樣一個笑話:
“是這樣的,張總, 您在家里的電腦上按了ctrl+c,然后在公司的電腦上再按ctrl+v是肯定不行的。即使同一篇文章也不行。不不,多貴的電腦都不行?!?/p>
這個笑話后來的結(jié)果是自己成了一個笑話,因為雖然時代的發(fā)展,網(wǎng)絡(luò)硬盤等云服務(wù)的普及,也有了跨電腦進行粘貼拷貝的功能……張總不再需要很貴的電腦就可以實現(xiàn)自己的操作。
這個故事的發(fā)展和這個笑話有些相似,在大約半年之后,我和工作室的另外一個同事將rpyc這樣一套中間件引入公司并基于它實現(xiàn)了跨進程的外掛式編輯框架?;谶@套框架就實現(xiàn)了策劃在編輯器內(nèi)編輯完數(shù)據(jù),只需要點擊重載數(shù)據(jù)的按鈕,就可以自動更新本地的客戶端和指定ip的一臺服務(wù)器中的數(shù)據(jù),不再需要提交到svn,甚至不需要重啟客戶端就可以看到修改之后的結(jié)果。

經(jīng)過樣的改進之后,之前需要10-20分鐘左右時間的操作,現(xiàn)在只需要2-5秒就可以實現(xiàn),極大地提升了策劃的工作效率。我和那位同事也因此拿了當年公司內(nèi)部的技術(shù)分享獎。
這個故事對于我的觸動還蠻大的,因為最初我所嘲諷的一個新人的想法,最終由我和另外的一個同事一起進行了實現(xiàn),這對于我來說也是一種諷刺。因此在之后我再聽到策劃或者其他職位的一些看上去“異想天開”的想法的時候,不會急于反駁或者指出其中的漏洞,而是先想想是否自己的思路被自己了解的技術(shù)所禁錮,是否有別的方式可以真的實現(xiàn)這些想法。
通過這兩個故事我想表達一個觀點,對于程序在團隊效率優(yōu)化方面應(yīng)當承擔什么樣的角色?借用《蜘蛛俠1》里非常有名的那句話來說——能力越大,責任越大。

因為程序是整個團隊中最了解技術(shù)和開發(fā)的人,也最有能力開發(fā)一些工具或者引入一些方法讓整個團隊的工作效率得到提升,因此也應(yīng)該肩負起相應(yīng)的責任。
4. 總結(jié)
最后,我們聊了這么多,進行一些總結(jié)。

我在游戲行業(yè)里也做了五六年,特別是自己在創(chuàng)業(yè)的這一年多的時間,讓我更加深切地感受到游戲開發(fā)非常符合這樣的冰山理論。

浮在冰面上的這一部分是玩家可以感受到的游戲內(nèi)容,比如精致的美術(shù)資源、有趣的玩法,而在這之下,有更多無法被玩家直接感受到的內(nèi)容,比如被迭代掉的玩法。而今天我們所聊的這些優(yōu)化的內(nèi)容,比如美術(shù)規(guī)范、代碼質(zhì)量、團隊的工作流構(gòu)建,它們大都是水面以下,無法被玩家切身感受到的部分。但它們又是如此地重要,是整座冰山不可或缺的一部分。
就像我之前所說,通過剛才的分享大家應(yīng)該也可以感受到,這些優(yōu)化的內(nèi)容非常的瑣碎繁雜,就像散布在各個地方的一個又一個點,是團隊的協(xié)作讓這些點可以連接成線,形成類似于美術(shù)資源的規(guī)范制定、規(guī)范執(zhí)行和規(guī)范檢查這樣的閉環(huán),而在整個游戲的開發(fā)周期過程中通過團隊持之以恒地去做這些事情,讓這些線連接成一張大網(wǎng),將水聚攏在周圍凝結(jié)成冰,托起了整座冰山,使得海面上可以被玩家感受到的內(nèi)容越來越多,這就是我眼中基于團隊的持續(xù)優(yōu)化之道。
最后的最后,我還是想把我在上次分享中也說過的一句話送給大家。
這一年多的創(chuàng)業(yè)經(jīng)歷讓我更加深刻地體會到游戲開發(fā)是一件艱難而且辛苦的事情,有一些朋友或者同事也找我聊作為一個程序進行游戲開發(fā)的迷茫,我自己內(nèi)心也曾有過彷徨和糾結(jié)。因為很多事情太過瑣碎,帶給我們的成就感可能也會偏低。但是我也發(fā)現(xiàn),現(xiàn)在做過幾年游戲行業(yè)之后,依然留在游戲行業(yè)中的人心中都有著對于游戲發(fā)自肺腑的熱愛和激情。它們或從小就喜歡游戲,或曾經(jīng)被游戲感動過心中最為柔軟的那個部分,在游戲行業(yè)內(nèi)堅持做這些著看似平凡的工作。
所以,我想把這句話送給所有依然在堅持的游戲開發(fā)者們——不忘初心,不愧平凡,相信通過團隊的協(xié)作和堅持不懈的努力,可以給這份平凡以不凡!

謝謝!
2018年5月17中午? 于杭州海創(chuàng)園 心光流美公司