木匣子

Web/Game/Programming/Life etc.

Unity3d 同步/异步加载资源

在游戏中,加载资源可以分为同步加载(sync)与异步加载(async)两种方式。同步加载易于使用,无需特别组织代码结构,加载过程会阻塞线程,好处是之后的代码可以立即使用加载后的资源:

Texture tex = LoadSync("background.png") as Texture;
image.texture = tex;

既然同步加载会阻塞线程,也就表明,如果在游戏过程使用同步加载的方式去加载较大的资源,会导致游戏掉帧,甚至卡顿,影响体验。

这时候就可以使用异步加载的方式来加载资源。异步加载有代码组织上有多种选择,一种是回调的方式:

LoadAsync("super-big-backgroud.png", (Object data) => {
    image.texture = data as Texture;
});

另一种是使用协程:

IEnumerator loadBackground () {
    AsyncOperation loadRequest = LoadAsync("super-big-background.png");
    yield return loadRequest;
    image.texture = loadRequest.bytes as Texture;
}

StartCoroutine (loadBackground());

协程是 Unity 官方的实现方式,好处是比回调方式看起来更“同步”一些。资源在 yield 返回后即可使用,代码比回调的方式容易组织。但是如果你更喜欢回调风格,可以稍微包装一下:

private IEnumerator _load (string path, Action onLoaded) {
    AsyncOperation loadRequest = LoadAsync(path);
    yield return loadRequest;
    onLoaded(loadRequest.bytes);
}

public void load(string path, Action onLoaded) {
    StartCoroutine (_load(path, onLoaded));
}

load(path, (Object data) => {
    image.texture = data as Texture
});

Unity3d

本文基于 unity3d 4.6.x 部分接口在 unity3d 5.x 有变动。

Unity3d 提供了两组接口用于资源加载:Resources 和 AssetBundle 。其中 Resources 用于加载打包后 resources.assets 内的资源,而 AssetBundle 用于加载来自本地文件、网络文件或内存中的 assetBundle 文件。

这意味着 Resources 使用的资源在游戏发布后是不可变动的。而 AssetBundle 可以使用 WWW 类从外部获得新的资源。这也使得对游戏资源进行热更成为可能。所以在项目中使用 AssetBundle 则成为手机网游的标配。

AssetBundle

要从 AssetBundle 中加载资源,首先要先读取 AssetBundle 到内存中,然后再使用 AssetBundle.Load() 或者 AssetBundle.LoadAsync() 来同步或异步加载资源。

但是读取 AssetBundle 到内存中这个过程的同步和异步又是怎么实现的呢?

Async

官方推荐使用 WWW + 协程的方式来异步加载 AssetBundle:

IEnumerator Start () {
    WWW www = new WWW("http://myserver/myBundle.unity3d");
    yield return www;

    // Get the designated main asset and instantiate it.
    Instantiate(www.assetBundle.mainAsset);
}

yield return www; 的时候,会将 www 对象抛给 unity3d 内核进行处理,虽然文档里没有太多内容,但是从接口提供的 WWW.isDone 和 WWW.progress 可以猜测 www 本质上是一个异步操作对象(AsyncOperation)。

官方的这个例子并没有给出如何获取加载进度,这里有个简单的方法,通过另一个协程来管理加载进度即可:

IEnumerator Start () {
    WWW www = new WWW("http://myserver/myBundle.unity3d");
    yield return StartCoroutine(loading(www));

    // Get the designated main asset and instantiate it.
    Instantiate(www.assetBundle.mainAsset);
}

IEnumerator loading(WWW www) {
    while (!www.isDone) {
        Debug.Log (www.progress); // do something with progress
        yield return null;
    }
}

可以把上面的方法封装成一个 Loader,就可以作为通用的 AssetBundler 加载器,并且可以通过事件来获得加载进度和完成情况。

即使是本地文件,即以 file:// 协议开头的资源,也是可以使用上述的异步方式进行加载。

Sync

那么要如何同步加载 AssetBundle 呢?对于远程文件,即 http(s):// 协议访问的文件,要想同步加载,前提是提前下载到本地,如热更新资源,然后通过同步接口读到内存中,最后使用 AssetBundle.CreateFromMemoryImmediate() 接口进行创建:

bytes[] data = File.ReadAllBytes(path)
AssetBundle ab = AssetBundle.CreateFromMemoryImmediate(data);

另外在测试的过程中,我发现如果用 WWW 访问本地资源,而不进行 yield return www; 它也能够同步加载 assetbundle:

WWW www = new WWW("file://" + "/path/to/assetbundle/file");
AssetBundle ab = www.assetBundle; // it works!

经测试,上面这种方法在 iPhone 真机上无法同步加载 assetbundle 。

AssetBundle 生命周期

最后附一张很棒的 AssetBundle 生命周期图:

assetbundle.jpg

参考资料:

全面理解Unity加载和内存管理