Unity Addressable 实战二(实现资源加载和使用)

877 阅读7分钟

  通过上一篇《Unity Addressable 实战一(初步认识Addressable)》,我们初步学习了Addressable的相关使用方法,这一篇我们的目标是利用Addressable的加载、卸载资源机制,实现一个立方体在屏幕上的移动和销毁,从而了解在实际的游戏开发当中,我们是怎么应用Addressable的。

  要实现我们的目标,第一步我们要实现一个资源加载和卸载的管理器。这个“资源加载和卸载的管理器”,下面我们就统称为“资源管理器”吧。这时候可能会有一些小伙伴就有疑问了,Addressable本身就已经有了资源加载和卸载的相关方法,直接调用就好了,为什么还要多此一举呢?要解除这个疑问其实也比较简单。想想我们日常生活中一些相关的例子,例如我们用摇控器打开电视机观看电视这一件事情。如果我们对“用摇控器打开电视”这个功能没什么要求的话,正常使用就已经可以满足我们的需求了。但如果摇控器在小孩子手上,小孩子的自控能力比较差的话,看电视的时间长短和频率次数,对于她的视力健康是有非常大的影响的。这时候我们就必须要对“用摇控器打开电视”这个功能进行一些限制。例如给摇控器增加一个打开电视次数的统计功能,并且达到多少次就不能再打开电视了。但摇控器本身是一个包装好的产品,我们不能破坏它来加上我们的功能,所以更好的办法是,我们再给它包装一层“外壳”来使用它的功能。而这个“外壳”内部的实现就是我们要添加的功能和调用摇控器的功能实现。

  回到我们本篇文章的话题,Addressable就相当于我们的摇控器,"外壳"就相当于我们的资源管理器。所以,后续我们如果需要扩展资源的相关功能就直接在这个“外壳”内部实现就好了。这种设计在真实的游戏开发当中是相当的普遍,它能承载一些游戏的未知业务需求和提高程序的健壮程度以及方便调试或输出log。那么,接下来我们正式进入如何实现一个“资源管理器”的阶段。

  我们首先来整理一下思路,实现资源管理器可能需要三个类,如下:

1. AssetHandle.cs :

  资源结果处理类,负责对加载后的资源进行处理,如提供给外部访问和释放等。

2. AssetOwner.cs :

  资源持有者类,负责对加载资源后的一些处理并返回AssetHandle,如计数需求或缓存结果等。

3. AssetOwnerManager.cs :

  资源持有者的管理类,负责统一处理持有者的创建、销毁等,在真实的游戏开发当中,经常会在切场景的时候需要批量的销毁一些资源,这时候它就排上用场了。

  简单点来说,AssetOwner主要负责取到资源,然后把资源用AssetHandle表示,并返回给使用方。AssetOwnerManager负责管理AssetOwner,单点操作或批量管理。

  到此为止,似乎整个资源管理器的类设计已经完成了。如果我们再往深一层思考,其实还有改进的地方。例如在实际的游戏开发当中,几乎到处都有用到资源的结果处理,所以AssetHandle的使用率是最高的,也是变化最大的。因此我们可以给它一个基类,这样它就变成多态的形式了,这样资源管理器就有了更高的可扩展性。这个基类我们就叫它为 AssetHandleBase.cs 吧。

  接下来,我们进入类的实现阶段。笔者用的是Microsoft Visual Studio Community 2019作为C#代码开发IDE, 读者也可以参考使用。

我们先创建上面提到的类,布局如下图:

image.png

下面,我们来看一看各个类的内部实现。

AssetHandleBase.cs:

using UnityEngine;
using System.Collections;
using UnityEngine.ResourceManagement.AsyncOperations;
using System;
using UnityEngine.AddressableAssets;

namespace Simple.Asset
{
    public abstract class AssetHandleBase
    {
        public int Id;
        public abstract void Release();

    }
}

AssetHandle.cs

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using UnityEngine.ResourceManagement.AsyncOperations;
using System;
using UnityEngine.AddressableAssets;

namespace Simple.Asset
{
    /// <summary>
    /// 资源异步加载结果处理器
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class AssetHandle<T> : AssetHandleBase
    {
        public object Key;
        public T Result;
        public IList<object> Keys;
        public IList<T> Results;
        public AsyncOperationHandle<T> AsyncHandle;
        public AsyncOperationHandle<IList<T>> AsyncHandles;
        private AssetOwner m_owner;
        private bool m_list = false;
        private void Create(int id,  AssetOwner owner)
        {
            Id = id;                             
            m_owner = owner;
            m_owner.AddAssetHandle(this);            
        }
        public AssetHandle(int id,object key, AssetOwner owner, AsyncOperationHandle<T> aoh)
        {
            Create(id, owner);         
            Key = key;
            AsyncHandle = aoh;
            Result = aoh.Result;        
            m_list = false;
        }

        public AssetHandle(int id, object key, AssetOwner owner, AsyncOperationHandle<IList<T>> aoh)
        {
            Create(id, owner);
            Key = key;
            AsyncHandles = aoh;
            Results = aoh.Result;          
            m_list = true;
        }      

        /// <summary>
        /// 释放资源
        /// </summary>
        public override void Release()
        {
            if(!m_list)
                Addressables.Release(AsyncHandle);            
            else
                Addressables.Release(AsyncHandles);
            m_owner.RemovAssetHandle(this);
        }
    
    }
}

