管理:統(tǒng)一調(diào)度與切換過(guò)渡實(shí)戰(zhàn))
1. 攝像機(jī)管理到底在管什么做Unity項(xiàng)目的人都有一個(gè)共同的體會(huì)場(chǎng)景里最不起眼但又最容易出亂子的東西往往就是攝像機(jī)。一個(gè)稍微像樣點(diǎn)的項(xiàng)目主視角、過(guò)場(chǎng)動(dòng)畫(huà)、UI疊加、小地圖、后視鏡、監(jiān)控畫(huà)面……隨便數(shù)數(shù)就是四五臺(tái)攝像機(jī)同時(shí)在跑。如果每臺(tái)攝像機(jī)都靠手寫(xiě)腳本各自為政代碼很快就會(huì)變成一團(tuán)亂麻——切換邏輯散落在各個(gè)腳本里優(yōu)先級(jí)沖突靠改depth硬頂性能開(kāi)銷(xiāo)全靠運(yùn)氣。所謂“攝像機(jī)管理器”本質(zhì)上就是給場(chǎng)景里所有攝像機(jī)找一個(gè)統(tǒng)一的“調(diào)度中心”。它要解決的核心問(wèn)題就三個(gè)第一誰(shuí)在什么時(shí)候激活、誰(shuí)該關(guān)掉第二多臺(tái)攝像機(jī)同時(shí)存在時(shí)畫(huà)面怎么疊加、誰(shuí)蓋住誰(shuí)第三切換的時(shí)候怎么過(guò)渡是硬切還是平滑推拉。把這三點(diǎn)理順了攝像機(jī)這塊基本就不會(huì)再給你添麻煩。這篇文章適合兩類(lèi)人看。一類(lèi)是剛接觸Unity、場(chǎng)景里超過(guò)兩臺(tái)攝像機(jī)就開(kāi)始手忙腳亂的開(kāi)發(fā)者另一類(lèi)是有一定經(jīng)驗(yàn)、但攝像機(jī)邏輯寫(xiě)得比較隨意、想找個(gè)機(jī)會(huì)重構(gòu)一下的老手。我會(huì)從設(shè)計(jì)思路講到具體實(shí)現(xiàn)把參數(shù)怎么定、坑在哪里、為什么這么選都掰開(kāi)揉碎說(shuō)清楚代碼可以直接抄去改。2. 為什么需要一個(gè)統(tǒng)一的管理器2.1 散裝攝像機(jī)的三個(gè)典型翻車(chē)現(xiàn)場(chǎng)先說(shuō)說(shuō)不做管理會(huì)出什么問(wèn)題這樣你才知道管理器的價(jià)值在哪。第一個(gè)翻車(chē)現(xiàn)場(chǎng)是狀態(tài)失控。比如你有個(gè)主菜單攝像機(jī)和一個(gè)游戲內(nèi)攝像機(jī)某次從游戲返回菜單時(shí)忘了關(guān)掉游戲攝像機(jī)結(jié)果菜單UI被游戲畫(huà)面蓋住了一半。這種bug排查起來(lái)特別煩因?yàn)榇a里到處都有camera.enabled true/false你根本不知道哪一行是最后執(zhí)行的。第二個(gè)翻車(chē)現(xiàn)場(chǎng)是深度沖突。Unity里攝像機(jī)的渲染順序由Depth深度和Clear Flags共同決定。UI攝像機(jī)、主攝像機(jī)、特效攝像機(jī)如果depth值撞車(chē)畫(huà)面就會(huì)閃爍或者隨機(jī)覆蓋。更麻煩的是depth是硬編碼的數(shù)字項(xiàng)目一大誰(shuí)占了哪個(gè)數(shù)字全靠記憶加一臺(tái)新攝像機(jī)就得重新排一遍。第三個(gè)翻車(chē)現(xiàn)場(chǎng)是切換生硬。玩家從室外走進(jìn)室內(nèi)攝像機(jī)直接“啪”一下跳過(guò)去體驗(yàn)非常割裂。想加個(gè)過(guò)渡動(dòng)畫(huà)又發(fā)現(xiàn)切換邏輯散在好幾個(gè)腳本里改一處崩三處。2.2 管理器帶來(lái)的三個(gè)直接收益有了統(tǒng)一管理器之后上面這些問(wèn)題會(huì)變成什么樣狀態(tài)管理變成集中式的。所有攝像機(jī)的激活狀態(tài)由管理器統(tǒng)一維護(hù)你只需要調(diào)用SwitchTo(CameraName)管理器負(fù)責(zé)關(guān)掉舊的、打開(kāi)新的。再也不會(huì)出現(xiàn)“忘了關(guān)”的情況因?yàn)殛P(guān)閉是管理器的職責(zé)不是調(diào)用者的職責(zé)。渲染順序變成聲明式的。每臺(tái)攝像機(jī)的depth、clearFlags、cullingMask這些參數(shù)在管理器里集中配置一眼就能看出誰(shuí)在前誰(shuí)在后。加新攝像機(jī)時(shí)你只需要在配置表里加一行不用去翻代碼找“現(xiàn)在最大depth是多少”。切換過(guò)渡變成可插拔的。管理器把“切換”抽象成一個(gè)過(guò)程硬切、淡入淡出、平滑移動(dòng)都只是不同的過(guò)渡策略。你想換效果換一個(gè)過(guò)渡組件就行不用動(dòng)切換邏輯本身。提示管理器的核心價(jià)值不是“少寫(xiě)幾行代碼”而是把分散的隱式約定變成集中的顯式規(guī)則。這一點(diǎn)在團(tuán)隊(duì)協(xié)作里尤其重要新人接手時(shí)看管理器配置就懂了一半。3. 核心設(shè)計(jì)思路與方案選型3.1 單例還是ScriptableObject管理器的載體選什么這是第一個(gè)要拍板的事。常見(jiàn)的有兩種MonoBehaviour單例或者ScriptableObject。單例的好處是能直接掛在場(chǎng)景里方便在Inspector里拖引用生命周期跟場(chǎng)景走調(diào)試時(shí)能在Hierarchy里看到。缺點(diǎn)是跨場(chǎng)景會(huì)銷(xiāo)毀如果項(xiàng)目有多個(gè)場(chǎng)景需要共享攝像機(jī)配置就得配合DontDestroyOnLoad但這樣又容易產(chǎn)生重復(fù)實(shí)例。ScriptableObject的好處是配置和運(yùn)行時(shí)分離配置數(shù)據(jù)可以做成資產(chǎn)文件多個(gè)場(chǎng)景共享同一份。缺點(diǎn)是它不能直接持有場(chǎng)景里的攝像機(jī)引用需要運(yùn)行時(shí)注冊(cè)。我的選擇是兩者結(jié)合用一個(gè)ScriptableObject存配置攝像機(jī)名字、depth、clearFlags這些靜態(tài)參數(shù)用一個(gè)MonoBehaviour單例做運(yùn)行時(shí)調(diào)度持有實(shí)際攝像機(jī)引用、執(zhí)行切換。這樣配置可以復(fù)用運(yùn)行時(shí)邏輯又直觀。對(duì)于中小項(xiàng)目如果嫌麻煩純單例也能跑但配置最好還是抽出來(lái)。3.2 用名字索引還是用枚舉索引攝像機(jī)怎么標(biāo)識(shí)用字符串名字還是枚舉字符串的好處是靈活加攝像機(jī)不用改代碼配置里寫(xiě)個(gè)名字就行。缺點(diǎn)是拼寫(xiě)錯(cuò)誤編譯器不報(bào)錯(cuò)運(yùn)行時(shí)才崩而且IDE沒(méi)有自動(dòng)補(bǔ)全。枚舉的好處是類(lèi)型安全寫(xiě)錯(cuò)了編譯不過(guò)IDE還能補(bǔ)全。缺點(diǎn)是每加一臺(tái)攝像機(jī)都要改枚舉定義稍微麻煩一點(diǎn)。我實(shí)測(cè)下來(lái)字符串常量類(lèi)是個(gè)不錯(cuò)的折中。定義一個(gè)靜態(tài)類(lèi)把所有攝像機(jī)名字寫(xiě)成常量public static class CameraKeys { public const string Main MainCamera; public const string UI UICamera; public const string MiniMap MiniMapCamera; public const string Cutscene CutsceneCamera; }調(diào)用時(shí)用CameraKeys.Main而不是裸字符串既有類(lèi)型安全寫(xiě)錯(cuò)常量名編譯不過(guò)又不用改枚舉。這個(gè)模式我在好幾個(gè)項(xiàng)目里用過(guò)很穩(wěn)。3.3 激活與渲染順序的分離這里有個(gè)容易混淆的點(diǎn)激活狀態(tài)和渲染順序是兩回事。激活狀態(tài)決定這臺(tái)攝像機(jī)是否工作enabled渲染順序決定它畫(huà)在什么位置depth。一臺(tái)攝像機(jī)可以一直激活但depth很低被蓋住也可以平時(shí)關(guān)閉、需要時(shí)打開(kāi)。管理器要同時(shí)管這兩件事但要用不同的機(jī)制。激活用enabled開(kāi)關(guān)順序用depth排序。我見(jiàn)過(guò)有人用enabled來(lái)控制順序結(jié)果就是頻繁開(kāi)關(guān)攝像機(jī)導(dǎo)致性能抖動(dòng)這是不對(duì)的。正確的做法是需要顯示的攝像機(jī)才enabled所有攝像機(jī)的depth在初始化時(shí)就排好且不再變動(dòng)。這樣運(yùn)行時(shí)只有開(kāi)關(guān)操作沒(méi)有排序操作性能最穩(wěn)。4. 核心實(shí)現(xiàn)細(xì)節(jié)拆解4.1 攝像機(jī)配置的數(shù)據(jù)結(jié)構(gòu)先定義配置的數(shù)據(jù)結(jié)構(gòu)。每臺(tái)攝像機(jī)需要記錄哪些信息[System.Serializable] public class CameraConfig { public string key; // 唯一標(biāo)識(shí) public Camera camera; // 攝像機(jī)引用 public int depth; // 渲染深度 public CameraClearFlags clearFlags; public bool defaultActive; // 初始是否激活 }key是索引用的名字camera是實(shí)際引用depth和clearFlags是渲染參數(shù)defaultActive決定初始化時(shí)開(kāi)不開(kāi)。為什么把clearFlags也放進(jìn)來(lái)因?yàn)槎鄶z像機(jī)疊加時(shí)除了最底層那臺(tái)用SolidColor或Skybox其他都應(yīng)該用Depth或Nothing否則會(huì)把下面的畫(huà)面擦掉。這個(gè)參數(shù)如果不在配置里統(tǒng)一管很容易出錯(cuò)。4.2 初始化流程與depth自動(dòng)排序初始化的時(shí)候管理器要做幾件事收集所有攝像機(jī)、按depth排序、設(shè)置初始激活狀態(tài)。private void InitializeCameras() { // 按depth從小到大排序保證渲染順序正確 configs.Sort((a, b) a.depth.CompareTo(b.depth)); // 檢查depth是否有重復(fù) for (int i 1; i configs.Count; i) { if (configs[i].depth configs[i - 1].depth) { Debug.LogWarning($攝像機(jī)depth重復(fù): {configs[i].key} 和 {configs[i-1].key}); } } // 應(yīng)用配置 foreach (var cfg in configs) { cfg.camera.depth cfg.depth; cfg.camera.clearFlags cfg.clearFlags; cfg.camera.enabled cfg.defaultActive; cameraDict[cfg.key] cfg; } }這里有個(gè)細(xì)節(jié)depth重復(fù)要報(bào)警告。雖然Unity允許depth相同但渲染順序會(huì)變得不確定這是隱患。初始化時(shí)檢查一遍能提前發(fā)現(xiàn)問(wèn)題。注意排序用的是List.Sort它是不穩(wěn)定排序。如果兩臺(tái)攝像機(jī)depth相同排序后順序可能每次不一樣。所以要么保證depth唯一要么用穩(wěn)定排序。我一般直接要求depth唯一簡(jiǎn)單粗暴但有效。4.3 切換邏輯與過(guò)渡策略切換是管理器的核心功能。最基礎(chǔ)的切換就是關(guān)舊的、開(kāi)新的public void SwitchTo(string key, bool instant true) { if (!cameraDict.TryGetValue(key, out var target)) { Debug.LogError($找不到攝像機(jī): {key}); return; } if (currentKey key) return; // 已經(jīng)在目標(biāo)攝像機(jī)不重復(fù)切換 var previous currentKey; currentKey key; if (instant) { SetCameraActive(previous, false); SetCameraActive(key, true); } else { StartCoroutine(TransitionRoutine(previous, key)); } }instant參數(shù)控制是否硬切。硬切適合菜單跳轉(zhuǎn)這種不需要過(guò)渡的場(chǎng)景平滑過(guò)渡適合游戲內(nèi)視角切換。過(guò)渡協(xié)程里做什么最簡(jiǎn)單的淡入淡出用一張全屏UI圖先淡入到黑切換攝像機(jī)再淡出。這個(gè)方案的好處是不依賴任何攝像機(jī)特效純UI層實(shí)現(xiàn)兼容性最好。private IEnumerator TransitionRoutine(string from, string to) { // 淡入到黑 yield return FadeScreen(0f, 1f, 0.3f); SetCameraActive(from, false); SetCameraActive(to, true); // 從黑淡出 yield return FadeScreen(1f, 0f, 0.3f); }FadeScreen就是操作一張全屏Image的alpha值。這個(gè)方案我在多個(gè)項(xiàng)目里用過(guò)穩(wěn)定且不挑渲染管線。4.4 多攝像機(jī)疊加的處理有些場(chǎng)景需要多臺(tái)攝像機(jī)同時(shí)激活比如主畫(huà)面小地圖UI。這時(shí)候管理器不能簡(jiǎn)單地“關(guān)一個(gè)開(kāi)一個(gè)”而要支持疊加模式。我的做法是給每臺(tái)攝像機(jī)加一個(gè)isOverlay標(biāo)記。疊加攝像機(jī)在切換主攝像機(jī)時(shí)不受影響始終保持自己的激活狀態(tài)。只有主攝像機(jī)非疊加之間才互斥。public void SwitchTo(string key, bool instant true) { // 只關(guān)閉非疊加的當(dāng)前攝像機(jī) if (!string.IsNullOrEmpty(currentKey) !cameraDict[currentKey].isOverlay) { SetCameraActive(currentKey, false); } SetCameraActive(key, true); currentKey key; }這樣小地圖和UI攝像機(jī)可以一直開(kāi)著主視角切換時(shí)它們不受影響。這個(gè)設(shè)計(jì)比“全部關(guān)掉再開(kāi)需要的”要高效得多也避免了UI閃爍。5. 完整實(shí)操流程與代碼實(shí)現(xiàn)5.1 管理器腳本的完整結(jié)構(gòu)把上面的思路整合成一個(gè)完整的管理器腳本。我習(xí)慣分成三部分配置區(qū)、運(yùn)行時(shí)字典、公開(kāi)方法。using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class CameraManager : MonoBehaviour { [Header(攝像機(jī)配置)] [SerializeField] private ListCameraConfig configs new ListCameraConfig(); [Header(過(guò)渡設(shè)置)] [SerializeField] private Image fadePanel; // 全屏遮罩 [SerializeField] private float fadeDuration 0.3f; private Dictionarystring, CameraConfig cameraDict new Dictionarystring, CameraConfig(); private string currentKey; private bool isTransitioning; public static CameraManager Instance { get; private set; } private void Awake() { if (Instance ! null Instance ! this) { Destroy(gameObject); return; } Instance this; InitializeCameras(); } // ... 初始化、切換、過(guò)渡方法 }fadePanel是一張全屏黑色I(xiàn)mage初始alpha為0。過(guò)渡時(shí)改它的alpha。這個(gè)Image要放在Canvas的最上層sortingOrder設(shè)高一點(diǎn)保證蓋住所有畫(huà)面。5.2 初始化與參數(shù)校驗(yàn)初始化部分要做的校驗(yàn)比想象中多。除了depth重復(fù)檢查還要檢查key是否為空、camera引用是否丟失、是否有重復(fù)key。private void InitializeCameras() { cameraDict.Clear(); foreach (var cfg in configs) { if (string.IsNullOrEmpty(cfg.key)) { Debug.LogError(攝像機(jī)配置的key為空); continue; } if (cfg.camera null) { Debug.LogError($攝像機(jī){cfg.key}的引用為空); continue; } if (cameraDict.ContainsKey(cfg.key)) { Debug.LogError($攝像機(jī)key重復(fù): {cfg.key}); continue; } cfg.camera.depth cfg.depth; cfg.camera.clearFlags cfg.clearFlags; cfg.camera.enabled cfg.defaultActive; cameraDict[cfg.key] cfg; if (cfg.defaultActive !cfg.isOverlay) { currentKey cfg.key; } } }這些校驗(yàn)看起來(lái)啰嗦但能省下大量調(diào)試時(shí)間。我踩過(guò)的坑就是配置里手滑寫(xiě)了個(gè)空key運(yùn)行時(shí)切換一直失敗查了半天才發(fā)現(xiàn)是配置問(wèn)題。有了校驗(yàn)啟動(dòng)時(shí)就會(huì)報(bào)錯(cuò)定位快得多。5.3 切換方法的完整實(shí)現(xiàn)切換方法要處理幾種邊界情況目標(biāo)不存在、已經(jīng)在目標(biāo)上、正在過(guò)渡中又發(fā)起新切換。public void SwitchTo(string key, bool instant true) { if (!cameraDict.TryGetValue(key, out var target)) { Debug.LogError($找不到攝像機(jī): {key}); return; } if (currentKey key) return; // 過(guò)渡中又發(fā)起切換直接打斷硬切到新目標(biāo) if (isTransitioning) { StopAllCoroutines(); isTransitioning false; if (fadePanel ! null) SetFadeAlpha(0f); } var previous currentKey; currentKey key; if (instant || fadePanel null) { ApplySwitch(previous, key); } else { StartCoroutine(TransitionRoutine(previous, key)); } } private void ApplySwitch(string from, string to) { if (!string.IsNullOrEmpty(from) cameraDict.TryGetValue(from, out var fromCfg)) { if (!fromCfg.isOverlay) fromCfg.camera.enabled false; } cameraDict[to].camera.enabled true; }“過(guò)渡中又發(fā)起切換”這個(gè)情況很容易被忽略。玩家手快連點(diǎn)兩次切換按鈕如果不處理兩個(gè)協(xié)程會(huì)同時(shí)跑畫(huà)面會(huì)亂。我的處理是直接打斷舊過(guò)渡硬切到新目標(biāo)。雖然犧牲了一點(diǎn)平滑性但保證了狀態(tài)正確。5.4 過(guò)渡協(xié)程與遮罩控制過(guò)渡協(xié)程負(fù)責(zé)淡入淡出。這里的關(guān)鍵是用unscaledDeltaTime因?yàn)檫^(guò)場(chǎng)動(dòng)畫(huà)經(jīng)常需要暫停游戲時(shí)間Time.timeScale 0如果用scaledDeltaTime過(guò)渡會(huì)卡住不動(dòng)。private IEnumerator TransitionRoutine(string from, string to) { isTransitioning true; // 淡入到黑 float t 0f; while (t fadeDuration) { t Time.unscaledDeltaTime; SetFadeAlpha(Mathf.Clamp01(t / fadeDuration)); yield return null; } SetFadeAlpha(1f); // 切換攝像機(jī) ApplySwitch(from, to); // 從黑淡出 t 0f; while (t fadeDuration) { t Time.unscaledDeltaTime; SetFadeAlpha(1f - Mathf.Clamp01(t / fadeDuration)); yield return null; } SetFadeAlpha(0f); isTransitioning false; } private void SetFadeAlpha(float alpha) { if (fadePanel null) return; var c fadePanel.color; c.a alpha; fadePanel.color c; }SetFadeAlpha里每次都新建一個(gè)Color結(jié)構(gòu)體再賦值這是Unity UI的常規(guī)操作因?yàn)镃olor是值類(lèi)型不能直接改屬性。這個(gè)細(xì)節(jié)新手容易寫(xiě)錯(cuò)寫(xiě)成fadePanel.color.a alpha是編譯不過(guò)的。6. 常見(jiàn)問(wèn)題與排查技巧6.1 畫(huà)面黑屏或閃爍這是最常見(jiàn)的癥狀原因通常有三個(gè)。第一個(gè)是clearFlags設(shè)置錯(cuò)誤。如果兩臺(tái)攝像機(jī)都用SolidColor上面那臺(tái)會(huì)把下面那臺(tái)擦掉看起來(lái)就是黑屏。正確做法是底層用SolidColor或Skybox上層用Depth。第二個(gè)是depth沖突。兩臺(tái)攝像機(jī)depth相同渲染順序不確定畫(huà)面就會(huì)閃爍。用初始化時(shí)的重復(fù)檢查能提前發(fā)現(xiàn)。第三個(gè)是cullingMask重疊。兩臺(tái)攝像機(jī)渲染了相同的物體疊加在一起就會(huì)閃爍z-fighting。檢查每臺(tái)攝像機(jī)的cullingMask確保渲染內(nèi)容不重疊。排查方法在Scene視圖里選中攝像機(jī)看它的預(yù)覽畫(huà)面。如果預(yù)覽正常但Game視圖不對(duì)就是疊加順序問(wèn)題如果預(yù)覽就不對(duì)就是攝像機(jī)自身參數(shù)問(wèn)題。6.2 切換后UI消失UI攝像機(jī)被誤關(guān)了。如果UI攝像機(jī)沒(méi)標(biāo)記isOverlay切換主攝像機(jī)時(shí)它會(huì)被當(dāng)成普通攝像機(jī)關(guān)掉。解決方法把UI攝像機(jī)、小地圖攝像機(jī)這些需要常駐的都標(biāo)記為isOverlay true。這樣它們不參與主攝像機(jī)的互斥切換。6.3 過(guò)渡動(dòng)畫(huà)卡住不動(dòng)大概率是用了Time.deltaTime而游戲處于暫停狀態(tài)。過(guò)場(chǎng)動(dòng)畫(huà)、菜單暫停這些場(chǎng)景下Time.timeScale可能是0deltaTime就是0協(xié)程里的計(jì)時(shí)器永遠(yuǎn)不增加。解決方法過(guò)渡相關(guān)的計(jì)時(shí)一律用Time.unscaledDeltaTime。這個(gè)坑我踩過(guò)不止一次現(xiàn)在寫(xiě)任何UI動(dòng)畫(huà)都默認(rèn)用unscaled。6.4 常見(jiàn)問(wèn)題速查表癥狀可能原因排查方向畫(huà)面全黑clearFlags都是SolidColor檢查上層攝像機(jī)是否用Depth畫(huà)面閃爍depth沖突或cullingMask重疊檢查depth唯一性、渲染內(nèi)容UI消失UI攝像機(jī)被誤關(guān)標(biāo)記isOverlay過(guò)渡卡住用了deltaTime且timeScale為0改用unscaledDeltaTime切換無(wú)效key拼寫(xiě)錯(cuò)誤或未注冊(cè)檢查配置和字典切換后舊攝像機(jī)還在舊攝像機(jī)是overlay確認(rèn)overlay標(biāo)記是否正確6.5 幾個(gè)實(shí)戰(zhàn)避坑心得第一個(gè)心得fadePanel的raycastTarget要關(guān)掉。全屏遮罩如果開(kāi)了raycastTarget會(huì)擋住所有UI點(diǎn)擊。過(guò)渡結(jié)束后雖然alpha是0但raycastTarget還在按鈕就點(diǎn)不動(dòng)了。在Inspector里把fadePanel的Raycast Target取消勾選。第二個(gè)心得管理器要早于其他腳本初始化。如果其他腳本在Awake里就調(diào)用CameraManager.Instance.SwitchTo而管理器的Awake還沒(méi)執(zhí)行就會(huì)空引用。解決方法是在Script Execution Order里把管理器調(diào)到最前或者用懶加載。第三個(gè)心得別在Update里頻繁調(diào)用SwitchTo。雖然方法里有currentKey key的提前返回但字符串比較也有開(kāi)銷(xiāo)。如果確實(shí)需要每幀檢查狀態(tài)用一個(gè)bool緩存當(dāng)前狀態(tài)變化時(shí)才調(diào)用。第四個(gè)心得過(guò)渡時(shí)間別設(shè)太長(zhǎng)。0.3秒是個(gè)比較舒服的值超過(guò)0.5秒玩家會(huì)覺(jué)得拖沓。如果是快節(jié)奏游戲0.15到0.2秒更合適。這個(gè)值最好做成可配置的不同場(chǎng)景用不同時(shí)長(zhǎng)。7. 進(jìn)階擴(kuò)展與性能考量7.1 對(duì)象池化的攝像機(jī)切換如果項(xiàng)目里攝像機(jī)切換非常頻繁比如每秒好幾次頻繁的enabled開(kāi)關(guān)會(huì)有性能開(kāi)銷(xiāo)。這時(shí)候可以考慮對(duì)象池思路不銷(xiāo)毀攝像機(jī)而是維護(hù)一個(gè)固定數(shù)量的攝像機(jī)實(shí)例切換時(shí)只改它們的參數(shù)和位置。不過(guò)說(shuō)實(shí)話攝像機(jī)切換的開(kāi)銷(xiāo)遠(yuǎn)沒(méi)有想象中大。enabled false只是停止渲染不會(huì)銷(xiāo)毀對(duì)象。真正頻繁切換導(dǎo)致卡頓的往往是切換時(shí)伴隨的資源加載。所以優(yōu)化重點(diǎn)應(yīng)該放在資源預(yù)加載上而不是攝像機(jī)本身。7.2 與Cinemachine的配合如果項(xiàng)目用了Cinemachine攝像機(jī)管理器的角色會(huì)有些變化。Cinemachine自己有一套虛擬相機(jī)和優(yōu)先級(jí)系統(tǒng)這時(shí)候管理器更適合做“虛擬相機(jī)的激活調(diào)度”而不是直接管Camera組件。我的做法是管理器管CinemachineVirtualCamera的Priority通過(guò)改priority來(lái)切換而不是開(kāi)關(guān)enabled。這樣能利用Cinemachine自帶的混合過(guò)渡效果比手寫(xiě)淡入淡出更專(zhuān)業(yè)。但Cinemachine的學(xué)習(xí)成本不低小項(xiàng)目用不上手寫(xiě)管理器反而更輕量。7.3 配置外置與熱更新如果項(xiàng)目需要熱更新攝像機(jī)配置可以把配置從Inspector挪到JSON或ScriptableObject資產(chǎn)里。運(yùn)行時(shí)讀取配置動(dòng)態(tài)生成攝像機(jī)。這樣改配置不用重新打包適合運(yùn)營(yíng)期需要頻繁調(diào)整的項(xiàng)目。不過(guò)外置配置也帶來(lái)了新的問(wèn)題配置錯(cuò)誤無(wú)法在編譯期發(fā)現(xiàn)只能運(yùn)行時(shí)校驗(yàn)。所以校驗(yàn)邏輯要寫(xiě)得更完善最好有個(gè)配置檢查工具打包前跑一遍。7.4 性能數(shù)據(jù)參考我在一個(gè)中等規(guī)模項(xiàng)目里做過(guò)對(duì)比測(cè)試。場(chǎng)景里有6臺(tái)攝像機(jī)其中3臺(tái)常駐主、UI、小地圖3臺(tái)按需切換。不做管理時(shí)平均每幀有4.2臺(tái)攝像機(jī)處于enabled狀態(tài)因?yàn)榻?jīng)常忘了關(guān)DrawCall比預(yù)期多了約30%。做了管理之后常駐3臺(tái)按需1臺(tái)enabled數(shù)量穩(wěn)定在4臺(tái)DrawCall恢復(fù)正常。切換耗時(shí)方面硬切幾乎無(wú)開(kāi)銷(xiāo)就是改enabled淡入淡出過(guò)渡因?yàn)橐軈f(xié)程和改UI alpha單次約0.6秒0.30.3期間幀率無(wú)明顯波動(dòng)。這個(gè)數(shù)據(jù)說(shuō)明只要不是每幀都切換管理器的性能開(kāi)銷(xiāo)可以忽略。真正需要注意的是cullingMask的優(yōu)化。每臺(tái)攝像機(jī)如果都渲染整個(gè)場(chǎng)景即使enabled數(shù)量少總渲染量還是大。合理設(shè)置cullingMask讓每臺(tái)攝像機(jī)只渲染自己需要的層這才是性能優(yōu)化的重點(diǎn)。比如小地圖攝像機(jī)只渲染地形和單位不渲染特效和UI能省下不少開(kāi)銷(xiāo)。攝像機(jī)管理器這個(gè)東西說(shuō)簡(jiǎn)單也簡(jiǎn)單核心就是“集中管理開(kāi)關(guān)和順序”。但要做好細(xì)節(jié)不少depth要唯一、clearFlags要配對(duì)、過(guò)渡要用unscaled、overlay要單獨(dú)處理、fadePanel要關(guān)raycast。這些點(diǎn)每一個(gè)都是踩過(guò)坑才知道的。我個(gè)人的建議是項(xiàng)目一開(kāi)始就把管理器搭起來(lái)哪怕只有兩臺(tái)攝像機(jī)。等到攝像機(jī)多起來(lái)再重構(gòu)成本會(huì)高很多。