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()[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();
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问答社区提供了回答
Script
Q:我遇到一个问题:在一个时间点一个GameObject的localScale会被设置成另外一个我不期望的值,但是找了半天相关引用的代码都没有发现localScale被改变。中途弹出了一个“ [Physics.PhysX] cleaning the mesh failed”错误,我本来以为是这个引起的,但是我逐帧打印localScale发现是在这个错误输出之后的N帧之后才出现的。相关引用方法也都打印了日志,但是都没有发现调用。
A:可以尝试下这个工具:
https://github.com/handzlikchris/Unity.MissingUnityEvents注意这个工具是需要在Windows使用的,通过注入Unity的DLL实现。简单写了个例子测试可用。
感谢范君@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:
2tan(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(原群已满员)