AssetOwner.cs:

using UnityEngine;
using System.Collections.Generic;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using System.Collections;
using System;

namespace Simple.Asset
{
    /// <summary>
    /// 资源持有者,提供资源的加载卸载以及实例化功能
    /// </summary>
    public class AssetOwner
    {
        private int m_assetHandleId = 0;
        private readonly List<AssetHandleBase> m_assetHandle = new List<AssetHandleBase>();       

        public AssetOwner()
        {
            AssetOwnerManager.Add(this);
        }

        public void AddAssetHandle(AssetHandleBase ahb)
        {
            m_assetHandle.Add(ahb);
        }
        public void RemovAssetHandle(AssetHandleBase ahb)
        {
            m_assetHandle.Remove(ahb);
        }


        public void Remove()
        {
            AssetOwnerManager.Remove(this);
        }
        #region 加载资源
        
        /// <summary>
        /// 同步加载资源,和Release方法配对使用
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="key"></param>
        /// <returns></returns>
        public AssetHandle<T> LoadAsset<T>(object key)
        {
            //Debug.Log("AssetOwner.LoadAsset " + key);
            AsyncOperationHandle<T> aoh = Addressables.LoadAssetAsync<T>(key);
            aoh.WaitForCompletion();
            m_assetHandleId++;
            AssetHandle<T> ah = new AssetHandle<T>(m_assetHandleId, key, this, aoh);                   
            return ah;
        }

        /// <summary>
        /// 同步加载资源,和Release方法配对使用
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="key"></param>
        /// <param name="completed"></param>
        /// <param name="mode"></param>
        /// <returns></returns>
        public AssetHandle<T> LoadAssets<T>(object key, Action<T> completed)
        {
            AsyncOperationHandle<IList<T>> aoh = Addressables.LoadAssetsAsync(key, completed);          
            aoh.WaitForCompletion();
            m_assetHandleId++;
            AssetHandle<T> ah = new AssetHandle<T>(m_assetHandleId, key, this, aoh);
            return ah;

        }

        /// <summary>
        /// 异步加载资源,和Release方法配对使用
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="key"></param>
        /// <param name="completed"></param>
        /// <returns></returns>
        public AssetHandle<T> LoadAssetAsync<T>(object key, Action<AssetHandle<T>> completed)
        {
            //Debug.Log("AssetOwner.LoadAssetAsync " + key);            
            AsyncOperationHandle<T> aoh = Addressables.LoadAssetAsync<T>(key);
            m_assetHandleId++;
            AssetHandle<T> ah = new AssetHandle<T>(m_assetHandleId, key, this, aoh);
            aoh.Completed += e =>
            {
                //Debug.Log("AssetOwner.LoadAsetAsync2 " + aoh.Status);     
                ah.Result = aoh.Result;
                completed?.Invoke(ah);
            };
            return ah;
        }

        /// <summary>
        /// 异步加载资源,和Release方法配对使用
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="key"></param>
        /// <param name="completed"></param>
        /// <returns></returns>
        public AssetHandle<T> LoadAssetsAsync<T>(object key, Action<T> completed, Action<AssetHandle<T>> allCompleted)
        {
            //Debug.Log("AssetOwner.LoadAssetAsync " + key);
            AsyncOperationHandle<IList<T>> aoh = Addressables.LoadAssetsAsync<T>(key, completed);
            m_assetHandleId++;
            AssetHandle<T> ah = new AssetHandle<T>(m_assetHandleId, key, this, aoh);
            aoh.Completed += e =>
            {
                //Debug.Log("AssetOwner.LoadAsetAsync2 " + aoh.Status);     
                ah.Results = aoh.Result;
                allCompleted?.Invoke(ah);
            };
            return ah;
        }
        #endregion

        #region 实例化资源
        private AssetHandle<GameObject> InstantiateAsync(object key, AsyncOperationHandle<GameObject> aoh, Action<AssetHandle<GameObject>> completed)
        {
            m_assetHandleId++;
            AssetHandle<GameObject> ah = new AssetHandle<GameObject>(m_assetHandleId, key, this, aoh);
            aoh.Completed += e =>
            {
                //Debug.Log("AssetOwner.InstantiateAsync2 " + aoh.Status);   
                ah.Result = aoh.Result;
                completed?.Invoke(ah);
            };
            return ah;
        }

