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问答社区提供了回答,欢迎大家转至社区交流:
https://answer.uwa4d.com/question/5fd4de5210a17c6c2b09d625


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问答社区提供了回答,欢迎大家转至社区交流:
https://answer.uwa4d.com/question/5fd82ca810a17c6c2b09d68b


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问答社区提供了回答,欢迎大家转至社区交流:
https://answer.uwa4d.com/question/5fd71e7310a17c6c2b09d658


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问答社区提供了回答,欢迎大家转至社区交流:
https://answer.uwa4d.com/question/5fd9bc3910a17c6c2b09d6cc


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问答社区提供了回答,欢迎大家转至社区交流:
https://answer.uwa4d.com/question/5fd38ac210a17c6c2b09d620

封面图来自网络


今天的分享就到这里。当然,生有涯而知无涯。在漫漫的开发周期中,您看到的这些问题也许都只是冰山一角,我们早已在UWA问答网站上准备了更多的技术话题等你一起来探索和分享。欢迎热爱进步的你加入,也许你的方法恰能解别人的燃眉之急;而他山之“石”,也能攻你之“玉”。

官网:www.uwa4d.com
官方技术博客:blog.uwa4d.com
官方问答社区:answer.uwa4d.com
UWA学堂:edu.uwa4d.com
官方技术QQ群:793972859(原群已满员)