关于游戏架构设计(一)

架构设计目录一

    • 游戏资源管理
      • ResourceUnit
      • ResourceCommon
      • Request
      • ResourceAsyncOperation
      • ResourcesManager
      • LoadUiResource
    • 自定义消息分发
      • 事件分类
      • 保存事件
      • 模版类
      • 事件监听
      • 事件移除
      • 事件分发
    • 对象池
      • 对象池类型
      • 对象信息
      • 获取对象
      • 显示对象
      • 释放对象
      • 更新缓存时间

游戏资源管理

对于一个游戏来说,游戏中资源的数量、大小是必须要去考虑的问题,因为一个游戏品质的好坏都是通过美术资源来表现的。所以管理这些资源的工作,作为开发者的我们是必须要好好考量的。对于游戏资源管理,一般的做法是封装几个接口来用于资源的加载,假如说只是做个 Demo,这样做是没有问题的,但是如果是做产品,其中资源的量是非常大的,如果资源的加载不恰当,会出现各种问题。而且游戏开发讲究的是团队协作,不同的人会有不同的需求,简单的封装几个接口不一定能满足需求,如果没有一个统一的资源架构管理,代码会出现各种接口版本,最后会出现大量的冗余代码,这样对游戏产品运行效率会产生影响。

另外,还要考虑游戏资源的动态更新,主要是为了减少游戏包体的大小。Unity 为用户提供了 AssetBundle 的方式打包资源,方便用户将资源打包后上传到资源服务器,在游戏启动时会通过本地存放资源的 MD5 码的文本文件与服务器上保存资源最新的 MD5 码的文本文件作对比,根据其资源对应的 MD5 码的不同,将最新的资源下载到本地使用,将资源文件代替本地的资源文件,同时更新MD5 码的文本文件,。我们在封装资源管理类时,也要从产品的角度考虑资源管理的问题。

下面开始介绍一下如何进行资源管理的代码封装,需要大家对资源管理的封装有一个比较完善的思考,代码模块如下图所示:关于游戏架构设计(一)_第1张图片
为什么这么设计呢?我们在游戏开发的时候,对于 Unity 的资源来说,每个资源都是一个 GameObject,但是单独的 GameObject 显然不能满足需求,因为资源既可以是 Prefab,也可以是 Scene,还可以是 Asset 文件。这当中就会涉及到不同的资源类型,那么如何表示这些资源类型呢?假如测试的时候是使用 prefab,但在正式发布时采用的是 asset,那如果不做分类,在游戏发布时还要去修改接口,这就非常麻烦了。如果设计一个通用的接口,对于资源类型可以使用枚举来进行表示,就可以解决一些不必要的麻烦。


ResourceUnit

首先来设计一个 ResourceUnit 的模块,它可以理解为资源的基本单位,也是程序封装的资源基本单位,ResourceUnit 类的代码如下所示:

public enum ResourceType
{
    ASSET,
    PREFAB,
    LEVELASSET,
    LEVEL,
}

这就是我们所定义的资源枚举,每一个加载的资源都是一个 ResourceUnit,下面继续完善:

 public class ResourceUnit : IDisposable
{
    private string mPath;
    private Object mAsset;
    private ResourceType mResourceType;
    private List<ResourceUnit> mNextLevelAssets;
    private AssetBundle mAssetBundle;
    private int mReferenceCount;

    internal ResourceUnit(AssetBundle assetBundle, int assetBundleSize, Object asset, string path, ResourceType resourceType)
    {
        mPath = path;
        mAsset = asset;
        mResourceType = resourceType;
        mNextLevelAssets = new List<ResourceUnit>();
        mAssetBundle = assetBundle;
        mAssetBundleSize = assetBundleSize;
        mReferenceCount = 0;
    }

    public List<ResourceUnit> NextLevelAssets
    {
        get
        {
            return mNextLevelAssets;
        }

        internal set
        {
            foreach (ResourceUnit asset in value)
            {
                mNextLevelAssets.Add(asset);
            }
        }
    }

    public int ReferenceCount
    {
        get
        {
            return mReferenceCount;
        }
    }
    //增加引用计数
    public void addReferenceCount()
    {
        ++mReferenceCount;
        foreach (ResourceUnit asset in mNextLevelAssets)
        {
            asset.addReferenceCount();
        }
    }
    //减少引用计数
    public void reduceReferenceCount()
    {
        --mReferenceCount;

        foreach (ResourceUnit asset in mNextLevelAssets)
        {
            asset.reduceReferenceCount();
        }
        if (isCanDestory())
        {
            Dispose();
        }
    }

    public bool isCanDestory() { return (0 == mReferenceCount); }

    public void Dispose()
    {
        ResourceCommon.Log("Destory " + mPath);

        if (null != mAssetBundle)
        {
            //mAssetBundle.Unload(true);
            mAssetBundle = null;
        }
        mNextLevelAssets.Clear();
        mAsset = null;
    }
}

ResourceUnit 同时还实现了对资源的引用计数,这种设计思想跟内存的使用比较像,这样便于程序知道对于加载的资源什么时候销毁,什么时候能够继续使用,它还声明了一些变量,比如资源的名字等。


ResourceCommon

