1)AssetBundle中加載SpriteAtlas圖集之后卸載異常
2)Shader相關問題
3)如何監聽GameObject的localScale改變
4)項目中大量的字節文件的合並和熱更新方案
5)一個關於相機的幾何數學問題
這是第232篇UWA技術知識分享的推送。今天我們繼續為大家精選了若干和開發、優化相關的問題,建議閱讀時間10分鍾,認真讀完必有收獲。
UWA 問答社區:answer.uwa4d.com
UWA QQ群2:793972859(原群已滿員)
Texture
Q:我從AssetBundle包中加載圖集和音頻,然后在卸載的時候使用Resources.UnloadAsset,發現音頻可以卸載,但是SpriteAtlas無法卸載。
代碼:
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.U2D; using UnityEngine.SceneManagement; public class Test_ResourceUnload : MonoBehaviour { public AudioClip[] clips; public SpriteAtlas[] atlas; private void Update() { if (Input.GetKeyDown(KeyCode.A)) StartCoroutine(LoadAB()); if (Input.GetKeyDown(KeyCode.Space)) { //SceneManager.LoadScene("222"); 加載場景自動卸載 for (int i = 0; i < atlas.Length; i++) { Resources.UnloadAsset(atlas[i]); //不能卸載 } for (int i = 0; i < clips.Length; i++) { Resources.UnloadAsset(clips[i]); //可以卸載 } //下面的可以卸載 //for (int i = 0; i < clips.Length; i++) // clips[i] = null; //for (int i = 0; i < charAtlas.Length; i++) // charAtlas[i] = null; //Resources.UnloadUnusedAssets(); } } private IEnumerator LoadAB() { atlas = new SpriteAtlas[5]; for (int i = 1; i < 6; i++) { string ABPath = Application.streamingAssetsPath + "/chars/" + i.ToString(); var ABRequest = AssetBundle.LoadFromFileAsync(ABPath); yield return ABRequest; AssetBundle charAB = ABRequest.assetBundle; if (charAB != null) { atlas[i - 1] = charAB.LoadAllAssets<SpriteAtlas>()[0]; charAB.Unload(false); } else Debug.LogError("加載關卡charAB錯誤 null"); } string ABPathAudios = Application.streamingAssetsPath + "/audiodubbing/1"; var ABRequestAudios = AssetBundle.LoadFromFileAsync(ABPathAudios); yield return ABRequestAudios; AssetBundle charABAudios = ABRequestAudios.assetBundle; if (charABAudios != null) { clips = charABAudios.LoadAllAssets<AudioClip>(); charABAudios.Unload(false); } else Debug.LogError("加載關卡charAB錯誤 null"); } }
在Proflier中查看(打包后電腦測試,非Editor),按下A加載如下:

按下空格卸載如下:

前后對比發現AudioClips已經卸載了,但是圖集卻沒有卸載。項目是簡單的測試項目並沒有在別處使用加載資源。
測試Unity版本2019.4.9。
A1:Resources.UnloadAsset在Unity的文檔中有這樣一句話:“This function can only be called on Assets that are stored on disk.”
所以SpriteAtlas是無法使用這個接口卸載的,而Texture是可以的。卸載SpriteAtlas可以將圖集單獨打AssetBundle,使用AssetBundle.Unload(true)來卸載,或者清空引用后由下一次Resources.UnloadUnusedAssets來卸載。
感謝范君@UWA問答社區提供了回答
A2:SpriteAtlas里面生成的圖集(Texture)確實是無法使用Resources.UnloadAsset來卸載的,使用這個接口只能卸載內存中SpriteAtlas對象,而不能卸載SpriteAtlas里面引用的sactx開頭的Texture。這種關系類似於Sprite和Texture。
![]()
可以看到內存中有SpriteAtlas,也有SpriteAtlas引用的Texture,這個Texture是被SpriteAtlas引用的。
![]()
調用Resoures.UnloadAsset(sa)之后,SpriteAtlas對象從內存里卸載了,但是那個sactx開頭的Texture還在內存中,只是沒有了SpriteAtlas引用它而已。在Sprite中,我們可以調用Resources.Unload(Sprite.texture)來卸載這個Sprite引用的紋理,但是SpriteAtlas沒有提供這樣的接口。我們可以曲線獲取到這個Texture,從SpriteAtlas里面加載一個小的Sprite,然后調用這個Resources.UnloadAsset(Sprite.texture),但是Unity會報錯。
報錯內容是“UnloadAsset can only be used on assets;”,所以只能清理完引用關系后調用Resources.UnloadUnusedAssets,或者AssetBundle.Unload(true)來卸載。
感謝Xuan@UWA問答社區提供了回答
Shader
Q:UWA報告中指出Shader.Parse調用頻繁,這里我們目前有二個疑問:
第一,Shader解析以后占用的ShaderLab內存,在我們釋放對應Shader以后是否也是正常釋放的?
第二,Shader重復解析除了預加載我們是否可以通過其他方式來避免?比如,對Shader依賴分析做好以后是否可以避免?

另外,關於Standard ,是否可以提供一個工具讓我們查詢有哪些使用到了Standard?
A:1. Shader釋放后,ShaderLab的內存是會相應下降的;如果Shader的依賴關系做好,可以很大程度上降低Shader資源的冗余問題;
- Standard Shader可以通過UWA在線AssetBundle檢測來查看,具體是打包到哪些AssetBundle文件中。同時,也可以通過UWA本地資源檢測來查看Standard Shader的具體情況。
以下服務登錄UWA官網均可免費使用:
在線AssetBundle資源檢測![]()
UWA本地資源檢測工具
![]()
感謝芭妮妮@UWA問答社區提供了回答
Script
Q:我遇到一個問題:在一個時間點一個GameObject的localScale會被設置成另外一個我不期望的值,但是找了半天相關引用的代碼都沒有發現localScale被改變。中途彈出了一個“ [Physics.PhysX] cleaning the mesh failed”錯誤,我本來以為是這個引起的,但是我逐幀打印localScale發現是在這個錯誤輸出之后的N幀之后才出現的。相關引用方法也都打印了日志,但是都沒有發現調用。
A:可以嘗試下這個工具:
https://github.com/handzlikchris/Unity.MissingUnityEvents注意這個工具是需要在Windows使用的,通過注入Unity的DLL實現。簡單寫了個例子測試可用。
Callstack可以看到調用信息:
![]()
而斷點跟進去通過Rider的反編譯可以看到目前的Transform的localScale的set方法已經有回調了:
![]()
感謝范君@UWA問答社區提供了回答
Script
Q:我們項目中有大量的字節文件,大到地圖數據,小到各種模塊自定義的字節數據。都是通過流的方式去加載的。需求是希望通過合並這些字節數據,減少打開流的數量,同時可以分塊壓縮。
現在的方案:
1. 定義一個Block的大小比如1MB。
2. 對於大於1MB的字節數據按1MB分割成Block,每個Block獨立壓縮,最后把這些壓縮后的Block合並成一個文件。需要讀取某一段數據的時候,通過壓縮前后記錄的位置,來判斷需要解壓哪幾塊Block,然后讀取。
3. 對於小於1MB的字節數據和其他字節合並,直到大小大於等於1MB。對合並之后的Block壓縮。需要讀取某一個文件的時候,把文件所在的Block解壓,通過之前記錄的位置來讀取數據。
最后,生成的文件里面,大文件還是一個文件(內部包含了多個1MB+的Block),但是小文件被合成了多個1MB左右的Block。
熱更新方面:
1. 對於大文件來說,某一個BlockA數據變化之后,會New一個新的文件,BlockA數據會從服務器下載,其他的Block從本地原來的文件中拷貝過去。
2. 對於小文件來說,其中一個文件刪除或者添加,會導致后續分Block的順序不同。
比如:本來有兩個小文件的Block->ABCD和EFG,之后把小文件B刪除了,生成的規則變成了ACDE和FG了,這樣就需要把之前ABCD和EFG全部重寫掉。
現在的方案對於熱更新不太友好,特別是小文件,一旦一個刪除了或者添加,后續的Block都需要修改。
A1:提供一個思路,僅供參考。
按這個邏輯,打包小文件時應該要把上一次的打包結果的Block Table也作為輸入,之前已經存在的資源並且也在Block Table中有對應的Block時,應首先考慮仍保留在這個Block中。在這個基礎上,針對文件新增、刪除和更新的情況處理(以問題中Block1:ABCD,Block2:EFG來說明)。
例子中提到的文件刪除、文件B被刪除,則新的版本中,Block1應為ACD。
文件新增,比如新增了文件H,如果大小大於Block Size,則按照你們的大文件邏輯處理,否則可以插入到某個仍有空間的Block內,如果沒有符合的Block,則新開一個Block存放。如果有文件更新,例如文件A更新為A1,更新后如果大於Block Size,則從Block1中拿出按大文件處理,Block1變更為BCD;如果小於Block Size,當A1 BCD的總大小仍然滿足Block Size的限制,則正常更新處理,如果A1 BCD的總大小大於Block Size的限制,則將其分割,例如:A1B為一個新的Block,Block1變成CD。
這類大文件存儲方式其實可以參考一些端游的實現方式,比如Blizzard早期使用的時MPQ格式及后期使用的CASC格式,GitHub上都有開源庫可以參考:
https://github.com/ladislav-zezula/StormLib
https://github.com/ladislav-zezula/CascLib
感謝范君@UWA問答社區提供了回答
Script
Q:在知道玩家的坐標點A,怪物的坐標點B,A和B在同一個水平面,相機的所有參數。A和B在視口的位置,可能是同一側,也可能是不同側,下圖只是一個情況。

中間的紅線是視口坐標X=0.5的位置,現在怪物的視口坐標X=y是在黃線的位置,現在想求相機繞着玩家的坐標點Y軸的方向,旋轉多少度可以讓怪物在視口的坐標變為X=x(就是綠線的位置)?目的是戰斗的時候保證怪物主體顯示在相機視口,即想顯示在相機的部分視口范圍內。
mul(VP, 怪物世界坐標).x = 指定值
mul(VP, 玩家世界坐標).xy = 指定值
攝像機位置和人的位置的距離 = 指定值
A1:如果是希望角色和怪物主體始終顯示在相機視口中,可以讓相機始終對准A、B兩點的中點(或中點附近的某一點),同時保持相機分別與AB的距離不小於某個值,看相機更靠近A點還是更靠近B點,以近的為准。插值計算應該可以實現你要的效果,思路供參考,還沒有實踐。
感謝eangulee@UWA問答社區提供了回答
A2:在前提是玩家是第一人稱視角下,屏幕上目標點A(ax,ay),換算到地面上對應的目標點B(bx,by,bz),假設玩家坐標P,當前怪物坐標M,剩下就是求PM和PB之間的夾角了。
感謝孫星星@UWA問答社區提供了回答
A3:以下幾點供參考:
- center:相機看向中心。
- d:相機與中心距離。
- monster:怪物坐標。
- fov:相機y軸方向的視野角度。
- aspect:相機視野的寬高比。
- viewRatio:怪物在視口的x方向的坐標比例(0到1)。
- 假設相機旋轉角度:a。
- 相機坐標:(center.x+dsina , 0, center.z+dcosa)。
- 相機x軸:(cosa, 0, -sina)。
- 相機y軸:(0, 1, 0)。
- 相機z軸:(sina, 0, cosa)。
- 怪物在相機空間的x坐標monsterCamX:dot(相機到怪物的向量,相機的x軸)
= (monster.x-center.x-d * sina) * cosa - (monster.z-center.z-d * cosa) * sina
= (monster.x - center.x) * cosa - (monster.z-center.z) * sina。- 怪物在相機空間的z坐標monsterCamZ:dot(相機到怪物的向量,相機的z軸)
= (monster.x-center.x) * sina - d * sina * sina + (monster.z - center.z) * cosa - d * cosa * cosa
= (monster.x - center.x) * sina +(monster.z - center.z) * cosa - d。- 相機在怪物的z坐標(深度)處可看到的xy面的寬度camWidth:
2*tan(fov/2) * aspect * monsterCamZ- 最后根據怪物視口比例:
viewRatio = monsterCamX / camWidth也可能會出現解這樣的方程:sina - 2cosa = 0.2,求角度a。
感謝Manchy@UWA問答社區提供了回答
A4:請參考下圖公式:
![]()
感謝Xuan@UWA問答社區提供了回答
封面圖來自網絡
今天的分享就到這里。當然,生有涯而知無涯。在漫漫的開發周期中,您看到的這些問題也許都只是冰山一角,我們早已在UWA問答網站上准備了更多的技術話題等你一起來探索和分享。歡迎熱愛進步的你加入,也許你的方法恰能解別人的燃眉之急;而他山之“石”,也能攻你之“玉”。
官網:www.uwa4d.com
官方技術博客:blog.uwa4d.com
官方問答社區:answer.uwa4d.com
UWA學堂:edu.uwa4d.com
官方技術QQ群:793972859(原群已滿員)