序言,保卫萝卜项目作为自己学习整体游戏项目的开始,还是很有收获的。
项目初步实现了分管关卡地图编辑、场景结构、关卡选择、游戏地图等主要功能,同时内部构架采用了MVC加单例的构架,对我这种初学者还是很有启发,至少对游戏项目的构架有了个初步的概念。
学编程写博客是总结提高的重要手段,之前刚用MVC加单例(以后简称为MCVM,即Model,Controller、View 和 单例 Manager)的构架重构了单机斗地主项目,回头再来看自己的学习过程中还是有很多需要总结提高的地方,所以虽然仅仅是刚摸到了门边,还是要总结提高写几篇博客来。
一、项目整体构架的理解
当然目前对项目构架的理解仅来源于保卫萝卜项目、单机斗地主项目和初读《游戏编程模式》的学习,所以对别人的参考意义不大,仅是对自己的学习阶段的总结
(一)项目构架
先贴一张自己的《游戏编程模式》的读书笔记。
1、项目构架的作用
就对我自己而言,项目构架就是让我在制作具体游戏这个“盖房子”的过程中,区分出来哪些是钢筋、水泥沙子、砖石、地板等。
比如我想实现斗地主的发牌功能时,玩家能看到和操作的部分放到View的子类里,内部具体各个子牌库的卡牌交换,制作卡牌和分发卡牌的功能都放到Model的子类里,至于具体子类的如何分类设置,还是根据个人理解和习惯来。
2、构架的主旨:应对变化的灵活性和简便性
比如想在斗地主的基础上开发超能力斗地主,比如玩家可以发动超能力复制几张牌,或者抽走别人的牌,这样的新功能在完善的构架上就可以很简单的开发出来。
3、构架具体实操
对目前我这个水平来说,就是能重用的尽量都拿出来独立重用,能解耦的尽量解耦。
(二)MVC+M
1、MVC
MVC是个历史悠久的构架了,我也查阅了很多博客,感觉似乎没有很明确的界限。
个人理解是核心的要素是数据层和用户层的分离。
还是以斗地主为例来说,
(1)数据层:单张数据卡牌(不包含显示)作为内部数据操作的单元,具体的制作整幅扑克,洗牌、发牌、留地主牌、抢地主牌、分发并显示地主牌这些内部操作都放在Mode类的子类中。
(2)用户层:就指让用户能看到的部分,比如自己手牌,对面玩家的牌的背面,桌面打出的牌,还有用户操作的部分,比如抢地主的操作、出牌选牌的操作。
(3)为什么要分离?
最初的原因可能是为了大型项目的多人开发,比如后端和前端的区分。
对于我来说:
首先是解耦,这样单独改动内部逻辑或者界面要素就比较方便。
其次是重用,比如斗地主的牌库业务模型拿出来做杀戮尖塔、炉石传说、够级都可以一用。
最后是思路,给自己一个构架的思路,也就是解耦和重用的方法论。
2、M——XXXmanager各种单例类
单例类:一个类只有一个实例,并提供一个全局访问入口
在《游戏编程模式》被专门反对过,原因也是很明显:
全局属性破坏代码的易读性!全局变量促进耦合!对并发多线程不友好!
不过某次unity大佬的演讲也着提到了项目内尽量包含三类重要的单例类
--PoolManger 对象池模式,用于取出和暂存预制体,减少不可控的GC(垃圾回收)
--LevelManager 提供同步或异步跨场景的方法
--Savemanager 提供全局的保存方法和日志记录
所以,个人理解:
单例类因为太方便了所有要克制使用,正所谓犹豫不决扔进单例类,长时间如此就跟可读性、解构和重用性说拜拜了。
单例类尽可能的提供一些必需的、独立的全局方法,并尽可能的限制其全局访问,把访问权限局限在一定范围内。
二、MVC+M使用的一点小经验
因为目前只重构了单机都地主这一个项目,而且基本功能模块没有大改,基本上是把之前集中在GameControlller中的各个功能都区分到Model和View中,所以理解还很肤浅,而且也没有完全实现自己的进一步改进想法。
目前仅仅是自己的一个学习阶段的总结。
(一)程序基础类的设定
基础类是指游戏中的基础单元,
比如棋牌游戏中的棋子、单牌,卡牌对战游戏中的角色、单牌,
2D平台跳跃游戏中的地图格、怪物基类、物品基类和技能基类等,
RPG游戏中地图基类,怪物基类,任务基类,物品基类和技能基类等。
所以,游戏项目构架的基础是先把基础类确定并尽可能完善。
1、基本属性
可以枚举类型加以限制,比如纸牌的数字、权重、花色、归属等。
2、标志位
一般是Bool类变量,比如纸牌是否已显示
3、Tag
主要配合Unity的FindWithTag使用,比如指向性技能,自动寻路子弹等
(二)Model
1、概念与理解
由程序的基础类衍生出基础类的集合类的管理类
那么,比如卡牌与子牌库,地图格与关卡地图,Model子类就是用于管理这些集合类,比如斗地主中管理所有子牌库(玩家、桌面、地主栏牌、总牌库),比如塔防游戏中管理整个回合的运行。
对我自己而言,直观来说,就是游戏内运行的主要部分。如果人能够直接与游戏内的代码互动,游戏做到这步就完成了,当然人类不能,所有还要依赖用户层来让用户看到、听到和操作到。
2、Model类的具体细节
这里的Model类的基本细节都暂时按保卫萝卜内定义的框架来定义,自己暂时没有那个水平来修改。
如结构图所示,Model只有基本的SendEvent方法,用来通知View或Controller。
所以,Model可以说是MVC类内自由度最低的类,
引申来说就是以下几点。
(1)简而言之,Model类就是提供集合类的相关属性和操作方法
比如在斗地主中,Model-卡片管理类就是提供创建全套扑克、洗牌、发牌、传递牌、回收牌等方法。
Model-回合管理类就是提供回合进度状态、当前大牌、当前大牌玩家、当前地主玩家、当前地主牌等属性和开始回合、结算回合等方法。
供VC来具体调用
(2)Model类尽量避免直接调用其他Model类。
因为这样其自身没有提供这样的方法,而通过静态MVC类来调用又破坏了其解耦性与独立性。
如果确实出想了这种情况,有两种处理方法。
一种就是把两个model类合并,当然这里是出现了大量相互调用的情况,这意味着确实这俩Model类分的不合适。
另一种就是把数据打包进SendEvents的参数内,传递给V或者C让他们处理。
(三)View
1、概念与理解
用户层可以区分为UI和可见对象:
(1)管理UI的View类:UI比较容易理解,就是状态栏、积分栏、各类菜单以及血条、角色、小地图等UI要素。而View类一般是针对一个完整的菜单或者UI。
(2)管理可见对象的View类:可见对象简而言之就是用户/玩家能看到的部分,比如地图块、手牌和塔防中的塔、怪物、子弹等。一般来说都是生成器,比如子弹生成器、怪物生成器、塔生成器,卡牌生成器。
2、结构与实现
(1)View类可以通过GetModel方法访问Model类
——View应该只读的访问Model的数据。
个人理解其实这样设定存在滥用和失控的危险。
比如说如果整个游戏流程中间存在View调用Model而推进的部分,那么M与V之间的分离就遭到了破坏。
应该加上对View访问的限制,View类对Model的访问应该是只读的,不引起Model类内部及基础类变化的。
(2)View类既可以发送事件也可以接收多个事件。
——View主要包含两大类内容:
一是提供将数据转为化显示的方法,供Controller调用
二是接收用户输入(键盘操作、点击、拖拽和滑动等)并将其初步处理,并将其作为Event发出的方法
3、注意事项
(1)应该严格的区分输出流程和输入流程。不要滥用对Model的访问,导致View与model层高度耦合,而是Controller类失去了应由的中继作用。
简单来说
输出流程:提供内部数据到用户显示的方法,供Controller调用
输入流程:接收用户输入的方法,初步归纳处理,然后发送Events给Controller处理
(四)Controller类
1、概念与理解
controller类作为用户层和数据层的中间层,可以理解为业务逻辑层。
2、结构与实现
controller类无法发出事件,只能关联并接收单个事件,所以它是事件的流程终点
controller类除了无法发出事件,几乎拥有所有的MVC类权限(注册三种类,访问M和V)
(1)事件流程终点
个人认为理解Controller类的主要核心是它是大部分Event的终点。
而Event由的发出者来区分的话分为Model发出的类和VIew发出的类
● Model发出的Event:一般是Model内方法调用完毕所发出的事件,比如斗地主中的卡牌创建完毕,发牌完毕;另外一种应该是节点状态类,比如回合开始、开始出怪等。但由于Model本身是比较被动的类,Model发出的Event大部分是Model内的方法被调用、状态参数被修改后发出的事件。
Controller接收,一般会实现两种功能,一是Model间的通信,二是对View进行调用以更新用户层
● View发出的Event:一般是用户输入或者可见对象互动所发出的事件,起点是由用户或游戏进程产生的。
Controller接收,一般会实现两种功能,一是View间的通信,二是对Model进行调用以更新用户层
3、使用时的注意
● Controller和M/V的定位要严格区分开。
M提供内部数据相关的方法,
V提供修改用户层显示的方法,
C就是根据事件和事件参数来调用M/V提供的方法!
●复杂且需要频繁调用M/V的方法,不要写在C中,而应该把其中对数据层和业务层的内容分开,分别由M或V来书写,尽量以参数传递完成交互,也就是说要充分解耦。
(五)Events
1、概念和结构
Events作为MVC中组件间通信的重要载体,主要构成为 事件名字符串+事件参数类。
事件名字符串:
(1)用于和Controller一对一关联并存储到MVC类中的静态字典里;
(2)添加到View类的监听事件链表内,而且可添加多个事件。
事件参数类:
根据事件传递的需要注册相应的参数类,本质上是数据和实例打包传递。
2、类别
根据发出和接收者可以区分为 M-->C , V-->C , V-->V,M-->V者四种Event。
但对于后两种,我个人目前认为是需要严格限制和尽量避免使用的,因为会严重的破坏程序可读性,让人疲于寻找事件处理的终点。
为了更直观的理解,以RPG游戏来举例子
(1)V-->C :由用户层输入发起的事件,特征是随机性
举个例子如下:
玩家点击地面,发送点击移动事件E_MOVE,
并将点击的地面坐标、控制对象等信息打包进事件参数MoveArgs发给对应的Controller:C_MOVE
然后C_MOVE调用Model_Player的Move方法,
控制玩家类向点击的地面坐标移动。
(玩家类内部调用动画组件播放移动动画;
调用寻路模块来决定道路,循环判定道路条件等)
(2)M-->C:由于内部属性变化或流程进度而发起的事件,特征是必然性和规律性。
属性内部变化发起:
比如玩家类的经验值不停累加,达到升级条件了。控制玩家类的Model_Player内部修改玩家属性并发送E_LevelUp事件,C_LevelUp接收事件,调用响应的View组件,播放升级特效、音效。
流程进度发起:
玩家进入Boss房间,先播放文本和音效,再播放Boss出场动画,之后Boss随着血量或者时间的变化而进入不同阶段,同时房间内出现不同的陷阱、特效和小怪等,这些流程触发基本都是又Model发出的事件。
(3)如何避免使用 V-->V,M-->V和单例类发出的事件
这几种事件放在一起讲是因为这几种方式都很方便容易被滥用。
之前自己在重构单机斗地主项目时就犯了这样的错误,model发送事件View直接接受并处理,觉得Event和Controller一对一对应太麻烦,懒得新建Controller都扔给View接收。最终后果就是事件系统的失控,这还只是一个简单的单机斗地主项目,如果项目再复杂点估计还没完成主要功能就崩溃了。
如何避免呢?
● 坚持Controller为所有事件的终点。
无论事件从哪发起,什么类型,带什么参数,都交给对应的Controller来处理。
● 增强Event和Controller的重用性。
Event和Controller是一一严格对应的,那么稍微复杂点的项目岂不是要建立茫茫多的Event和Controller吗?比如Boss流程,难道每个Boss都要弄一套Event吗?
所以要对Event的数量和类别要提前规划和涉及,通过事件参数的设计来提高Event和controller的复用性。比如刚才那个例子,把事件对应的Boss信息和阶段信息都打包进事件参数,这样Boss流程就可以重复使用了。
● 谨慎并显式的使用不以Controller为终点的Event
(1)个人目前认为M-->V或直接调用MVC类而发送事件这两种是完全可以避免的。如果认识更深入了有变化的话再更新。
(2)V-->V 在特定情况下使用时要在事件命名上体现,比如 E_VV_SkillEffect。
目前思考来说应该局限在不涉及数据变化和调用时View组件间的通信,比如刷新出怪物的时候带一些特效,这样可能就需要怪物孵化器View跟特效渲染View之间进行调用。虽然也可以把特效渲染直接包含在孵化器View内部,但是为了解耦和功能相对独立,这样的VV通信也是可以接受的。
其实刚才这个例子想了下用V-C反而更好,所以如果确实不是VV能有巨大优势,还是尽量避免使用。
(六)部分关键类
1、封装参数类——Args:
● 将事件等部件需要传递,将事件相关的数据打包进参数内进行传递。每次发送时间前都要重新进行打包,接收后根据需要提取相关数据。
● 注意空对象检测,
● 具体结构可以把数据简单的参数类放到一个文件中,结构比较复杂的参数类再单独建文件
2、基础类——Data
● 将游戏的静态数据和基础对象封装为类
举例:Card.cs单个卡片、Level.cs关卡类、Point.cs路径点类、Round.cs回合类、Tile.cs地图格类格、事件名、基础路径名等关键数据
3、工具和规则类——Tools.cs
● 核心的工具类,提供XML读写、图片载入等功能。
● 游戏的静态规则,比如牌子判断、权重判断可以写成静态方法来使用。
4、可重用对象脚本——Objects:
● 一般挂在在自动生成对象的预制体上,全面管理预制体的生命周期。
● 游戏中主要可重用游戏对象的附属脚本,比如玩家、敌人、地块等
● 注意对象归纳和层级设置
5、静态数据类——StaticData:
● 主要游戏对象的信息封装和储存,比如角色初始属性、地图结构、游戏的核心参数、对象的三围属性等
● 角色存档的功能实现
● 可以进一步优化为XML文件或者数据库。