引言
游戏因为华丽而精彩!这是所有游戏开发者发自肺腑的不懈追求!绚丽的技能/魔法效果将游戏的内涵渲染得淋漓尽致,本节我将继续拓展游戏中的战斗系统,以最简单直接的方式实现超酷的技能/魔法攻击效果。
13.1战斗系统之技能/魔法攻击(交叉参考:大法师 - 华丽经典之轮回 超酷万变的矢量魔法 雷、混、冰、毒、火、风 - 幻化中的魔法魅力!锦上添花之魔法特效装饰 落雷!治疗!陷阱!连锁闪电!多段群伤!魔法之终极五重奏① 落雷!治疗!陷阱!连锁闪电!多段群伤!魔法之终极五重奏② )
传统即时类RPG游戏通常以右键作为技能/魔法的触发,战士类职业以技能为主,法师类职业以魔法为主,区别在于近身与远距离之分。这样我们大致可将其进行如下归类:近/远距离单体/群体技能攻击、近/远距离单体/群体魔法攻击;其中的单体又可分为速效型、持续型或按受益性质化分可分为伤害型、辅助型和特殊型等;而群体则大多以伤害范围区分类型,常见的有圆形、扇型、十字、蝶形、随机路径、特殊图形等。
自Silverlight4开始已能完美的支持鼠标右键事件,于是作为施放Magic(技能/魔法)的开始,我们在游戏LayoutRoot的右键事件中编写如下代码,告诉主角开始执行Casting动作了,并将Magic参数MagicArgs传递过去:
/// 游戏中鼠标右键点击
/// </summary>
void LayoutRoot_MouseRightButtonDown(object sender, MouseButtonEventArgs e) {
e.Handled = true;
Point p0 = e.GetPosition(scene);
Point p1 = Scene.GetWindowCoordinate(hero.Coordinate);
hero.Target = null;
hero.SetDirection(p1, p0);
hero.Casting(new MagicArgs() {
Code = Convert.ToInt32(((ComboBoxItem)comboBox3.SelectedItem).Tag),
Coordinate = Convert.ToInt32(((ComboBoxItem)comboBox2.SelectedItem).Tag) == 0 ? p1 : p0,
Target = p0,
Z = -1,
Level = Convert.ToInt32(((ComboBoxItem)comboBox1.SelectedItem).Tag),
});
}
其中的Magic类继承自Animation,区别在于它俩资源存放的路径不同,同时Magic在未被放出前仅仅以参数MagicArgs的形态出现:
using System.Linq;
using System.Windows;
using System.Xml.Linq;
using Components.Static;
using System.Windows.Media.Animation;
using Controls.Base;
namespace Controls {
#region 类
public class MagicArgs {
/// <summary>
/// 获取或设置代号
/// </summary>
public int Code { get; set; }
/// <summary>
/// 获取或设置出现坐标
/// </summary>
public Point Coordinate { get; set; }
/// <summary>
/// 获取或设置目标
/// </summary>
public Point Target { get; set; }
/// <summary>
/// 获取或设置Z层次
/// </summary>
public int Z { get; set; }
/// <summary>
/// 获取或设置等级
/// </summary>
public int Level { get; set; }
/// <summary>
/// 获取或设置最小伤害
/// </summary>
public int DamageMin { get; set; }
/// <summary>
/// 获取或设置最大伤害
/// </summary>
public int DamageMax { get; set; }
/// <summary>
/// 获取或设置范围半径
/// </summary>
public int Radius { get; set; }
/// <summary>
/// 获取或设置魔法类型
/// </summary>
public MagicTypes MagicType { get; set; }
/// <summary>
/// 获取或设置装饰特效
/// </summary>
public SpecialEffects SpecialEffect { get; set; }
/// <summary>
/// 获取或设置附加效果
/// </summary>
public AdditionalEffects AdditionalEffect { get; set; }
/// <summary>
/// 获取或设置数量
/// </summary>
public int Number { get; set; }
}
#endregion
#region 枚举
/// <summary>
/// 魔法类型
/// </summary>
public enum MagicTypes {
/// <summary>
/// 圆形范围
/// </summary>
Circle = 0,
/// <summary>
/// 扇形范围
/// </summary>
Sector = 1,
/// <summary>
/// 穿梭范围
/// </summary>
Cross = 2,
/// <summary>
/// 多重圆范围
/// </summary>
MultipleCircle = 3,
}
/// <summary>
/// 特殊效果
/// </summary>
public enum SpecialEffects {
/// <summary>
/// 麻痹
/// </summary>
Mine = 7,
/// <summary>
/// 风切
/// </summary>
Wind = 8,
/// <summary>
/// 灼烧
/// </summary>
Fire = 9,
/// <summary>
/// 冰冻
/// </summary>
Ice = 10,
}
/// <summary>
/// 附加效果
/// </summary>
public enum AdditionalEffects {
/// <summary>
/// 击伤
/// </summary>
Injure = 0,
/// <summary>
/// 弹开
/// </summary>
Bounce = 1,
}
#endregion
/// <summary>
/// 魔法/技能类
/// </summary>
public class Magic : Animation {
MagicArgs _Args;
/// <summary>
/// 或取或设置参数
/// </summary>
public MagicArgs Args {
get { return _Args; }
set {
_Args = value;
Code = value.Code;
Coordinate = value.Coordinate;
Z = value.Z;
}
}
/// <summary>
/// 获取或设置配置下载完毕
/// </summary>
public bool IsConfigReady { get; set; }
public Magic() {
ResPath = "Magic";
EventHandler handler = null;
ConfigReady += handler = delegate {
ConfigReady -= handler;
IsConfigReady = true;
string key = string.Format("{0}Info{1}", ResPath, Code);
XElement args = Global.ResInfos[key].DescendantsAndSelf(ResPath).Single().Element("Levels").Elements().Single(X => X.Attribute("ID").Value == Args.Level.ToString());
Args.DamageMin = (int)(args.Attribute("DamageMin"));
Args.DamageMax = (int)(args.Attribute("DamageMax"));
Args.Radius = (int)(args.Attribute("Radius"));
Args.MagicType = (MagicTypes)(int)(args.Attribute("MagicType"));
Args.SpecialEffect = (SpecialEffects)(int)(args.Attribute("SpecialEffect"));
Args.AdditionalEffect = (AdditionalEffects)(int)(args.Attribute("AdditionalEffect"));
Args.Number = (int)(args.Attribute("Number"));
};
}
}
}
一旦精灵播放Casting动作到关键帧时,将触发DoCasting事件,此时我们将根据MagicArgs的Code代号赋予Magic创建实体,同时下载该代号Magic的配置和所需素材资源。
void sprite_DoCasting(object sender, DoCastingEventArgs e) {
Sprite caster = sender as Sprite;
List<Sprite> sprites = new List<Sprite>();
//创建主魔法
Magic magic = new Magic() { Args = e.MagicArgs };
if (magic.IsConfigReady) {
EventHandler handler = null;
magic.Disposed += handler = delegate {
magic.Disposed -= handler;
scene.RemoveAnimation(magic);
};
scene.AddAnimation(magic);
} else{
magic.Dispose();
}
......
}
其中以Code为0的Magic的xml配置文件为例,它的具体内容大致如下:
<Magic FullName="圆月斩" CenterX="160" CenterY="110" FrameTotal="5" Interval="120" Format="1" Kind="1">
<Frames>
<Frame ID="0" OffsetX="0" OffsetY="0"/>
<Frame ID="1" OffsetX="0" OffsetY="0"/>
<Frame ID="2" OffsetX="0" OffsetY="0"/>
<Frame ID="3" OffsetX="0" OffsetY="0"/>
<Frame ID="4" OffsetX="0" OffsetY="0"/>
</Frames>
<Levels>
<Level ID="1" DamageMin="560" DamageMax="860" Radius="4" SpecialEffect="8" AdditionalEffect="1" MagicType="0" Number="1"/>
<Level ID="2" DamageMin="730" DamageMax="970" Radius="4" SpecialEffect="8" AdditionalEffect="1" MagicType="0" Number="1"/>
<Level ID="3" DamageMin="950" DamageMax="1430" Radius="5" SpecialEffect="8" AdditionalEffect="1" MagicType="0" Number="1"/>
<Level ID="4" DamageMin="1220" DamageMax="1950" Radius="5" SpecialEffect="8" AdditionalEffect="1" MagicType="0" Number="1"/>
<Level ID="5" DamageMin="1560" DamageMax="2300" Radius="6" SpecialEffect="8" AdditionalEffect="1" MagicType="0" Number="1"/>
<Level ID="6" DamageMin="1900" DamageMax="2940" Radius="6" SpecialEffect="8" AdditionalEffect="1" MagicType="0" Number="1"/>
<Level ID="7" DamageMin="2510" DamageMax="3600" Radius="7" SpecialEffect="8" AdditionalEffect="1" MagicType="0" Number="1"/>
<Level ID="8" DamageMin="3230" DamageMax="4780" Radius="7" SpecialEffect="8" AdditionalEffect="1" MagicType="0" Number="1"/>
<Level ID="9" DamageMin="4560" DamageMax="6600" Radius="7" SpecialEffect="8" AdditionalEffect="1" MagicType="0" Number="1"/>
</Levels>
</Magic>
除了和Animation一样有Frame外,它还多了9个等级,每个等级的伤害值、伤害距离(范围、半径)、特殊效果、附加效果、子对象数量等均可差异化设置;实际开发中参数越详细,魔法的组合及动态效果越多样,干起群架来画面就显得越华丽。
另外,正规来讲每种技能/魔法应该按类型写成独立继承自Magic的类,显然这是一个高难度的任务。因为不同类型的魔法千变万化,天马行空,比如有的单体出现,有的多体出现,根据类型存在顺序,不同路径,追踪效果,是否循环,何时消失等等一系列复杂且常常无定律;我们惟一能做到的是在共性中提炼特性,在特性中萃取共性。
于是本节中我仅以功能实现为主,代码拙劣了些,后续有时间了再考虑进一步优化:
void sprite_DoCasting(object sender, DoCastingEventArgs e) {
Sprite caster = sender as Sprite;
List<Sprite> sprites = new List<Sprite>();
//创建主魔法
Magic magic = new Magic() { Args = e.MagicArgs };
if (magic.IsConfigReady) {
EventHandler handler = null;
magic.Disposed += handler = delegate {
magic.Disposed -= handler;
scene.RemoveAnimation(magic);
};
scene.AddAnimation(magic);
} else{
magic.Dispose();
}
switch (magic.Args.MagicType) {
case MagicTypes.Circle:
//捕获圆形范围内将要伤害的精灵表
for (int i = 0; i < scene.sprites.Count; i++) {
if (scene.sprites[i].IsVisible && caster.IsHostileTo(scene.sprites[i].Camp)) {
if (scene.sprites[i].InCircle(Scene.GetGameCoordinate(magic.Args.Coordinate), magic.Args.Radius)) {
sprites.Add(scene.sprites[i]);
}
}
}
break;
case MagicTypes.Sector:
double angle = Global.GetAngle(Global.GetRadian(Scene.GetWindowCoordinate(caster.Coordinate), magic.Args.Target)) + 180;
Point p = new Point(magic.Coordinate.X + magic.Args.Radius * Math.Cos(Global.GetRadian(angle)), magic.Coordinate.Y + magic.Args.Radius * Math.Sin(Global.GetRadian(angle)));
magic.Move(magic.Coordinate, p, 2, MoveModes.Normal);
//其他额外魔法
double singleAngle = 18;
double startAngle = angle - ((magic.Args.Number - 1) / 2 * singleAngle);
double endAngle = angle + ((magic.Args.Number - 1) / 2 * singleAngle);
for (int i = 0; i < magic.Args.Number; i++) {
double otherAngle = startAngle + singleAngle * i;
if (otherAngle == angle) { continue; }
Magic otherMagic = new Magic() { Args = e.MagicArgs };
if (otherMagic.IsConfigReady) {
EventHandler handler = null;
otherMagic.Disposed += handler = delegate {
otherMagic.Disposed -= handler;
scene.RemoveAnimation(otherMagic);
};
scene.AddAnimation(otherMagic);
} else {
otherMagic.Dispose();
}
p = new Point(otherMagic.Coordinate.X + magic.Args.Radius * Math.Cos(Global.GetRadian(otherAngle)), otherMagic.Coordinate.Y + magic.Args.Radius * Math.Sin(Global.GetRadian(otherAngle)));
otherMagic.Move(otherMagic.Coordinate, p, 2, MoveModes.Normal);
}
double s0 = 0, s1 = 1, e0 = 0, e1 = 0;
if (startAngle < 180 && endAngle > 180) {
s0 = startAngle; e0 = 180; s1 = -180; e1 = endAngle - 360;
} else if (startAngle >= 180) {
s0 = startAngle - 360; e0 = endAngle - 360;
} else {
s0 = startAngle; e0 = endAngle;
}
for (int i = 0; i < scene.sprites.Count; i++) {
if (scene.sprites[i].IsVisible && caster.IsHostileTo(scene.sprites[i].Camp)) {
double spriteAngle = Global.GetAngle(Global.GetRadian(Scene.GetWindowCoordinate(scene.sprites[i].Coordinate), magic.Args.Coordinate));
if ((spriteAngle >= s0 && spriteAngle <= e0) || (spriteAngle >= s1 && spriteAngle <= e1)) {
sprites.Add(scene.sprites[i]);
}
}
}
break;
case MagicTypes.Cross:
angle = Global.GetAngle(Global.GetRadian(Scene.GetWindowCoordinate(caster.Coordinate), magic.Args.Target)) + 180;
//测试
magic.RenderTransform = new RotateTransform() {
CenterX = 75,
CenterY = 115,
Angle = angle
};
singleAngle = 360 / magic.Args.Number;
for (int i = 1; i < magic.Args.Number; i++) {
Magic otherMagic = new Magic() { Args = e.MagicArgs };
if (otherMagic.IsConfigReady) {
EventHandler handler = null;
otherMagic.Disposed += handler = delegate {
otherMagic.Disposed -= handler;
scene.RemoveAnimation(otherMagic);
};
scene.AddAnimation(otherMagic);
} else {
otherMagic.Dispose();
}
otherMagic.RenderTransform = new RotateTransform() {
CenterX = 75,
CenterY = 115,
Angle = angle + singleAngle * i
};
}
//捕获十字(圆)范围内将要伤害的精灵表
for (int i = 0; i < scene.sprites.Count; i++) {
if (scene.sprites[i].IsVisible && caster.IsHostileTo(scene.sprites[i].Camp)) {
if (scene.sprites[i].InCircle(Scene.GetGameCoordinate(magic.Args.Coordinate), magic.Args.Radius)) {
sprites.Add(scene.sprites[i]);
}
}
}
break;
case MagicTypes.MultipleCircle:
angle = Global.GetAngle(Global.GetRadian(magic.Args.Coordinate, magic.Args.Target)) + 180;
int count = 0;
int number = 4;
double width = 80;
magic.Dispose();
EventHandler timerHandler = null;
DispatcherTimer timer = new DispatcherTimer() { Interval = TimeSpan.FromMilliseconds(100) };
timer.Tick += timerHandler = delegate {
if (count == magic.Args.Number) {
timer.Tick -= timerHandler;
timer.Stop();
} else {
count++;
number += 2;
width += 80;
createCircleMagic(e.MagicArgs, count, number, angle, width);
}
};
timer.Start();
//捕获圆范围内将要伤害的精灵表
for (int i = 0; i < scene.sprites.Count; i++) {
if (scene.sprites[i].IsVisible && caster.IsHostileTo(scene.sprites[i].Camp)) {
if (scene.sprites[i].InCircle(Scene.GetGameCoordinate(magic.Args.Coordinate), magic.Args.Radius)) {
sprites.Add(scene.sprites[i]);
}
}
}
break;
}
//对精灵表中所有精灵进行魔法/技能伤害
foreach (Sprite sprite in sprites) {
caster.CastingToHurt(sprite, magic.Args);
}
sprites.Clear();
}
void createCircleMagic(MagicArgs magicArgs, int count, double number, double angle, double width) {
for (int i = 0; i < number; i++) {
//if (count == 0 && i == 0) { continue; }
Magic otherMagic = new Magic() { Args = magicArgs };
if (otherMagic.IsConfigReady) {
EventHandler handler = null;
otherMagic.Disposed += handler = delegate {
otherMagic.Disposed -= handler;
scene.RemoveAnimation(otherMagic);
};
scene.AddAnimation(otherMagic);
} else {
otherMagic.Dispose();
}
double tempAngle = angle + i * (360 / number);
double tempRadian = Global.GetRadian(tempAngle);
double x = width * Math.Cos(tempRadian) + otherMagic.Coordinate.X;
double y = width * Math.Sin(tempRadian) + otherMagic.Coordinate.Y;
otherMagic.Coordinate = new Point(x, y);
}
}
绚,缘自对游戏设计的狂热;魔法都做烂了,那么换一下吧。以上代码将为大家演绎维美而精彩的战士技能:
能够征服玩家的Magic必须拥有强而有力且富含打击质感的体验,辅以百态特效比如:通过Storyboard的CubicEase缓动效果实现的震飞,还有挑高、冰冻、下毒、定身(麻痹)、迟缓等等,配上不同等级、不同模式时的各异姿态体现,魅力超级无限!
当然,到此为止还需要完善的内容有很多,如魔法效果的拓展、进一步封装等等;而最大的问题出在:为什么第一次放魔法时常常会没有效果出现?也不产生任何伤害?问题关键在于Magic的xml配置是动态下载的,所以实际开发需要将xml配置文件一开始就全部加载好(可适当考虑使用独立存储)。比方说假如游戏是SLG回合战斗RPG,可以在每场战斗初始化时分析所有角色所掌握的技能一并下载完成后再回合Start。而即时类的就比较麻烦了,通常的做法是游戏开始时创建一个可维护的队列,一旦有新的对象(新场景、新精灵、新魔法、新界面等)加入到游戏中,则将它所需要的一切资源配置信息添加到队列的末尾或开头,依次类推。
Silverlight 5 Beta明年春天即将发布,从新特性上我们不难看出MS在Silverlight于网页方面的应用相当给力,外加微软Silverlight团队开始大规模全球招聘!!种种迹象不难看出Silveright 5在实际发布时誓将给世界一个大大的惊叹!性能的进一步提升和3D骨骼动画的API支持,哇塞!绝对的翘首以盼!
本课小结:关于RPG游戏中的战斗系统讲解到此已全部结束。或许你会觉得很简单;确实代码相当的少,Silverlight底层框架给了我们很大的支持,让我们可以把更多的时间花在具体功能的实现逻辑上;然而实际情况是极其复杂多变的,到底什么才能使得每天面对着同一款游戏的玩家恋恋不舍,茶余饭后回味谈资?中国的网游行业年复一年的在上演换一套UI就是一款游戏的贺岁强档,凭借《传奇》发家的盛大10多年了还依旧在吃着《传奇》饭,问题到底出在哪?
经典不需要Copy,也没必要More,玩家真正需要的是一部旷世之作! - The Only One!
本课源码:点击进入目录下载
参考资料:中游在线[WOWO世界] 之 Silverlight C# 游戏开发:游戏开发技术
教程Demo在线演示地址:http://silverfuture.cn