1. 程式人生 > 實用技巧 >AssetBundle中載入SpriteAtlas圖集之後解除安裝異常

AssetBundle中載入SpriteAtlas圖集之後解除安裝異常

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資源的冗餘問題;

  1. 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:以下幾點供參考:

  1. center:相機看向中心。
  2. d:相機與中心距離。
  3. monster:怪物座標。
  4. fov:相機y軸方向的視野角度。
  5. aspect:相機視野的寬高比。
  6. viewRatio:怪物在視口的x方向的座標比例(0到1)。
  7. 假設相機旋轉角度:a。
  8. 相機座標:(center.x+dsina , 0, center.z+dcosa)。
  9. 相機x軸:(cosa, 0, -sina)。
  10. 相機y軸:(0, 1, 0)。
  11. 相機z軸:(sina, 0, cosa)。
  12. 怪物在相機空間的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。
  13. 怪物在相機空間的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。
  14. 相機在怪物的z座標(深度)處可看到的xy面的寬度camWidth:
    2*tan(fov/2) * aspect * monsterCamZ
  15. 最後根據怪物視口比例:
    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(原群已滿員)