另外,要加载资源,还要知道资源的加载路径,其次要知道资源的类型。我们通常会使用一个类专门用于资源路径的设置,包括获取资源文件夹、资源路径、获取资源文件以及获取 AssetBundle 包体文件的大小等等。该类的代码实现如下所示:

 public class ResourceCommon
    {
        public static string textFilePath = Application.streamingAssetsPath;

        public static string assetbundleFilePath = Application.dataPath + "/assetbundles/";

        public static string assetbundleFileSuffix = ".bytes";

        public static string DEBUGTYPENAME = "Resource";

        //根据资源路径获取资源名称
        public static string getResourceName(string resPathName)
        {
            int index = resPathName.LastIndexOf("/");
            if (index == -1)
                return resPathName;
            else
            {
                return resPathName.Substring(index + 1, resPathName.Length - index - 1);
            }
        }
        //获取文件名字
        public static string getFileName(string fileName)
        {
            int index = fileName.IndexOf(".");
            if (-1 == index)
                throw new Exception("can not find .!!!");
            return fileName.Substring(0, index);
        }
        //获取文件名字
        public static string getFileName(string filePath, bool suffix)
        {
            if (!suffix)
            {
                string path = filePath.Replace("\\", "/");
                int index = path.LastIndexOf("/");
                if (-1 == index)
                    throw new Exception("can not find .!!!");
                int index2 = path.LastIndexOf(".");
                if (-1 == index2)
                    throw new Exception("can not find /!!!");
                return path.Substring(index + 1, index2 - index - 1);
            }
            else
            {
                string path = filePath.Replace("\\", "/");
                int index = path.LastIndexOf("/");
                if (-1 == index)
                    throw new Exception("can not find /!!!");
                return path.Substring(index + 1, path.Length - index - 1);
            }
        }
        //获取文件夹
        public static string getFolder(string path)
        {
            path = path.Replace("\\", "/");
            int index = path.LastIndexOf("/");
            if (-1 == index)
                throw new Exception("can not find /!!!");
            return path.Substring(index + 1, path.Length - index - 1);
        }
        //获取文件后缀
        public static string getFileSuffix(string filePath)
        {
            int index = filePath.LastIndexOf(".");
            if (-1 == index)
                throw new Exception("can not find Suffix!!! the filePath is : " + filePath);
            return filePath.Substring(index + 1, filePath.Length - index - 1);
        }
        //获取文件
        public static void getFiles(string path, bool recursion, Dictionary<string, List<string>> allFiles, bool useSuffix, string suffix)
        {
            if (recursion)
            {
                string[] dirs = Directory.GetDirectories(path);
                foreach (string dir in dirs)
                {
                    if (getFolder(dir) == ".svn")
                        continue;
                    getFiles(dir, recursion, allFiles, useSuffix, suffix);
                }
            }

            string[] files = Directory.GetFiles(path);
            foreach (string file in files)
            {
                string fileSuffix = getFileSuffix(file);
                if (fileSuffix == "meta" || fileSuffix == "dll")
                    continue;
                if (useSuffix && fileSuffix != suffix)
                    continue;
                string relativePath = file.Replace("\\", "/");
                relativePath = relativePath.Replace(Application.dataPath, "");
                string fileName = getFileName(file, true);
                if (allFiles.ContainsKey(fileName))
                {
                    allFiles[fileName].Add(relativePath);
                }
                else
                {
                    List<string> list = new List<string>();
                    list.Add(relativePath);
                    allFiles.Add(fileName, list);
                }
            }
        }
        //检查文件夹
        public static void CheckFolder(string path)
        {
            if (!Directory.Exists(path))
                Directory.CreateDirectory(path);
        }
        //获取文件路径
        public static string getPath(string filePath)
        {
            string path = filePath.Replace("\\", "/");
            int index = path.LastIndexOf("/");
            if (-1 == index)
                throw new Exception("can not find /!!!");
            return path.Substring(0, index);
        }
        //获取本地路径
        public static string getLocalPath(string path)
        {
            string localPath = string.Format("{0}/{1}", Application.persistentDataPath, path);
            if (!File.Exists(localPath))
            {
                if (Application.platform == RuntimePlatform.Android)
                {
                    localPath = string.Format("{0}/{1}", Application.streamingAssetsPath, path);
                }
                else if (Application.platform == RuntimePlatform.WindowsEditor || Application.platform == RuntimePlatform.WindowsPlayer)
                {
                    localPath = string.Format("file://{0}/{1}", Application.streamingAssetsPath, path);
                }
                return localPath;
            }
            return "file:///" + localPath;
        }
        //获取AssetBundles文件字节
        public static byte[] getAssetBundleFileBytes(string path, ref int size)
        {
            string localPath;

            //Andrio跟IOS环境使用沙箱目录
            if (Application.platform == RuntimePlatform.Android || Application.platform == RuntimePlatform.IPhonePlayer)
            {
                localPath = string.Format("{0}/{1}", Application.persistentDataPath, path + ResourceCommon.assetbundleFileSuffix);
            }
            //Window下使用assetbunlde资源目录
            else
            {
                localPath = ResourceCommon.assetbundleFilePath + path + ResourceCommon.assetbundleFileSuffix;
            }

            Debug.Log(localPath);

            //首先检测沙箱目录中是否有更新资源         
            if (File.Exists(localPath))
            {
                try
                {
                    FileStream bundleFile = File.Open(localPath, FileMode.Open, FileAccess.Read);
                    byte[] bytes = new byte[bundleFile.Length];
                    bundleFile.Read(bytes, 0, (int)bundleFile.Length);
                    size = (int)bundleFile.Length;
                    bundleFile.Close();
                    return bytes;
                }
                catch (Exception e)
                {
                    Debug.LogError(e.Message);
                    return null;
                }
            }
            //原始包中
            else
            {
                TextAsset bundleFile = Resources.Load(path) as TextAsset;

                if (null == bundleFile)
                    Debug.LogError("load : " + path + " bundleFile error!!!");
                size = bundleFile.bytes.Length;
                return bundleFile.bytes;
            }
        }
    }
}

Request

上面的类封装了资源模块所通用的一些接口,以便于我们在开发中使用。在游戏处理资源的过程中,还需要考虑一个问题,那就是程序在请求资源时,要知道资源是在加载过程当中,还是已经卸载完成了。通常在程序中会使用一个枚举值去进行设置,通知程序该资源的使用状态,同时会使用委托函数进行具体的回调操作,比如资源加载完成,我要知道什么时候它加载完成了。根据这些想法,我们用一个类来实现,也就是 Request 类,代码实现如下:

