用Unity做的一个类Moba游戏Demo
但是!!在本游戏中,每个单位都是有技能的,这些单位的技能毫无疑问应该是之前已经编辑好的技能,这会遇到以下几个问题:
根据上面两点,可以知道如果单位、物品、状态技能都是深拷贝自技能列表中的技能,那么可能游戏的内存消耗会相当巨大了,因为很多时候技能都是有共性的。
举个例子,比如多个技能之间释放的技能特效可能是同一个特效对象(Prefab)。这个比较好解决,只要在深拷贝的时候,不深拷贝特效对象就行了。
但是,同种技能之间的共性更多在他们的MP消耗,技能范围,技能图标,技能描述等属性上面。
举个例子,在游戏中有一招“地狱火”技能,随着等级的提升,它的MP消耗,技能范围可能会扩大,但是他的技能描述、技能名、技能图标这些是不会变化的。
而如果有一个英雄(比如“恐惧魔王”),他会这招“地狱火”技能,那么按照上面说的方法,他就深拷贝了一份技能列表中的地狱火。此时内存里有一份地狱火的技能描述、技能名、技能图标等属性。
这时,又有一个物品(比如“恶魔岩石”),使用这个物品的效果同样是“地狱火”技能的效果,那么按照上面说的方法,这里又得深拷贝一份技能列表中的地狱火。此时内存已经有两份同样的地狱火技能描述、技能名、技能图标等属性了。这就会带来多余的内存占用率。
同样的思路,如果此时又有一个状态,比如死亡后会释放地狱火技能,又得深拷贝一份。
这里其实可以敏锐的察觉到,其实,技能对象中,大多数属性是不用变化,不用深拷贝的,这些属性,不论这些技能是在什么对象上,无论这些技能是由英雄释放的,还是物品释放的,都是不变的。
而技能对象的另外一些属性,才是真正会发生变化的,比如它的等级(由英雄加点发生变化),缓存的CD时间(经过多长时间可以继续释放它,由上一次释放它的时间决定)等属性,是每个具体的技能对象不同的,而其余的基本都相同。
下面以主动技能为例,列举一下其中变化和不变的属性。
不变的属性有以下几种:
......等等
变化的属性有以下几种:
可以看到,每个技能不变的属性有相当多,而每个技能对象不同的属性却只有两个(暂时只有两个,或许后续会增加)。
这种时候,就应该使用享元模式来管理技能对象的数据。
使用享元模式管理,那么最后在每个单位上的技能不变的那部分属性都只需要引用技能列表中的技能就够了,而不变的属性则由它所依附的那个对象来使用。
那么如何使用享元模式来管理呢?
我的做法是,新增一个SkillModel类,每个Skill对象都会引用一个SkillModel对象,这个Model类中的属性即为这个Skill不变的属性,即技能名、技能描述等属性。
而该技能会变化的属性,即Level等属性,则作为Skill类的属性,在游戏中会发生变化。
最后,我们在外部编辑器编辑的SkillModel的属性,读取进来也是SkillModel对象,全局存储的也是类似List这类属性,而具体的技能则是必须通过SkillModel来进行创建。
这样,每次都需要深拷贝的一部分技能类的属性,就变成了公共的属性,只需要Skill类中存储一个指向这些Model类的指针的就行了。
前面提到,在本游戏中,由技能组设定(SkillGroup)类增加的技能的易扩展性,也就是多个操作来组成一个技能,技能的种类由操作的排列组合形成。
实际上,我们在外部编辑器中,输入的就是这些操作的参数。每个原子技能类都各自继承自ActiveSkill和PassiveSkill类,他们也会各自派生出其他属性。
举个例子,比如派生于ActiveSkill的AdditionStateSkill(为某个目标增加一个状态的技能),就会有一个独立属性StateID,即要附加的状态的ID,这是ActiveSkill所没有的。这个时候,按照上面的做法,还得再派生自基类ActiveSkillModel得到一个AdditionStateSkillModel类,若是有多个原子技能,就会有多个这样的Model类,如果要一个个都全部编写,那效率会比较低下。
这里我采取一个方案,在BaseSkillModel类中使用一个类型的字典,存储这些所有的子类中的额外属性(相比于父类)。
而当要使用这些属性时,就直接从字典中取出该属性并进行强转。
前面提到享元模式中基类的属性就是在整场游戏中基本不会变化的属性,同时,这里的Model类还担任一项职责,那就是保存技能数据编辑器中的技能基本数据。也就是说,这个技能需要的mp,伤害等,一开始也是由编辑器输入的。
但是!对于一个技能来说,当它的等级变化的时候,它的一些属性理应也发生变化,如它的伤害,需要的mp,冷却CD等等。魔兽地图编辑器的做法是将这些属性按照等级变成N个序列值,某等级对应序列中一个值,但这样的话如果技能有100级,那设置技能的人岂不是要累死。。。
这里我采用的做法是在技能编辑器中设置基础属性,其变化属性根据公式及基础属性来进行计算。这样,在Model类我们设置的就是基础属性,即技能等级为1级时的默认属性,而当技能等级上升后,对应技能的属性会根据公式进行计算。
使用享元模式蛇者技能系统最重要的一点就是,可以很轻松的将技能(Skill)拷贝给某一对象(单位,技能组,物品,状态等),而无需使用深拷贝.
下面描述使用样例:
首先从AssetBundle中读取已设置好的所有技能对象
将得到的技能对象(Skill : BaseSkill)用数组的形式保存在内存中,之后要使用的时候,就直接访问这个全局的技能数组对象(假设为skills)
假设此时有一个物品它需要3号技能,那么,将物品的skill设置为 skills[3].copy();
copy方法的伪代码实现如下:
public override BaseSkill copy(){ BaseSkill skill = new BaseSkill(this.skillModel); return skill; }
上述代码的意思是,直接新建了一个新的技能对象,然后简单的让他的SkillModel指针指向这个需要被拷贝的技能对象的Model对象.
这是因为Model对象中保存了这个技能对象基本不变的属性(已预先在编辑器中设置好的),所以Model对象就是享元模式中不变的那个基类对象,只需简单用个指针引用即可.
在MOBA游戏中,一个物品有多种功能。下面简单举例:
可以看出物品其实可以看成是拥有一堆主动、被动技能的物体,只要携带在英雄身上(物体未消耗完毕),英雄就可以使用这些主动、被动技能。
对于游戏策划来说,他只需要设计每个物品的前缀装备就行了,也就是,每个高级物品,他需要的低级物品列表,对于后缀装备列表,我编写一套输入系统根据前缀装备进行自动输入.
一个最直接的原因是,实现装备合成的方法,我放在了人物的GetItem方法中,也就是说,当玩家获得一件装备时,系统会首先观察这个新获得的装备是否可以和目前装备栏的现有装备进行合成出高级装备.
这个判断的过程也就是查找的过程,如果没有后缀装备这个属性,要遍历整个物品表来查找是否可以合成.
当有后缀装备属性时,只需遍历后缀装备列表,查看是否物品栏内有装备可以一起合成这个高级装备.
状态是一种持续影响某一物体的表现,最典型的状态比如:中毒、伤害加深。
当单位受到中毒状态时,他会每隔一段时间受到一定比例的伤害。
而当单位受到伤害加深状态时,他会在遭受攻击时,受到的伤害加深。
更为特殊的状态,当单位拥有某一种特殊状态时,它的攻击会带有某种特效之类的。
从上面的描述中,可以看出状态有以下特点:
根据功能描述,可以把状态看成是一个在Update状态不停作用单位的一种Mono类。
但是,对于状态拥有的某些特殊功能,仅仅是上面这些还不够。对于状态的特殊效果,可以看出它的描述特别像被动技能。
在这里,可以将状态定义为:
英雄单位相对于普通单位,有以下三点特殊属性:
这三点属性的增加会导致单位的基本属性增加(攻击力、防御力、闪避率、hp、mp等等),下面给出详细叙述。
力量:
每点增加x点HP
每点增加x点hp回复速度
每点增加x点攻击力(主属性加成)
敏捷:
每点增加x点防御力
每点增加x点攻击速度
每点增加x点攻击力(主属性加成)
智力:
每点增加x点MP
每点增加x点MP回复速度
每点增加x点攻击力(主属性加成)
投射物是一个比较特殊的单位,在一些技能和单位的远程攻击里面出现,投射物一般有一个目标位置。
要明确的一点逻辑是,投射物本身是不具有伤害的,具有伤害的是发出投射物的单位,所有投射物都是由单位(继承自CharacterMono的所有子类)产生的,由该CharacterMono类指定伤害。
总结一下,也就是,由CharaacterMono类监听投射物的事件,当投射物到达目标地点后,触发伤害事件,调用CharacterMono的GetDamage的方法对敌人造成伤害。
对于一些攻击,如普通攻击等,需要计算敌我的防御值、攻击值,才能产生伤害,这些都是由Character类做的事情,投射物本身不进行任何伤害计算,他只是一个传递者。
对于一个投射物来说,他有两种飞行轨迹:
一种是直线的,也就是直接从某处平移到某处,这种飞行轨迹容易实现,只需让投射物对着目标地点进行均匀的平移就可以了
一种是有弧线的,这种飞行轨迹一般用于弓箭、炮弹、飞行生物降落的攻击等投射物上,这种飞行轨迹较难实现,因为对于弓箭这种投射物,它的旋转角度也会发生改变,对于从高处降下,基于DoTween插件可以较为容易的完成这项功能。
为了统一两种飞行轨迹,可以直接都使用DoTween插件中的DOLocalPath方法进行计算,对于弧线轨迹中的旋转基于DoRotate来进行旋转(计算好duration)。
在游戏中有很多地方需要播放音乐,我将播放音乐的单位(或者情况)分为场景音频、单位音频和UI音频。所有音频的接受者都是摄像机。场景音频、UI音频为2D音频,单位音频为3D音频。场景音频、UI音频的AudioSource在摄像机上,而单位音频的AudioSource在单位身上。
场景音频主要是整个游戏的BGM播放,由摄像机作为音频的播放者。需要注意的场景BGM是轮播的,也就是说,需要有一个脚本来管理轮播的BGM,以及决定是否轮播以及什么时候播放下一首BGM。
单位是游戏中主要发声对象,而单位在游戏中我分为两种,一种是生命实体(泛指一切拥有CHaracterMono组件的GameObject对象),一种是特效,特效粒子在后面进行解释,现在先看生命实体的发声机制。
他有可能在以下情况播放音频:
两种方法设置单位音频,一是为单位设置声音模块,基于观察者模式在单位特定事件触发(攻击、死亡、施法)时播放音频,二是基于反射对各个方法进行拦截,使用AOP的方法在单位执行特定方法(Attack,Spell)时执行。
因为基于反射的方法性能低下,同时基于AOP的方法只能在方法执行前后对音频进行播放,而不能在方法执行时进行播放(这样的话,可能会造成单位闪避了,但还是播放了单位受伤的音频),所以放弃方案2,启用方案1。
下面简单介绍一下方案一:
单位基于一个声音模块CharacterAudio来对单位播放的音频进行管理,CharacterAudio在初始化时,根据CharacterModel设置的基本属性(攻击时使用什么音效,死亡时使用什么音效,此处使用的是这些音效的地址),对Resource(也可以使用AssetsBundle)进行读取,根据这些音效的地址,读取一个个实质的AudioClip对象进内存,并以字典的形式保存到单位身上(CharacterMono)。
同时,声音模块拥有一个绑定方法Bind,在初始化时,声音模块订阅单位身上的事件,当特定事件触发(攻击、死亡),播放特定的声音。
在玩家对对应的UI进行点击的时候,会有对应的音频进行播放,大致有以下事件,下面简单列举一下:
在游戏中,特效基本为GameObject对象,因为一个GameObject对象可能同时包含多个粒子特效系统,所以在创建一个特效时,一般我都以GameObject对象为初始化的模板。
在游戏中,出现特效的时机一般为以下几点:
一个单位出现特效的地点有以下几点(根据单位身上的collider来判断位置):
对于特效出现的时机,我目前使用的方法是将特效出现的代码 耦合 到技能类BaseSkill类中,在技能类的Execute方法中,负责在双方身上创建特效对象。对于特效对象的创建,使用临时对象工厂。
一个特效对象,是有其存在时间(生命周期)的,一般来说分为以下几种:
一开始的思路是通过一个临时对象工厂(TransientObjectFactory)来负责创建临时对象并管理临时对象的生命周期,但是,因为临时对象最常用的一个方法--在N秒后自动消失,播放完自动消失等特性,工厂如果不开辟协程,将很难实现这种效果。但是没开辟一个协程,都会造成内存的浪费,故这里放弃这个方案。
我采用的方案是,创建一个用于管理特效对象生命周期的MonoBehavior组件,还是用工厂模式来创建粒子特效对象,但是,这里的工厂模式不负责管理对象的生命周期,只负责创建,在创建时,工厂对象自动为这个GameObject对象AddComponent一个EffectsLifeCycle组件类。
EffectsLifeCycle组件在Update(或FixedUpdate)方法中检查对象消失条件,一旦满足对象消失条件(如:经历一段时间、播放完粒子特效),就Destory目标对象。
对于状态型的持久型特效对象,由状态对这些特效对象进行管理。也就是说,当进入某个状态时,自动产生某些例子了特效对象。当离开状态时,自动Destroy这些粒子特效对象。
下面列举一下各种特效对象生命周期的管理:
编辑器用来对游戏中常见的数据进行编辑,如单位属性、技能属性等等。编辑器可以让非程序员完全不用与数据存储格式打交道(也就是说,使用编辑器的人不用关心到底是用哪种格式来存数据,如txt/json/xml/sqlite)。
从而可以让策划快速验证一些东西,比如某些数值公式、自定义编辑的技能的效果等待。
好的编辑器,要做的事情只有两点:
第一点,必须要完全没有学习成本,并且可以大量减少策划和程序工作量。也就是说,对于数据和公式的编辑,是不复杂的(就算是复杂的,最后也要简化成基本类型的数据填写)
第二点,必须要在策划编辑完数据后(或者编写的同时)展示数据编辑后的效果。举个例子,对于技能编辑器来说,策划必须要知道刚才编辑的技能特效在游戏中具体是什么样的,最后编辑好的技能在游戏中释放后具体是怎么样的。
暂无开放 Issues,或尚未同步最近议题。