战斗即将开始!要实现MMORPG中的攻击系统,必须为精灵增加相关的参数及属性,这些内容及它们之间的牵连关系设计决定着游戏的新颖度与耐玩性;就好比当年的传奇,系统再普通不过了,但是却因为有着恰如其分的系统参数设定与完美的世界观定位,成就了一代不朽巨作。那么本节开始,我将首先对精灵控件进行属性完善,使之具传统经典游戏中的角色属性。
首先看下图:
这些属性是目前最经典的角色属性设置(不同的游戏中,某些属性的命名可能会不同。例如上图中的“体质”属性在本节示例游戏中我称之为“体格”;而“运势”属性我则称之为“运气”等等,但意义是一样的);我们先分析:力量、智慧、敏捷、体质、运气这5个属性为所有基本属性中的根属性,何谓根属性?就是所有基值属性的最底层。当角色升级时,通过手动或系统自动根据角色的职业特性对这5个根属性进行加值,而其他所有与之相关联的基值属性都将同时受到影响。例如我是这样定义这5个根属性的:
double _VPower;
/// <summary>
/// 获取或设置力量
/// 影响最大负重,物理攻击力(最小 - 最大)
/// --[相关基值属性计算公式]:
/// 最大负重 = ABase[0] + Equip[0] + Buff[0] + VPower * Coefficient[0]
/// 物理攻击力最小值 = ABase[1] + Equip[1] + Buff[1] + VPower * Coefficient[1]
/// 物理攻击力最大值 = ABase[2] + Equip[2] + Buff[2] + VPower * Coefficient[2]
/// </summary>
public double VPower {
get { return _VPower + Equip[16] + Buff[16]; }
set { _VPower = value; }
}
double _VAgile;
/// <summary>
/// 获取或设置敏捷
/// 影响命中,闪避,跑速,攻速,施法速度
/// --[相关基值属性计算公式]:
/// 命中 = ABase[3] + Equip[3] + Buff[3] + VAgile * Coefficient[3]
/// 闪避 = ABase[4] + Equip[4] + Buff[4] + VAgile * Coefficient[4]
/// 跑速 = ABase[5] - Equip[5] + Buff[5] - VAgile * Coefficient[5]
/// 物攻速度 = ABase[6] - Equip[6] + Buff[6] - VAgile * Coefficient[6]
/// 施法速度 = ABase[7] - Equip[7] + Buff[7] - VAgile * Coefficient[7]
/// </summary>
public double VAgile {
get { return _VAgile + Equip[17] + Buff[17]; }
set { _VAgile = value; }
}
double _VPhysique;
/// <summary>
/// 获取或设置体格
/// 影响最大生命值,物理防御力,格档率(与暴击率相反,物理或魔法攻击伤害减半)
/// --[相关基值属性计算公式]:
/// 最大生命值 = ABase[8] + Equip[8] + Buff[8] + VPhysique * Coefficient[8]
/// 物理防御力 = ABase[9] + Equip[9] + Buff[9] + VPhysique * Coefficient[9]
/// 格档率 = ABase[10] + Equip[10] + Buff[10] + VPhysique * Coefficient[10]
/// </summary>
public double VPhysique {
get { return _VPhysique + Equip[18] + Buff[18]; }
set { _VPhysique = value; }
}
double _VWisdom;
/// <summary>
/// 获取或设置智慧
/// 影响最大魔法值,魔法防御力,魔法攻击力(最小 - 最大)
/// --[相关基值属性计算公式]:
/// 最大魔法值 = ABase[11] + Equip[11] + Buff[11] + VWisdom * Coefficient[11]
/// 魔法防御力 = ABase[12] + Equip[12] + Buff[12] + VWisdom * Coefficient[12]
/// 魔法攻击力最小值 = ABase[13] + Equip[13] + Buff[13] + VWisdom * Coefficient[13]
/// 魔法攻击力最大值 = ABase[14] + Equip[14] + Buff[14] + VWisdom * Coefficient[14]
/// </summary>
public double VWisdom {
get { return _VWisdom + Equip[19] + Buff[19]; }
set { _VWisdom = value; }
}
double _VLucky;
/// <summary>
/// 获取或设置幸运
/// 影响暴击率(物理或魔法攻击加倍)及其他
/// --[相关基值属性计算公式]:
/// 暴击率 = ABase[15] + Equip[15] + Buff[15] + VLucky * ACoefficient[15]
/// </summary>
public double VLucky {
get { return _VLucky + Equip[20] + Buff[20]; }
set { _VLucky = value; }
}
/// <summary>
/// 获取或设置属性基数
/// 0-14对应基础属性值
/// </summary>
public double[] ABase { get; set; }
/// <summary>
/// 获取或设置装备加成总和
/// 0-14对应基础属性值
/// 15-19对应5大属性
/// </summary>
public double[] Equip { get; set; }
/// <summary>
/// 获取或设置加持/减持值总和
/// 0-14对应基础属性值
/// 15-19对应5大属性
/// </summary>
public double[] Buff { get; set; }
/// <summary>
/// 获取或设置属性系数(成长率)
/// 0-14对应基础属性值
/// </summary>
public double[] Coefficient { get; set; }
以力量属性VPower为例,它影响着最大负重、物理攻击力最小值、物理攻击力最大值这3个基值属性,又以其中的物理攻击力最大值为例:
/// <summary>
/// 获取物理攻击力最大值
/// </summary>
public double VAttackMax {
get { return ABase[2] + Equip[2] + Buff[2] + VPower * Coefficient[2]; }
}
从它的CLR构造可以看出VAttackMax = ABase[2] + Equip[2] + Buff[2] + VPower * Coefficient[2],并且最关键的它是只读的(即通过多个影响其值的相关属性值集合而成,并非通过直接赋值得到,这样做可以轻松构建出通用且易于维护的精灵属性系统)。其中Abase[2]为该精灵物理攻击力最大值的原始基础值,例如战士的ABase[2]可以定为30,那么魔法师的ABase[2]大约为10左右等等;Equip[2]为所有装备的力量加成值总和,这个不用说大家也明白;Buff[2]为所有加持减持等效果的附加值;Coefficient[2]为该精灵的力量系数,例如狂战士的Coefficient[2]可以定为10,而牧师的Coefficient[2]则约为2左右等等。那么其他的诸如敏捷属性VAgile,体格属性VPhysique等等均是类似的关联设计。
属性定义完成后,接下来便可以着手设计精灵的物理攻击系统。
同样的我们首先对物理攻击的需求进行分析,即物理攻击的触发、过程及结束处理:
当我们在其他精灵身上点击鼠标左键后,首先进行敌对判断,如果是敌对的则将物理攻击对象锁定为该精灵(敌人),然后主角向其位置跑去,当该敌人进入主角的攻击距离(范围)内时,主角向其发起物理攻击,然后通过公式计算出伤害值并输出显示出来。这就是最典型的物理攻击发生过程。
下面是我根据此原理过程写的一些关键逻辑代码:
步骤一,主角与对象精灵之间的敌对判断,根据注释并且凭借大家N年的网游PK经验应该不难理解^_^:
/// <summary>
/// 判断主角是否与监视对象敌对
/// </summary>
/// <param name="obj">自身精灵</param>
/// <param name="obj">对象精灵</param>
/// <returns>是/否</returns>
private bool IsOpposition(QXSpirit me, QXSpirit obj) {
//假如对象为自己则返回否
if (me == obj) {
return false;
//假如对象的PK值大于或等于7则返回是
} else if (obj.VPK >= 7) {
return true;
} else {
//根据自身的PK模式与对方的PK模式进行比较进行相应返回
switch (me.PKMode) {
case PKModes.Peace:
return false;
case PKModes.Whole:
return true;
case PKModes.GoodAndEvil:
//在善恶模式下对方为全部攻击模式时返回是
return obj.PKMode == PKModes.Whole ? true : false;
case PKModes.Faction:
return me.VFaction != obj.VFaction ? true : false;
case PKModes.Clan:
return me.VClan != obj.VClan ? true : false;
default:
return false;
}
}
}
步骤二,跑向敌人时,只要敌人进入攻击范围时即发起攻击:
/// <summary>
/// 判断是否将要向锁定对象发起攻击
/// </summary>
private bool WillAttack(QXSpirit attacker, QXSpirit defenser) {
return defenser == null ? false : Super.InCircle(attacker.X, attacker.Y, defenser.X, defenser.Y, attacker.AttackRange);
}
/// <summary>
/// 判断点是否在圆内
/// </summary>
/// <param name="targetX">目标点X坐标</param>
/// <param name="targetY">目标点Y坐标</param>
/// <param name="centerX">圆心X坐标</param>
/// <param name="centerY">圆心X坐标</param>
/// <param name="radius">圆半径</param>
/// <returns></returns>
public static bool InCircle(double targetX, double targetY, double centerX, double centerY, double radius){
return Math.Pow(targetX - centerX, 2) + Math.Pow(targetY - centerY, 2) <= Math.Pow(radius, 2) ? true : false;
}
我设定的主角发起实质性攻击的前提条件是:主角进入为以敌人脚底为圆心以主角攻击距离(范围)为半径的圆内:
通过圆来构造攻击范围并实施判断在范围型攻击魔法中同样被广泛应用到,这是后话了。
步骤三,当主角向敌人挥剑并击中对方时,运行计算公式并显示伤害输出。这里涉及到精灵动作的分帧处理(分帧处理架构的优势在此体现得淋漓尽致),也就是根据精灵当前动作的进度来实现相应功能。举个最简单的例子,当精灵死亡时会运行死亡的一系列动作帧,当播放到最后一帧(精灵躺在地上)时才算真正的死亡,此后我们才会对其进行例如线程释放、控件移除等相关处理:
本游戏中的主角攻击动作由7帧组成,当播放到第6帧时才会实质性的对敌人产生伤害:
而这列帧在主角的动作合成图中所处的位置为第20列,这样,我们为每个精灵控件添加一个名为EffectiveFrame(起效帧)的属性,当精灵动作播放到此帧时,即会启动相应的处理:
private void Timer_Tick(object sender, EventArgs e) {
……
//如果触动起效帧
if (FrameCounter == EffectiveFrame[(int)Action]) {
Super.DoInjure(this);
}
}
这里的处理调用DoInjure方法进行伤害计算、输出及值更新:
/// <summary>
/// 精灵攻击并产生伤害及输出
/// </summary>
/// <param name="spirit">发起攻击精灵</param>
public static void DoInjure(QXSpirit spirit) {
//捕获敌人精灵
QXSpirit Enemy = (spirit.Parent as Canvas).FindName(spirit.LockObject) as QXSpirit;
//产生随机数
Random random = new Random();
//首先进行闪避判断
if (random.Next(100) >= (spirit.VHit - Enemy.VDodge)) {
Super.ShowText(Enemy, true, true, "Avoid", "Miss", 34, Colors.SkyBlue, 2);
} else {
int Injure = 0;
if (spirit.Action == Actions.Attack) {
Injure = Convert.ToInt32(spirit.VAttackMin + random.Next((int)(spirit.VAttackMax - spirit.VAttackMin)) - Enemy.VDefense);
} else if (spirit.Action == Actions.Magic) {
Injure = Convert.ToInt32(spirit.VMagicMin + random.Next((int)(spirit.VMagicMax - spirit.VMagicMin)) - Enemy.VMagicDefense);
}
//判断是否暴击
if (random.Next(100) <= (spirit.VBurst - Enemy.VBlock)) {
Injure *= 2;
Super.ShowText(Enemy, true, true, "Avoid", string.Format("Burst!{0}", Injure >= 0 ? Injure.ToString() : "0"), 30, Colors.Red, 2);
} else {
Super.ShowText(Enemy, true, false, "Avoid", Injure >= 0 ? Injure.ToString() : "0", 30, Colors.Yellow, 0.5);
}
//实际产生去血效果
Enemy.VLife = Enemy.VLife < Injure ? 0 : Enemy.VLife - Injure;
}
}
本节示例代码中我为主角赋予了一定的根属性值从而得出65%的命中与18%的暴击率。当向敌人发起攻击时系统会首先进行命中判断,没有命中则无任何伤害;如果命中,则在此基础上再进行暴击(伤害加倍)判断,否则则为普通攻击。接着根据不同的伤害类型通过ShowText()方法(该方法显示出来的是QXText控件的实例,即第十四节中的BorderText,这里我重构并Open了)将伤害显示到游戏画面中,最后如果有命中还需要让敌人减去相应的生命值。DoInjure()方法算是最基本的伤害处理方法,在后面的章节中还会有更多更复杂的处理,我们可以通过DoInjure()多态来实现,最终完善游戏的伤害系统。
通常的网络游戏中,伤害值会显示在对应的精灵头顶,并不断的渐隐上升,最后消失并被系统移除。本节示例游戏中我们可以轻松的在AllMove()中实现此功能:
……
if (obj is QXText) {
QXText text = obj as QXText;
if (text.Opacity <= 0) {
Remove(text);
} else {
text.Opacity -= 0.005;
text.Y -= 0.5;
}
……
每次游戏画面刷新时,伤害字体透明度-0.005且匀速上升0.5,当透明度由1->0后,游戏便将之移除从而及时的释放掉资源:
在攻击的过程中,大家不妨观察被攻击的怪物头上的血条及监视头像面板中的血条是否都时时更新且保持一致,通过前面几节的努力,这方面的扩展均是无缝的。
额外的,伤害显示方式并非只此一种,您完全可以充分的激活自己的大脑细胞做出更多的创新。例如利用第二十六节的彩虹笔刷来美化文字;模仿JS的数字验证码来创造出夸张或抽象美的字体;更甚者,您完全可以将文字作成动画封装成控件来使用,当暴击时给予玩家极好的视觉享受等等:
整个过程完美衔接,构建出经典又不失完美的物理攻击系统。下一节我将对为所有精灵加入简单AI,使精灵们更加栩栩如生,美妙的虚拟世界在向你招手,敬请关注。