//资源请求类型
    public enum RequestType
    {
        LOAD,
        UNLOAD,
        LOADLEVEL,
        UNLOADLEVEL,
    }

    class Request
    {
        internal string mFileName;              //请求资源相对Assets/完整路径名称
        internal ResourceType mResourceType;
        //委托回调函数
        internal ResourcesManager.HandleFinishLoad mHandle;
        internal ResourcesManager.HandleFinishLoadLevel mHandleLevel;
        internal ResourcesManager.HandleFinishUnLoadLevel mHandleUnloadLevel;
        internal RequestType mRequestType;
        internal ResourceAsyncOperation mResourceAsyncOperation;

        //构造函数
        internal Request(string fileName, ResourceType resourceType, ResourcesManager.HandleFinishLoad handle, RequestType requestType, ResourceAsyncOperation operation)
        {
            mFileName = fileName;
            mResourceType = resourceType;
            mHandle = handle;
            mRequestType = requestType;
            mResourceAsyncOperation = operation;
        }
        //构造函数
        internal Request(string fileName, ResourceType resourceType, ResourcesManager.HandleFinishLoadLevel handle, RequestType requestType, ResourceAsyncOperation operation)
        {
            mFileName = fileName;
            mResourceType = resourceType;
            mHandleLevel = handle;
            mRequestType = requestType;
            mResourceAsyncOperation = operation;
        }

    }

ResourceAsyncOperation

在场景与场景之间进行切换过渡时,尤其对于比较大的资源加载,通常使用一个进度条来进行过渡,为此在框架中封装了一个通用的资源过渡类,代码实现如下:

 public class ResourceAsyncOperation
    {
        internal RequestType mRequestType;
        internal int mAllDependencesAssetSize;
        internal int mLoadDependencesAssetSize;
        internal bool mComplete;
        public AsyncOperation asyncOperation;

        internal ResourceUnit mResource;

        internal ResourceAsyncOperation(RequestType requestType)
        {
            mRequestType = requestType;
            mAllDependencesAssetSize = 0;
            mLoadDependencesAssetSize = 0;
            mComplete = false;
            asyncOperation = null;
            mResource = null;
        }

        public bool Complete
        {
            get
            {
                return mComplete;
            }
        }
        //资源加载进度
        public int Prograss
        {
            get
            {
                if (mComplete)
                    return 100;
                else if (0 == mLoadDependencesAssetSize)
                    return 0;
                else
                {
                    //使用assetbundle
                    if (ResourcesManager.Instance.UsedAssetBundle)
                    {
                        if (RequestType.LOADLEVEL == mRequestType)
                        {
                            int depsPrograss = (int)(((float)mLoadDependencesAssetSize / mAllDependencesAssetSize) * 100);
                            int levelPrograss = asyncOperation != null ? (int)((float)asyncOperation.progress * 100.0f) : 0;
                            return (int)(depsPrograss * 0.8) + (int)(levelPrograss * 0.2);
                        }
                        else
                        {
                            return (int)(((float)mLoadDependencesAssetSize / mAllDependencesAssetSize) * 100);
                        }
                    }
                    //不使用
                    else
                    {
                        if (RequestType.LOADLEVEL == mRequestType)
                        {
                            int levelPrograss = asyncOperation != null ? (int)((float)asyncOperation.progress * 100.0f) : 0;
                            return levelPrograss;
                        }
                        else
                        {
                            return 0;
                        }
                    }
                }
            }
        }
    }

ResourcesManager

