到引擎原理與性能優(yōu)化)
又到校招季經(jīng)常有人拿著筆試鏈接來問我“網(wǎng)易這套Unity3D題怎么準(zhǔn)備”。說實(shí)話2020屆提前批這套題在當(dāng)年算是游戲行業(yè)校招筆試?yán)锉容^有代表性的它考察的并不只是你背了多少API而是你在真實(shí)項(xiàng)目里有沒有踩過坑、有沒有自己的工程判斷。我自己當(dāng)年做這套題的時(shí)候也吃過虧后來幫學(xué)弟學(xué)妹輔導(dǎo)時(shí)又反復(fù)拆過幾遍這里把完整的拆解思路、技術(shù)考點(diǎn)和實(shí)操方案整理出來希望能給正在準(zhǔn)備Unity3D開發(fā)崗筆試的同學(xué)一些真正能落地的參考。先說清楚這篇文章適合誰目標(biāo)崗位是Unity3D客戶端開發(fā)或游戲研發(fā)方向筆試階段需要系統(tǒng)梳理C#、Unity引擎原理、圖形渲染和項(xiàng)目經(jīng)驗(yàn)的同學(xué)。如果你已經(jīng)工作幾年、對(duì)引擎熟得不行可以直接跳到后面的實(shí)操題部分看那些題即便放到社招場(chǎng)景里也很值得琢磨。1. 先聊聊這套筆試的考察思路1.1 筆試到底在篩什么人網(wǎng)易三道大題的考核核心并不是“你知道多少個(gè)Unity API”而是“你有沒有完整的客戶端工程意識(shí)”。提前批的題目本就比正式批有更高的篩選意愿它要的是零培養(yǎng)成本、入職就能進(jìn)項(xiàng)目組干活的人。所以在題目設(shè)計(jì)上你能明顯看出幾個(gè)傾向第一算法和計(jì)算機(jī)基礎(chǔ)基本功不能拉胯。字符串處理、數(shù)據(jù)結(jié)構(gòu)、復(fù)雜度分析這些屬于通用考核項(xiàng)基本是所有大廠筆試的第一關(guān)篩子這一塊不過線Unity技術(shù)面再強(qiáng)也很難到面試官手里。第二Unity的API理解和生命周期意識(shí)是重中之重。題目里會(huì)頻繁出現(xiàn)Transform、GameObject、MonoBehaviour、物理碰撞、UI事件這類基礎(chǔ)API的操作但真正拉開差距的是你對(duì)主循環(huán)、生命周期、內(nèi)存分配的理解。比如Update和FixedUpdate的區(qū)別很多人能背出來一個(gè)跟幀率相關(guān)、一個(gè)跟物理頻率相關(guān)但放在具體場(chǎng)景里比如“角色跳躍后速度賦值放哪個(gè)回調(diào)里”就懵了這就是典型的只背概念不會(huì)落地。第三項(xiàng)目經(jīng)驗(yàn)和調(diào)試能力會(huì)被重點(diǎn)試探。網(wǎng)易的題目里通常會(huì)有開放性場(chǎng)景題比如“玩家進(jìn)入戰(zhàn)斗場(chǎng)景時(shí)黑屏卡頓你怎么定位”這類問題。這種題沒有標(biāo)準(zhǔn)答案但能看出你有沒有真實(shí)上線項(xiàng)目的調(diào)試經(jīng)驗(yàn)有沒有用過Profiler、Frame Debugger能不能從CPU、GPU、內(nèi)存、加載策略幾個(gè)維度去定位問題。1.2 四類題型的分?jǐn)?shù)權(quán)重我把這套筆試的題型大致分為四類基礎(chǔ)編程題、Unity引擎原理題、圖形與渲染題、項(xiàng)目設(shè)計(jì)題。按照歷年考生的反饋大致權(quán)重如下題型大致占比考察核心典型題目方向基礎(chǔ)編程與數(shù)據(jù)結(jié)構(gòu)25%-30%鏈表、樹、字符串、排序、動(dòng)態(tài)規(guī)劃純代碼輸出限時(shí)ACC#與Unity API25%-30%生命周期、物理、協(xié)程、序列化基礎(chǔ)選擇、填空題為主渲染與資源優(yōu)化20%渲染管線、批處理、內(nèi)存管理概念理解加場(chǎng)景判斷開放設(shè)計(jì)與項(xiàng)目經(jīng)驗(yàn)15%-20%系統(tǒng)架構(gòu)、性能定位、團(tuán)隊(duì)協(xié)作簡(jiǎn)答題、場(chǎng)景設(shè)計(jì)題這個(gè)權(quán)重意味著你不能再“只刷LeetCode不碰引擎”那是走不通的。反過來只看Unity教程不刷算法同樣過不了。真正穩(wěn)妥的策略是兩條腿走路算法保持手感Unity工程能力通過復(fù)盤項(xiàng)目來補(bǔ)。1.3 提前批和正式批的差別很多同學(xué)會(huì)忽略一個(gè)關(guān)鍵點(diǎn)提前批和正式批的筆試側(cè)重點(diǎn)完全不同。從我接觸到的真題和回憶來看提前批的題目通常更“偏”一些。正式批的題目更像教科書問“Unity3D支持哪幾種光源類型”答案很明確背過就有分。提前批則喜歡問“在移動(dòng)端使用實(shí)時(shí)陰影有哪些注意點(diǎn)你會(huì)怎么優(yōu)化”這已經(jīng)不是背題能解決的了你得真的在手機(jī)上跑過場(chǎng)景、見過陰影瑕疵和性能損耗才能答得出來。所以如果你拿到的是提前批的筆試邀請(qǐng)準(zhǔn)備策略就要果斷偏“原理實(shí)踐”而不是“名詞概念”。多看引擎源碼解析、多復(fù)盤自己的Demo項(xiàng)目、多記錄優(yōu)化前后的數(shù)據(jù)變化這比把官方文檔翻三遍都管用。2. C#語法與Unity API高頻考點(diǎn)拆解2.1 值類型與引用類型的陷阱這一塊幾乎是筆試必出而且出題人特別喜歡挖看似簡(jiǎn)單、實(shí)則容易說錯(cuò)的細(xì)節(jié)。C#里值類型包括結(jié)構(gòu)體struct、枚舉enum和所有內(nèi)置數(shù)值類型引用類型則是class、string、數(shù)組、委托等這一點(diǎn)大部分人都知道但放進(jìn)Unity場(chǎng)景里就開始亂了。最常見的一道題類似這樣struct類型作為Transform組件的一個(gè)字段在代碼里修改它能不能直接生效很多人想都不想就回答“能修改”但正確答案是如果結(jié)構(gòu)體已經(jīng)作為Unity引擎內(nèi)部數(shù)據(jù)的一部分序列化你拿到的往往是副本修改的只是臨時(shí)變量并不會(huì)寫回引擎。這也是為什么Unity官方一直強(qiáng)調(diào)不要在自定義Editor腳本里直接改Transform的局部坐標(biāo)而要通過本地變量轉(zhuǎn)換后再賦值。另一個(gè)高頻坑是string的不可變性。每次字符串拼接都會(huì)產(chǎn)生新的托管堆對(duì)象放在Update里每幀執(zhí)行會(huì)持續(xù)觸發(fā)GC。筆試雖然不直接考GC的源碼邏輯但會(huì)問“下面的代碼在移動(dòng)端每幀執(zhí)行可能存在什么問題”這時(shí)候你要能指出字符串拼接、隱式裝箱、LINQ臨時(shí)對(duì)象分配這三個(gè)最典型的GC來源。我建議平時(shí)寫代碼時(shí)養(yǎng)成用StringBuilder的習(xí)慣。即便是小字符串拼接如果在Update或高頻事件里執(zhí)行也優(yōu)先考慮StringBuilder或結(jié)構(gòu)體拆分這個(gè)習(xí)慣在真實(shí)項(xiàng)目里能明顯降低卡頓頻率。2.2 生命周期與事件執(zhí)行順序生命周期這個(gè)考點(diǎn)幾乎是網(wǎng)易必考。Awake、OnEnable、Start、FixedUpdate、Update、LateUpdate、OnDisable、OnDestroy這八個(gè)回調(diào)的執(zhí)行順序至少要能默寫出來。但光默寫還不夠你得知道每條關(guān)鍵鏈路上的“為什么”。舉個(gè)例子Awake和Start的差別很多人只知道Awake先執(zhí)行、Start在第一次Update前執(zhí)行但不清楚Awake是為了初始化自身Start則是為了等待其他對(duì)象已完成Awake。如果A對(duì)象需要在Start里調(diào)用B對(duì)象的初始化數(shù)據(jù)那么必須保證B的Awake已經(jīng)執(zhí)行過這在加載場(chǎng)景時(shí)是有順序風(fēng)險(xiǎn)的。筆試?yán)锟赡艹霈F(xiàn)的變形考法一個(gè)物體在Awake里把自己SetActive(false)它的OnDisable什么時(shí)候觸發(fā)答案是在當(dāng)前幀的Awake之后、Start之前。這個(gè)細(xì)節(jié)如果不實(shí)測(cè)過很容易答錯(cuò)。我看過很多應(yīng)屆生的答案普遍以為OnDisable會(huì)隨SetActive立即同步觸發(fā)但引擎為了保證幀內(nèi)一致性會(huì)把它放進(jìn)幀末統(tǒng)一處理。所以備考時(shí)要特別注意兩個(gè)維度的順序一是生命周期回調(diào)的先后順序二是同一回調(diào)在不同對(duì)象之間的執(zhí)行順序后者在幀首幀尾的處理上尤其重要也最能體現(xiàn)你有沒有真實(shí)跑過項(xiàng)目。2.3 序列化與Inspector面板的關(guān)系Unity的序列化是很多自學(xué)者容易忽略的點(diǎn)但它同時(shí)是筆試和工作中特別現(xiàn)實(shí)的問題。筆試常考一個(gè)public字段和一個(gè)[SerializeField] private字段在Inspector面板里的表現(xiàn)有什么區(qū)別為什么public字段改完名之后Inspector里的數(shù)據(jù)會(huì)丟核心在于Unity的序列化是基于字段名和類型存儲(chǔ)的你改了字段名對(duì)應(yīng)關(guān)系就斷了之前調(diào)好的數(shù)據(jù)自然就丟了。這就是為什么很多項(xiàng)目要求所有可配置字段加[FormerlySerializedAs]標(biāo)簽或盡量使用[SerializeField] private最大程度減少改名導(dǎo)致的數(shù)據(jù)丟失。還有一個(gè)高頻題為什么自定義類作為字段時(shí)在Inspector里默認(rèn)不顯示要加[System.Serializable]才能顯示這其實(shí)關(guān)系到Unity序列化系統(tǒng)的類型白名單機(jī)制。C#的普通class默認(rèn)是不被Unity識(shí)別為可序列化類型的只有加標(biāo)簽或用Unity自帶類型如Vector3、Quaternion、AnimationCurve才能進(jìn)入Inspector的序列化流程。備考這一塊時(shí)建議自己開一個(gè)空工程實(shí)測(cè)一遍把public字段、[SerializeField]、[System.Serializable]、[HideInInspector]這幾種組合的顯示和存儲(chǔ)行為都跑一遍比你在網(wǎng)上看十篇文章都有用。筆試問到這里時(shí)你還能順手補(bǔ)一句“這里涉及到Unity序列化系統(tǒng)的類型注冊(cè)機(jī)制”面試官對(duì)你的印象會(huì)明顯不一樣。3. 筆試實(shí)操題創(chuàng)作工具與文件處理3.1 用代碼創(chuàng)建Animation Clips這個(gè)考點(diǎn)幾乎是游戲公司筆試?yán)锏尼斪討粢驗(yàn)閯?dòng)畫系統(tǒng)在客戶端開發(fā)里太常用了。很多同學(xué)只知道在Unity編輯器里右鍵Create Animation Clip但筆試考的是讓你用C#代碼動(dòng)態(tài)創(chuàng)建并保存一個(gè)Animation Clip這考察的是對(duì)AnimationCurve、Keyframe、AnimationClip.SetCurve這套API的掌握程度。換在游戲里最常見的場(chǎng)景就是“捏臉系統(tǒng)”玩家拖動(dòng)滑桿調(diào)整面部表情或體型參數(shù)我們需要把調(diào)整結(jié)果實(shí)時(shí)記錄成一段動(dòng)畫例如表情變化、動(dòng)作過渡而不是讓美術(shù)手動(dòng)K幀。如果你的代碼只能寫死動(dòng)畫沒法動(dòng)態(tài)生成編輯曲線那這套捏臉系統(tǒng)根本做不完整。完整代碼如下在編輯器腳本里運(yùn)行using UnityEngine; using UnityEditor; using System.Collections.Generic; public static class AnimationClipGenerator { [MenuItem(Tools/Generate Animation Clip)] public static void CreateClip() { // 1. 創(chuàng)建動(dòng)畫資源 AnimationClip clip new AnimationClip(); clip.frameRate 30; // 2. 用一個(gè)封裝好的方法設(shè)置曲線 // 這里設(shè)置的是localPosition.x和localRotation.y SetCurve(clip, localPosition.x, new Keyframe[] { new Keyframe(0f, 0f), new Keyframe(0.5f, 5f), new Keyframe(1f, 0f) }); SetCurve(clip, localRotation.y, new Keyframe[] { new Keyframe(0f, 0f), new Keyframe(0.5f, 180f), new Keyframe(1f, 0f) }); // 3. 保證目錄存在后保存資源 if (!AssetDatabase.IsValidFolder(Assets/Animations)) { AssetDatabase.CreateFolder(Assets, Animations); } AssetDatabase.CreateAsset(clip, Assets/Animations/GeneratedClip.anim); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); } private static void SetCurve(AnimationClip clip, string propertyName, Keyframe[] keyframes) { AnimationCurve curve new AnimationCurve(keyframes); // 清掉默認(rèn)切線改成更平滑的Auto曲線避免動(dòng)畫看起來發(fā)跳 for (int i 0; i keyframes.Length; i) { curve.SmoothTangents(i, 0f); } clip.SetCurve(, typeof(Transform), propertyName, curve); } }這里面有幾個(gè)關(guān)鍵點(diǎn)筆試和面試都可能追問Keyframe的value和time分別代表什么如果你要設(shè)置的是rotation為什么用四元數(shù)分量localRotation.y而不是歐拉角因?yàn)锳nimationClip的內(nèi)部存儲(chǔ)和計(jì)算都是基于四元數(shù)分量的歐拉角在Animator窗口里是顯示層幫你轉(zhuǎn)換了底層綁定的是對(duì)應(yīng)的四元數(shù)分量。還要注意一個(gè)坑如果你要在運(yùn)行時(shí)非編輯器動(dòng)態(tài)創(chuàng)建AnimationClip用AssetDatabase.CreateAsset是不可用的它屬于編輯器API。運(yùn)行時(shí)創(chuàng)建只需要new AnimationClip()加上SetCurve然后賦給AnimatorController的AnimationClip引用或Animation組件即可。運(yùn)行時(shí)創(chuàng)建的clip不會(huì)自動(dòng)保存成資源除非你走AssetBundle或序列化流程。筆試如果問“代碼創(chuàng)建Animation Clip后如何讓角色播放”你需要能答出兩條鏈路編輯器工具鏈生成.anim資產(chǎn)文件給策劃配置和運(yùn)行時(shí)鏈路從AssetBundle加載后賦值到AnimatorOverrideController或直接替換Controller中clip。兩條鏈路都說得清說明你確實(shí)在項(xiàng)目里實(shí)現(xiàn)過類似功能。3.2 跨平臺(tái)文件路徑Application.persistentDataPath的正確理解熱詞里的一句代碼filePath Path.Combine(Application.persistentDataPath, filename);看起來簡(jiǎn)單但這里面的坑非常多也是筆試?yán)锟疾炜缙脚_(tái)意識(shí)的經(jīng)典切入點(diǎn)。Application.persistentDataPath在不同平臺(tái)對(duì)應(yīng)的實(shí)際路徑是不一樣的這張表建議記下來平臺(tái)實(shí)際路徑特點(diǎn)WindowsC:/Users/用戶名/AppData/LocalLow/公司名/產(chǎn)品名可讀寫用戶目錄下不同用戶路徑不同macOS~/Library/Application Support/公司名/產(chǎn)品名可讀寫但路徑帶空格iOSApp沙盒/Library/Application Support會(huì)被系統(tǒng)備份到iCloud注意隱私文件勿放此處Android/data/data/包名/files應(yīng)用私有目錄卸載即刪除無外部存儲(chǔ)權(quán)限要求為什么這個(gè)考點(diǎn)重要因?yàn)楹芏嗤瑢W(xué)在Windows上開發(fā)時(shí)能用Application.dataPath寫東西一上Android就發(fā)現(xiàn)找不到文件或者一上iOS發(fā)現(xiàn)寫進(jìn)去的文件被系統(tǒng)清理了。實(shí)際項(xiàng)目中可下載的關(guān)卡配置、緩存的熱更新資源、截圖存檔類文件都應(yīng)該用persistentDataPath作為根目錄而StreamingAssets在Android上是被壓縮進(jìn)APK的只能讀不能寫部分版本通過特殊路徑也不能穩(wěn)定寫。此外還有一個(gè)高頻追問為什么不直接Application.dataPath / filename因?yàn)閐ataPath在不同平臺(tái)語義完全不同在Android上dataPath指的是APK內(nèi)部的jar路徑并不代表你有權(quán)限直接寫在iOS上dataPath指向的是App Bundle只讀。只有persistentDataPath和temporaryCachePath是專門留給運(yùn)行時(shí)讀寫用的前者的內(nèi)容是持久化的后者是緩存系統(tǒng)可能在存儲(chǔ)緊張時(shí)自動(dòng)清理。筆試遇到“文件讀寫”相關(guān)題目時(shí)建議答出先用Application.persistentDataPath拼路徑再判斷文件是否存在不存在則通過File.Exists或Directory.Exists創(chuàng)建目錄。目錄創(chuàng)建這步是新手最容易漏的直接File.WriteAllText到一個(gè)不存在的目錄分分鐘IOException。真實(shí)項(xiàng)目里我們還會(huì)封裝一層路徑管理器統(tǒng)一處理平臺(tái)差異和路徑拼接不會(huì)在業(yè)務(wù)代碼里到處寫死路徑。另外強(qiáng)烈建議在代碼里使用Path.Combine而非字符串直接相加??缙脚_(tái)路徑分隔符的差異Windows是反斜杠macOS/Linux是正斜杠在編輯器環(huán)境很可能不報(bào)錯(cuò)但一打包到iOS或Linux服務(wù)器上就炸了。Path.Combine會(huì)自動(dòng)處理當(dāng)前平臺(tái)的分隔符這是標(biāo)準(zhǔn)的跨平臺(tái)寫法。3.3 Unity3D視頻流的正確打開姿勢(shì)熱詞里有“unity3d視頻流”這個(gè)方向我在項(xiàng)目里正好踩過不少坑筆試也是常見的場(chǎng)景題。視頻播放看起來簡(jiǎn)單但牽涉到平臺(tái)兼容性、內(nèi)存占用、視頻格式和渲染紋理。最常見的需求有兩種播放視頻UI比如劇情動(dòng)畫、廣告、開場(chǎng)動(dòng)畫和播放視頻到3D表面比如場(chǎng)景里的電視機(jī)屏幕、廣告牌。前者直接UGUI加VideoPlayer組件就行后者需要把VideoPlayer的targetTexture指向一張RenderTexture然后把RenderTexture貼給材質(zhì)球的_BaseMap或_MainTex。這里要特別注意VideoPlayer的source如果是URL模式本地文件路徑要加file://前綴。很多同學(xué)在Windows上直接寫C:\videos\a.mp4能放打包到Android和iOS后路徑拼接不上就是因?yàn)闆]走URL協(xié)議。使用Application.streamingAssetsPath時(shí)也有一致性問題Android上StreamingAssets在壓縮包里VideoPlayer不能直接通過文件路徑訪問需要通過UnityWebRequest先拷貝到persistentDataPath再播放。這個(gè)坑在Android真機(jī)上極其常見。如果筆試或面試問“視頻流卡頓如何優(yōu)化”你需要從三個(gè)方面回答解碼方式Android平臺(tái)上硬件解碼比軟件解碼快得多VideoPlayer默認(rèn)會(huì)嘗試硬件解碼但有些編碼格式比如特定碼率的H.265可能不被硬件支持需要降級(jí)或轉(zhuǎn)碼。建議用H.264 Main Profile、分辨率不超過屏幕分辨率、碼率控制在2-4Mbps這是絕大多數(shù)移動(dòng)端硬件解碼的舒適區(qū)。加載方式大視頻不要直接放StreamingAssets里打整包建議用AssetBundle或首次啟動(dòng)后下載到persistentDataPath播放時(shí)按需加載。內(nèi)存視頻解碼幀會(huì)占用大量?jī)?nèi)存如果是3D表面播放RenderTexture分辨率建議按實(shí)際顯示尺寸的1:1設(shè)置不要設(shè)置成4K否則內(nèi)存和帶寬都吃緊。還有一點(diǎn)特別容易忽略視頻播放完要主動(dòng)調(diào)用VideoPlayer.Stop()并釋放RenderTexture參考否則即使切換場(chǎng)景底層的視頻解碼器依然可能有殘留資源Android上會(huì)出現(xiàn)黑屏或綠屏。真實(shí)項(xiàng)目里我們遇到過一次線上崩潰排查下來就是游戲里兩個(gè)場(chǎng)景共用一張RenderTexture視頻組件銷毀后解碼線程還在寫紋理解決辦法是切場(chǎng)景前先暫停視頻、解綁targetTexture、延遲一幀銷毀組件。3.4 SolidWorks模型導(dǎo)入U(xiǎn)nity3D的工程化方案“solidworks模型導(dǎo)入unity3d”這個(gè)熱詞說明不少同學(xué)正在走工業(yè)模型轉(zhuǎn)游戲引擎這條路。這個(gè)需求常見于數(shù)字孿生項(xiàng)目、工業(yè)仿真、機(jī)械產(chǎn)品展示甚至部分偏物理模擬的游戲。但筆試不會(huì)直接讓你導(dǎo)入模型它更可能從“模型導(dǎo)入后比例不對(duì)”“模型方向不對(duì)”這類現(xiàn)象題切入考察你對(duì)資源管線的理解。SolidWorks默認(rèn)的模型格式是SLDPRT和SLDASMUnity3D不能直接識(shí)別。工程上最常見的導(dǎo)入路徑是“SolidWorks導(dǎo)出中間格式再在Unity里處理”首選導(dǎo)出FBX如果版本支持或者導(dǎo)出STEP、IGES、OBJ、STL其中STL只有網(wǎng)格沒有材質(zhì)適合做碰撞或3D打印預(yù)覽不適合直接做游戲顯示模型。導(dǎo)入后最典型的問題是單位。SolidWorks默認(rèn)單位是毫米Unity的默認(rèn)單位是米。一個(gè)1000mm的零件如果直接導(dǎo)入U(xiǎn)nity會(huì)變成1000米場(chǎng)景里根本沒法看。解決辦法是在導(dǎo)入設(shè)置里調(diào)整Scale Factor或者在SolidWorks導(dǎo)出時(shí)先把單位設(shè)置為米再導(dǎo)出。很多人在這一步栽過跟頭實(shí)際項(xiàng)目里還要統(tǒng)一規(guī)范整個(gè)項(xiàng)目約定好建模軟件內(nèi)統(tǒng)一用毫米導(dǎo)入U(xiǎn)nity時(shí)統(tǒng)一用縮放0.001然后寫一個(gè)自動(dòng)掃描導(dǎo)入資源的后處理腳本防止有人手工漏改。方向問題也很常見SolidWorks的Z軸通常對(duì)應(yīng)Unity的Y軸模型導(dǎo)入后可能會(huì)躺倒。解決辦法是導(dǎo)出時(shí)在源軟件里做一個(gè)旋轉(zhuǎn)變換或者在Unity里包一層父節(jié)點(diǎn)做固定旋轉(zhuǎn)千萬不要直接旋轉(zhuǎn)模型網(wǎng)格否則后續(xù)做物理碰撞、粒子掛點(diǎn)、動(dòng)畫綁定都會(huì)亂套。這塊屬于典型“看著簡(jiǎn)單、做起來全是細(xì)節(jié)”的工作流筆試答到“統(tǒng)一軸向后生成Prefab避免每次手動(dòng)旋轉(zhuǎn)”就能明顯比只說“旋轉(zhuǎn)一下”的同學(xué)得分高。4. 引擎原理與項(xiàng)目場(chǎng)景題4.1 渲染管線與性能優(yōu)化網(wǎng)易筆試對(duì)渲染的考察不會(huì)特別深但一定會(huì)涉及幾個(gè)固定話題光照、陰影、批處理、Overdraw。因?yàn)閁nity項(xiàng)目在移動(dòng)端最容易遇到的性能瓶頸就是渲染。先說批處理。動(dòng)態(tài)批處理有嚴(yán)格的頂點(diǎn)數(shù)限制Unity官方文檔說通常小于900個(gè)頂點(diǎn)、且材質(zhì)要一致靜態(tài)批處理需要標(biāo)記Static且會(huì)額外占用內(nèi)存。筆試如果問“為什么我的場(chǎng)景里動(dòng)態(tài)合批沒有生效”你需要能列出幾個(gè)常見原因Shader中使用了實(shí)例化不支持的屬性、材質(zhì)實(shí)例化后參數(shù)不一致、頂點(diǎn)屬性不一致比如有的模型帶UV2有的不帶等。這些是典型的“原因定位”類題目光靠背合批條件很難全覆蓋建議自己開個(gè)小場(chǎng)景實(shí)測(cè)一下Frame Debugger的DrawCall變化。陰影這塊移動(dòng)端實(shí)時(shí)陰影對(duì)性能的壓力非常大。方向光開實(shí)時(shí)陰影會(huì)導(dǎo)致每個(gè)陰影接收物多一次深度渲染加上多個(gè)光源會(huì)成倍增加。筆試常見問法“角色腳下的陰影怎么優(yōu)化”答案是能用假陰影一張圓形貼圖或Planar Shadow就不用實(shí)時(shí)陰影尤其是MOBA、吃雞類游戲里大量角色同屏的情況。假陰影雖然不如實(shí)時(shí)陰影真實(shí)但在性能預(yù)算緊張的項(xiàng)目里是絕對(duì)的主流方案。還有一個(gè)高頻點(diǎn)是Overdraw。半透明物體疊太多GPU片元著色器會(huì)重復(fù)執(zhí)行很多遍。筆試會(huì)問“UI界面打開后場(chǎng)景卡頓為什么”這很可能是UI Overdraw過高或者背景相機(jī)沒有裁剪。排查手段就是開Scene窗口的Overdraw模式紅的越厲害說明重復(fù)繪制越多這也是真實(shí)開發(fā)里每天都在用的方法。4.2 內(nèi)存管理與資源加載方案資源管理是Unity項(xiàng)目長治久安的命根子筆試和面試幾乎必問。AssetBundle是重點(diǎn)中的重點(diǎn)考題方向集中在依賴管理、卸載策略和加載方式。AssetBundle依賴管理是個(gè)經(jīng)典大坑。A資源依賴B資源如果你只加載A而不加載BA的貼圖或材質(zhì)會(huì)顯示紫色。解決方案是給每個(gè)AssetBundle配置Manifest文件加載A之前先用Manifest查它依賴了哪些包按依賴順序全部加載。筆試如果問“如何避免AssetBundle重復(fù)打包”你需要說出“設(shè)置資源分組策略共用資源單獨(dú)打一個(gè)包通過Manifest管理依賴”。這個(gè)答案雖然簡(jiǎn)單但能體現(xiàn)出你對(duì)AB依賴鏈路的理解。卸載也是易錯(cuò)點(diǎn)。AssetBundle.Unload(false)只卸載內(nèi)存里的資源實(shí)例AssetBundle本身類型信息還保留但之后不能通過該Bundle加載新資源了Unload(true)會(huì)強(qiáng)制卸載所有已加載的Asset如果場(chǎng)景里還有引用就會(huì)出現(xiàn)Missing或材質(zhì)變紫。筆試常見問法是“卸載AssetBundle后為什么預(yù)制體上的材質(zhì)消失”這里要答出“資源引用未處理卸載時(shí)機(jī)不對(duì)”同時(shí)給出正確方案確保場(chǎng)景內(nèi)所有引用該資源的對(duì)象全部銷毀后再調(diào)用Unload(true)或者在切場(chǎng)景徹底確定不再使用時(shí)再卸載。對(duì)象池也是一個(gè)被反復(fù)問到的點(diǎn)。筆試題目往往這樣問“一個(gè)射擊游戲中子彈頻繁生成和銷毀有什么優(yōu)化方案”答案不是單純說對(duì)象池而要強(qiáng)調(diào)池化后的組件復(fù)用、自動(dòng)回收機(jī)制和預(yù)生成策略。推薦寫一個(gè)可復(fù)用的PoolManager核心API是Spawn和Despawn內(nèi)部用Stack或Queue存儲(chǔ)實(shí)例并且用回調(diào)方式讓業(yè)務(wù)方做重置邏輯避免殘留狀態(tài)污染。4.3 熱更新方案選型“Unity熱更新”幾乎是國內(nèi)游戲公司的標(biāo)配問題。網(wǎng)易筆試提前批層面不太會(huì)考得太深但會(huì)考“你用過哪些熱更新方案各自優(yōu)缺點(diǎn)”這類選型題。目前主流方案有幾條路Lua系xLua/tolua、ILRuntime、HybridCLR。Lua系的好處是純解釋執(zhí)行、包體增量小、與C#代碼隔離適合拿來做UI邏輯和活動(dòng)玩法壞處是跨語言調(diào)用的性能損耗和數(shù)據(jù)映射成本高ILRuntime不用學(xué)Lua直接用C#編寫邏輯但運(yùn)行時(shí)通過反射和解釋執(zhí)行IL性能表現(xiàn)一般且熱更代碼里不能隨便用值類型泛型等特性HybridCLR屬于原生AOT解釋器補(bǔ)丁方案性能比前兩者好很多但學(xué)習(xí)成本高、需要處理AOT泛型問題業(yè)界采用率正在上升。筆試回答這類問題建議按“業(yè)務(wù)場(chǎng)景團(tuán)隊(duì)技術(shù)棧性能要求”來組織答案如果團(tuán)隊(duì)熟悉Lua且項(xiàng)目追求兼容性優(yōu)先xLua/tolua如果是純C#團(tuán)隊(duì)、熱更邏輯以UI為主可以選ILRuntime如果對(duì)性能要求高、且能承擔(dān)接入成本選HybridCLR。同時(shí)還要提到熱更新不只是代碼層資源熱更AssetBundle下載更新和配置表熱更是同一套系統(tǒng)里必須一起考慮的。還有一點(diǎn)容易加分熱更流程里強(qiáng)更、弱更、灰度發(fā)布的邏輯。強(qiáng)更是指客戶端版本過低必須整包更新App弱更是說下載資源包覆蓋即可灰度是先放量給一部分玩家觀察異常和崩潰率再逐步放量。這些運(yùn)維側(cè)的細(xì)節(jié)雖然筆試不一定會(huì)考但面試追問時(shí)能答出來會(huì)讓人覺得你不是只寫過單機(jī)Demo。4.4 Unity3D與UE5引擎選型怎么看熱詞里出現(xiàn)了“unity3d和ue5區(qū)別”說明現(xiàn)在確實(shí)有不少同學(xué)在跨引擎做選擇。筆試和面試題里出現(xiàn)這種對(duì)比往往不是讓你簡(jiǎn)單念兩個(gè)引擎的賣點(diǎn)而是考察你在實(shí)際項(xiàng)目語境下做技術(shù)選型的判斷力。核心差異非常明顯Unity主語言是C#UE使用C和藍(lán)圖Unity側(cè)重輕量、快速迭代、移動(dòng)端適配好UE側(cè)重高保真渲染、超大型開放世界、主機(jī)和PC端。做二次元卡牌、休閑游戲、移動(dòng)端MMOUnity的工程效率和包體控制有天然優(yōu)勢(shì)做3A級(jí)開放世界、仿真模擬、對(duì)畫面表現(xiàn)有極致要求的項(xiàng)目UE的Nanite、Lumen、MetaHuman等工具鏈更合適。但選型不是只看引擎性能更要看團(tuán)隊(duì)經(jīng)驗(yàn)和項(xiàng)目長期維護(hù)成本。一個(gè)只有Unity經(jīng)驗(yàn)的團(tuán)隊(duì)突然轉(zhuǎn)UE前三個(gè)月的產(chǎn)出效率會(huì)大幅下降這筆隱性成本比引擎本身的授權(quán)費(fèi)貴得多。筆試如果問你“一個(gè)新項(xiàng)目如何選型”我建議回答中包含三個(gè)維度目標(biāo)平臺(tái)移動(dòng)端還是PC/主機(jī)、畫面表現(xiàn)需求、團(tuán)隊(duì)已有技術(shù)沉淀。這三個(gè)維度缺一不可。另外現(xiàn)在Unity和UE都在互相吸收對(duì)方優(yōu)點(diǎn)Unity的DOTS和SRP讓大規(guī)模場(chǎng)景和高定制渲染成為可能UE的Mobile Render Pipeline也在不斷補(bǔ)足移動(dòng)端短板。所以不要覺得選了A引擎就萬事大吉核心還是吃透引擎原理原理通了換引擎只是換一層皮的事。5. 常見問題與筆試避坑實(shí)錄5.1 時(shí)間分配是最容易被忽視的問題很多同學(xué)拿到筆試題目后的第一個(gè)動(dòng)作是悶頭從第一題做到最后一題結(jié)果前面選擇題花了太多時(shí)間最后的大題寫了一半就到交卷時(shí)間了。我見過太多這樣的案例所以第一條建議就是拿到試卷先花30秒掃一遍全卷把題型分布和大題分值看清楚心里有個(gè)取舍優(yōu)先級(jí)。我的個(gè)人習(xí)慣是把時(shí)間分成三塊基礎(chǔ)題壓縮在50%時(shí)間內(nèi)快速解決不會(huì)的標(biāo)記跳過不要戀戰(zhàn)剩下40%留給大題和場(chǎng)景題這是主要拉分項(xiàng)最后10%回頭補(bǔ)漏。選擇題和填空題的性價(jià)比通常不如大題高因?yàn)橐粋€(gè)選擇題1-2分而一道場(chǎng)景設(shè)計(jì)題可能直接占15-20分花20分鐘去摳一道不確定的單選題遠(yuǎn)不如寫一個(gè)完整的大題思路框架。還有一個(gè)細(xì)節(jié)一定要留意題目里“寫出完整思路即可”和“需要完整代碼”的區(qū)別。有的同學(xué)看到場(chǎng)景設(shè)計(jì)題就拼命寫代碼代碼還沒調(diào)通但題目其實(shí)只要設(shè)計(jì)思路反過來有的題明確要代碼實(shí)現(xiàn)他寫了一大堆思路沒有一行能跑。審題審不對(duì)答得再好也是白搭。5.2 答題過程中最常見的五類失誤結(jié)合我?guī)н^的應(yīng)屆生復(fù)盤結(jié)果筆試?yán)锔哳l失誤集中在下面五個(gè)方面生命周期順序記混。Awake和Start的分工、OnEnable的觸發(fā)時(shí)機(jī)、協(xié)程和Update的執(zhí)行關(guān)系都是重災(zāi)區(qū)。建議考前自己畫一張生命周期流程圖把每個(gè)回調(diào)對(duì)應(yīng)到實(shí)際項(xiàng)目里的用途都寫一遍考試時(shí)就不容易亂。忽略空引用。代碼題里寫for循環(huán)遍歷數(shù)組不判空還假設(shè)數(shù)組一定非空這種低級(jí)錯(cuò)誤最容易扣分。筆試閱卷時(shí)會(huì)看代碼健壯性補(bǔ)幾行判空能明顯提升印象分。不考慮編輯器和真機(jī)差異。編輯器和真機(jī)的資源加載路徑、編碼、性能表現(xiàn)都有差異寫代碼時(shí)如果不主動(dòng)考慮平臺(tái)分支很可能被扣分。比如前面提到的StreamingAssets在Android上的讀取問題。把“優(yōu)化”等同于“改代碼”。題目問性能優(yōu)化有人一上來就貼代碼沒有說清楚瓶頸定位和驗(yàn)證方法。更穩(wěn)妥的答題順序是用Profiler采樣定位瓶頸分析是CPU還是GPU還是內(nèi)存再給出對(duì)應(yīng)的優(yōu)化策略和預(yù)期收益。開放性題答得太空。題目問“如何設(shè)計(jì)一個(gè)背包系統(tǒng)”有人只寫“用List 存數(shù)據(jù)顯示在UI上”這種答案等于沒寫。正確的答法應(yīng)該包含數(shù)據(jù)結(jié)構(gòu)怎么組織、UI和數(shù)據(jù)的雙向綁定怎么解耦、增刪排序的耗時(shí)點(diǎn)在哪、存檔怎么處理、道具堆疊和唯一ID怎么分配。5.3 筆試結(jié)束后的復(fù)盤方法筆試結(jié)束不等于這件事完了復(fù)盤才是提升能力的關(guān)鍵環(huán)節(jié)。我自己的習(xí)慣是考完當(dāng)天趁熱把每道題重新做一遍尤其是那些沒寫出來的大題一定要在編輯器里或者白板上完整實(shí)現(xiàn)一遍。有些場(chǎng)景題甚至值得擴(kuò)展成一個(gè)可運(yùn)行的小Demo比如“用代碼創(chuàng)建Animation Clip”這個(gè)考點(diǎn)你完全可以做一個(gè)完整的編輯器工具加上動(dòng)畫預(yù)覽和曲線編輯既練了API又積累了項(xiàng)目輸出。另外要把錯(cuò)題按知識(shí)點(diǎn)分類是C#基礎(chǔ)問題還是Unity API不熟還是算法弱還是圖形知識(shí)缺失。做錯(cuò)題統(tǒng)計(jì)之后你會(huì)發(fā)現(xiàn)自己真正薄弱的地方往往只有兩三個(gè)模塊有針對(duì)性地補(bǔ)齊比大面積刷題效率高得多。我當(dāng)年考完提前批筆試后就把“Unity資源管線”這類模塊系統(tǒng)補(bǔ)了一遍正式批的筆試成績(jī)反而提升很大所以別把一次筆試失利當(dāng)成能力的終點(diǎn)。復(fù)盤時(shí)還有一個(gè)很容易忽略的維度時(shí)間記錄。每一題實(shí)際花了多久、卡在哪一步這個(gè)數(shù)據(jù)能幫你在下一次筆試前更合理地分配時(shí)間。比如發(fā)現(xiàn)自己在渲染題上總是超時(shí)10分鐘那下次就該提前準(zhǔn)備一套標(biāo)準(zhǔn)答題模板先從光源類型、陰影策略、后處理順序三個(gè)維度鋪開寫這樣不會(huì)慌也不會(huì)漏。6. 一點(diǎn)備考心得回頭看網(wǎng)易2020校招提前批這套Unity3D題它真正考驗(yàn)的不是你記住了多少零碎知識(shí)而是你有沒有把Unity當(dāng)成一門工程學(xué)科去對(duì)待。刷題和背文檔當(dāng)然有用但上限很低真正能拉開差距的是你有沒有在實(shí)際項(xiàng)目中踩過坑、總結(jié)過原因、沉淀出自己的一套處理方式。如果你現(xiàn)在還在校、還有時(shí)間我特別建議認(rèn)真做一兩個(gè)完整的Unity小項(xiàng)目哪怕是很小的玩法Demo也要走完“需求分析—技術(shù)選型—實(shí)現(xiàn)—打包—真機(jī)測(cè)試—性能優(yōu)化”的完整閉環(huán)。筆試的很多大題其實(shí)都來自這些日常工程里的真實(shí)痛點(diǎn)。你把這些痛點(diǎn)親手解決過一遍筆試時(shí)根本不需要背答案憑經(jīng)驗(yàn)和直覺就能寫出讓閱卷人眼前一亮的回答。備考的過程注定會(huì)有反復(fù)和焦慮但換個(gè)角度看筆試其實(shí)是把你平時(shí)積累的工程能力系統(tǒng)展示一次的機(jī)會(huì)。把心態(tài)放平把每個(gè)考點(diǎn)吃透穩(wěn)扎穩(wěn)打走完這一段你一定能拿到屬于你的那份Offer。祝順利。