        private AssetHandle<GameObject> Instantiate(object key, AsyncOperationHandle<GameObject> aoh)
        {
            m_assetHandleId++;
            AssetHandle<GameObject> ah = new AssetHandle<GameObject>(m_assetHandleId, key, this, aoh);          
            return ah;
        }

        /// <summary>
        /// 异步实例化资源,和Release方法配对使用
        /// </summary>
        /// <param name="key"></param>
        /// <param name="completed"></param>
        /// <returns></returns>
        public AssetHandle<GameObject> InstantiateAsync(object key, Action<AssetHandle<GameObject>> completed)
        {
            AsyncOperationHandle<GameObject> aoh = Addressables.InstantiateAsync(key);        
            return InstantiateAsync(key, aoh,completed);
        }
        /// <summary>
        /// 异步实例化资源,和Release方法配对使用
        /// </summary>
        /// <param name="key"></param>
        /// <param name="position"></param>
        /// <param name="rotation"></param>
        /// <param name="completed"></param>
        /// <returns></returns>
        public AssetHandle<GameObject> InstantiateAsync(object key, Vector3 position, Quaternion rotation, Action<AssetHandle<GameObject>> completed)
        {
            AsyncOperationHandle<GameObject> aoh = Addressables.InstantiateAsync(key, position, rotation);
            return InstantiateAsync(key,aoh, completed);
        }
        /// <summary>
        /// 异步实例化资源,和Release方法配对使用
        /// </summary>
        /// <param name="key"></param>
        /// <param name="parent"></param>
        /// <param name="completed"></param>
        /// <returns></returns>
        public AssetHandle<GameObject> InstantiateAsync(object key, Transform parent, Action<AssetHandle<GameObject>> completed)
        {
            AsyncOperationHandle<GameObject> aoh = Addressables.InstantiateAsync(key, parent);
            return InstantiateAsync(key, aoh, completed);
        }
        /// <summary>
        /// 异步实例化资源,和Release方法配对使用
        /// </summary>
        /// <param name="key"></param>
        /// <param name="parent"></param>
        /// <param name="position"></param>
        /// <param name="rotation"></param>
        /// <param name="completed"></param>
        /// <returns></returns>
        public AssetHandle<GameObject> InstantiateAsync(object key, Transform parent, Vector3 position, Quaternion rotation, Action<AssetHandle<GameObject>> completed)
        {
            AsyncOperationHandle<GameObject> aoh = Addressables.InstantiateAsync(key, position, rotation, parent);
            return InstantiateAsync(key, aoh, completed);
        }      
        /// <summary>
        /// 同步实例化资源,和Release方法配对使用
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public AssetHandle<GameObject> Instantiate(object key)
        {
            AsyncOperationHandle<GameObject> aoh = Addressables.InstantiateAsync(key);
            aoh.WaitForCompletion();
            return Instantiate(key, aoh);
        }
        /// <summary>
        /// 同步实例化资源,和Release方法配对使用
        /// </summary>
        /// <param name="key"></param>
        /// <param name="position"></param>
        /// <param name="rotation"></param>
        /// <param name="completed"></param>
        /// <returns></returns>
        public AssetHandle<GameObject> Instantiate(object key, Vector3 position, Quaternion rotation)
        {
            AsyncOperationHandle<GameObject> aoh = Addressables.InstantiateAsync(key, position, rotation);
            aoh.WaitForCompletion();
            return Instantiate(key, aoh);
        }
        /// <summary>
        /// 同步实例化资源,和Release方法配对使用
        /// </summary>
        /// <param name="key"></param>
        /// <param name="parent"></param>
        /// <returns></returns>
        public AssetHandle<GameObject> Instantiate(object key, Transform parent)
        {
            AsyncOperationHandle<GameObject> aoh = Addressables.InstantiateAsync(key, parent);
            aoh.WaitForCompletion();
            return Instantiate(key, aoh);
        }        
        /// <summary>
        /// 同步实例化资源,和Release方法配对使用
        /// </summary>
        /// <param name="key"></param>
        /// <param name="parent"></param>
        /// <param name="position"></param>
        /// <param name="rotation"></param>
        /// <param name="completed"></param>
        /// <returns></returns>
        public AssetHandle<GameObject> Instantiate(object key, Transform parent, Vector3 position, Quaternion rotation)
        {
            AsyncOperationHandle<GameObject> aoh = Addressables.InstantiateAsync(key, position, rotation, parent);
            aoh.WaitForCompletion();
            return Instantiate(key, aoh);
        }

        #endregion