关于资源管理的架构思想,基本已经完成了,接下来就要考虑如何使用了。不能直接使用它们,因为它们既不是单例,也不是静态类,没有提供对外的接口,那怎么办呢?管理类就出现了,我们可以使用管理类提供对外的接口,也就是 ResourceManager 类,它通常是单例模式,我们把游戏中的单例分为两种:一种是继承 mono 的单例,一种是不继承 mono 的。我们设计的资源管理类是可以挂接到对象上的,这主要是为了资源更新时使用的。管理类它可以加载资源、销毁资源等等。它的内容实现代码如下:

 public class ResourcesManager : UnitySingleton<ResourcesManager>
    {
        //是否通过assetbundle加载资源
        public bool UsedAssetBundle = false;

        private bool mInit = false;
        private int mFrameCount = 0;
        private Request mCurrentRequest = null;
        private Queue<Request> mAllRequests = new Queue<Request>();

        //保存读取的Resource信息
        private Dictionary<string, string> mResources = new Dictionary<string, string>();
        //加载的资源信息
        private Dictionary<string, ResourceUnit> mLoadedResourceUnit = new Dictionary<string, ResourceUnit>();

        public delegate void HandleFinishLoad(ResourceUnit resource);
        public delegate void HandleFinishLoadLevel();
        public delegate void HandleFinishUnLoadLevel();


        public void Init()
        {
            mInit = true;
        }

        public void Update()
        {
            if (!mInit)
                return;

            if (null == mCurrentRequest && mAllRequests.Count > 0)
                handleRequest();

            ++mFrameCount;
            if (mFrameCount == 300)
            {
                mFrameCount = 0;
            }
        }

        private void handleRequest()
        {
            //使用assetbundle打包功能
            if (UsedAssetBundle)
            {
                mCurrentRequest = mAllRequests.Dequeue();

                //相对Asset的完整资源路径
                string fileName = mCurrentRequest.mFileName;

                switch (mCurrentRequest.mRequestType)
                {
                    case RequestType.LOAD:
                        {
                            switch (mCurrentRequest.mResourceType)
                            {
                                case ResourceType.ASSET:
                                case ResourceType.PREFAB:
                                    {
                                        if (mLoadedResourceUnit.ContainsKey(fileName))
                                        {

                                            mCurrentRequest.mResourceAsyncOperation.mComplete = true;
                                            mCurrentRequest.mResourceAsyncOperation.mResource = mLoadedResourceUnit[fileName] as ResourceUnit;

                                            if (null != mCurrentRequest.mHandle)
                                                mCurrentRequest.mHandle(mLoadedResourceUnit[fileName] as ResourceUnit);
                                            handleResponse();
                                        }

                                   }
                                   break;
                              case ResourceType.LEVELASSET:
                              case ResourceType.LEVEL:

                            }
                        }
                        break;
                    case RequestType.UNLOAD:
                        {
                            if (!mLoadedResourceUnit.ContainsKey(fileName))
                                Debug.LogError("can not find " + fileName);
                            else
                            {

                            }
                            handleResponse();
                        }
                        break;
                    case RequestType.LOADLEVEL:
                        {
                            StartCoroutine(_loadLevel(fileName, mCurrentRequest.mHandleLevel, ResourceType.LEVEL, mCurrentRequest.mResourceAsyncOperation));
                        }
                        break;
                    case RequestType.UNLOADLEVEL:
                        {
                            if (!mLoadedResourceUnit.ContainsKey(fileName))
                                Debug.LogError("can not find level " + fileName);
                            else
                            {
                                if (null != mCurrentRequest.mHandleUnloadLevel)
                                    mCurrentRequest.mHandleUnloadLevel();
                            }
                            handleResponse();
                        }
                        break;
                }
            }
            //不使用打包
            else
            {
                mCurrentRequest = mAllRequests.Dequeue();

                switch (mCurrentRequest.mRequestType)
                {
                    case RequestType.LOAD:
                        {
                            switch (mCurrentRequest.mResourceType)
                            {
                                case ResourceType.ASSET:
                                case ResourceType.PREFAB:
                                    {
                                        //暂时不处理,直接使用资源相对路径
                                    }
                                    break;
                                case ResourceType.LEVELASSET:
                                    {

                                    }
                                    break;
                                case ResourceType.LEVEL:
                                    {

                                    }
                                    break;
                            }
                        }
                        break;
                    case RequestType.UNLOAD:
                        {
                            handleResponse();
                        }
                        break;
                    case RequestType.LOADLEVEL:
                        {
                            StartCoroutine(_loadLevel(mCurrentRequest.mFileName, mCurrentRequest.mHandleLevel, ResourceType.LEVEL, mCurrentRequest.mResourceAsyncOperation));
                        }
                        break;
                    case RequestType.UNLOADLEVEL:
                        {
                            if (null != mCurrentRequest.mHandleUnloadLevel)
                                mCurrentRequest.mHandleUnloadLevel();
                            handleResponse();
                        }
                        break;
                }
            }
        }

        private void handleResponse()
        {
            mCurrentRequest = null;
        }

        //传入Resources下相对路径名称 例如Resources/Game/Effect1    传入Game/Effect1
        public ResourceUnit loadImmediate(string filePathName, ResourceType resourceType, string archiveName = "Resources")
        {
            //使用assetbundle打包
            if (UsedAssetBundle)
            {
                //添加Resource
                string completePath = "Resources/" + filePathName;
                //加载本身预制件
                ResourceUnit unit = _LoadImmediate(completePath, resourceType);

                return unit;
            }
            //不使用
            else
            {
                Object asset = Resources.Load(filePathName);
                ResourceUnit resource = new ResourceUnit(null, 0, asset, null, resourceType);
                return resource;
            }

        }
        //加载场景
        public ResourceAsyncOperation loadLevel(string fileName, HandleFinishLoadLevel handle, string archiveName = "Level")
        {

            {
                ResourceAsyncOperation operation = new ResourceAsyncOperation(RequestType.LOADLEVEL);
                mAllRequests.Enqueue(new Request(fileName, ResourceType.LEVEL, handle, RequestType.LOADLEVEL, operation));
                return operation;
            }
        }

        private IEnumerator _loadLevel(string path, HandleFinishLoadLevel handle, ResourceType resourceType, ResourceAsyncOperation operation)
        {
            //使用assetbundle打包
            if (UsedAssetBundle)
            {
                //加载场景assetbundle     
                int scenAssetBundleSize = 0;
                byte[] binary = ResourceCommon.getAssetBundleFileBytes(path, ref scenAssetBundleSize);
                AssetBundle assetBundle = AssetBundle.LoadFromMemory(binary);
                if (!assetBundle)
                    Debug.LogError("create scene assetbundle " + path + "in _LoadImmediate failed");

                //添加场景大小
                operation.mLoadDependencesAssetSize += scenAssetBundleSize;

                AsyncOperation asyncOperation = SceneManager.LoadSceneAsync(ResourceCommon.getFileName(path, false));
                operation.asyncOperation = asyncOperation;
                yield return asyncOperation;

                 handleResponse();

                operation.asyncOperation = null;
                operation.mComplete = true;
                operation.mResource = null;

                if (null != handle)
                    handle();

            }
            //不使用
           else
            {
                ResourceUnit level = new ResourceUnit(null, 0, null, path, resourceType);

                //获取加载场景名称
                string sceneName = ResourceCommon.getFileName(path, true);
                AsyncOperation asyncOperation = Application.LoadLevelAsync(sceneName);
                operation.asyncOperation = asyncOperation;
                yield return asyncOperation;

                handleResponse();

                operation.asyncOperation = null;
                operation.mComplete = true;

                if (null != handle)
                    handle();
            }
        }

        //单个资源加载
        ResourceUnit _LoadImmediate(string fileName, ResourceType resourceType)
        {
            //没有该资源,加载
            if (!mLoadedResourceUnit.ContainsKey(fileName))
            {
                //资源大小
                int assetBundleSize = 0;
                byte[] binary = ResourceCommon.getAssetBundleFileBytes(fileName, ref assetBundleSize);
                AssetBundle assetBundle = AssetBundle.LoadFromMemory(binary);

                if (!assetBundle)
                    Debug.LogError("create assetbundle " + fileName + "in _LoadImmediate failed");

                Object asset = assetBundle.LoadAsset(fileName);
                if (!asset)
                    Debug.LogError("load assetbundle " + fileName + "in _LoadImmediate failed");

                ResourceUnit ru = new ResourceUnit(assetBundle, assetBundleSize, asset, fileName, resourceType);

                //添加到资源中
                mLoadedResourceUnit.Add(fileName, ru);

                return ru;
            }
            else
            {
                return mLoadedResourceUnit[fileName];
            }
        }

LoadUiResource

资源管理类到这里还没有完成,因为还没有 UI 资源的处理。比如要把一个 UI 资源动态的挂接到父物体的下面,该怎么处理,所以在资源管理框架中会有专用于处理 UI 资源的类。该类主要作用是提供了加载 UI 资源的接口,同时会将资源放到字典中便于统一处理。该类代码实现如下:

public class LoadUiResource
{
    public static GameObject LoadRes(Transform parent,string path)
    {
        if(CheckResInDic(path))         
        {
            if(GetResInDic(path) != null){
                return GetResInDic(path);
            }
            else{
                LoadResDic.Remove(path);
            }
        }

        GameObject objLoad = null;

        ResourceUnit objUnit = ResourcesManager.Instance.loadImmediate(path, ResourceType.PREFAB);

        if (objUnit == null || objUnit.Asset == null)
        {
            Debug.LogError("load unit failed" + path);
            return null;
        }
        objLoad = GameObject.Instantiate(objUnit.Asset) as GameObject;

        objLoad.transform.parent = parent;
        objLoad.transform.localScale = Vector3.one;
        objLoad.transform.localPosition = Vector3.zero;
        LoadResDic.Add(path,objLoad);
        return objLoad;
    }

    //创建窗口子对象,不加入资源管理
    public static GameObject AddChildObject(Transform parent, string path)
    {
        GameObject objLoad = null;

        ResourceUnit objUnit = ResourcesManager.Instance.loadImmediate(path, ResourceType.PREFAB);
        if (objUnit == null || objUnit.Asset == null)
        {
            Debug.LogError("load unit failed" + path);
            return null;
        }
        objLoad = GameObject.Instantiate(objUnit.Asset) as GameObject;
        objLoad.transform.parent = parent;
        objLoad.transform.localScale = Vector3.one;
        objLoad.transform.localPosition = Vector3.zero;

        return objLoad;
    }
    //删除所有的孩子
    public static void ClearAllChild(Transform transform)
    {
        while (transform.childCount > 0)
        {
            GameObject.DestroyImmediate(transform.GetChild(0).gameObject);
        }
        transform.DetachChildren();
    }

    public static void ClearOneChild(Transform transform,string name)
    {
        for (int i = 0; i < transform.childCount; i++)
        {
            if (transform.GetChild(i).gameObject.name == name)
            {
                GameObject.DestroyImmediate(transform.GetChild(i).gameObject);
            }
        }
    }
    //删除加载
    public static void DestroyLoad(string path)
    {
        if(LoadResDic == null || LoadResDic.Count == 0)
            return;
        GameObject obj = null;
        if (LoadResDic.TryGetValue(path, out obj) && obj != null)
        {
            GameObject.DestroyImmediate(obj);
            LoadResDic.Remove(path);
            //System.GC.Collect();
        }
    }

    public static void DestroyLoad(GameObject obj)
    {
        if(LoadResDic == null || LoadResDic.Count == 0)
            return;
        if(obj == null)
            return;
        foreach(string key in LoadResDic.Keys)
        {
            GameObject objLoad;
            if(LoadResDic.TryGetValue(key,out objLoad) && objLoad == obj)
            {
                GameObject.DestroyImmediate(obj);
                LoadResDic.Remove(key);
                break;
            }
        }       
    }

    //获取在目录中的资源
    public static GameObject GetResInDic(string path)
    {
        if(LoadResDic == null || LoadResDic.Count == 0)
            return null;
        GameObject obj = null ;
        if(LoadResDic.TryGetValue(path,out obj))
        {
            return obj;
        }
        return null;
    }
    //检查资源是否存在
    public  static bool CheckResInDic(string path)
    {
        if(LoadResDic == null || LoadResDic.Count == 0)
            return false;
        return LoadResDic.ContainsKey(path);
    }

    public static void Clean()
    {
        if(LoadResDic == null || LoadResDic.Count == 0)
            return;
        for(int i = LoadResDic.Count - 1;i >=0;i--)
        {
            GameObject obj = LoadResDic.ElementAt(i).Value ;
            if( obj != null)
            {
                GameObject.DestroyImmediate(obj);
            }
        }
        LoadResDic.Clear();
    }

    public static Dictionary<string,GameObject> LoadResDic = new Dictionary<string, GameObject>();

}

自定义消息分发

首先 Unity 引擎为开发者提供的消息分发函数有这些:SendMessage、SendMessageUpwards、BroadcastMessage,它们可以实现简单的消息发送。那么为什么不选择它们,因为它们的执行效率相对委托来说是比较低的,而且扩展性方面也不好,比如要使用很多的参数来进行传递的话,它们就很难满足我们的需求,游戏开发还会有更多的类似需求。所以我们放弃它们,选择使用委托自己去封装消息分发。

举一个需求说明的例子,当玩家杀怪获取掉落下来的道具时,玩家的经验值加1。这是一个非常基础的功能需求,这类需求充斥着游戏的各处。最初我们可以不使用事件系统,直接在 OnTriggerEnter 方法中给玩家的生命值加1,但是,这将使得检测碰撞的这块代码直接引用了玩家属性管理的代码,也就是代码的紧耦合。而且,在后面的开发中,我们又想让玩家接到道具的同时还要在界面上显示一个图标,这时又需要在这里引用界面相关的代码。后来,又希望能播放一段音效……,这样随着需求的增加,其中的逻辑会越来越复杂。解决此问题的好办法就是,在 OnTrigerEnter 中加入消息分发函数,这样具体的操作就在另一个类的函数中进行,可以降低耦合性。

在网络游戏中,我们也会遇到服务器发送给客户端角色信息后,客户端接收到消息后,会将得到的角色信息在 UI 上显示出来。如果不用事件系统对其进行分离,那么网络消息跟 UI 就会混在一起了。这样随着逻辑的需求增加,耦合性会越来越大,最后会导致项目很难维护。所以我们必须要使用事件系统来解耦合各个模块。


事件分类

游戏中会有很多种事件,事件的分类表示我们可以采用字符串或者采用枚举值,事件分类枚举代码:

public enum EGameEvent
{
    eWeaponDataChange = 1,
    ePlayerShoot = 2,

    //UI
    eLevelChange = 3,
    eBloodChange = 4,
    ePowerChange = 5,
    eSkillInit = 6,
    eSkillUpdate = 7,
    eBuffPick = 8,
    eTalent = 9,

    //data
    eBlood = 10,
    eMp = 11,
    eScore = 12,
    ePower = 13,
    eTalentUpdate = 14,
    ePickBuff = 15,

    eGameEvent_LockTarget,

    //Login
    eGameEvent_LoginSuccess,  //登陆成功
    eGameEvent_LoginEnter, 
    eGameEvent_LoginExit,
    eGameEvent_RoleEnter,
    eGameEvent_RoleExit,
    
    //Play
    eGameEvent_PlayEnter,
    eGameEvent_PlayExit,

    ePlayerInput,

    eActorDead,
}

保存事件

这些事件分类还可以继续扩展,事件系统贯穿于整个游戏,从 UI 界面、数据、战斗等等。我们的事件系统实现主要分为三步:事件监听、事件分发、事件移除,使用字典 Dictionary 用于保存事件和委托。

static public Dictionary<EGameEvent, Delegate> mEventTable = new Dictionary<EGameEvent, Delegate>();

模版类

事件系统中的委托,也是需要封装的。委托该如何封装?我们使用的委托函数的参数可能会有多个,而且不同的委托函数对应的类型可能也是不同的,比如 GameObject、float、int 等等。针对这些需求,唯一能解决问题的就是模版类,回调函数对应的代码如下,从无参数到可以有五个参数:

public delegate void Callback();
public delegate void Callback<T>(T arg1);
public delegate void Callback<T, U>(T arg1, U arg2);
public delegate void Callback<T, U, V>(T arg1, U arg2, V arg3);
public delegate void Callback<T, U, V, X>(T arg1, U arg2, V arg3, X arg4);


事件监听

首先封装监听函数:

    //无参数
    static public void AddListener(EGameEvent eventType, Callback handler) {
        OnListenerAdding(eventType, handler);
        mEventTable[eventType] = (Callback)mEventTable[eventType] + handler;
    }

    //一个参数
    static public void AddListener<T>(EGameEvent eventType, Callback<T> handler) {
        OnListenerAdding(eventType, handler);
        mEventTable[eventType] = (Callback<T>)mEventTable[eventType] + handler;
    }

    //两个参数
    static public void AddListener<T, U>(EGameEvent eventType, Callback<T, U> handler) {
        OnListenerAdding(eventType, handler);
        mEventTable[eventType] = (Callback<T, U>)mEventTable[eventType] + handler;
    }

    //三个参数
    static public void AddListener<T, U, V>(EGameEvent eventType, Callback<T, U, V> handler) {
        OnListenerAdding(eventType, handler);
        mEventTable[eventType] = (Callback<T, U, V>)mEventTable[eventType] + handler;
    }

    //四个参数
    static public void AddListener<T, U, V, X>(EGameEvent eventType, Callback<T, U, V, X> handler) {
        OnListenerAdding(eventType, handler);
        mEventTable[eventType] = (Callback<T, U, V, X>)mEventTable[eventType] + handler;
    }

每个函数都是比较简单的,从没有参数,到最多四个参数的函数,这些函数都调用了函数 OnListenerAdding 用于将事件和委托寸放到字典中:

static public void OnListenerAdding(EGameEvent eventType, Delegate listenerBeingAdded) {
        if (!mEventTable.ContainsKey(eventType)) {
            mEventTable.Add(eventType, null );
        }
 
        Delegate d = mEventTable[eventType];
        if (d != null && d.GetType() != listenerBeingAdded.GetType()) {
            throw new ListenerException(string.Format("Attempting to add listener with inconsistent signature for event type {0}. Current listeners have type {1} and listener being added has type {2}", eventType, d.GetType().Name, listenerBeingAdded.GetType().Name));
        }
    }

事件移除

监听函数有了,对应的就是移除监听函数,移除就是从 Dictionary 字典中将其移除掉,它跟监听函数是一一对应的,函数如下:

   //无参数
    static public void RemoveListener(EGameEvent eventType, Callback handler) {
        OnListenerRemoving(eventType, handler);   
        mEventTable[eventType] = (Callback)mEventTable[eventType] - handler;
        OnListenerRemoved(eventType);
    }

    //一个参数
    static public void RemoveListener<T>(EGameEvent eventType, Callback<T> handler) {
        OnListenerRemoving(eventType, handler);
        mEventTable[eventType] = (Callback<T>)mEventTable[eventType] - handler;
        OnListenerRemoved(eventType);
    }

    //两个参数
    static public void RemoveListener<T, U>(EGameEvent eventType, Callback<T, U> handler) {
        OnListenerRemoving(eventType, handler);
        mEventTable[eventType] = (Callback<T, U>)mEventTable[eventType] - handler;
        OnListenerRemoved(eventType);
    }

    //三个参数
    static public void RemoveListener<T, U, V>(EGameEvent eventType, Callback<T, U, V> handler) {
        OnListenerRemoving(eventType, handler);
        mEventTable[eventType] = (Callback<T, U, V>)mEventTable[eventType] - handler;
        OnListenerRemoved(eventType);
    }

    //四个参数
    static public void RemoveListener<T, U, V, X>(EGameEvent eventType, Callback<T, U, V, X> handler) {
        OnListenerRemoving(eventType, handler);
        mEventTable[eventType] = (Callback<T, U, V, X>)mEventTable[eventType] - handler;
        OnListenerRemoved(eventType);
    }    

这些函数都调用了函数 OnListenerRemoving 和 OnListenerRemoved:

static public void OnListenerRemoving(EGameEvent eventType, Delegate listenerBeingRemoved) { 
        if (mEventTable.ContainsKey(eventType)) {
            Delegate d = mEventTable[eventType];
 
            if (d == null) {
                throw new ListenerException(string.Format("Attempting to remove listener with for event type \"{0}\" but current listener is null.", eventType));
            } else if (d.GetType() != listenerBeingRemoved.GetType()) {
                throw new ListenerException(string.Format("Attempting to remove listener with inconsistent signature for event type {0}. Current listeners have type {1} and listener being removed has type {2}", eventType, d.GetType().Name, listenerBeingRemoved.GetType().Name));
            }
        } else {
            throw new ListenerException(string.Format("Attempting to remove listener for type \"{0}\" but Messenger doesn't know about this event type.", eventType));
        }
    }
 
 
    static public void OnListenerRemoved(EGameEvent eventType) {
        if (mEventTable[eventType] == null) {
            mEventTable.Remove(eventType);
        }
    }

事件分发

如何触发监听函数呢,那就要说到广播函数了,它与监听和移除也是一一对应的:

    //无参数
    static public void Broadcast(EGameEvent eventType) {
        OnBroadcasting(eventType);
        Delegate d;
        if (mEventTable.TryGetValue(eventType, out d)) {
            Callback callback = d as Callback;

            if (callback != null) {
                callback();
            } else {
                throw CreateBroadcastSignatureException(eventType);
            }
        }
    }

    //一个参数
    static public void Broadcast<T>(EGameEvent eventType, T arg1) {

        OnBroadcasting(eventType);

        Delegate d;
        if (mEventTable.TryGetValue(eventType, out d)) {
            Callback<T> callback = d as Callback<T>;

            if (callback != null) {
                callback(arg1);
            } else {
                throw CreateBroadcastSignatureException(eventType);
            }
        }
    }

    //两个参数
    static public void Broadcast<T, U>(EGameEvent eventType, T arg1, U arg2) {

        OnBroadcasting(eventType);

        Delegate d;
        if (mEventTable.TryGetValue(eventType, out d)) {
            Callback<T, U> callback = d as Callback<T, U>;

            if (callback != null) {
                callback(arg1, arg2);
            } else {
                throw CreateBroadcastSignatureException(eventType);
            }
        }
    }

    //三个参数
    static public void Broadcast<T, U, V>(EGameEvent eventType, T arg1, U arg2, V arg3) {

        OnBroadcasting(eventType);

        Delegate d;
        if (mEventTable.TryGetValue(eventType, out d)) {
            Callback<T, U, V> callback = d as Callback<T, U, V>;

            if (callback != null) {
                callback(arg1, arg2, arg3);
            } else {
                throw CreateBroadcastSignatureException(eventType);
            }
        }
    }

    //四个参数
    static public void Broadcast<T, U, V, X>(EGameEvent eventType, T arg1, U arg2, V arg3, X arg4) {

        OnBroadcasting(eventType);

        Delegate d;
        if (mEventTable.TryGetValue(eventType, out d)) {
            Callback<T, U, V, X> callback = d as Callback<T, U, V, X>;

            if (callback != null) {
                callback(arg1, arg2, arg3, arg4);
            } else {
                throw CreateBroadcastSignatureException(eventType);
            }
        }
    }    
}

