unity3d游戏框架的编写_unity游戏资源管理框架编写 - CSDN
  • 在进行项目架构阶段,游戏框架可以解决一部分问题。剩下的架构问题还需要根据不同的项目解决。总之游戏框架游戏架构的一部分。 关于锤子和钉子:   最近又拿起了《代码大全》和《暗时间》,想起来...
     在进行项目架构阶段,游戏框架可以解决一部分问题。剩下的架构问题还需要根据不同的项目解决。总之游戏框架是游戏架构的一部分。

    关于锤子和钉子:

      最近又拿起了《代码大全》和《暗时间》,想起来《暗时间》的作者维护了一个个人博客,就去逛一逛。
      这几天一直琢磨一句话:手里拿着锤子看什么都像钉子。于是翻到了博客《锤子和钉子》http://mindhacks.cn/2009/01/16/hammers-and-nails/。我的这个行为很好的阐述了什么叫:手里拿着锤子看什么都想钉子- -。
      看完之后深度自省了一下- -
      文章很有趣,推荐大家读下。
      对于框架,用锤子和钉子来比喻不太恰当,框架就像一把剑,而项目就是锤子。
      框架需要经过项目的千锤百炼,才会越来越锋利(当然我的意思不是为了写框架而写框架,框架是副产品,真正锋利的是自己)。
      对于这句话:手里拿着锤子看什么都像钉子。我的观点是:如果以前没使用过这把锤子,当你使用这把锤子的时候,就会给你带来新的视野,新的角度去思考问题。
      比如以前自己开发游戏都没有框架这种概念的,写代码都是重新造个小轮子,轮子很不好用,当时视野又小又不知道有其他替代的解决方案,当听到游戏框架这个词的时候才开始去思考关于框架的问题,当开始着手搭建自己的框架时,才会开始注意到以前没注意到的东西。
      所以,开始打造自己的框架吧!

    关于架构:

           首先推荐一篇关于架构的好文10年感触:架构是什么?——消灭架构!,文中作者很通俗地解释了什么是架构:
           架构是一个约定,一个规则,一个大家都懂得遵守的共识。那这是什么样的约定、什么样的规则、什么样的共识呢?
           我以包为例,我经常出差,双肩背包里装了不少东西。笔记本电脑、电源、2个上网卡、鼠标、USB线、一盒大的名片、一盒小的名片、口香糖、Mini-DisplayPort转VGA接口、U盘、几根笔、小螺丝刀、洗漱用品、干净衣服、袜子、香水、老婆给我带的抹脸膏(她嫌我最近累,脸有点黄)、钱包、Token卡、耳机、纸巾、USB线、U盘等。这个包有很多格子,最外面的格子我放常用的,比如笔、纸、一盒小的名片等;中间的格子一般放的是衣服、袜子、洗漱用品、香水等;靠背的那个大格子放了笔记本电脑,和笔记本电脑相近的小格子放的是两个上网卡、Mini-DisplayPort转VGA接口、大盒名片、记事本,和笔记本电脑相近的大格子放的是电源、鼠标、口香糖等。
      我闭着眼睛都可以将我的东西从包里掏出来,闭着眼睛都可以将东西塞到包里!但是,非常不幸的是,一旦我老婆整理过我的包,那我就很惨了,老是因为找不到东西而变得抓狂!更不幸的,要是我那个不到两岁的“小可爱”翻过,就更不得了了。
        这个包就是我放所有物品的“架构”,每一个东西放置的位置就是我的“约定、规则、共识”。倘若我老婆也知道我的“架构”、我的“约定、规则、共识”,那么不管她怎么动我的包,我都照样能够轻易的拿东西或者放东西。进一步,如果我的同事也知道我的“架构”,知道我的“约定、规则、共识”,那么他们什么时候动我的包,我也毫无所知!

    架构的典型组成部分:

      以下分类来自《代码大全》3.5小节的《架构的典型组成部分》,并用我的框架来做了一遍对比。

    由框架解决的架构问题:

    主要的类:

      书上写的是:架构应该详细定义所用的主要的类。在我的框架里,提供的主要的的类有QFSM(状态机),QMsgDispatcher(消息分发器)。在未来还会提供ResourceManager,GameManager等,现在框架中有一份实现,但是其实现有些复杂,不易阅读和理解,等有比较容易理解的实现时候会对其写一篇文章,还有一些主要的模块需要客户端自己实现。

    资源管理:

      包括资源加载/卸载,音频、模型、纹理等,是模块化管理资源还是统一管理资源?,我的框架目前有一份实现,但不够易用,未来会提供易用版本。

    国际化/本地化:

      很多游戏都会有海外版,国内版,各个国家的版本,如何进行切换/翻译?(我的框架未来会提供)。

    输入输出、错误处理:

      我的框架未来会提供错误日志。

    性能:

      1.如何检测?框架应该提供相应的数据。
      2.指标如何确定?速度?内存?成本?,游戏开发中还有Draw Call,GC等等(我的框架未来会提供)。

    由客户端解决的架构问题:

    程序组织:

      包括文件结构应该反映文件或文件夹之间的关系,要思考以什么方式组织比如:先按照模块分文件结构再按照MVC或者先按照MVC分,然后再按照每个模块来区分,再推荐一篇好文:3D手游开发实践《腾讯桌球》客户端开发经验总结:http://www.gameres.com/654759.html(文章略长),文章的第一小节就有讲到关于文件组织。

    数据设计:

      书中指的是设计数据库表。在游戏框架中是指提供给客户端使用的数据结构定义,包括何种结构定义玩家的数据信息,策划表结构的定义等等。好的数据结构定义 + 烂的代码质量 >> 坏的数据结构定义 + 好的代码质量。

    业务规则:

      属于游戏逻辑范畴,需客户端实现。

    用户界面设计:

      用户界面组件之间如何通信,如何管理?用户界面和业务规则还有数据之间如何通信?通信方面已提供消息分发器(QMsgDispatcher)。

    安全性:

      资源如何加密,如何防破解,防反编译,安全数据检查,服务器验证等等。

    可伸缩性:

      可伸缩性是指满足未来需求的能力,包括程序的可扩展性,用户量增长时系统的策略等等。

    互用性:

      如果预计这个系统会与其他软件或硬件共享数据或资源,架构应该描述如何完成这一任务。

    容错性:

      举个例子:当界面跳转时,系统不可以接受输入(我的框架不提供)。

    关于买还是造的决策:

      Unity可以有很多可以使用的插件、C#库可以使用,很多问题迎刃而解,(我的框架不会包含任何插件,项目不同需要的插件也不同)。

    核对表摘自《代码大全》:

        1.程序的整体组织结构是否清晰?是否包含一个良好的架构全局观(及其理由)?
        2.是否明确定义了主要的构造块(包括每个构造块的职责范围及其他构造块接口)?
        3.是否明显涵盖了"需求"中列出的所有功能(每个功能对应的架构块不太多也不太少)?
        4.是否描述并论证了那些最关键的类?
        5.是否描述并论证了数据设计?
        6.书否详细定义了数据库的组织结构和内容?
        7.是否支出了所用关键的业务规则,并描述其对系统的影响?
        8.是否描述了用户界面设计的策略?
        9.是否将用户界面模块化,使界面的变更不会影响程序其余部分?
        10.是否描述并论证了处理I/O的策略?
        11.是否估算了稀缺资源(如线程、数据库连接、句柄、网络带宽等)的使用量,是否描述并论证了资源管理的策略?
        12.是否描述了架构的安全需求?
        13.架构是否为每个类、每个子系统、或每个功能域(functionality area)提供空间与实践预算?
        14.架构是否描述了如何达到可伸缩性?
        15.架构是否关注互操作性?
        16.是否描述了国际化/本地化的策略?
        17.是否提供了一套内聚的错误处理策略?
        18.是否规定了容错的办法(如果需要)?
        19.是否正式了系统各个部分的技术可行性?
        20.是否详细描述了过度工程的方法?
        21.是否包含了必要的"买 vs 造"的决策?
        22.架构是否描述了如何加工被复用的代码,使之符合其他架构的目标?
        23.是否将架构设计得能够使用很可能出现的变更?
        24.你,作为一名实现该系统的程序员,是否对这个架构感觉良好?
    欢迎讨论!
    附:我的框架地址:https://github.com/liangxiegame/QFramework
    转载请注明地址:凉鞋的笔记:http://liangxiegame.com/
    展开全文
  • 2:Unity3D游戏客户端基础框架 一些通用的基础系统的框架搭建,其中包括: UI框架(UGUI+MVC) 消息管理(Message Manager) 网络层框架(Socket + Protobuf) 表格数据(Protobuf) 资源管理(Unity5.x的.....

    “游戏框架”作为整个游戏的框架,具有核心的地位,一个游戏的层次和后期维护性就取决于游戏框架。


    1:3D引擎框架图


    2:Unity3D游戏客户端基础框架

    一些通用的基础系统的框架搭建,其中包括:

    UI框架(UGUI+MVC)

    消息管理(Message Manager)

    网络层框架(Socket + Protobuf)

    表格数据(Protobuf)

    资源管理(Unity5.x的AssetBundle方案)

    热更框架(tolua)


    2.1:UI框架

     

    编写UI框架的意义:

    1:打开、关闭、层级、页面跳转等管理问题集中化,将外部切换等逻辑交给UIManager处理。

    2:功能逻辑分散化,每个页面维护自身逻辑,依托于框架便于多人协同开发,不用关心跳转和显示关闭细节。


    --通用性框架能够做到简单的代码复用和“项目经验”沉淀。

    基于Unity3D和UGUI实现的简单的UI框架,实现内容:

    1. 加载、显示、隐藏、关闭页面,根据标示获得相应界面实例;
    2. 提供界面显示隐藏动画接口;
    3. 单独界面层级,Collider,背景管理;
    4. 根据存储的导航信息完成界面导航;
    5. 界面通用对话框管理;
    6. 便于进行需求和功能扩展;
       

    2.2:消息管理(Message Manager)

    1. 一个消息系统的核心功能:
    2. 一个通用的事件监听器;
    3. 管理各个业务监听的事件类型(注册和解绑事件监听器);
    4. 全局广播事件;
    5. 广播事件所传参数数量和数据类型都是可变的(数量可以是0~3,数据类型是泛型)

    消息管理设计思路:在消息系统初始化时将每个模块绑定的消息列表,根据消息类型分类(用一个string类型的数据类标识),即建立一个字典Dictionary<string, List<Model>>:每条消息触发时需要通知的模块列表:某条消息触发,遍历字典中绑定的模块列表。


    2.3:网络层框架(NetworkManager)

    1. 除了单机游戏,限制绝大多数的网游都是以强联网的方式实现的,选用Socket通信可以实时地更新玩家状态。
    2. 选定了联网方式后,还需要考虑网络协议定制的问题,Protobuf无疑是个比较好的选择,一方面是跨平台特性好,另一方面是数据量小可以节省通信成本。
    3. Socket通信:联网方式、联网步骤,数据收发以及协议数据格式。(加入线程池管理已经用一个队列来管理同时发起的请求,让Socket请求和接收异步执行,基本的思路就是引入多线程和异步等技术。)
    4. Protobuf网络框架主要用途是:数据存储(序列化和反序列化),功能类似xml和json等;制作网络通信协议等。(Protobuf不仅可以进行excel表格数据的导出,还能直接用于网络通信协议的定制。)
    5. Protobuf是由Google公司发布的一个开源的项目,是一款方便而又通用的数据传输协议。(在Unity中可借助Protobuf来进行数据存储和网络协议两方面的开发。)

    2.4:表格数据

    1. 在游戏开发中,有很多数据是不需要通过网络层从服务器拉取下来的,而是通过表格配置的格式存储在本地。
    2. 游戏中的一个道具,通常服务器只下发该道具的ID(唯一标识)和LV(等级),然后客户端从本地数据中检索到该道具的具体属性值。(通常使用Excel表格来配置数据,可以使用Protobuf、JSON、XML等序列化和反序列化特性对表格数据转化。)
       

    2.5:资源管理(AssetBundle)

    1. AssetBundle是Unity引擎提供的一种资源压缩文件,文件扩展名通常为unity3d或assetbundle。
    2. 对于资源的管理,其实是为热更新提供可能,Unity制作游戏的资源管理方式就通过AssetBundle工具将资源打成多个ab包,通过网络下载新的ab包来替换本地旧的包,从而实现热更的目的。
    3. AssetBundle是Unity编辑器在编辑环境创建的一系列的文件,这些文件可以被用在项目的运行环境中。(包括的资源文件有:模型文件(models)、材质(materials)、纹理(textures)和场景(scenes)等。)

    Editor打包AssetBundle:
     

    //Editor打包AssetBundle
    [MenuItem(“Assets/Build AssetBundles”)]
    static void BuildAllAssetBundles(){
        BuildPipeline.BuildAssetBundles(Application.dataPath+“/AssetBundles”,
        BuildAssetBundleOptions.None, BuildTarget.StandaloneOSXIntel);
    }
    

    2.6、热更新框架(tolua)

    1. 使用C#编写底层框架,使用lua编写业务逻辑,这是业内最常见的设计方式,还有一个非常成熟的热更新框架tolua。
    2. 通常可热更新的有:图片资源、UI预制和lua脚本,而处于跨平台的考虑,C#脚本是不允许进行热更的。

     

     

     

     

     

     

     

     

     

     

    展开全文
  • 框架概述:做了那么久的业务开发,也做了一年多的核心战斗开发,最近想着自己倒腾一套游戏框架,当然暂不涉及核心玩法类型和战斗框架,核心战斗的设计要根据具体的游戏类型而定制,这里只是一些通用的基础系统的框架...

    框架概述:

    做了那么久的业务开发,也做了一年多的核心战斗开发,最近想着自己倒腾一套游戏框架,当然暂不涉及核心玩法类型和战斗框架,核心战斗的设计要根据具体的游戏类型而定制,这里只是一些通用的基础系统的框架搭建,其中包括:

    • UI框架(NGUI + MVC
    • 消息管理(Advanced CSharp Messenger
    • 网络层框架(Socket + Protobuf
    • 表格数据(Protobuf
    • 资源管理(Unity 5.xAssetBundle 方案)
    • 热更框架(tolua

    这里使用的引擎版本是:Unity 5.5.0f3

    UI框架:

    使用 Unity 5.x 进行游戏开发的朋友,估计大都想过用系统自带的 UGUI 来搭建UI框架,这并没有什么不好的,只是对于引擎升级并不友好,假如Unity升级对 UGUI 做了比较大的修改,那么对一个成型的项目进行引擎的升级,修改成本是不可估量的,所以这里我还是选用 NGUI 作为搭建UI框架的工具,除了结合 MVC 架构还要考虑后期接入热更新。

    消息管理:

    这里我们使用 Advanced CSharp Messenger 这种C#事件实现的消息管理器,特点就是可以将游戏对象作为参数发送。而且,这个先进的c#版本的消息传递系统会自动清理事件表在一个新的水平加载,这将防止程序员意外调用销毁方法,从而有助于防止许多 MissingReferenceExceptions这个消息传递系统是基于杆海德 CSharpMessenger 和马格努斯Wolffelt CSharpMessenger扩展。

    《Unity 3D游戏客户端基础框架》消息系统

    网络层框架:

    随着移动网络的升级,在4G网络早已普及的今天,除了单机游戏,现在绝大多数的网游都是以强联网的方式实现的,选用 Socket 通信可以实时地更新玩家状态,选定了联网方式之后,还需要考虑网络协议定制的问题,Protobuf 无疑也是个比较好的选择,一方面是跨平台特性好,另一方面是数据量小可以节省通信成本。

    《Unity 3D游戏客户端基础框架》多线程异步 Socket 框架构建
    《Unity 3D游戏客户端基础框架》 protobuf网络框架

    表格数据:

    在游戏开发中,有很多数据是不需要通过网络层从服务器拉取下来的,而是通过表格配置的格式存储在本地,例如:游戏中的一个道具,通常服务器之下发该道具的 Id (唯一标识)和 lv(等级),然后客户端从本地数据中检索到该道具的具体属性值。通常使用 Excel 表格来配置数据,但我们一般不会直接将 .xlsx 格式的表格原文件打包到游戏应用包中,通常会通过工具序列化为二进制文件的格式,读取数据的时候再进行反序列化。上面我们提到了使用 Protobuf 定制网络协议,但是其实 Protobuf 的序列化和反序列化特性可以满足我们对于表格数据转化的需求。

    《Unity 3D游戏客户端基础框架》protobuf 导excel表格数据

    资源管理:

    对于资源的管理,其实是为了给后面接入热更新提供可能,Unity制作游戏的资源管理方式就通过 AssetBundle 工具将资源打成多个 ab 包,对于可热更新的资源进行热更的时候,并非单文件的热更,而是通过网络下载新的 ab 包来替换本地旧的包,从而实现热更的目的。

    热更新框架:

    通常使用C#来进行Unity的开发,但纯C#脚本只能支持Android系统下的热更新,而对于iPhone系统则无能为力,所以这里通常需要引入一门脚本语言 lua ,使用C#编写底层框架,使用lua编写业务逻辑,这是业内最常见的设计方式,还有一个非常成熟的热更新框架 tolua (前称 ulua)。通常可热更新的有:图片资源、UI预制和lua脚本,而出于跨平台的考虑,C#脚本是不允许进行热更的。


    小结:

    上面只是大致说了一遍整个框架会涉及到的工具和技术点,下面我会对每个点进行逐篇细化,有兴趣的可以继续关注下,当然可以根据这些建议自己自学,那样也可能会快很多。

    展开全文
  • 本课程是 Unity 3D 系列教程,目标是带领读者搭建一个商业游戏的网络架构设计,该架构设计是游戏的核心技术,将采用 Unity 2017.2 最新版本作为开发工具。内容分为 UI 架构、技能架构、服务器和网络同步四大部分,共...

    课程简介

    本课程是 Unity 3D 系列教程,目标是带领读者搭建一个商业游戏的网络架构设计,该架构设计是游戏的核心技术,将采用 Unity 2017.2 最新版本作为开发工具。内容分为 UI 架构、技能架构、服务器和网络同步四大部分,共 13 篇文章。

    认真读完本系列文章之后,将会深入理解架构的设计,具备独立搭建网络游戏框架的能力,并在此基础上可以独立开发一款网络游戏。

    作者介绍

    姜雪伟,从事 IT 行业15年,现担任创业公司技术合伙人。著作有:《手把手教你架构 3D 游戏引擎》、《Unity 3D 实战核心技术详解》,《Cocos2d-x 3.x 图形学渲染技术讲解》等,参与或主导过十多款网络游戏研发。

    课程内容

    导读:网络游戏架构设计综述

    随着 Stream、TapTap 等游戏平台的崛起,越来越多的网络游戏在此平台投放,而且很多新发布的游戏收入都颇丰,这些发布的游戏很多都是几个人开发完成的,而且开发周期都比较短,如何才能快速开发网络游戏?一个比较好的游戏框架是非常必要的。另外,这些平台的崛起,对于独立游戏开发者来说,也是一个非常好的机会,换句话说独立开发者的春天又来了,当然对于那些想从事游戏开发或者说已经在这个行业从事游戏开发的人也是一个机会。

    现在游戏不只限于抄袭了,更强调创新,只要有好的创意,再加上一个比较好的游戏框架,几个志道同合的小伙伴就可以开发一款网络游戏。在国外有很多这方面的案例,几个人在不同的地方,一起开发一款游戏。而在国内很多普通程序员在游戏公司估计只是从事某项单一的逻辑功能编写,对整体架构设计并不是很了解,即使自己有好的想法局限于自己的能力估计也是很难做出一款游戏,在游戏公司很少有人会教你架构设计,而且对于开发者来说,要么只会客户端,要么只会服务器,非常少的人同时精通二者,这也困扰着那些想自己做游戏的开发者。本课程正是基于解决这些困扰程序员的问题,提出了一种基于网络服务器的架构设计,让程序员一个人可以同时进行客户端和服务器的网络游戏的开发,这样再加上美术和策划就可以搞定一款网络游戏。

    搭建游戏框架首先要搞清楚什么是框架?其实搭建框架的主要目的是便于游戏逻辑的编写,这样非常有利于开发者快速的开发游戏,框架的核心思想是模块之间的耦合性要降低。那我们先搞清楚游戏框架中主要包括哪些技术点,从大的方面说,每款游戏都有自己的 UI 系统、角色系统、技能系统、网络系统等等,往小的方面说就是编码的细节——每个类的编写。下面就把游戏中的几个核心系统的架构设计思想逐步介绍给读者,架构设计没有好坏之分,用着方便就可以,在这里就当是抛砖引玉,读者也可以在此基础上去扩展,去重新编写架构。这样本篇教程的目的就达到了。

    先介绍 UI 系统,这个是老生常谈的,UI 架构常用的设计模式是 MVC。读者应该对 MVC 都比较了解,原理就不介绍了,可以去网上查阅。下面讲下 MVC 模式如何在 UI 系统中使用?先看下面这幅架构图:

    UI框架设计

    我们就围绕着这幅图给读者介绍模块设计。

    在设计 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 系统思想设计完成,接下来再介绍技能模块和角色系统的架构设计。

    技能模块设计思想

    技能模块在游戏中的表现非常重要,也是常见的,在实现之前先把技能设计架构给读者展示,如下图所示:

    enter image description here

    关于技能的设计,首先要考虑的是这个技能是谁释放的,也就是说的游戏实体类,实体类的设计在此分了三层:IEntity、IPlayer 和 Player,这三个模块同样不继承 Mono,也就是说不挂接到任何对象上,具体的实现会在后面的章节中结合代码详细介绍,技能释放者找到了,接下来设计技能了。

    游戏中的技能分好多种:正常释放的技能、被动技能、远程技能等等,这些不同的技能我们也将其进行模块化设计,其实它们的内容是类似的,可以考虑使用脚本自动生成代码。当然对于游戏中众多特效的使用,我们也需要写一个特效管理类,用于创建不同的特效,特效采用的就是模块化管理,特效实现了后,就要考虑特效是根据游戏实体对象的不同动作进行释放的,不同的动作对应着不同的技能,这当然就是不同动作之间的切换,在这里使用了 FSM 有限状态机进行统一调度。

    再介绍一个重要的模块——对象池,因为我们的特效会频繁的创建、销毁,还有游戏中的怪物 NPC 也是一样的。当然,其他的游戏管理类在游戏中都比较常见,其他的一些系统比如背包系统、任务系统,这些可以根据消息或者配置文件进行加载读取,这里就不一一说明了。

    接下来介绍比较重要的网络游戏服务器,我们的服务器使用的是 Photon Server,用户直接搭建非常方便,在本教程也会把服务器的搭建过程介绍给读者,我们的网络架构采用的是房间模式,同房间的人可以在场景中实时同步,包括技能、动作等等。而该实时同步的实现方式采用的是状态同步,接下来介绍一下 Photon 服务器的体系结构:

    enter image description here

    为什么选择 Photon Server 作为服务器,因为该服务器提供了负载均衡,以及做大型网络游戏 MMO 等技术实现,用户无需太关心。它的核心使用的是 C++ 编写的,效率无需使用者关心,同时该服务器支持 UDP、TCP、HTTP 和 Web 套接字,它的应用层使用的是 C# 编写的,对于用户编写逻辑非常方便,而且它也支持数据库和非数据库模式,比如 MySQL、SQL Server 等数据库,以及 MongoDB、Redis 等非数据库。

    再介绍一下关于服务器的基本工作流程,从客户端角度来看,工作流程也非常简单,非常适合新手学习,客户端连接到主服务器,可以加入大厅,并检索打开游戏列表。当他们在 Master 主服务器上 CreateGame 操作时,游戏实际上并不创建游戏服务器,而是确定人数比较少的游戏服务器,将 IP 地址返回给客户端。当客户端在主服务器上调用 JoinGame 或 JoinRandomGame 操作时,主服务器查找运行游戏的游戏服务器,并将其 IP 返回给客户端。流程图如下所示:

    enter image description here

    如果客户端与主服务器断开连接,使用刚收到的 IP 连接到游戏服务器,再次调用 CreateGame 或 JoinGame 操作,断线重连都没有任何问题。下面介绍游戏中比较重要的部分,MMO 游戏同步思想。

    关于使用 Photon Server 做 MMO 游戏同步实现的思想

    客户端中的地图,同样也会在服务器中虚拟一个跟客户端大小完全一样的地图,角色就是在这些虚拟空间中同步,角色同步是在一定的区域内进行同步的,也就是在一定的区域内是互相“看见”的,这种看见与客户端的相机裁剪是完全不同的。效果如下图所示:

    enter image description here

    计算哪些对象在某些区域会频繁移动,这些对象可能会非常耗费 CPU 资源。加速这一计算的一个简单的方法是将虚拟空间划分为固定区域,然后计算哪些区域重叠。客户应该接收这些重叠区域中的项目的所有事件。最简单的算法使用方形的网格,有时我们也称为九宫格算法,如下所示:

    enter image description here

    物体通过当前的区域推送事件,一旦特定的区域重叠,它自动订阅区域的事件通道,并开始接收包括物品推送的区域事件。为了避免在区域边界频繁地订阅和取消订阅改变,引入了另外的更大的兴趣区域半径:跨越此外半径的订阅区域被取消订阅,客户端停止接收区域事件。用通俗的语言讲就是在服务器虚拟的场景中,会通过不同的玩家生成各自的九宫格区域,其他 NPC 或者玩家在对方的九宫格区域里面,物体都会显示,离开自己的九宫格区域就剪掉,这样也会是考虑到效率问题,因为如果整个场景实时同步计算,这对于客户端和服务器压力都是很大的。九宫格区域如果重合那就把重合的部分都显示出来。如下图所示:

    enter image description here

    本教程实现的网络游戏架构设计,最终实现的效果图如下所示:

    enter image description here

    该图是简单的创建房间以及加入房间进行网络同步界面,进入游戏后实现的游戏中的效果如下图所示:

    enter image description here

    用户创建房间,其他用户加入房间,多人场景在同一房间中同步的效果如下所示:

    enter image description here

    通过此网络游戏框架可以快速的把网络游戏实现出来,本课程的最后会把服务器和客户端代码都奉献给读者,希望对开发者有所帮助。从下章开始,本教程进行详细介绍架构设计实现。

    第01课:游戏资源管理实现

    游戏中的资源量是必须要考虑的问题,游戏品质的好坏都是通过资源表现的,这些资源的管理,作为开发者必须要处理的。对于游戏资源管理,通常的做法是简单的封装几个接口用于资源的加载,如果只是做个 Demo,这样做是没问题的,但是如果做产品,对于资源的需求量是非常大的,而且各个资源的加载也会由于使用不当,出现各种问题,而且游戏讲究的是团队协作,不同的人会有不同的需求,简单的封装几个接口很难满足需求,如果没有一个统一的资源架构管理,代码会出现各种接口版本,最后会出现大量的冗余代码,这样对游戏产品运行效率会产生影响。

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

    下面开始讲解如何进行资源管理的代码封装,我们对资源管理的封装做了一个比较完善的思考,代码模块如下图所示:

    enter image description here

    下面来告诉读者为什么这么设计。我们在游戏开发时,对于 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 类挂接到对象上,目的是为了同资源更新模块结合起来。

    第02课:自定义消息分发类模块

    为什么要使用消息分发函数?在 Unity 代码设计中,这个问题是不可回避的,因为在开发产品时,不可避免的是各个模块之间会有或多或少的联系,但是为了模块的扩展性,各个代码模块之间的耦合性必须降低,否则产品上线后,版本迭代会出现各种问题。有人可能会说,可以使用单例模式、静态类等等,在此就给读者普及一下知识点。

    先说一下单例模式,如果逻辑相对来说比较简单,它是可以的,但是如果逻辑比较复杂,那单例的调用会非常频繁,从而导致逻辑混乱,这是不可取的。静态类是常驻内存的,在游戏开发中除了一些指定的加载数据常驻内存,一般不会使用过多的静态类,所以也是不可取的。而且单例和静态二者也不会降低模块之间的耦合性,最终我们只能考虑消息分发函数,下面先介绍 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);
    第03课:游戏对象池设计
    第04课:UI 架构设计
    第05课:UI 架构案例实现
    第06课:角色系统设计(上)
    第07课:角色系统设计(下)
    第08课:技能系统设计
    第09课:游戏文件加载读取
    第10课:PhotonServer 服务器部署
    第11课:角色同步解决方案
    第12课:网络游戏案例讲解

    阅读全文: http://gitbook.cn/gitchat/column/5a3921aec5896e6e1cf1a129

    展开全文
  • 今天要和大家分享的是基于Unity3D开发2D游戏,博主一直钟爱于国产武侠RPG,这个我在开始写Unity3D游戏开发系列文章的时候就已经说过了,所以我们今天要做的就是利用Unity3D来实现在2D游戏中人物的走动控制。...
  • Unity3d实战之Unity3d网络游戏实战篇(6):服务端框架的搭建 学习书籍《Unity3d网络游戏实战》 罗培羽著 机械工业出版社 本文是作者在学习过程中遇到的认为值得记录的点,因此引用的代码等资源基本出资罗培羽...
  • 在经历了一段时间的忙碌后,博主终于有时间来研究新的东西啦,今天博客向和大家一起交流的内容是在Unity3D游戏开发中使用SQLite进行数据库开发,坦白来讲,在我的技术体系中Web和数据库是相对薄弱的两个部分,因此...
  • 引言:如何将 tolua 框架接入 Unity 项目中,这里假设我们已经有一个项目,并且已经实现了一些基础架构或者项目已经是完整的,此时,如何将 tolua 这个热更新框架接入进来而不影响原项目的结构?tolua 引入:1.资源...
  • MVC在桌面应用程序,以及网页架构上面用的比较多,那么怎么应用到Unity3d中呢,下面就带大家去了解这个设计框架,以及如何在Unity中应用。 二、MVC介绍 简介 MVC全名是Model View Controller...
  • unity3D 游戏开发之工程代码框架设计思路MVC 设计目的 1.使工程结构更规范。 2.提高代码可读性,封装性,拓展性  3.提高工作效率。 正文内容:  1.Frame的组成结 (1)视图层(View) (2) 控制层(Control)  (3)数据...
  • 这篇文章主要想大家说明一下我在Unity3D游戏开发中是如何写游戏脚本的,对于Unity3D这套游戏引擎来说入门极快,可是要想做好却非常的难。这篇文章的目的是让哪些已经上手Unity3D游戏引擎的朋友学会如何更好的写游戏...
  • Unity3d之MVC框架的使用

    2017-07-26 23:22:17
    Unity游戏的开发当中,我并没有刻意地采用MVC框架,因为不像网站开发那样,Model,View,Controller在游戏这个领域里还没有很清晰的定义。 究其原因,可能是由于不同游戏类型本身的软件架构可以相差很远,而且游戏...
  • 在本系列中,我们将在Unity3D中使用丰富的控件创建一个简单的3D游戏。 第一部分将介绍如何设置Unity3D。 第二部分将教您如何使用C#控制Unity3D。 本系列的第三篇也是最后一篇文章将深入研究使用Unity3D实际制作一...
  • 引言 最近到看一个 《贪吃蛇大战开发实例》,其中 贪吃蛇大作战游戏开发实战(3):系统构架设计 提供的系统架构的设计思路我觉得还是值得学习一下的,接下来的内容是我看完视频后的一点笔记。
  • Unity3d实战之Unity3d网络游戏实战篇(9):协议 学习书籍《Unity3d网络游戏实战》 罗培羽著 机械工业出版社 本文是作者在学习过程中遇到的认为值得记录的点,因此引用的代码等资源基本出资罗培羽老师的书籍,...
  • 今天想和大家分享的是目前在移动平台上较为流行的关卡系统,关卡系统通常是单机手机游戏如《愤怒的小鸟》、《保卫萝卜》中对游戏内容的组织形式,玩家可通过已解锁的关卡(默认第一关是已解锁的)获取分数进而解锁新的...
  • 掌握Unity3D基本元素 1.1 简单的游戏 1.1.1在场景中创建一个立方体 1.1.2编写可以使立方体运动的程序 1.1.3测试游戏1.1.4总结1.2 资源导入1.3 山体系统1.4 灯光1.5 材质1.6 预设1.6.1制作预设1.6.2例子1.7 声音...
  • 承接前面的内容,继续来学习Unity中的程序基础框架 音效管理模块 顾名思义,作用:统一管理音乐音效相关 ProjectBase下创建一个目录Music,再在下面创建一个MusicMgr,它也是继承自单例模式模块 以前我们处理...
  • 但凡只要懂一门编程语言的人都能使用 Unity 3D 引擎开发,另外 Unity 3D 的内部架构设计非常好,采用的是组件开发,开发者能快速通过组件堆积出一个游戏。既然使用 Unity 3D 引擎开发游戏这么简单,那它有没有坑呢?...
1 2 3 4 5 ... 20
收藏数 3,580
精华内容 1,432
关键字:

unity3d游戏框架的编写