资源管理|01 常用资源管理方式
常用资源加载方式
-
拖到组件上,直接引用
- Resources
- 编辑器/实机均可以使用
- 资源必须位于Resources目录下,加载路径是相对于Resources的路径
- Resources目录下的文件大小有限制
- AssetDataBase
- 只可以在编辑器下使用
- 路径以Asssets开始,要求加入资源后缀
- AssetBundle
AssetBundle入门
基本定义
AssetBundle(以下简称AB)可以理解为一种压缩包,能够压缩任何能够放入U3D中的资源,并且还能够自动保存资源之间的依赖关系
一般来说AB包里的内容可以分为两类
- 不可以在电脑上直接预览的资源,比如Prefab,所有此类资源会被统一打入一个Serialized File中
- 可以再电脑上直接预览的资源,比如图片,音频,每一个资源都会被保存为一个Resources File
因此,一个AB包里有1个Serialized文件和N个Resource文件
基本流程
1.指定AB包的打包属性
点击想要打包的资源
在下方可以对目标AB包进行设置,第一个是所属包名,第二个是包后缀,后缀无实际意义
可以通过XXX/包名,目标AB包的打包路径进行设置
2.打包脚本
public class Build
{
[MenuItem("Tools/打包")]
public static void BuildAB()
{
if (!Directory.Exists("AssetBundles"))
Directory.CreateDirectory("AssetBundles");
BuildPipeline.BuildAssetBundles("AssetBundles", BuildAssetBundleOptions.None, BuildTarget.StandaloneWindows64);
}
}
- 第一个参数是打包路径,这里的目录是从工程根目录开始计算的,如果路径不存在会报错
- 第二个参数是压缩格式
- BuildAssetBundleOptions.None:默认构建AssetBundle的方式。使用LZMA算法压缩,此算法压缩包小,但是加载时间长,而且使用之前必须要整体解压。解压以后,这个包又会使用LZ4算法重新压缩,之后使用就不用整体解压了,也就是第一次解压很慢,之后就变快了
- BuildAssetBundleOptions.UncompressedAssetBundle:不压缩数据,包大,但是加载很快
- BuildAssetBundleOptions.ChunkBaseCompression:使用LZ4算法压缩,压缩率没有LZMA高,但是加载资源不必整体解压。这种方法中规中矩,比较常用
- BuildAssetBundleOptions.DisableWriteTypeTree:不会在AssetBundle中包含类型信息
- BuildAssetBundleOptions.DeterministicAssetBundle:使用存储在Asset Bundle中的对象的id的哈希构建Asset Bundle
- BuildAssetBundleOptions.ForceRebuildAssetBundle:强制重建Asset Bundles
- BuildAssetBundleOptions.IgnoreTypeTreeChanges:执行增量构建检查时忽略类型树更改
- BuildAssetBundleOptions.AppendHashToAssetBundleName:将哈希附加到assetBundle名称
- BuildAssetBundleOptions.StrictMode:如果在其中报告任何错误,则不允许构建成功
- BuildAssetBundleOptions.DryRunBuild:不实际构建它们
- BuildAssetBundleOptions.DisableLoadAssetByFileName:通过文件名禁用Asset Bundle的加载
- BuildAssetBundleOptions.DisableLoadAssetByFileNameWithExtension:通过带扩展名的文件名禁用Asset Bundle 的加载
- 第三个参数是目标平台,不同平台的AB包不能够通用
3.加载资源
public class ResourceLoadTest : MonoBehaviour
{
void Start()
{
var ab = AssetBundle.LoadFromFile("AssetBundles/test/model.ab");
var go = ab.LoadAsset<GameObject>("Attack");
Instantiate(go);
}
}
首先加载AB包,需要带后缀,目录是以项目根目录开始
然后加载资源,资源名称就是打包时的资源名
加载的只是资源,最终需要实例化
运行之后应该是可以成功加载的
依赖打包
假如多个Prefab资源都依赖了某一个资源
此时如果不对引用资源进行打包,只对两个Prefab进行打包,那么每一个AB包里都会保存一份资源引用
我们直接打包
两个包的总大小是66+66=132kb
很明显我们的材质和贴图资源只需要打包一份就好了,我们可以选择把材质和资源也打入一个AB包中
此时Cube包和Sphere包会自动维持都材质包的引用
对不同资源进行分包
再次打包
很明显,资源只会被打包一次,包体缩小了一半
Mainfest文件
打开上面打包的cube.mainfest
...
...
Assets:
- Assets/GameData/Prefab/Cube.prefab
Dependencies:
- D:/__Project/Unity/AssetModule/AssetBundles/share
- Assets:这个AB包内有哪些资源
- Dependencies:这个AB包依赖于哪一个AB包
当AB包存在依赖时,如果我们直接加载当前AB包,而没有加载依赖包,那么会导致资源丢失
public class ResourceLoadTest : MonoBehaviour
{
void Start()
{
var ab = AssetBundle.LoadFromFile("AssetBundles/cube");
var go = ab.LoadAsset<GameObject>("Cube");
Instantiate(go);
}
}
因为cube包依赖于share包,但我们没有加载share包
public class ResourceLoadTest : MonoBehaviour
{
void Start()
{
var share = AssetBundle.LoadFromFile("AssetBundles/share");
var cube = AssetBundle.LoadFromFile("AssetBundles/cube");
var go = cube.LoadAsset<GameObject>("Cube");
Instantiate(go);
}
}
只要加载过share包,那么我们就可以正常读取资源,加载包的顺序并不重要,只需要保证我们读取资源时,所有需要的包都已经加载
AB包的加载方式
1.AssetBundle.LoadFromMemory(Async)
从内存中加载,有同步和异步两种方式,需要提供AB包的字节码(可以直接来自本地,也可以下载自服务器)
public class ResourceLoadTest : MonoBehaviour
{
IEnumerator Start()
{
var share = AssetBundle.LoadFromMemoryAsync(File.ReadAllBytes("AssetBundles/share"));
yield return share;
var cube = AssetBundle.LoadFromMemoryAsync(File.ReadAllBytes("AssetBundles/cube"));
yield return cube;
Instantiate(cube.assetBundle.LoadAsset<GameObject>("Cube"));
}
}
示例中的字节码直接是从本地读取的,只是提供参考
2.AssetBundle.LoadFromFile(Async)
直接从本地加载,只需要提供路径
3.UnityWebRequest
public class ResourceLoadTest : MonoBehaviour
{
IEnumerator Start()
{
var request = UnityWebRequestAssetBundle.GetAssetBundle("http://localhost:6009/AssetBundles/share");
yield return request.SendWebRequest();
var share = (request.downloadHandler as DownloadHandlerAssetBundle).assetBundle;
request = UnityWebRequestAssetBundle.GetAssetBundle("http://localhost:6009/AssetBundles/cube");
yield return request.SendWebRequest();
var cube = (request.downloadHandler as DownloadHandlerAssetBundle).assetBundle;
Instantiate(cube.LoadAsset<GameObject>("Cube"));
}
}
如果想把下载的内容保存下来也很简单
File.WriteAllBytes("path",request.downloadHandler.data);
依赖包的加载
打包时Unity会自动在目标目录生成一个总的AssetBundle信息包,我们可以加载他获取到所有信息
public class ResourceLoadTest : MonoBehaviour
{
void Start()
{
var mainFestAB = AssetBundle.LoadFromFile("AssetBundles/AssetBundles");
var mainFest = mainFestAB.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
foreach (var bundle in mainFest.GetAllAssetBundles())
{
Debug.Log(bundle);
}
}
}
获取某一个AB包所依赖的包也很简单
public class ResourceLoadTest : MonoBehaviour
{
void Start()
{
var mainFestAB = AssetBundle.LoadFromFile("AssetBundles/AssetBundles");
var mainFest = mainFestAB.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
var deps = mainFest.GetAllDependencies("cube");
foreach (var dep in deps)
{
Debug.Log(dep);
}
}
}
只有一个share
然后加载
public class ResourceLoadTest : MonoBehaviour
{
void Start()
{
var mainFestAB = AssetBundle.LoadFromFile("AssetBundles/AssetBundles");
var mainFest = mainFestAB.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
var deps = mainFest.GetAllDependencies("cube");
foreach (var dep in deps)
AssetBundle.LoadFromFile($"AssetBundles/{dep}");
var cube = AssetBundle.LoadFromFile("AssetBundles/cube");
Instantiate(cube.LoadAsset<GameObject>("Cube"));
}
}
AB包的卸载
当我们加载AB包,并且加载某个资源使用后,AB包与资源是保持一种引用关系的
1.AssetBundle.UnLoad(false)
释放所有没有被使用的资源,如果某个资源正在被使用,那么不会卸载这个资源,并且这个资源与AB包的引用关系会被打断,相当于内存还没释放,但指针置空了,那就再也找不到未释放的内存地址了,无法在释放那一份内存,一般来说不使用这一个API
只能通过Resources.UnloadUnusedAssets释放,但这相当于一次很大的GC,很耗费时间
2.AssetBundle.UnLoad(true)
释放所有资源,不论这个资源是否正在使用
XML序列化
序列化为XML
[System.Serializable]
public class Test
{
public string name;
}
[System.Serializable]
public class TestXML
{
public int id;
public Test test;
public List<int> list=new List<int>();
}
在类前加入[System.Serializable]标签后,类内的public属性都会被序列化
public class ResourceLoadTest : MonoBehaviour
{
void Start()
{
using (var stream = new FileStream("AssetBundles/test.xml", FileMode.OpenOrCreate, FileAccess.ReadWrite))
{
using (var writer = new StreamWriter(stream,Encoding.UTF8))
{
var xmlSerializer = new XmlSerializer(typeof(TestXML));
var instance = new TestXML();
instance.id = 1;
instance.list.Add(2);
instance.list.Add(3);
var test = new Test();
test.name = "999";
instance.test = test;
xmlSerializer.Serialize(writer,instance);
}
}
}
}
序列化结果如下
反序列化也非常简单
public class ResourceLoadTest : MonoBehaviour
{
void Start()
{
using (var stream = new FileStream("AssetBundles/test.xml", FileMode.OpenOrCreate, FileAccess.ReadWrite))
{
var xmlSerializer = new XmlSerializer(typeof(TestXML));
var instance = xmlSerializer.Deserialize(stream) as TestXML;
Debug.Log(instance.id);
Debug.Log(instance.test.name);
}
}
}
序列化为Byte
public class ResourceLoadTest : MonoBehaviour
{
void Start()
{
var instance = new TestXML();
instance.id = 1;
instance.list.Add(2);
instance.list.Add(3);
var test = new Test();
test.name = "999";
instance.test = test;
using (var stream = new FileStream("AssetBundles/test.bytes", FileMode.OpenOrCreate, FileAccess.ReadWrite))
{
var bf = new BinaryFormatter();
bf.Serialize(stream, instance);
}
}
}
然后反序列化
public class ResourceLoadTest : MonoBehaviour
{
void Start()
{
using (var stream = new FileStream("AssetBundles/test.bytes", FileMode.Open, FileAccess.Read, FileShare.Read))
{
var bf = new BinaryFormatter();
var test = bf.Deserialize(stream) as TestXML;
Debug.Log(test.id);
Debug.Log(test.test.name);
}
}
}
对比
如果我们添加10w个数据,然后分别序列化为XML和BYTES
public class ResourceLoadTest : MonoBehaviour
{
void Start()
{
var instance = new TestXML();
instance.list = new List<int>();
// 给list添加10w个数据
for (int i = 0; i < 100000; i++)
instance.list.Add(i);
using (var stream = new FileStream("AssetBundles/test.bytes", FileMode.OpenOrCreate, FileAccess.ReadWrite))
{
var bf = new BinaryFormatter();
bf.Serialize(stream, instance);
}
}
}
很明显bytes的体积小了非常非常多是吧,所以从节省资源角度来说肯定是解析为bytes的
但是bytes完全不可读,这是个问题