这样整个事件系统就封装完成了,那么如何使用呢,首先需要先添加监听,将监听函数放在对应的类中:

EventCenter.AddListener(EGameEvent.eGameEvent_GamePlayEnter, Show);

在要使用的地方,可以播放此消息:

EventCenter.Broadcast(EGameEvent.eGameEvent_GamePlayEnter);

对象池

对象池就是放置物体对象的池子,是一个虚拟的用于存储对象的 Dictionary 或者是 List、Stack 等数据存储。

对象池主要是针对游戏中频繁生成、频繁销毁的对象而设立的,目的是优化内存。如果对象频繁的生成,就表示它每次生成都要从内存中去申请空间,而每次释放就会导致很多的内存碎片,当再去生成大物体对象时,会导致程序运行的卡顿,因为没有一个整体的内存供加载的物体使用。而使用对象池可以避免这些问题的产生,优化内存。

对象池可以把这些频繁生成的对象先放置到 Dictionary 中,在生成的时候使用,先在 Dictionary 中查找,如果找到,就可以将其显示出来,再根据需求对其进行操作。
而如果找不到,就需要重新生成,然后将其放置到 Dictionary 中,方便下次使用。当然这些对象也不是永久存放在 Dictionary 中的,我们会对其设置一个缓存时间,如果长时间不用就将其清除掉,也就是从 Dictionary 中删除掉。这样就可以避免频繁的操作内存。