        #region 释放资源
        /// <summary>
        /// 释放资源
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="handleId"></param>
        public void Release<T>(int handleId)
        {
            for(int i = 0; i < m_assetHandle.Count; i++)
            {
                AssetHandleBase ahb = m_assetHandle[i];
                if(handleId == ahb.Id)
                {
                    ahb.Release();
                    break;
                }
            }         
        }
        /// <summary>
        /// 释放资源
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="ah"></param>
        public void Release<T>(AssetHandle<T> ah)
        {
            ah.Release();                   
        }
        /// <summary>
        /// 释放所有资源
        /// </summary>
        public void ReleaseAll()
        {      
            for(int i = m_assetHandle.Count - 1; i >= 0; i--)
            {               
                m_assetHandle[i].Release();

            }        
            m_assetHandle.Clear();
        }
       

        #endregion
    }

}

AssetOwnerManager.cs:

using UnityEngine;
using System.Collections.Generic;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using System.Collections;
using System;

namespace Simple.Asset
{
    /// <summary>
    /// 资源持有者管理器,提供统一的资源控制管理功能
    /// </summary>
    public class AssetOwnerManager
    {        
        private static readonly List<AssetOwner> m_aos = new List<AssetOwner>();        

        public static AssetOwner Create()
        {
            AssetOwner ao = new AssetOwner();
            return ao;
        }

        public static void Add(AssetOwner ao)
        {
            m_aos.Add(ao);
        }

        public static void Remove(AssetOwner ao)
        {
            for(int i = m_aos.Count - 1; i >= 0; i--)
            {
                AssetOwner indexAo = m_aos[i];
                if(indexAo == ao)
                {
                    indexAo.ReleaseAll();
                    m_aos.RemoveAt(i);
                    break;
                }    
            }
        }
        
        public static void Clear()
        {
            for(int i = 0; i < m_aos.Count; i++)
            {
                AssetOwner indexAo = m_aos[i];
                indexAo.ReleaseAll();
            }
            m_aos.Clear();
        }
    }

}

至此,我们用代码实现了一个叫做“资源管理器”的东西,要用上它我们还要利用上一篇文章《Unity Addressable 实战一(初步认识Addressable)》学习到的知识,往Addressable里面添加资源,然后我们用它加载出来。

我们制作一个立方体Prefab,如下图模样:

image.png

把立方体Prefab添加到Addressable里面,如下图:

image.png

接着,我们新建一个C#脚本 AddressableTest.cs,如下图:

image.png

AddressableTest.cs 内容如下:

using System.Collections;
using UnityEngine;

namespace Assets.Scripts
{
    public class AddressableTest : MonoBehaviour
    {

        // Use this for initialization
        void Start()
        {

        }

        // Update is called once per frame
        void Update()
        {

        }
    }
}

接着我们在脚本中先声明一个AssetOwner 资源持有者,然后利用它来加载我们刚才创建的立方体Prefab, 结果代码如下:

using System.Collections;
using UnityEngine;
using Simple.Asset;
namespace Assets.Scripts
{
    public class AddressableTest : MonoBehaviour
    {

        private AssetOwner _ao = new AssetOwner();
        // Use this for initialization
        void Start()
        {
            var ah = _ao.Instantiate("Cube");
            
        }

        // Update is called once per frame
        void Update()
        {

        }
    }
}

接下来我们将AdressableTest绑定到场景中的摄像机上,如下图:

image.png

点击“运行”按钮,可以看到立方体已经加截出来了,如下图:

image.png

接下来我们让立方体移动一段距离后,然后卸载它,让它消失,最终代码如下:

using System.Collections;
using UnityEngine;
using Simple.Asset;
namespace Assets.Scripts
{
    public class AddressableTest : MonoBehaviour
    {
        
        private AssetOwner _ao = new AssetOwner();//资源管理中持有者
        private Vector3 _target;                  //要移动到的目标点
        private AssetHandle<GameObject> _ah;      //得到的资源结果处理者
        private bool _run;                        
        
        // Use this for initialization
        void Start()
        {
             _ah = _ao.Instantiate("Cube");       //加载立方体资源
            _target = new Vector3(3, 3, 3);       //要移动到点
            
        }

        // Update is called once per frame
        void Update()
        {
            if(Input.GetKeyDown(KeyCode.R))
            {
                _run = true;

            }
            if(_run)
            {
                _ah.Result.transform.position += Vector3.one * Time.deltaTime;  //开始移动
                if(_ah.Result.transform.position.x > _target.x)        
                { //到达目标点后销毁资源
                    _ah.Release();
                    _run = false;
                }
            }
        }
    }
}

再点击运行按钮,按下键盘“R”键后,我们会在场景视图中看到立方体慢慢的移动一段距离后消失。

  至此,我们实现了一个“资源管理器”,并利用它实现了资源的加载和卸载。通过这一篇文章,我们对Addressable的了解和使用又加深了一步,下一篇我们将会探讨一下游戏开发当中的资源管理和打包策略,敬请期待。