正交尺寸實戰(zhàn))
接手一個2D橫版項目的第二周我還沒把客戶那臺19.5:9的新手機(jī)當(dāng)回事測試?yán)显O(shè)備時主界面右上角的技能按鈕直接跑到了屏幕邊緣外面數(shù)字被截掉一半。當(dāng)時項目里UI按16:9的參考分辨率擺得整整齊齊代碼全是固定坐標(biāo)一換設(shè)備就露餡。后來抽了兩天時間把UI和2D相機(jī)兩套適配邏輯徹底理了一遍才真正解決。這篇內(nèi)容想做的事就是把Unity的屏幕適配從根源到落地講清楚覆蓋UICanvas和2D相機(jī)Orthographic Camera兩條線不搞玄學(xué)給可復(fù)現(xiàn)的操作路徑。適合正在做2D游戲、卡牌UI、橫版過關(guān)或者任何需要跨分辨率穩(wěn)定收放的Unity開發(fā)者。里面有參數(shù)計算、錨點原理、相機(jī)尺寸推導(dǎo)還有我實際踩過的一堆坑按順序看完基本能解決九成適配問題。開頭先把結(jié)論放在這里屏幕適配的核心難點從來不是分辨率夠不夠高而是設(shè)備之間的寬高比差異——這個認(rèn)知如果沒建立起來后面所有調(diào)參都是憑感覺遲早翻車。1. 屏幕適配的根本矛盾分辨率、寬高比以及Unity的抽象坐標(biāo)系1.1 為什么同一套界面在不同設(shè)備上變形嚴(yán)重很多剛?cè)胄械耐瑢W(xué)會把適配問題歸結(jié)為設(shè)備分辨率不同這個說法不完全對。拿兩臺手機(jī)來說一臺是1920x1080的16:9另一臺是2400x1080的20:9兩者的像素密度可能很接近但屏幕的形狀不一樣。同樣是1080p級別的分辨率一個偏方正一個被拉成長條同一張UI稿擺上去一個是上下扁一點一個是左右空出大截表現(xiàn)完全不同。我習(xí)慣把適配問題拆成三個層面像素分辨率Screen.width / Screen.height決定輸出畫面的清晰度和采樣范圍但它只是最表層的變量邏輯分辨率設(shè)備無關(guān)像素常見于移動平臺的概念決定布局系統(tǒng)按什么密度來擺放元素寬高比aspect ratio決定同一套相對布局會在哪個方向被壓縮、哪個方向被留白——這才是適配真正要處理的核心矛盾。舉個例子一臺老款16:9手機(jī)可用高度相對充足一臺21:9的電影屏手機(jī)高度被橫向?qū)挾葦偙×?。如果UI是對的屏幕畫的那在寬屏上就會左右多出一截空白區(qū)域而上下因為代碼固定死而顯示不全。反過來說如果UI是按21:9畫的放在16:9上就左右被裁切。所以適配的本質(zhì)不是讓UI在每臺設(shè)備上保持完全相同的像素位置而是保持設(shè)計稿中的相對關(guān)系——比如背包按鈕永遠(yuǎn)釘在右下角、標(biāo)題永遠(yuǎn)橫跨頂欄、彈窗永遠(yuǎn)居中并且核心內(nèi)容不被裁出屏幕。這個目標(biāo)需要在UI側(cè)和2D相機(jī)側(cè)分別落實。1.2 Unity坐標(biāo)系里的一個單位并不等于一個像素Unity里有個很容易被忽略的事實Transform中的坐標(biāo)單位和屏幕上的像素沒有直接對應(yīng)關(guān)系。2D相機(jī)的世界坐標(biāo)經(jīng)過投影矩陣變換最終映射到屏幕像素上UI的Canvas則維護(hù)自己的縮放因子把參考分辨率映射到實際屏幕區(qū)域。中間至少隔了兩層換算。這個坐標(biāo)系設(shè)計的初衷是為了讓同一場景能在不同分辨率的設(shè)備上以不同的縮放比例呈現(xiàn)而不要求開發(fā)者針對每臺設(shè)備硬編碼坐標(biāo)。代價就是你必須在項目初期就決定一個單位應(yīng)該覆蓋多少屏幕空間否則后面會發(fā)現(xiàn)用World Space方式擺放UI或2D物體時不同設(shè)備看到的世界范圍不一致用Screen Space Overlay放UI時Canvas Scaler的縮放規(guī)則又會影響所有子節(jié)點的尺寸相機(jī)的orthographicSize決定世界可視高度卻又和畫布的分辨率選擇互不相干。理解這層抽象后再看適配方案就清晰了UI側(cè)負(fù)責(zé)設(shè)計稿到屏幕區(qū)域的等比例映射2D相機(jī)側(cè)負(fù)責(zé)世界可視范圍到屏幕長寬比的映射兩者協(xié)同才能保證界面上按鈕的位置和2D世界里的物體在視覺上對得上。比如一個典型的2D游戲界面血條浮在角色頭頂這個UI可能用的是World Space或者Screen Space加一個世界坐標(biāo)到屏幕坐標(biāo)的換算。如果Canvas和相機(jī)各自按不同的寬高比做適配角色在屏幕上的位置會變血條卻釘在錯誤的地方很令人抓狂。所以下面兩章分別展開UI和2D相機(jī)的適配它們是兩條線但不能各管各的。2. UI側(cè)適配Canvas三種縮放模式與RectTransform的錨點邏輯2.1 Canvas Scaler三種模式怎么選Unity的Canvas組件只能決定UI怎么渲染真正管縮放的其實是掛在Canvas上的CanvasScaler。它有三種模式對應(yīng)的完全是不同的設(shè)計思路模式核心邏輯適用場景Constant Pixel SizeUI元素尺寸保持固定像素值不隨屏幕縮放像素風(fēng)游戲、需要精確控制物理大小的HUD元素Scale With Screen Size根據(jù)參考分辨率等比縮放整個Canvas絕大多數(shù)UI項目的主力方案Constant Physical Size按物理尺寸英寸/厘米顯示UI實際大小隨DPI變化印刷類、需要按物理大小展示的極其罕見場景平時用到最多的就是Scale With Screen Size。它的思想很簡單項目里定一個參考分辨率比如960x540或1920x1080Canvas在這個分辨率下是1:1繪制設(shè)備實際分辨率與參考分辨率不同時整個Canvas統(tǒng)一乘上一個縮放因子。這個縮放因子由匹配度Match決定下面詳細(xì)拆。2.2 Scale With Screen Size的匹配度計算CanvasScaler里有幾個關(guān)鍵參數(shù)Reference Resolution參考分辨率、Match Width Or Height匹配度0到1之間滑條。很多人直接把Match拖到0.5就完事完全不知道這個值意味著什么結(jié)果在不同設(shè)備上UI忽大忽小。匹配度的邏輯是這樣的Match 0按寬度匹配縮放因子 設(shè)備屏幕寬度 / 參考寬度。也就是說UI整體寬度永遠(yuǎn)和設(shè)計稿寬度同比例屏幕高不參與計算Match 1按高度匹配縮放因子 設(shè)備屏幕高度 / 參考高度Match在0到1之間寬高兩個縮放因子做指數(shù)插值混合介于兩者之間。我舉個例子參考分辨率設(shè)為960x54016:9目標(biāo)設(shè)備是2400x108020:9Match 0縮放 2400 / 960 2.5UI繪制尺寸是設(shè)計稿的2.5倍寬、2.5倍高??善聊桓叨戎挥?080按2.5倍縮放后設(shè)計高度540 * 2.5 1350頂部底部各有超出部分被裁出屏幕Match 1縮放 1080 / 540 2.0UI高度完全貼合屏幕但設(shè)計稿寬度960 * 2.0 1920屏幕實際寬2400左右多出480像素的橫向空間——就需要背景圖拉寬或設(shè)計上留出擴(kuò)展區(qū)Match 0.5介于上面兩者之間兩邊都妥協(xié)一點。從這個例子里能看出關(guān)鍵問題無論Match選哪個值UI都是整體等比縮放不會拉伸變形但可顯示范圍在不同方向上會多出來或不夠用。所以選擇Match值的本質(zhì)是決定哪個方向優(yōu)先保住設(shè)計稿范圍豎屏游戲通常優(yōu)先高度匹配Match靠近1因為玩家上下滑動看內(nèi)容是主要場景橫屏游戲通常優(yōu)先寬度匹配Match靠近0因為左右視野更重要。2.3 錨點與Offset把UI按住而不是擺過去解決了縮放因子還要解決位置。很多人的界面在編輯模式下好看切換分辨率就亂飛多半是錨點Anchor沒用對。RectTransform的定位邏輯和普通Transform完全不同。它有幾個核心概念anchorMin 和 anchorMax錨點在父矩形內(nèi)的相對位置范圍0到1。比如(0,0)是左下角(1,1)是右上角pivot自身旋轉(zhuǎn)和縮放的中心點也影響offset計算offsetMin 和 offsetMax子矩形邊緣相對錨點的偏移量anchoredPositionpivot相對錨點的位置。新手最常見的錯誤是直接把元素拖動到屏幕中間然后用絕對坐標(biāo)記錄位置——一旦屏幕尺寸變化這個元素就在錯誤的位置上。正確的做法是讓元素的錨點去釘住父容器上的某個位置元素自身只維護(hù)相對偏移。舉三個高頻案例全屏背景圖anchorMin (0,0)anchorMax (1,1)offsetMin offsetMax (0,0)。這樣不管Canvas怎么縮放背景永遠(yuǎn)和父容器完全重合。固定在左下角的按鈕anchorMin anchorMax (0,0)pivot (0,0)。然后設(shè)置anchoredPosition為(20, 20)就是這個按鈕距離左下角20、20個邏輯單位。屏幕怎么變它都釘在左下角。居中彈窗anchorMin anchorMax (0.5,0.5)pivot (0.5,0.5)。位置保持(0,0)即為屏幕正中央與寬高比無關(guān)。錨點的本質(zhì)是把UI元素的參考系從整個屏幕收窄到一個相對穩(wěn)定的父區(qū)域。配合上面說的Canvas Scaler只要錨點設(shè)置正確屏幕寬高比變化引起的只是整體縮放和多出的可見空間而不是元素亂飄。3. 2D相機(jī)側(cè)正交尺寸、寬屏策略與SafeArea3.1 orthographicSize決定縱向可視范圍2D游戲畫面由Camera渲染如果你用的是Orthographic正交投影那么相機(jī)上有個叫orthographicSize的屬性它的含義是屏幕縱向可視范圍的一半單位是世界單位。橫向可視范圍是縱向乘以aspect。舉個例子orthographicSize 5意味著屏幕從下邊緣到上邊緣共能看到10個世界單位的高度如果當(dāng)前寬高比是16:9aspect約1.78那水平方向能看到10 * 1.78 17.8個世界單位。換成21:9的設(shè)備水平方向就能看到10 * 2.33 23.3個世界單位多出來一大截。所以2D相機(jī)適配的第一個問題是你要讓不同寬高比的設(shè)備看到一樣的縱向范圍還是看到一樣的橫向范圍固定orthographicSize縱向可視高度恒定寬屏設(shè)備看到更多左右內(nèi)容。優(yōu)點是場景縱向布局穩(wěn)定缺點是橫屏設(shè)備上必須額外繪制延伸的地形/背景否則左右露白動態(tài)調(diào)整orthographicSize讓橫向可視寬度恒定寬高比變化通過改變縱向范圍來湊。優(yōu)點是橫向布局穩(wěn)定缺點是不同的寬高比下角色的縮放感差異較大豎屏體驗比較差。一個比較穩(wěn)妥的項目做法是固定orthographicSize用主世界區(qū)域保持縱向穩(wěn)定然后額外處理橫向擴(kuò)展區(qū)。比如你按16:9設(shè)計場景在左右兩側(cè)各多畫30%的安全范圍外背景這樣19.5:9、20:9的設(shè)備也能覆蓋不至于露白。3.2 寬高比不相同時裁剪、擴(kuò)展還是黑邊具體到代碼層面有幾種處理策略策略一不動相機(jī)讓寬屏看更多。最簡單主世界固定orthographicSizeUI不遮住的地方自然顯示更多內(nèi)容。壞處是你得保證場景資源覆蓋得住最寬設(shè)備的橫向范圍否則出現(xiàn)兩側(cè)空白。適合關(guān)卡類游戲。策略二裁剪視口留黑邊。固定一個設(shè)計寬高比比如16:9相機(jī)始終只渲染這個比例的視口設(shè)備寬高比不同時多余區(qū)域用黑邊填充。這個方案能保證構(gòu)圖完全一致代價是左右或上下有黑邊手機(jī)用戶可能不太適應(yīng)但很多橫版游戲、視頻游戲會采用。黑邊方案的相機(jī)rect計算很簡單我貼一段常用代碼using UnityEngine; public class CameraLetterbox : MonoBehaviour { public float designAspect 16f / 9f; void Awake() { Camera cam GetComponentCamera(); float deviceAspect Screen.width / (float)Screen.height; if (deviceAspect designAspect) { // 設(shè)備更寬左右黑邊不設(shè)備寬度更大實際是上下黑邊 // 設(shè)備更寬意味著單位width對應(yīng)的height更小應(yīng)該是上下黑邊 float scale designAspect / deviceAspect; cam.rect new Rect(0f, (1f - scale) / 2f, 1f, scale); } else { // 設(shè)備更窄左右黑邊 float scale deviceAspect / designAspect; cam.rect new Rect((1f - scale) / 2f, 0f, scale, 1f); } } }注意這段代碼里cam.rect是視口矩形0到1之間。設(shè)備更寬時實際顯示區(qū)域的高度被壓縮到designAspect / deviceAspect上下各留一半黑邊設(shè)備更窄時顯示區(qū)域?qū)挾缺粔嚎s到deviceAspect / designAspect左右各留一半黑邊。這個方案適合那些對構(gòu)圖有強(qiáng)迫要求的項目。策略三根據(jù)寬高比動態(tài)調(diào)整orthographicSize。當(dāng)設(shè)備寬高比大于設(shè)計寬高比時把orthographicSize調(diào)大讓縱向看更多一點以維持橫向范圍不變。本質(zhì)上是在看更多和裁剪之間取折中適合操作型2D游戲限制縱向可見區(qū)域保證關(guān)鍵戰(zhàn)斗信息都在屏幕內(nèi)。我自己的實測經(jīng)驗是如果項目要上很多安卓機(jī)型最省事的組合是固定orthographicSize 左右擴(kuò)展背景 用SafeArea處理UI既沒有黑邊的存在感也不容易出現(xiàn)鏡頭抖動。3.3 劉海屏、狀態(tài)欄與SafeArea處理現(xiàn)在手機(jī)普遍有劉海、挖孔、圓角還有動態(tài)島。這條不是2D相機(jī)專屬但直接影響了2D和UI的布局。Screen.safeArea返回一個矩形表示系統(tǒng)保證可交互的安全區(qū)域避開了劉海和底部手勢條。如果忽略SafeArea可能會出現(xiàn)UI頂部被劉海遮住、底部被導(dǎo)航條頂起、2D游戲的操作按鈕跑到手勢區(qū)域里被系統(tǒng)吞掉觸摸。處理方式通常是在UI根節(jié)點創(chuàng)建一個SafeArea容器用代碼把容器的錨點設(shè)置為安全區(qū)域的歸一化位置using UnityEngine; public class SafeAreaAdapter : MonoBehaviour { private RectTransform rectTransform; private Rect lastSafeArea; void Awake() { rectTransform GetComponentRectTransform(); ApplySafeArea(Screen.safeArea); } void Update() { if (lastSafeArea ! Screen.safeArea) { ApplySafeArea(Screen.safeArea); } } void ApplySafeArea(Rect safeArea) { lastSafeArea safeArea; Canvas canvas GetComponentInParentCanvas(); Rect pixelRect canvas.pixelRect; Vector2 minAnchor new Vector2( safeArea.xMin / pixelRect.width, safeArea.yMin / pixelRect.height ); Vector2 maxAnchor new Vector2( safeArea.xMax / pixelRect.width, safeArea.yMax / pixelRect.height ); rectTransform.anchorMin minAnchor; rectTransform.anchorMax maxAnchor; rectTransform.offsetMin Vector2.zero; rectTransform.offsetMax Vector2.zero; } }把這個組件掛在一個空RectTransform上讓所有UI元素都放在它下面就能保證內(nèi)容不被劉海和底部手勢條遮擋。2D相機(jī)側(cè)其實很少需要做SafeArea裁剪一般通過UI容器來避讓就夠用了。4. 基準(zhǔn)分辨率與參考值的選擇一套可落地的配置流程4.1 參考分辨率不是什么迷信數(shù)字很多項目的參考分辨率是從某個同事的舊工程里復(fù)制過來的沒人知道當(dāng)時為什么這么選。參考分辨率的本質(zhì)是設(shè)計稿的坐標(biāo)系它決定了你在Unity里擺放UI時用的是哪套邏輯尺寸。選參考分辨率的目標(biāo)是讓大部分目標(biāo)設(shè)備在默認(rèn)Match下不會出現(xiàn)明顯的UI超出或留白。如果你項目主要面向安卓/iOS手機(jī)最常見的做法是選16:9的分辨率比如960x540或1920x1080。960x540的好處是數(shù)值小、有余量、性能好做1920x1080的好處是和美術(shù)出圖規(guī)范接近。但如果你做的是平板手機(jī)雙端參考分辨率就不能只按手機(jī)來定。一個更穩(wěn)的思路是選一個寬高比介于主要設(shè)備范圍中間值的參考分辨率配合Match讓兩端都能看。比如主要設(shè)備覆蓋16:9到20:9參考分辨率可以取1920x1080Match取0.5這樣寬屏端有橫向擴(kuò)展窄屏端有縱向擴(kuò)展兩邊都不至于崩。我推薦的做法是在項目初期就確定一份目標(biāo)設(shè)備清單列出覆蓋的寬高比范圍再倒推參考分辨率和Match取值??照劇白赃m應(yīng)”是沒法落地的只有采樣到具體設(shè)備配置才有方向。4.2 Match用0還是1橫屏豎屏的差異這個問題極其致命因為很多人用一套Match做了所有界面。我的實踐結(jié)論是豎屏游戲比如卡牌、休閑、豎版射擊推薦Match 1高匹配。豎屏設(shè)備的高寬比差異主要體現(xiàn)在寬度上不同手機(jī)寬度差異相對小高度被劉海/手勢條影響更大高度匹配能保證UI在長屏上不會因為高度變化而頂出屏幕。代價只是左右多一些擴(kuò)展空間可以通過背景拉伸或錨點布局消化掉橫屏游戲比如橫版過關(guān)、MOBA、賽車推薦Match 0寬匹配。橫屏下高度差異相對小寬度變化是主要矛盾寬度匹配能讓水平方向的布局穩(wěn)定縱向多出的范圍用SafeArea容器吸收。如果項目橫豎屏都支持那就別偷懶了必須在切換時動態(tài)重算Match見下一節(jié)。4.3 用代碼動態(tài)調(diào)整Match的時機(jī)跨屏動態(tài)調(diào)整的需求來自橫豎屏切換、平板適配、以及某些安卓設(shè)備的異常寬高比。你不能指望讓策劃手動去調(diào)CanvasScaler應(yīng)該在代碼里自動處理。我寫過一個簡單的自適應(yīng)組件思路是根據(jù)當(dāng)前設(shè)備的寬高比把Match映射到一個預(yù)設(shè)范圍同時把相機(jī)的orthographicSize也聯(lián)動更新using UnityEngine; using UnityEngine.UI; public class AdaptiveScaler : MonoBehaviour { public CanvasScaler scaler; public Camera targetCamera; public float designAspect 16f / 9f; public float baseOrthographicSize 5f; // 根據(jù)寬高比偏離設(shè)計值的程度動態(tài)調(diào)整匹配度 void Awake() { float deviceAspect Screen.width / (float)Screen.height; // 豎屏?xí)raspect 1優(yōu)先保證高度穩(wěn)定 if (deviceAspect 1f) { scaler.matchWidthOrHeight 1f; } // 橫屏但接近16:9用寬匹配穩(wěn)定橫向范圍 else if (Mathf.Abs(deviceAspect - designAspect) 0.3f) { scaler.matchWidthOrHeight 0f; } // 超寬屏介于兩者之間避免過分裁剪或過分留白 else { float t Mathf.InverseLerp(1f, 21f / 9f, deviceAspect); scaler.matchWidthOrHeight Mathf.Lerp(0f, 0.5f, t); } // 如果使用“保持橫向可視范圍”策略則需要調(diào)整orthographicSize if (targetCamera ! null targetCamera.orthographic) { float aspectRatio Screen.width / (float)Screen.height; targetCamera.orthographicSize baseOrthographicSize * (designAspect / aspectRatio); } } }這段代碼的注釋已經(jīng)寫了判斷邏輯。要注意的是動態(tài)調(diào)整Match只會影響UI不會自動修復(fù)2D相機(jī)的可視范圍所以兩者必須放在一起聯(lián)動處理否則UI合了、畫面崩了或者反過來。實際項目里我不建議每次設(shè)備旋轉(zhuǎn)都反復(fù)改Match值頻繁修改有沒有問題——確實會有問題尤其是Canvas子節(jié)點很多、有動畫和布局重建時會卡頓。比較好的做法是在場景加載時一次性算出Match之后除非橫豎屏切換否則不二次修改。5. 我踩過的屏幕適配坑5.1 嵌套Canvas導(dǎo)致縮放疊乘有個需求是彈窗要有獨立的遮罩和渲染層級于是我在彈窗Root上多掛了一個Canvas還把CanvasScaler改成了別的模式。結(jié)果在不同分辨率下彈窗有時大得離譜有時小得和原圖完全不符。排查后發(fā)現(xiàn)子Canvas會受父Canvas的縮放影響而子Canvas的CanvasScaler如果也生效等于在父Canvas的縮放因子上又疊了一層縮放形成縮放的平方。尤其是嵌套Canvas里如果用了Screen Space - Overlay還會出現(xiàn)奇怪的坐標(biāo)偏移。正確的做法是大多數(shù)彈窗場景根本不需要額外Canvas——把彈窗掛在同一個Canvas下用SortingOrder控制它的渲染順序就夠了。如果確實需要獨立Canvas比如做特效、做不同UI空間的平行投影請把子Canvas的模式設(shè)置成Screen Space - Camera并指向同一個UI相機(jī)不要再用Overlay疊一層自己的Scaler。5.2 明明設(shè)置了錨點UI還是亂跑有一次一個按鈕錨點已經(jīng)設(shè)成(0.5, 0.5)了屏幕一換它仍然跑偏。查了很久發(fā)現(xiàn)是pivot被我不小心拖到了(0.8, 0.5)導(dǎo)致錨點和pivot不在同一個點上position計算自然就對不齊。錨點決定元素相對父容器的哪一點定位pivot決定元素自身的哪個點作為旋轉(zhuǎn)/縮放中心兩者最好保持坐標(biāo)系一致。排查路徑建議是查看anchorMin和anchorMax是否如預(yù)期查看pivot是否為(0.5, 0.5)查看offsetMin、offsetMax是否有殘留值確認(rèn)不是父節(jié)點尺寸變化引起的連鎖位移。這個坑看起來基礎(chǔ)但在復(fù)雜UI層級里極其隱蔽——你改的是子Item的錨點它的父容器卻在屏幕適配時變了尺寸最終子Item被推到不該在的地方。5.3 屏幕坐標(biāo)轉(zhuǎn)世界坐標(biāo)時忘了考慮Canvas縮放做個2D戰(zhàn)斗游戲需要根據(jù)鼠標(biāo)點擊位置在世界里生成特效。代碼里直接用了Camera.ScreenToWorldPoint(Input.mousePosition)一開始在編輯器里完全正常打包到平板上就出現(xiàn)了點擊位置和特效位置偏移的bug。原因很簡單ScreenToWorldPoint輸入的是像素坐標(biāo)而UI系統(tǒng)的坐標(biāo)經(jīng)過了Canvas縮放和裁切特別是Canvas被Scale With Screen Size縮放后原始像素坐標(biāo)和邏輯坐標(biāo)的對應(yīng)關(guān)系變了。如果特效是在世界空間里生成應(yīng)該用Camera.ScreenToWorldPoint這時要把Canvas的縮放因子考慮進(jìn)去或者干脆先轉(zhuǎn)成RectTransformUtility.ScreenPointToLocalPointInRectangle拿到局部坐標(biāo)再換算成世界坐標(biāo)。我后來統(tǒng)一封裝了一個工具方法所有需要屏幕坐標(biāo)轉(zhuǎn)世界坐標(biāo)的地方都走它才徹底消滅這類偏移問題。這個坑的教訓(xùn)是UI適配改了之后所有依賴像素坐標(biāo)的邏輯都要跟著檢查一遍不只是UI顯示本身。5.4 文字與特效在縮放下的模糊和位置偏移Canvas整體縮放上去之后TMPro的文字在高DPI設(shè)備上容易出現(xiàn)邊緣模糊原因是動態(tài)字體在沒有足夠像素密度時會渲染在低分辨率圖集上。解決方向有兩個一是開啟TMPro的ExtraPadding二是提高動態(tài)字體的Atlas Resolution或者直接給主要文字用SDF圖集。粒子特效掛在UI下更是個大坑。粒子系統(tǒng)的World和Local模式在Scale With Screen Size下表現(xiàn)完全不同Local模式下粒子的發(fā)射范圍會跟著Canvas縮放屏幕越大粒子越疏World模式下又可能完全不跟隨UI出現(xiàn)特效和按鈕位置分離。我的建議是UI特效單獨開一個相機(jī)渲染層粒子用World模式并和UI錨點保持固定偏移不要依賴Canvas的縮放來控制大小。個人的兩條適配經(jīng)驗適配這件事真正讓人頭大的不是計算公式有多難而是你可能在配置完當(dāng)天覺得萬事大吉隔一個月?lián)Q一批測試設(shè)備又冒出新問題。我習(xí)慣在項目初期就做一張設(shè)備測試矩陣橫軸是分辨率/寬高比縱軸是UI錨點組、2D相機(jī)可視范圍、SafeArea三個檢查項每臺設(shè)備都截圖存檔。這個方法讓我在后來的迭代中省了非常多時間一旦某個界面出了回歸直接對照截圖就能定位是哪一層的適配邏輯破壞了。另一條經(jīng)驗是所有動態(tài)調(diào)整都要在游戲早期完成——不要在運行時反復(fù)改CanvasScaler和相機(jī)屬性盡量在場景初始化階段一次性算好。運行時改這些參數(shù)不但會打斷動畫、觸發(fā)布局重建還會讓特效和UI的同步時序變得不可控。如果這篇文章能幫你在接項目的早期把適配邊界想清楚少走幾天彎路我就覺得非常值了。等你的2D游戲跑過十幾種真機(jī)仍然穩(wěn)定那種感覺比解決任何單一版本的bug都踏實。