关于游戏架构设计(一)_第2张图片
上面是一张对象池模块的实现框架图。先介绍一下传统的对象池设计,传统的设计,一般会使用两个队列,一个用于存储正在使用的对象,另一个存储使用过的或者没被使用的对象。这种设计方式,如果只是做个简单的 Demo 是可以的,但是如果做产品就不能这么简单的设计了。要考虑的问题比较多,比如设置了对象池中对象的缓存时间,还有要生成不同类型的对象池等。


对象池类型

我们首先定义游戏需要使用对象生成的对象池类型,这个可以使用枚举值表示:

    public enum PoolObjectType
 {
    POT_Effect,
    POT_MiniMap,
    POT_Entity,
    POT_UITip,
    POT_XueTiao,
 }

该枚举定义了特效、小地图、实体、UI 提示框、还有血条这些需要使用对象池的游戏类型。


对象信息

还要考虑这些游戏对象包含哪些信息,比如可以通过名字或者 ID 查找对象池中的对象,还有每个对象应该有自己的缓存时间,对象是属于哪种对象池等等属性,这些可以使用结构体或者类来定义:

public class PoolGameObjectInfo
{  
    public string name;

    //缓存时间
    public float mCacheTime= 0.0f;

    //缓存物体类型
    public PoolObjectType type;

