问答 AssetBundle 中加载 SpriteAtlas 图集之后卸载异常

侑虎科技 · 2021年03月17日 · 663 次阅读

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 资源的冗余问题;

2. 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(原群已满员)

暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册