本课程是 Unity 3D 系列教程,目标是带领读者搭建一个商业游戏的网络架构设计,该架构设计是游戏的核心技术,将采用 Unity 2017.2 最新版本做为开发工具。内容分为 UI 架构、技能架构、服务器和网络同步四大部分,共 13 篇文章。git
认真读完本系列文章以后,将会深刻理解架构的设计,具有独立搭建网络游戏框架的能力,并在此基础上能够独立开发一款网络游戏。程序员
姜雪伟,从事 IT 行业15年,现担任创业公司技术合伙人。著做有:《手把手教你架构 3D 游戏引擎》、《Unity 3D 实战核心技术详解》,《Cocos2d-x 3.x 图形学渲染技术讲解》等,参与或主导过十多款网络游戏研发。算法
随着 Stream、TapTap 等游戏平台的崛起,愈来愈多的网络游戏在此平台投放,并且不少新发布的游戏收入都颇丰,这些发布的游戏不少都是几我的开发完成的,并且开发周期都比较短,如何才能快速开发网络游戏?一个比较好的游戏框架是很是必要的。另外,这些平台的崛起,对于独立游戏开发者来讲,也是一个很是好的机会,换句话说独立开发者的春天又来了,固然对于那些想从事游戏开发或者说已经在这个行业从事游戏开发的人也是一个机会。数据库
如今游戏不仅限于抄袭了,更强调创新,只要有好的创意,再加上一个比较好的游戏框架,几个志道同合的小伙伴就能够开发一款网络游戏。在国外有不少这方面的案例,几我的在不一样的地方,一块儿开发一款游戏。而在国内不少普通程序员在游戏公司估计只是从事某项单一的逻辑功能编写,对总体架构设计并非很了解,即便本身有好的想法局限于本身的能力估计也是很难作出一款游戏,在游戏公司不多有人会教你架构设计,并且对于开发者来讲,要么只会客户端,要么只会服务器,很是少的人同时精通两者,这也困扰着那些想本身作游戏的开发者。本课程正是基于解决这些困扰程序员的问题,提出了一种基于网络服务器的架构设计,让程序员一我的能够同时进行客户端和服务器的网络游戏的开发,这样再加上美术和策划就能够搞定一款网络游戏。设计模式
搭建游戏框架首先要搞清楚什么是框架?其实搭建框架的主要目的是便于游戏逻辑的编写,这样很是有利于开发者快速的开发游戏,框架的核心思想是模块之间的耦合性要下降。那咱们先搞清楚游戏框架中主要包括哪些技术点,从大的方面说,每款游戏都有本身的 UI 系统、角色系统、技能系统、网络系统等等,往小的方面说就是编码的细节——每一个类的编写。下面就把游戏中的几个核心系统的架构设计思想逐步介绍给读者,架构设计没有好坏之分,用着方便就能够,在这里就当是抛砖引玉,读者也能够在此基础上去扩展,去从新编写架构。这样本篇教程的目的就达到了。服务器
先介绍 UI 系统,这个是老生常谈的,UI 架构经常使用的设计模式是 MVC。读者应该对 MVC 都比较了解,原理就不介绍了,能够去网上查阅。下面讲下 MVC 模式如何在 UI 系统中使用?先看下面这幅架构图:网络
咱们就围绕着这幅图给读者介绍模块设计。架构
首先,要作到 UI 资源和代码逻辑的分离,由于 UI 资源是常常更换的,若是两者不分离,很容易在更换资源时出现各类各样的脚本丢失以及资源和代码逻辑对应不上问题,这个对于程序来讲必需要避免的,程序员不该该把时间都浪费在这些事情上面。负载均衡
其次,逻辑代码之间的耦合性要下降,下降耦合性的方法经过事件的方式进行处理,不少程序使用 SendMessage 这种 Unity 自带的消息发送机制,其实它是很是消耗 CPU 的,为了优化这些,咱们会本身封装事件机制。框架
以上两点是指导咱们作架构的指导纲领,不论怎么设计最好围绕两者进行。
接下来介绍 UI 架构搭建的各个逻辑模块,上图中显示的窗体模块并不全面,游戏中的窗体是很是多的,在此以登陆窗体和英雄窗体为例进行说明:Loginwindow 和 HeroWindow 它们是负责显示的,也就是说,它对应具体的窗体逻辑,它对应的 MVC 模式中的 V,至关于 View 显示,该模块是不继承 Mono 的,也就是不挂接任何 UI 对象。LoginCtrl、HeroCtrl 模块至关于 MVC 中的 C,Control 控制,用于操做 LoginWindow,HeroWindow 所对应的 UI 窗体,好比用于控制不一样窗体的显示、隐藏、删除等等操做,在图中没有列出 MVC 中的 Model 模块,这个模块主要是用于网络消息数据的接收,也能够经过文本文件直接赋值的,它可使用列表进行存储,相对来讲用处并非不可替代的。
游戏中存在的窗体是很是多的,这么多窗体,若是不一样的开发者写逻辑,会搞的不少,不利于统一管理。由此须要一个类 WindowManager 管理类进行统一注册管理各个窗体类模块,这种处理方式也就咱们常常说的工厂模式。
另外,窗体之间是常常会进行不一样的切换,这些切换也能够对它们进行流程管理,由于窗体之间的切换是一种固定的流程。既然常常转换,咱们难免会想到状态机用于处理这种流程。在此,引入了状态机进行统一管理不一样窗体状态的变换。各个模块之间的耦合性也是要重点考虑的问题,在此采用了本身封装的事件机制进行解耦合。
具体实现逻辑以下,每一个窗体对应本身的类,以登陆 UI 为例进行说明,每一个 UI 都是一个 Window 窗体对象,它对应着 Loginwindow 类、LoginCtrl 类、LoginState 类。其余的窗体相似,而这些类都不继承 Mono 也就是说不挂接到任何 UI 窗体对象上,这样,完全实现了资源和代码的分离,UI 系统思想设计完成,接下来再介绍技能模块和角色系统的架构设计。
技能模块在游戏中的表现很是重要,也是常见的,在实现以前先把技能设计架构给读者展现,以下图所示:
关于技能的设计,首先要考虑的是这个技能是谁释放的,也就是说的游戏实体类,实体类的设计在此分了三层:IEntity、IPlayer 和 Player,这三个模块一样不继承 Mono,也就是说不挂接到任何对象上,具体的实现会在后面的章节中结合代码详细介绍,技能释放者找到了,接下来设计技能了。
游戏中的技能分好多种:正常释放的技能、被动技能、远程技能等等,这些不一样的技能咱们也将其进行模块化设计,其实它们的内容是相似的,能够考虑使用脚本自动生成代码。固然对于游戏中众多特效的使用,咱们也须要写一个特效管理类,用于建立不一样的特效,特效采用的就是模块化管理,特效实现了后,就要考虑特效是根据游戏实体对象的不一样动做进行释放的,不一样的动做对应着不一样的技能,这固然就是不一样动做之间的切换,在这里使用了 FSM 有限状态机进行统一调度。
再介绍一个重要的模块——对象池,由于咱们的特效会频繁的建立、销毁,还有游戏中的怪物 NPC 也是同样的。固然,其余的游戏管理类在游戏中都比较常见,其余的一些系统好比背包系统、任务系统,这些能够根据消息或者配置文件进行加载读取,这里就不一一说明了。
接下来介绍比较重要的网络游戏服务器,咱们的服务器使用的是 Photon Server,用户直接搭建很是方便,在本教程也会把服务器的搭建过程介绍给读者,咱们的网络架构采用的是房间模式,同房间的人能够在场景中实时同步,包括技能、动做等等。而该实时同步的实现方式采用的是状态同步,接下来介绍一下 Photon 服务器的体系结构:
为何选择 Photon Server 做为服务器,由于该服务器提供了负载均衡,以及作大型网络游戏 MMO 等技术实现,用户无需太关心。它的核心使用的是 C++ 编写的,效率无需使用者关心,同时该服务器支持 UDP、TCP、HTTP 和 Web 套接字,它的应用层使用的是 C# 编写的,对于用户编写逻辑很是方便,并且它也支持数据库和非数据库模式,好比 MySQL、SQL Server 等数据库,以及 MongoDB、Redis 等非数据库。
再介绍一下关于服务器的基本工做流程,从客户端角度来看,工做流程也很是简单,很是适合新手学习,客户端链接到主服务器,能够加入大厅,并检索打开游戏列表。当他们在 Master 主服务器上 CreateGame 操做时,游戏实际上并不建立游戏服务器,而是肯定人数比较少的游戏服务器,将 IP 地址返回给客户端。当客户端在主服务器上调用 JoinGame 或 JoinRandomGame 操做时,主服务器查找运行游戏的游戏服务器,并将其 IP 返回给客户端。流程图以下所示:
若是客户端与主服务器断开链接,使用刚收到的 IP 链接到游戏服务器,再次调用 CreateGame 或 JoinGame 操做,断线重连都没有任何问题。下面介绍游戏中比较重要的部分,MMO 游戏同步思想。
客户端中的地图,一样也会在服务器中虚拟一个跟客户端大小彻底同样的地图,角色就是在这些虚拟空间中同步,角色同步是在必定的区域内进行同步的,也就是在必定的区域内是互相“看见”的,这种看见与客户端的相机裁剪是彻底不一样的。效果以下图所示:
计算哪些对象在某些区域会频繁移动,这些对象可能会很是耗费 CPU 资源。加速这一计算的一个简单的方法是将虚拟空间划分为固定区域,而后计算哪些区域重叠。客户应该接收这些重叠区域中的项目的全部事件。最简单的算法使用方形的网格,有时咱们也称为九宫格算法,以下所示:
物体经过当前的区域推送事件,一旦特定的区域重叠,它自动订阅区域的事件通道,并开始接收包括物品推送的区域事件。为了不在区域边界频繁地订阅和取消订阅改变,引入了另外的更大的兴趣区域半径:跨越此外半径的订阅区域被取消订阅,客户端中止接收区域事件。用通俗的语言讲就是在服务器虚拟的场景中,会经过不一样的玩家生成各自的九宫格区域,其余 NPC 或者玩家在对方的九宫格区域里面,物体都会显示,离开本身的九宫格区域就剪掉,这样也会是考虑到效率问题,由于若是整个场景实时同步计算,这对于客户端和服务器压力都是很大的。九宫格区域若是重合那就把重合的部分都显示出来。以下图所示:
本教程实现的网络游戏架构设计,最终实现的效果图以下所示:
该图是简单的建立房间以及加入房间进行网络同步界面,进入游戏后实现的游戏中的效果以下图所示:
用户建立房间,其余用户加入房间,多人场景在同一房间中同步的效果以下所示:
经过此网络游戏框架能够快速的把网络游戏实现出来,本课程的最后会把服务器和客户端代码都奉献给读者,但愿对开发者有所帮助。从下章开始,本教程进行详细介绍架构设计实现。
游戏中的资源量是必需要考虑的问题,游戏品质的好坏都是经过资源表现的,这些资源的管理,做为开发者必需要处理的。对于游戏资源管理,一般的作法是简单的封装几个接口用于资源的加载,若是只是作个 Demo,这样作是没问题的,可是若是作产品,对于资源的需求量是很是大的,并且各个资源的加载也会因为使用不当,出现各类问题,并且游戏讲究的是团队协做,不一样的人会有不一样的需求,简单的封装几个接口很难知足需求,若是没有一个统一的资源架构管理,代码会出现各类接口版本,最后会出现大量的冗余代码,这样对游戏产品运行效率会产生影响。
另外,还要考虑游戏资源的动态加载更新,主要是为了减小游戏包体的大小,Unity3D 虽然为用户提供了 AssetBundle 资源打包,方便用户将资源打包上传到资源服务器,在游戏启动时会经过本地存放资源的 MD5 文本文件与服务器的保存资源最新的 MD5 码的文本文件做对比,根据其资源对应的 MD5 码不一样,将新的资源下载到本地使用,同时将资源文件代替本地的资源文件。咱们在封装资源管理类时,也是从产品的角度考虑资源管理问题。
下面开始讲解如何进行资源管理的代码封装,咱们对资源管理的封装作了一个比较完善的思考,代码模块以下图所示:
下面来告诉读者为何这么设计。咱们在游戏开发时,对于 Unity 资源,每一个资源都是一个 GameObject,只是单独的 GameObject 显然不能知足需求,由于资源既能够是 Scene,也能够是 Prefab,同时也能够是 Asset 文件。这就会涉及到不一样的资源类型,如何表示这些资源类型,好比我测试的时候可使用 prefab,而在正式发布时采用 asset,若是不作分类,在游戏发布时还要修改接口,很是麻烦。但若是设计一个通用的接口,对于资源类型可使用枚举进行表示,有了这些想法后,开始逐步去实施咱们的思想。
首先须要设计一个 ResourceUnit 模块,它是资源的基本单位,也是程序本身封装的资源基本单位,ResourceUnit 类的代码以下所示:
public enum ResourceType{ ASSET, PREFAB, LEVELASSET, LEVEL,}
上面就是咱们定义的资源枚举,每个加载的资源都是一个 ResourceUnit,它能够是 assetbundle,能够是 prefab 实例化,固然也能够是 scene。下面继续完善 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 类同时实现了资源的引用计数,该设计思想跟内存的使用比较相似,这样便于程序知道对于加载的资源何时销毁,何时能够继续使用,它还声明了一些变量,好比资源的名字等。
另外,程序要加载资源,首先要知道资源加载路径,其次要知道资源类型是 asset bundle 仍是 prefab。咱们一般会使用一个类专用于资源路径的设置,包括获取资源文件夹、资源路径、获取资源文件以及获取 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 AssetInfoManager mAssetInfoManager = null; 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(); private void Start() { } 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(); } else { } } break; case ResourceType.LEVELASSET: { } break; case ResourceType.LEVEL: { // } break; } } 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 资源的类处理。代码实现以下:
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>();}
该类主要做用是提供了加载 UI 资源的接口,同时会将资源放到字典中便于统一处理。
这样整个资源管理的设计就完成了,在使用时须要把 ResourceManager 类挂接到对象上,目的是为了同资源更新模块结合起来。
为何要使用消息分发函数?在 Unity 代码设计中,这个问题是不可回避的,由于在开发产品时,不可避免的是各个模块之间会有或多或少的联系,可是为了模块的扩展性,各个代码模块之间的耦合性必须下降,不然产品上线后,版本迭代会出现各类问题。有人可能会说,可使用单例模式、静态类等等,在此就给读者普及一下知识点。
先说一下单例模式,若是逻辑相对来讲比较简单,它是能够的,可是若是逻辑比较复杂,那单例的调用会很是频繁,从而致使逻辑混乱,这是不可取的。静态类是常驻内存的,在游戏开发中除了一些指定的加载数据常驻内存,通常不会使用过多的静态类,因此也是不可取的。并且单例和静态两者也不会下降模块之间的耦合性,最终咱们只能考虑消息分发函数,下面先介绍 Unity 引擎自带的消息分发函数。
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, 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 用于将事件和委托粗放到字典中,监听函数有了,对应的就是移除监听函数,移除就是从 Dictionary 字典中将其移除掉,它跟监听函数是一一对应的函数以下:
//No parameters static public void RemoveListener(EGameEvent eventType, Callback handler) { OnListenerRemoving(eventType, handler); mEventTable[eventType] = (Callback)mEventTable[eventType] - handler; OnListenerRemoved(eventType); } //Single parameter static public void RemoveListener<T>(EGameEvent eventType, Callback<T> handler) { OnListenerRemoving(eventType, handler); mEventTable[eventType] = (Callback<T>)mEventTable[eventType] - handler; OnListenerRemoved(eventType); } //Two parameters 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); } //Three parameters 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); } //Four parameters 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); }
监听函数和移除监听函数都封装完了,那么如何触发监听函数这就是咱们一般所说的广播函数,它与监听和移除也是一一对应的,代码片断以下所示:
//No parameters 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); } } } //Single parameter 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); } } } //Two parameters 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); } } } //Three parameters 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); } } } //Four parameters 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); } } } }
另外把 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)); }}
这样咱们的整个事件系统就封装完成了,最后告诉读者如何使用?首先须要先监听,将监听函数放在对应的类中,代码以下所示:
EventCenter.AddListener(EGameEvent.eGameEvent_GamePlayEnter, Show);
而后在另外一个类文件中,能够播放此消息。代码以下所示:
EventCenter.Broadcast(EGameEvent.eGameEvent_GamePlayEnter);
阅读全文: http://gitbook.cn/gitchat/column/5a3921aec5896e6e1cf1a129