    //可以重用
    public bool mCanUse = true;

    //重置时间
    public float mResetTime = .0f;
} 

以上结构是对象池中的对象信息的定义,每个对象都对应着自己的对象信息,使用字典存放:

//GameObject缓存池
public class PoolInfo
{
    //缓存队列
    public Dictionary<GameObject, PoolGameObjectInfo> mQueue = new Dictionary<GameObject, PoolGameObjectInfo>();
}

这个类定义的是池信息,每个对象池对应着自己的信息,另外再定义存储对象池信息的字典,通过名字进行区分,我们将其也用 Dictionary 字典存放:

public class GameObjectPool : Singleton<GameObjectPool>
{
   private Dictionary<String, PoolInfo> mPoolDic = new Dictionary<String, PoolInfo>();
}

获取对象

下面开始设计接口。在生成对象池对象时,首先要获取一下,看看以前是否生成过,如果已经生成过,那就直接拿来,否则就要重新加载资源进行创建,该函数主要是用于获取是否已经存在对象池的物体,同时判断一下该物体是否可以使用,该函数只对内,不对外:

    //取得可用对象
    private bool TryGetObject(PoolInfo poolInfo, out KeyValuePair<GameObject, PoolGameObjectInfo> objPair)
    {
        if (poolInfo.mQueue.Count > 0)
        {
            foreach (KeyValuePair<GameObject, PoolGameObjectInfo> pair in poolInfo.mQueue)
            {
                GameObject go = pair.Key;
                PoolGameObjectInfo info = pair.Value;

                if (info.mCanUse)
                {
                    objPair = pair;
                    return true;
                }
            }
        }

        objPair = new KeyValuePair<GameObject, PoolGameObjectInfo>();
        return false;
    }

