Prefab实例化
我们都知道Prefab是一种存储格式,加载后是一个asset格式文件,需要实例化后才是一个可用的GameObject。
而如果每次我们都去加载asset,然后实例化,然后代码管理每一个GameObject的内存,这当然不是我们想要的。那我们想要的是什么?
我想要的是——自动化管理
提供一个接口,加载到指定GameObject
不用关心销毁,内存管理,实例克隆等操作
可以实现同步加载,异步加载和手动销毁
Unity自身肯定不包含上述功能,需要我们自己实现框架
自动销毁核心——ObjInfo
我们先不关心管理器内部逻辑,先关心如何实现自动销毁。
我们都知道Awake和OnDestroy函数,顾名思义,创建和销毁的时候调用,那么我们很简单地构造一个MonoBehaviour,实现如下
using UnityEngine; public class ObjInfo : MonoBehaviour { void OnDestroy() { //被动销毁,保证引用计数正确 PrefabLoadMgr.I.Destroy(this.gameObject); } }
我们可以对每个通过Prefab加载管理器创建的GameObject添加一个ObjInfo,这个GameObject在销毁的时候,会调用ObjInfo的OnDestroy方法,会通知管理器,销毁自身,,这样就实现了自动销毁
如果GameObject被克隆了呢,就会造成引用计数出错,所以还要修正引用计数的正确性,如下
public int InstanceId = -1; public string AssetName = string.Empty; void Awake() { if (string.IsNullOrEmpty(AssetName)) return; //非空,说明通过克隆实例化,添加引用计数 InstanceId = gameObject.GetInstanceID(); PrefabLoadMgr.I.AddAssetRef(AssetName, this.gameObject); }
我们很开心地以为可以了,但Unity并没有按我们理想化的方式运行。
实例化——必须的active
Unity提供的Awake和OnDestroy函数,必须在GameObject节点被active的情况下,才会触发运行,也就是说新加载的节点GameObject, 必须挂在启用的GameObject节点下。
那这样我们就 先将节点创建在一个通用节点_assetParent.transform下,然后再把它移到目标节点上,我们就得到了如下代码:
private GameObject InstanceAsset(PrefabObject _prefabObj, Transform _parent) { GameObject go = GameObject.Instantiate(_prefabObj._asset, _assetParent.transform;) as GameObject; go.name = go.name.Replace("(Clone)", ""); ObjInfo obgInfo = go.AddComponent<ObjInfo>(); if(!go.activeSelf) {//保证GameObject active一次,ObjInfo才能触发Awake,未Awake的脚本不能触发OnDestroy go.SetActive(true); go.SetActive(false); } if (obgInfo != null) { obgInfo.InstanceId = go.GetInstanceID(); obgInfo.AssetName = _prefabObj._assetName; } if (_parent != null) go.transform.SetParent(_parent); return go; }
这样终于实现了自动销毁,不过笔者测试发现,go.transform.SetParent是一个非常耗时的操作,而我们大部分情况下,挂载的父节点都是active的,所以我们优化成可以挂载在目标节点下,直接挂载,不能再挂载在公用节点上,具体代码见下文。
Prefab加载管理器
加载管理器,在前面几篇描述了很多了,关键词是——加载单元,队列,外部接口。这一部分也不例外,只不过简单了很多。
加载单元
加载单元的数据结构
public class PrefabObject { public string _assetName; public int _lockCallbackCount; //记录回调当前数量,保证异步是下一帧回调 public List<PrefabLoadCallback> _callbackList = new List<PrefabLoadCallback>(); public List<Transform> _callParentList = new List<Transform>(); public UnityEngine.Object _asset; public int _refCount; public HashSet<int> _goInstanceIDSet = new HashSet<int>(); //实例化的GameObject引用列表 }
这里回调_callParentList
需要保存目标节点,是为了性能考虑。_goInstanceIDSet
用于记录当前已经创建和使用的节点。
队列
private Dictionary<string, PrefabObject> _loadedList; private List<PrefabObject> _loadedAsyncList; //异步加载,延迟回调 private Dictionary<int, PrefabObject> _goInstanceIDList; //创建的实例对应的asset
这边相对于上一篇文章,简化了很多不必要的队列,只保留了加载队列
和异步加载队列
。实现的内容与上一篇文章大同小异,就不重复阐述了,具体看代码。
这里增加了实例的hash保存,方便查找。
异步加载
异步加载代码如下
public void LoadAsync(string _assetName, PrefabLoadCallback _callFun, Transform _parent = null) { PrefabObject prefabObj = null; if (_loadedList.ContainsKey(_assetName)) { prefabObj = _loadedList[_assetName]; prefabObj._callbackList.Add(_callFun); prefabObj._callParentList.Add(_parent); prefabObj._refCount++; if(prefabObj._asset != null) _loadedAsyncList.Add(prefabObj); return; } prefabObj = new PrefabObject(); prefabObj._assetName = _assetName; prefabObj._callbackList.Add(_callFun); prefabObj._callParentList.Add(_parent); prefabObj._refCount = 1; _loadedList.Add(_assetName, prefabObj); AssetsLoadMgr.I.LoadAsync(_assetName, (string name, UnityEngine.Object obj) => { prefabObj._asset = obj; prefabObj._lockCallbackCount = prefabObj._callbackList.Count; DoInstanceAssetCallback(prefabObj); } ); }
逻辑比较简单,讲几个关键点
1. 代码LoadAsync(string _assetName, PrefabLoadCallback _callFun, Transform _parent = null)这里需要三个参数,第三个也就是GameObject需要挂载的父节点,是为了加载性能。当然,如果不传_parent,也可以在回调中设置父节点。
2. _loadedAsyncList当且仅当prefabObj._asset已经加载完成,需要异步回调才会启动,如果读者能够接受异步回调直接回调,可以去掉相关代码,直接回调
3. 引用计数与上文不同的是,有请求就增加,销毁就减少,是这边简化了逻辑,不需要大量队列操作,没必要提取到使用时
4. asset的加载依赖AssetsLoadMgr.I.LoadAsync,回调后,会将所有请求回调,具体看DoInstanceAssetCallback。
回调逻辑
private void DoInstanceAssetCallback(PrefabObject _prefabObj) { if (_prefabObj._callbackList.Count == 0) return; //先将回掉提取保存,再回调,保证回调中加载和销毁不出错 int count = _prefabObj._lockCallbackCount; var callbackList = _prefabObj._callbackList.GetRange(0, count); var callParentList = _prefabObj._callParentList.GetRange(0, count); _prefabObj._lockCallbackCount = 0; _prefabObj._callbackList.RemoveRange(0, count); _prefabObj._callParentList.RemoveRange(0, count); for (int i = 0; i < count; i++) { if (callbackList[i] != null) { GameObject newObj = InstanceAsset(_prefabObj, callParentList[i]);//prefab需要实例化 try { callbackList[i](_prefabObj._assetName, newObj); } catch (System.Exception e) { Utils.LogError(e); } //如果回调之后,节点挂在默认节点下,认为该节点无效,销毁 if (newObj.transform.parent == _assetParent.transform) Destroy(newObj); } } }
这里面向大量外部加载代码,所以要考虑稳健性,做了很多极端考虑
1.回调前,先提取所有回调函数,保证回调中调用加载和销毁操作队列不出错(划重点)
2.回调时,使用try...catch...保证正常运行不出错
3.回调后,要考虑GameObject节点不使用后,回收节点问题
销毁
销毁分2种,销毁GameObject和销毁CallBack
public void Destroy(GameObject _obj) { if (_obj == null) return; int instanceID = _obj.GetInstanceID(); if (!_goInstanceIDList.ContainsKey(instanceID)) {//非从本类创建的资源,直接销毁即可 UnityEngine.Object.Destroy(_obj); return; } var prefabObj = _goInstanceIDList[instanceID]; if (prefabObj._goInstanceIDSet.Contains(instanceID)) {//实例化的GameObject prefabObj._refCount--; prefabObj._goInstanceIDSet.Remove(instanceID); _goInstanceIDList.Remove(instanceID); UnityEngine.Object.Destroy(_obj); } if (prefabObj._refCount == 0) { _loadedList.Remove(prefabObj._assetName); AssetsLoadMgr.I.Unload(prefabObj._asset); prefabObj._asset = null; } }
销毁GameObject,就是简单地判断和减引用计数,比较简单。注意,这边能够直接通过引用计数销毁,是因为引用计数是在创建的时候就加上的。
再来看看销毁CallBack
public void RemoveCallBack(string _assetName, PrefabLoadCallback _callFun) { if (_callFun == null) return; PrefabObject prefabObj = null; if (_loadedList.ContainsKey(_assetName)) prefabObj = _loadedList[_assetName]; if (prefabObj != null) { int index = prefabObj._callbackList.IndexOf(_callFun); if (index >= 0) { prefabObj._refCount--; prefabObj._callbackList.RemoveAt(index); prefabObj._callParentList.RemoveAt(index); if (index < prefabObj._lockCallbackCount) {//说明是加载回调过程中解绑回调,需要降低lock个数 prefabObj._lockCallbackCount--; } } if (prefabObj._refCount == 0) { _loadedList.Remove(prefabObj._assetName); AssetsLoadMgr.I.Unload(prefabObj._asset); prefabObj._asset = null; } } }
代码简单,关注一下
if (index < prefabObj._lockCallbackCount) {//说明是加载回调过程中解绑回调,需要降低lock个数 prefabObj._lockCallbackCount--; }
这里考虑到,加载结束回调可能导致销毁CallBack的情况(比较极端啦),很可能影响原先回调队列的稳定,所以要保证批量回调的队列操作正确。
增加引用计数
public void AddAssetRef(string _assetName, GameObject _gameObject) { if (!_loadedList.ContainsKey(_assetName)) return; PrefabObject prefabObj = _loadedList[_assetName]; int instanceID = _gameObject.GetInstanceID(); if(_goInstanceIDList.ContainsKey(instanceID)) { string errormsg = string.Format("PrefabLoadMgr AddAssetRef error ! assetName:{0}", _assetName); Utils.LogError(errormsg); return; } prefabObj._refCount++; prefabObj._goInstanceIDSet.Add(instanceID); _goInstanceIDList.Add(instanceID, prefabObj); }
用于外部克隆导致Prefab引用计数增加的情况。外部克隆,一般用于UI列表类节点,大量重复特效等,还是比较常见的。
同步回调
讲完异步,还是必须要有同步的。
public GameObject LoadSync(string _assetName, Transform _parent = null) { PrefabObject prefabObj = null; if (_loadedList.ContainsKey(_assetName)) { prefabObj = _loadedList[_assetName]; prefabObj._refCount++; if (prefabObj._asset == null) {//说明在异步加载中,需要不影响异步加载,加载后要释放 prefabObj._asset = AssetsLoadMgr.I.LoadSync(_assetName); var newGo = InstanceAsset(prefabObj, _parent); AssetsLoadMgr.I.Unload(prefabObj._asset); prefabObj._asset = null; return newGo; } else return InstanceAsset(prefabObj, _parent); } prefabObj = new PrefabObject(); prefabObj._assetName = _assetName; prefabObj._refCount = 1; prefabObj._asset = AssetsLoadMgr.I.LoadSync(_assetName); _loadedList.Add(_assetName, prefabObj); return InstanceAsset(prefabObj, _parent); }
同步加载遇上异步加载,就要考虑未加载,正在加载,已加载。未加载和已加载都好处理,正在加载才难。
if (prefabObj._asset == null) {//说明在异步加载中,需要不影响异步加载,加载后要释放 prefabObj._asset = AssetsLoadMgr.I.LoadSync(_assetName); var newGo = InstanceAsset(prefabObj, _parent); AssetsLoadMgr.I.Unload(prefabObj._asset); prefabObj._asset = null; return newGo; }
这里用了先加载,后销毁的方式,不影响异步加载的方式获得需要的GameObject,这种方式依托于AssetsLoadMgr的内部实现。AssetsLoadMgr内部实现了同步异步不冲突的处理方式,具体参见上一篇文章。
整合代码
好啦,让我们看看完整代码吧
using UnityEngine; public class ObjInfo : MonoBehaviour { public int InstanceId = -1; public string AssetName = string.Empty; void Awake() { if (string.IsNullOrEmpty(AssetName)) return; //非空,说明通过克隆实例化,添加引用计数 InstanceId = gameObject.GetInstanceID(); PrefabLoadMgr.I.AddAssetRef(AssetName, this.gameObject); } void OnDestroy() { //被动销毁,保证引用计数正确 PrefabLoadMgr.I.Destroy(this.gameObject); } }
using System.Collections.Generic; using UnityEngine; public class PrefabLoadMgr { private static PrefabLoadMgr _instance = null; public static PrefabLoadMgr I { get { if (_instance == null) _instance = new PrefabLoadMgr(); return _instance; } } public delegate void PrefabLoadCallback(string name, GameObject obj); public class PrefabObject { public string _assetName; public int _lockCallbackCount; //记录回调当前数量,保证异步是下一帧回调 public List<PrefabLoadCallback> _callbackList = new List<PrefabLoadCallback>(); public List<Transform> _callParentList = new List<Transform>(); public UnityEngine.Object _asset; public int _refCount; public HashSet<int> _goInstanceIDSet = new HashSet<int>(); //实例化的GameObject引用列表 } private Dictionary<string, PrefabObject> _loadedList; private List<PrefabObject> _loadedAsyncList; //异步加载,延迟回调 private Dictionary<int, PrefabObject> _goInstanceIDList; //创建的实例对应的asset private GameObject _assetParent; private PrefabLoadMgr() { _loadedList = new Dictionary<string, PrefabObject>(); _loadedAsyncList = new List<PrefabObject>(); _goInstanceIDList = new Dictionary<int, PrefabObject>(); #if UNITY_EDITOR if (UnityEditor.EditorApplication.isPlaying) { _assetParent = new GameObject("AssetsList"); GameObject.DontDestroyOnLoad(_assetParent); } #else _assetParent = new GameObject("AssetsList"); GameObject.DontDestroyOnLoad(_assetParent); #endif } private GameObject InstanceAsset(PrefabObject _prefabObj, Transform _parent) { Transform tempParent = _parent; if (_parent == null || _parent.gameObject == null || !_parent.gameObject.activeInHierarchy) tempParent = _assetParent.transform; GameObject go = GameObject.Instantiate(_prefabObj._asset, tempParent) as GameObject; go.name = go.name.Replace("(Clone)", ""); int instanceID = go.GetInstanceID(); ObjInfo obgInfo = go.AddComponent<ObjInfo>(); if(!go.activeSelf) {//保证GameObject active一次,ObjInfo才能触发Awake,未Awake的脚本不能触发OnDestroy go.SetActive(true); go.SetActive(false); } if (obgInfo != null) { obgInfo.InstanceId = instanceID; obgInfo.AssetName = _prefabObj._assetName; } _prefabObj._goInstanceIDSet.Add(instanceID); _goInstanceIDList.Add(instanceID, _prefabObj); if (_parent != null) go.transform.SetParent(_parent); return go; } private void DoInstanceAssetCallback(PrefabObject _prefabObj) { if (_prefabObj._callbackList.Count == 0) return; //先将回掉提取保存,再回调,保证回调中加载和销毁不出错 int count = _prefabObj._lockCallbackCount; var callbackList = _prefabObj._callbackList.GetRange(0, count); var callParentList = _prefabObj._callParentList.GetRange(0, count); _prefabObj._lockCallbackCount = 0; _prefabObj._callbackList.RemoveRange(0, count); _prefabObj._callParentList.RemoveRange(0, count); for (int i = 0; i < count; i++) { if (callbackList[i] != null) { GameObject newObj = InstanceAsset(_prefabObj, callParentList[i]);//prefab需要实例化 try { callbackList[i](_prefabObj._assetName, newObj); } catch (System.Exception e) { Utils.LogError(e); } //如果回调之后,节点挂在默认节点下,认为该节点无效,销毁 if (newObj.transform.parent == _assetParent.transform) Destroy(newObj); } } } public GameObject LoadSync(string _assetName, Transform _parent = null) { PrefabObject prefabObj = null; if (_loadedList.ContainsKey(_assetName)) { prefabObj = _loadedList[_assetName]; prefabObj._refCount++; if (prefabObj._asset == null) {//说明在异步加载中,需要不影响异步加载,加载后要释放 prefabObj._asset = AssetsLoadMgr.I.LoadSync(_assetName); var newGo = InstanceAsset(prefabObj, _parent); AssetsLoadMgr.I.Unload(prefabObj._asset); prefabObj._asset = null; return newGo; } else return InstanceAsset(prefabObj, _parent); } prefabObj = new PrefabObject(); prefabObj._assetName = _assetName; prefabObj._refCount = 1; prefabObj._asset = AssetsLoadMgr.I.LoadSync(_assetName); _loadedList.Add(_assetName, prefabObj); return InstanceAsset(prefabObj, _parent); } public void LoadAsync(string _assetName, PrefabLoadCallback _callFun, Transform _parent = null) { PrefabObject prefabObj = null; if (_loadedList.ContainsKey(_assetName)) { prefabObj = _loadedList[_assetName]; prefabObj._callbackList.Add(_callFun); prefabObj._callParentList.Add(_parent); prefabObj._refCount++; if(prefabObj._asset != null) _loadedAsyncList.Add(prefabObj); return; } prefabObj = new PrefabObject(); prefabObj._assetName = _assetName; prefabObj._callbackList.Add(_callFun); prefabObj._callParentList.Add(_parent); prefabObj._refCount = 1; _loadedList.Add(_assetName, prefabObj); AssetsLoadMgr.I.LoadAsync(_assetName, (string name, UnityEngine.Object obj) => { prefabObj._asset = obj; prefabObj._lockCallbackCount = prefabObj._callbackList.Count; DoInstanceAssetCallback(prefabObj); } ); } public void Destroy(GameObject _obj) { if (_obj == null) return; int instanceID = _obj.GetInstanceID(); if (!_goInstanceIDList.ContainsKey(instanceID)) {//非从本类创建的资源,直接销毁即可 if (_obj is GameObject) UnityEngine.Object.Destroy(_obj); #if UNITY_EDITOR else if (UnityEditor.EditorApplication.isPlaying) { Utils.LogError("PrefabLoadMgr destroy NoGameObject name=" + _obj.name + " type=" + _obj.GetType().Name); } #else else Utils.LogError("PrefabLoadMgr destroy NoGameObject name=" + _obj.name + " type=" + _obj.GetType().Name); #endif return; } var prefabObj = _goInstanceIDList[instanceID]; if (prefabObj._goInstanceIDSet.Contains(instanceID)) {//实例化的GameObject prefabObj._refCount--; prefabObj._goInstanceIDSet.Remove(instanceID); _goInstanceIDList.Remove(instanceID); UnityEngine.Object.Destroy(_obj); } else {//error string errormsg = string.Format("PrefabLoadMgr Destroy error ! assetName:{0}", prefabObj._assetName); Utils.LogError(errormsg); return; } if (prefabObj._refCount < 0) { string errormsg = string.Format("PrefabLoadMgr Destroy refCount error ! assetName:{0}", prefabObj._assetName); Utils.LogError(errormsg); return; } if (prefabObj._refCount == 0) { _loadedList.Remove(prefabObj._assetName); AssetsLoadMgr.I.Unload(prefabObj._asset); prefabObj._asset = null; } } //用于解绑回调 public void RemoveCallBack(string _assetName, PrefabLoadCallback _callFun) { if (_callFun == null) return; PrefabObject prefabObj = null; if (_loadedList.ContainsKey(_assetName)) prefabObj = _loadedList[_assetName]; if (prefabObj != null) { int index = prefabObj._callbackList.IndexOf(_callFun); if (index >= 0) { prefabObj._refCount--; prefabObj._callbackList.RemoveAt(index); prefabObj._callParentList.RemoveAt(index); if (index < prefabObj._lockCallbackCount) {//说明是加载回调过程中解绑回调,需要降低lock个数 prefabObj._lockCallbackCount--; } } if (prefabObj._refCount < 0) { string errormsg = string.Format("PrefabLoadMgr Destroy refCount error ! assetName:{0}", prefabObj._assetName); Utils.LogError(errormsg); return; } if (prefabObj._refCount == 0) { _loadedList.Remove(prefabObj._assetName); AssetsLoadMgr.I.Unload(prefabObj._asset); prefabObj._asset = null; } } } // 用于外部实例化,增加引用计数 public void AddAssetRef(string _assetName, GameObject _gameObject) { if (!_loadedList.ContainsKey(_assetName)) return; PrefabObject prefabObj = _loadedList[_assetName]; int instanceID = _gameObject.GetInstanceID(); if(_goInstanceIDList.ContainsKey(instanceID)) { string errormsg = string.Format("PrefabLoadMgr AddAssetRef error ! assetName:{0}", _assetName); Utils.LogError(errormsg); return; } prefabObj._refCount++; prefabObj._goInstanceIDSet.Add(instanceID); _goInstanceIDList.Add(instanceID, prefabObj); } private void UpdateLoadedAsync() { if (_loadedAsyncList.Count == 0) return; int count = _loadedAsyncList.Count; for (int i = 0; i < count; i++) { _loadedAsyncList[i]._lockCallbackCount = _loadedAsyncList[i]._callbackList.Count; } for (int i = 0; i < count; i++) { DoInstanceAssetCallback(_loadedAsyncList[i]); } _loadedAsyncList.RemoveRange(0, count); } public void Update() { UpdateLoadedAsync(); } }
00