更新
2020.9.13更新 有效个jb,直接用async/awake不比手写状态机香?
为什么需要加载管理
为什么会要写加载管理(LoadManager)
使用Unity提供的Addressable包来管理资产时时,发现所有加载操作都是异步方法,而实际操作中为了等待资源加载完毕还要编写额外代码,很不舒服。
异步加载的完成时间不确定,且某些资源和资源间存在依赖关系,会导致无法引用目标资源,抛出异常。
加载管理者需要提供的功能:加载请求,分批加载资源,单个资源完成时的回调,同一批次资源完成时的回调。
实现
状态枚举
我们选择内置一个状态机来描述LoadManager的当前状态。
1 2 3 4 5 6 7 public enum LoadState { Sleep, Ready, Working, Finish }
资源接口
1 2 3 4 5 6 public interface IAsyncHandleWrapper { bool IsDone { get ; } void OnComplete () ; }
请求队列
1 2 3 4 5 6 7 public class LoadManager : MonoBehaviour { [SerializeField ] private LoadState nowState; private Queue<IAsyncHandleWrapper> _requestQueue; public LoadState NowState => nowState; }
请求方法
在开始添加加载请求前,需要显式调用Ready方法,因为需要确定当前状态是否可以进行下一批次的加载
1 2 3 4 5 6 public void Ready () { if (nowState != LoadState.Sleep) throw new InvalidOperationException("休眠状态才能准备加载" ); _requestQueue = new Queue<IAsyncHandleWrapper>(); nowState = LoadState.Ready; }
每批次加载完后,会释放掉队列的实例,所以调用Ready时才会又实例化一个,大概能省点内存?
1 2 3 4 5 public void Request (IAsyncHandleWrapper wrapper ) { if (nowState != LoadState.Ready) throw new InvalidOperationException("准备状态才能添加请求" ); _requestQueue.Enqueue(wrapper); }
其实就是将请求插入队列中
在发布所有请求后,便可以开始工作了。先来编写工作时的方法。每帧检查一下队列头部的请求是否完成。个人不喜欢写Update,因为LoadManager并不是整个游戏过程中都需要Update,所以这里用协程代替。顺便发布一个当前批次完成后的事件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 private Coroutine _workingCoroutine;private Action _completeCallback;public event Action Complete { add { if (nowState != LoadState.Ready) throw new InvalidOperationException("准备状态才能添加回调" ); _completeCallback += value ; } remove { if (nowState != LoadState.Ready) throw new InvalidOperationException("准备状态才能删除回调" ); _completeCallback = (Action) Delegate.Remove(_completeCallback, value ); } }private IEnumerator QueueAsyncOpHandles () { while (_requestQueue.Count > 0 ) { var head = _requestQueue.Peek(); if (head.IsDone) { head.OnComplete(); _requestQueue.Dequeue(); } yield return null ; } nowState = LoadState.Finish; _completeCallback?.Invoke(); _completeCallback = null ; _workingCoroutine = null ; _requestQueue = null ; nowState = LoadState.Sleep; }
编写开始工作方法
1 2 3 4 5 6 public void Work () { if (nowState != LoadState.Ready) throw new InvalidOperationException("准备状态才能开始工作" ); nowState = LoadState.Working; _workingCoroutine = StartCoroutine(QueueAsyncOpHandles()); }
资源接口实现
现在,我们的LoadManager已经可以工作了。来试试请求Addressable的资源。
包装Addressable的异步请求 1 2 3 4 5 6 7 8 9 10 public class AddrAsyncWrapper : IAsyncHandleWrapper { public AsyncOperationHandle Handle { get ; } public bool IsDone => Handle.IsDone; public AddrAsyncWrapper (AsyncOperationHandle handle ) { Handle = handle; } public void OnComplete () { } }
使用扩展方法为LoadManager添加方法 1 2 3 4 5 6 public static void Request <T >(this LoadManager load, string addr, Action<T> callback ) where T : UnityEngine.Object { var handle = Addressables.LoadAssetAsync<T>(addr); handle.Completed += handleCallback => { callback?.Invoke(handleCallback.Result); }; load.Request(new AddrAsyncWrapper(handle)); }
例子
例子 1 2 3 4 5 6 7 8 9 10 11 12 13 private List<GameObject> _skins;private LoadManager _load;private void Awake () { _load = new LoadManager(); _skins = new List<GameObject>(); _load.Ready(); _load.Complete += () => _load.ForEach(skin => UnityEngine.Object.Instantiate(skin)); _load.Request<GameObject>("steve" , steve => _skins.Add(steve)); _load.Request<GameObject>("alex" , alex => _skins.Add(alex)); _load.Work(); }