[Unity]加载管理

更新

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();
}
  • 简单,粗暴,有效(真的有效)

[Unity]加载管理
https://ksgfk.github.io/2020/06/25/Unity-加载管理/
作者
ksgfk
发布于
2020年6月25日
许可协议