首页 > 代码库 > 游戏开发——战斗系统之技能设计
游戏开发——战斗系统之技能设计
现在在做的项目大致分为两块:战斗系统和除战斗系统之外的(简称外围系统),而我一直在做的是外围系统的开发,至少在6月份返校毕业答辩之前没有动过战斗系统。答辩回来之后很长一段时间内也是在做外围系统的bug修复,可是由于种种原因项目赶不上所谓的进度了,上周五主管问我和另外一个也主要负责外围系统开发的同事谁更忙,我一句我没啥事干,结果主管说战斗系统的主动技能让我来做。这周一开会负责人一纸任务安排扔下来,上面写着XX同事这周完成主动技能的开发及相关系统的bug修复,还说没完成任务就XXX,此处省略XX字。
再稍微说下目前本人的情况:不是计算机相关专业毕业的,去年学了两个月C++,一个月Cocos2dx,然后十月份入职现在的这个公司,因此相关技能水平就。。。现在在做的项目加上被砍掉的项目,共经历过两个项目开发。本着一腔热情加上强大的互联网资源,到目前为止项目组安排的任务都按时完成了。所以可以说是个稍微合格的游戏开发从业者吧。
扯了这么远回到正题吧。技能的设计,首先我来理解一番游戏中的技能。在回合制游戏中,主动技能的流程大致为:释放技能,得到技能影响值(项目组天天喊的buff)并确定技能对象,技能检测+影响值添加到技能对象。而这次的主动技能设计我也是照着这个思路来的,因此本文可以据此大致分为三块来讲:技能释放部分,buff生成部分(不添加至对象),技能检测部分。(本项目的技能相对来说较简单,技能释放不考虑魔法值,buff生成不考虑别的影响因素,技能检测不涉及命中率)
战斗:
己方队伍+敌方队伍,说白了就相当于一个卡牌游戏。
一、释放技能
本项目的释放流程大致为:战斗场景(BattleScene)中,当前操控的己方队员达到技能CD时间,玩家主动执行某个操作释放当前战斗队员的技能。释放的技能可能是一个组合技能也可能是单个技能。
在这里要控制的是:技能信息(影响值、技能释放者、技能作用者、技能影响回合),技能管理(触发检测、产生技能影响、清除影响)。因此我首先想到的时候要在该场景里生成一个技能管理器,负责处理这里要做的技能控制。
写了一个技能管理类:skill_command(称为技能控制器吧),大致成员如下:
class skill_command { public: /** *这个成员函数返回值用来做对是否是即时生效型技能的判读 **/ bool init(); private: skill_info _info; //技能的信息 role* _user; //技能使用者 role* _obj; //技能作用者 }
因为技能是在BattleScene释放,且需要在该场景下做一系列的检测,所以我的想法是将技能管理作为该场景内的成员,在该场景内执行技能管理类的检测函数。而考虑到技能可能是组合技能,所以在场景类中,我以一个vector存储技能管理对象。
class BattleScene { private: vector<skill_command*> vecSkillCmd; }
采取了这样的设计之后,每一次操作队员释放某个技能,先分解技能组合,产生一个技能信息则new一个指针存储在vecSkillCmd中:
vec = analyze(skill) for(//做个循环) { auto skill_cmd = new skill_command(); skill_cmd->init();//在这里还需要设置技能信息及作用对象等 vecSkillCmd.push_back(skill_cmd) }做这样的设计考虑的是:释放一个技能,将其组合技能分解出来单独控制,化繁为简。也因为我已经得到了一个技能产生作用需要的信息(记录了技能信息、技能作用者和使用者),所以我在技能作用、检测阶段就不需要额外再向用户需要诸如技能对象等信息,只需要在场景中提供检测条件即可,条件达成了就可以直接在skill_command内部实现技能影响。想当初上一个项目我也参与到了战斗系统后期的维护工作,做的一块就是优化技能的效果表现,当初碰到最大的麻烦就是很难确定技能对象,所以那时候好苦逼的在找技能对象。所以这一次我的想法是,不管技能如何我首先要做的就是拆解开来,单独的确定其使用者和作用者,这样每次一次做技能检测和产生技能效果我都能找到对象。
设计案中也提到,技能可以分为即时作用和延时作用。指的是某些技能是一使用就会产生效果(如发射子弹、发射激光等),而有些是需要达到某个条件(如碰撞到了敌方队员)才会产生效果。所以基于化繁为简的考虑,在分解技能信息,产生技能控制器的阶段我就将这两类技能分开处理。具体做法是在BattleScene场景内再添加一个成员专门用来存储即时作用的技能:
class BattleScene { private: vector<skill_command*> vecSkillCmd; vectpr<skill_command*> vecInstanceSkillCmd; }这样处理之后,在生成技能控制器阶段依据一技能生效类型分开存储至不同的管理容器内。在检测阶段就只需要在不同的条件阶段,循环检测不同的容器内容,就可以达到我们的设计目的。如对于即时生效的技能,我只需要在释放完技能后对vecInstanceSkillCmd的成员做检测即可。当然会说直接将vecSkillCmd放在这里检测不就可以了么?这就是我如何将一个技能管理器skill_command存储的考虑了。
上文提到了,依据技能的作用类型分别会将技能控制器存储至vecSkillCmd和vecInstanceSkillCmd。这里考虑了其作用时间,同时我也考虑了其作用条件。因为有的技能是即时无条件作用,而有的是即时有条件作用。针对即使无条件作用的,因为此种技能即时无条件作用了,所以我不需要在释放完之后再做管理,因此我没有存储至vec中。而即时有条件作用的才会存储至vec中以备之后做检测,这里我们用了一个成语函数releaseSkill表示技能释放动作:
void BattleScene::releaseSkill() { vec = analyze(skill); for(//做个循环) { auto skill_cmd = new skill_command();//生成一个技能控制器 skill_cmd->init();//在这里还需要设置技能信息及作用对象等 if(//如果不是立即生效) { vecSkillCmd.push_back(skill_cmd); } else//即时生效 { if(//无条件触发) //执行触发效果 { skill_cmd->triggerSkill(); CC_SAFE_DELETE(_cmd);//删除该指针 } else { vecInstanceSkillCmd.push_back(skill_cmd); } } } }进行到这一步,关于技能的释放部分就差不多了。而在项目中我做的技能释放也就是这样一个操作。
二、buff生成
其实上周五安排我做技能的时候,我一开始想的是如何做buff的生成。同事也提到要写个通用的接口,可以做道具的处理(加血、加速等道具),这样就可以省时省力。所以首先做的工作便是做buff,当然也没办法,因为那个时候没策划跟进我该如何做技能,直到这周三勉勉强强才把方案拿过来,此处又要省略XX字了。
因为我们已经有了一个role角色类,我想到buff不就是加到角色上的么,因此我的设计方案是如之前写技能控制器一样,在role角色类内添加成员作为buff控制器。也是基于化繁为简的考虑,一个buff就生成一个控制器,buff控制器内部也是包含buff信息,作用者和使用者等信息:
class buff_command { private: buff_info _info; //buff的信息 buff_user _user; //buff的使用者 buff_obj _obj; //buff的作用者 }每一次产生了一个buff,就生成一个控制器,与技能控制器的设计思路类似:在role角色类内部以容器管理这些控制器,每次使用技能或者使用道具而得到了一个buff,就生成一个buff存储至容器内,以便做后面的技能检测和buff效果生成:
class role { public: void addBuff(); private: vector<buff_command*> vecBuffCmd; }
role角色类一个buff管理成员和一个buff的生成函数。buff的生成函数在技能控制器的初始化函数内部调用,其大致内容如下:
void role::addBuff() { //检测重复buff check(); auto buff_cmd = new buff_command(); buff_cmd->init(); vecBuffCmd.push_back(buff_cmd); }
跟技能控制器的生成类似,这里有一步的内容是做buff重复性检测:已经触发的buff删除其buff效果,而没有触发的则直接从容器中删除。
结合第一部分的内容大致做个流程介绍:
1、首先在SceneBattle场景触发技能使用:
battle_role->releaseSkill();2、触发技能使用之后解析技能组合生成技能控制器,初始化技能控制器信息:
auto skill_cmd = new skill_commant(); skill_cmd->init();3、生成技能buff:
bool skill_command::init() { initSkillInfo(); //技能作用对象生成buff信息 skill_obj->addBuff(); }经过以上三步,简单的一个buff便已生成。
三、技能检测
这一部分我感觉有点复杂,也是我拿不准的地方。周六刚测试了一遍整个释放-添加-检测逻辑,在现有的几套技能中是没有问题的,就是不知道之后的会怎么样。
项目的技能检测设计为:首先要检测触发条件,再检测生效条件。既首先一个技能要因某个条件而被触发,如己方队员碰了一下对方,或者己方队员碰到了某个场景元素,那么该技能便达到了触发条件;满足了触发条件之后,接着去做技能的生效条件,如该技能是要释放者碰到了对方才会释放,或者碰撞到了己方队友才会释放等等。所以在技能控制器skill_command内部写了两个成员函数:
class skill_command { public: //用来做触发条件的检测 void checkTriggerCondition(); //达到触发条件了,则触发技能 void triggerSkill(); private: bool _bOk; }
技能的检测主要就是靠这这两个成员函数来实现,当然实际上这两个函数是带参数的。调用函数的地方是在BattleScene场景中:
BattleScene:://某个动作 for(//) { auto skill_cmd = vecSkillCmd[i]; //每次先做条件检测,达到条件了之后控制器内部的bool型成员为true skill_cmd->checkTriggerCondition(); //内部bool型成员为true的时候执行该成员函数的内部动作 skill_cmd->triggerSkill(); }
因为之前的设计中在技能控制器内部有技能作用者这一成员_obj,所以实际上技能的触发是调用_obj的成员函数来实现:
bool buff_command::triggerSkill() { if(_bOK) { _obj->triggerBuff(); } }而我设计的触发buff这个成员函数,里面的实际动作是对role这个类里面的buff容器进行管理:
void role::triggerBuff(//带能找到该buff的一个参数) { for(//) { if(//是该buff) { auto buff_cmd = vecBuffCmd[i]; buff_cmd->trigger(); } } }这一步便实现了一个buff的触发。回到我们之前提到的buff_command这个控制器:
class buff_command { public: void trigger(); private: buff_info _info; //buff的信息 buff_user _user; //buff的使用者 buff_obj _obj; //buff的作用者 }成员函数trigger实际上是对buff的作用者_obj执行buff结算。依据buff类型(血、速)等,对角色的相应数值进行调整。而在实际的设计过程中,考虑到buff信息的独立性,以一个独立的结构体:buff_info作为role的成员变量,每次做buff结算的时候实际上调整的是该成员的值,然后再将这些值添加到角色role的相应信息上去。这样相对独立的设计,一方面适合直观的展现每次技能使用带来的技能影响(当然是对我们开发人员来说);一方面因为是改别人的代码,这样独立的设计一个buff信息不会影响到已有代码所实现的功能(改代码好苦逼,主要是容易偏离原有设计者的思路)。
这样buff的检测+产生效果功能便已实现,最后的就是buff的清理工作了。
回合制游戏的一大特点就是一回合一回合的走下去,buff的影响效果也是有回合影响的;而技能的释放也只是会在当前回合有效:即释放了一个技能,只会在当前回合做有效性的检测——vecSkillCmd/vecInstanceCmd执行成员的内部检测函数,而当前回合结束后要删除所有成员(因为释放动作已经结束了,当然还要内存释放)。
技能控制器的控制很简单,只需要在每次回合结束的时候清理vecSkillCmd的成员及内存释放,而vecInstanceCmd由于是管理的即时生效型技能,所以这个应该在释放完技能(即技能特效播放完)就需要清理及释放。
而对于buff控制器则稍微复杂点,因为涉及到buff有没有被触发:
1、对于触发了的buff,要做回合检测:达到了回合限制则开始清理buff——调整角色类role的成员buff_info的值(即清理buff的影响值,假如造成了10点扣血,则清理的时候要加回10点血)。这里可能需要解释一番buff的生效机制:假如触发了10点扣血的buff,那么role的成员buff_info里面的_hp数值就要调整为-10,表示扣血。每次对角色的数值信息进行结算的时候加上了这个buff值-10,就相当于实现了扣血功能,所以在清理buff的时候要加回10,buff_info里面的_hp数值就成为了0.
2、对于没有触发的buff,则直接清理删除掉,这个跟技能控制器的清除管理思路类似。
这个清理工作是在每回合结束时进行,清理函数设计为角色类role的成员函数,对于场景中的每个角色都执行内部设计的清理检查函数。
至此这一周所做的工作便是这些了,写这篇博客相当于是对一周工作的总结,也不知道设计上哪些地方不合理了。不合理也木有办法,我所会的就是这些了。。。其实本来还想结合设计模式相关的知识来梳理一番这周的工作,可是还没有看相关的书籍所以不好怎么去梳理。总之设计模式相关方面的书肯定是要看的,最近在看《STL源码剖析》和《Programming in Lua 3》这两本书,看完其中一本之后便需要看关于设计模式相关的资料了。
总结:
在写完之后其实自己也想了一些改进:针对buff的最终影响类型——血、速、攻、魔等,写不同的类,然后统一让buff_command进行管理,这样是否便达到了可扩展的要求呢?今后如果还需要有额外的buff影响,那么只需要再添加额外的类让buff_command类进行管理即可?
不管这次的设计思路如何,总归是对自己的一种锻炼吧,之前毕竟没做过这些而今后总是会要做这些工作的~所谓的一些术语如:代码耦合度、代码复用、可扩展性等都是需要进一步的理解,在工作中需要再多注意的。
游戏开发——战斗系统之技能设计