对于一个游戏来说,游戏中资源的数量、大小是必须要去考虑的问题,因为一个游戏品质的好坏都是通过美术资源来表现的。所以管理这些资源的工作,作为开发者的我们是必须要好好考量的。对于游戏资源管理,一般的做法是封装几个接口来用于资源的加载,假如说只是做个 Demo,这样做是没有问题的,但是如果是做产品,其中资源的量是非常大的,如果资源的加载不恰当,会出现各种问题。而且游戏开发讲究的是团队协作,不同的人会有不同的需求,简单的封装几个接口不一定能满足需求,如果没有一个统一的资源架构管理,代码会出现各种接口版本,最后会出现大量的冗余代码,这样对游戏产品运行效率会产生影响。
另外,还要考虑游戏资源的动态更新,主要是为了减少游戏包体的大小。Unity 为用户提供了 AssetBundle 的方式打包资源,方便用户将资源打包后上传到资源服务器,在游戏启动时会通过本地存放资源的 MD5 码的文本文件与服务器上保存资源最新的 MD5 码的文本文件作对比,根据其资源对应的 MD5 码的不同,将最新的资源下载到本地使用,将资源文件代替本地的资源文件,同时更新MD5 码的文本文件,。我们在封装资源管理类时,也要从产品的角度考虑资源管理的问题。
下面开始介绍一下如何进行资源管理的代码封装,需要大家对资源管理的封装有一个比较完善的思考,代码模块如下图所示:
为什么这么设计呢?我们在游戏开发的时候,对于 Unity 的资源来说,每个资源都是一个 GameObject,但是单独的 GameObject 显然不能满足需求,因为资源既可以是 Prefab,也可以是 Scene,还可以是 Asset 文件。这当中就会涉及到不同的资源类型,那么如何表示这些资源类型呢?假如测试的时候是使用 prefab,但在正式发布时采用的是 asset,那如果不做分类,在游戏发布时还要去修改接口,这就非常麻烦了。如果设计一个通用的接口,对于资源类型可以使用枚举来进行表示,就可以解决一些不必要的麻烦。
首先来设计一个 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 同时还实现了对资源的引用计数,这种设计思想跟内存的使用比较像,这样便于程序知道对于加载的资源什么时候销毁,什么时候能够继续使用,它还声明了一些变量,比如资源的名字等。
另外,要加载资源,还要知道资源的加载路径,其次要知道资源的类型。我们通常会使用一个类专门用于资源路径的设置,包括获取资源文件夹、资源路径、获取资源文件以及获取 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 类,代码实现如下:
//资源请求类型
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;
}
}
在场景与场景之间进行切换过渡时,尤其对于比较大的资源加载,通常使用一个进度条来进行过渡,为此在框架中封装了一个通用的资源过渡类,代码实现如下:
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;
}
}
}
}
}
}
关于资源管理的架构思想,基本已经完成了,接下来就要考虑如何使用了。不能直接使用它们,因为它们既不是单例,也不是静态类,没有提供对外的接口,那怎么办呢?管理类就出现了,我们可以使用管理类提供对外的接口,也就是 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];
}
}
}
资源管理类到这里还没有完成,因为还没有 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 中删除掉。这样就可以避免频繁的操作内存。
上面是一张对象池模块的实现框架图。先介绍一下传统的对象池设计,传统的设计,一般会使用两个队列,一个用于存储正在使用的对象,另一个存储使用过的或者没被使用的对象。这种设计方式,如果只是做个简单的 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);
}
}
}
以上整个对象池的设计就完成了