接着编写创建对象池物体的函数:

//获取缓存物体
public GameObject GetGO(String res)
{
    //有效性检查
    if (null == res)
    {
        return null;
    }

    //查找对应pool,如果没有缓存
    PoolInfo poolInfo = null;
    KeyValuePair<GameObject, PoolGameObjectInfo> pair;
    if (!mPoolDic.TryGetValue(res, out poolInfo) || !TryGetObject(poolInfo, out pair))
    {
        //新创建
        ResourceUnit unit = ResourcesManager.Instance.loadImmediate(res, ResourceType.PREFAB);
        if (unit.Asset == null)
        {
            Debug.Log("can not find the resource" + res);
        }
        return GameObject.Instantiate(unit.Asset) as GameObject;            
    }

    //出队列数据
    GameObject go = pair.Key;
    PoolGameObjectInfo info = pair.Value;

    poolInfo.mQueue.Remove(go);

    //使有效
    EnablePoolGameObject(go, info);

    //返回缓存Gameobjec
    return go;
}

该函数首先判断对象是否在 Dictionary 中,如果不在字典中,就直接生成一个实例化对象。


显示对象

如果已经创建则调用函数 EnablePoolGameObject 将物体设置为 true:

public void EnablePoolGameObject(GameObject go, PoolGameObjectInfo info)
    {       
        if (info.type == PoolObjectType.POT_Effect)
        {         
            go.SetActive(true);                   
            go.transform.parent = null;
        }
        else if (info.type == PoolObjectType.POT_MiniMap)
        {
            go.SetActive(true);
            go.transform.parent = null;
        }
        else if (info.type == PoolObjectType.POT_Entity)
        {
            go.SetActive(true);
            go.transform.parent = null;
        }
        else if (info.type == PoolObjectType.POT_UITip)
        {
            go.SetActive(true);
            go.transform.parent = null;
        }
        else if (info.type == PoolObjectType.POT_XueTiao)
        {

        }

        info.mCacheTime = 0.0f;
    }

上面的函数就是将隐藏的对象设置为 true 将其显示出来,另外,我们还要知道对象的存放位置,还有在使用时判断其是否还有用,如果还有用,就将其隐藏掉,没有用就删除掉。


释放对象

public void ReleaseGO(String res, GameObject go, PoolObjectType type)
    {
        //获取缓存节点,设置为不可见位置
        if (objectsPool == null)
        {
            objectsPool = new GameObject("ObjectPool");
            objectsPool.AddComponent<Canvas>();            
            objectsPool.transform.localPosition = new Vector3(0, -5000, 0);
        }

        if (null == res || null == go)
        {
            return;
        }

        PoolInfo poolInfo = null;
        //没有创建 
        if (!mPoolDic.TryGetValue(res, out poolInfo))
        {
            poolInfo = new PoolInfo();            
            mPoolDic.Add(res, poolInfo);
        }

        PoolGameObjectInfo poolGameObjInfo = new PoolGameObjectInfo();     
        poolGameObjInfo.type = type;
        poolGameObjInfo.name = res;

        //无效缓存物体
        DisablePoolGameObject(go, poolGameObjInfo);

        //保存缓存GameObject,会传入相同的go, 有隐患          
        poolInfo.mQueue[go] = poolGameObjInfo;           
    }

上面的函数可以用于批量生成对象,比如从配置文件中加载的特效、物体等。先将其隐藏掉,如果使用时就调用 GetGo 函数,ReleaseGO 负责将生成的对象隐藏掉,而 GetGo 函数负责将已生成的对象显示出来,如果没有就直接实例化出来。


更新缓存时间

另外因为加了缓存时间,所以要在每一帧中去判断缓存的物体,看看这些物体的缓存时间是否有效,无效就删除掉,这是要在 Update 函数中判断的:

 public void OnUpdate()
    {
        //每隔0.1更新一次
        mTotalTime += Time.deltaTime;
        if (mTotalTime <= 0.1f)
        {
            return;
        }
        else
        {
            mTotalTime = 0;
        }

        float deltaTime = Time.deltaTime;

        //遍历数据
        foreach(PoolInfo poolInfo in mPoolDic.Values)
        {
            //死亡列表
            mDestoryPoolGameObjects.Clear();

            foreach (KeyValuePair<GameObject, PoolGameObjectInfo> pair in poolInfo.mQueue)
            {
                GameObject obj = pair.Key;
                
                PoolGameObjectInfo info = pair.Value;

                info.mCacheTime += deltaTime;

                float mAllCachTime = mCachTime;

                //POT_UITip,缓存3600秒
                if (info.type == PoolObjectType.POT_UITip)
                    mAllCachTime = 3600;

                //缓存时间到
                if (info.mCacheTime >= mAllCachTime)
                {
                    mDestoryPoolGameObjects.Add(obj);                 
                }

                //拖尾重置计时
                if (!info.mCanUse)
                {
                    info.mResetTime += deltaTime;

                    if (info.mResetTime > 1.0f)
                    {
                        info.mResetTime = .0f;
                        info.mCanUse = true;
                        obj.SetActive(false);
                    }
                }
            }

            //移除
            for(int k=0; k < mDestoryPoolGameObjects.Count; k++)
            {
                GameObject obj = mDestoryPoolGameObjects[k];
                GameObject.DestroyImmediate(obj);
                poolInfo.mQueue.Remove(obj);
            }                 
        }
   }

以上整个对象池的设计就完成了

你可能感兴趣的:(游戏架构设计)