引言
通常来说RPG游戏中的战斗系统按类型(AttackType)大致可划分为普通攻击和技能攻击两大类。其中较常见的普通攻击有近距离单体物理攻击、远距离单体物理攻击和远距离单体魔法攻击。本节,我们将通过为精灵控件添加一些新的事件和属性,轻松实现RPG战斗系统中的简单AI及普通攻击战斗效果。
12.1战斗系统之普通攻击(交叉参考:经典式属性设计及完美的物理攻击系统 战斗前夜之构建动态障碍物系统 人工智能(AI)之追踪者 锦上添花之魔法特效装饰 远距离单体攻击与单体魔法)
11.1中我们利用Silverlight中的命中测试来获取鼠标划过时的精灵对象,ARPG中的战斗操作大都需要通过鼠标左、右键的点击实现普通攻击和技能攻击,因此本节我们首先为游戏的LayoutRoot注册鼠标左键点击(路由)事件:
/// <summary>
/// 游戏中鼠标左键点击
/// </summary>
void LayoutRoot_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) {
Point p = e.GetPosition(scene);
List<Sprite> hitSprites = HitSprite(e.GetPosition(LayoutRoot));
bool IsHitSprite = false;
for (int i = 0; i < hitSprites.Count; i++) {
if (hitSprites[i].InPhysicalRange(e.GetPosition(hitSprites[i]))) {
bool isHostile = hero.IsHostileTo(hitSprites[i].Camp);
if (leftButtonClickSprite != hitSprites[i]) {
if (leftButtonClickSprite != null) {
//移除上一次点中精灵选中特效
leftButtonClickSprite.RemoveSelectedEffect();
//移除上一次点中精灵的事件
leftButtonClickSprite.LifeChanged -= UpdateTargetInfoLife;
leftButtonClickSprite.LifeMaxChanged -= UpdateTargetInfoLife;
}
leftButtonClickSprite = hitSprites[i];
//注册选中精灵更新生命值事件
hitSprites[i].LifeChanged += UpdateTargetInfoLife;
hitSprites[i].LifeMaxChanged += UpdateTargetInfoLife;
//更新HUD中目标精灵资料
targetInfo.Update(hitSprites[i].FullName, hitSprites[i].Avatar, hitSprites[i].Level, hitSprites[i].Life, hitSprites[i].LifeMax);
//添加选中特效
Animation animation = new Animation() { Code = isHostile ? 5 : 4, Coordinate = hitSprites[i].Center, Z = -1 };
EventHandler handler = null;
animation.Disposed += handler = delegate {
animation.Disposed -= handler;
hitSprites[i].RemoveSelectedEffect();
};
hitSprites[i].AddSelectedEffect(animation);
}
//主角向敌对目标精灵移动
if (isHostile) {
hero.Target = hitSprites[i];
hero.FightDetect -= sprite_FightDetect;
hero.FightDetect += sprite_FightDetect;
if (hero.InCircle(hero.Target.Coordinate, hero.AttackDistance)) {
hero.TowardTo(hero.Target);
hero.Attack();
} else {
hero.RunTo(Scene.GetWindowCoordinate(hero.Target.Coordinate));
}
}
targetInfo.Visibility = Visibility.Visible;
IsHitSprite = true;
break;
}
}
if (!IsHitSprite) {
hero.Target = null;
hero.FightDetect -= sprite_FightDetect;
//主角向目标点移动
hero.RunTo(p);
//加入点击水滴
Animation water = new Animation() { Code = 3, Coordinate = p, Z = -1 };
EventHandler handler = null;
water.Disposed += handler = delegate {
water.Disposed -= handler;
scene.RemoveAnimation(water);
};
scene.AddAnimation(water);
}
}
战斗存在一个前置条件,主角将要面对的精灵是否处于敌对阵营?于是我们还需为精灵添加一个描述阵营的属性Camp:
/// 获取或设置阵营
/// </summary>
public Camps Camp { get; set; }
/// <summary>
/// 阵营
/// </summary>
public enum Camps {
/// <summary>
/// 正义
/// </summary>
Justice = 0,
/// <summary>
/// 中立
/// </summary>
Neutrality = 1,
/// <summary>
/// 邪恶
/// </summary>
Eval = 2
}
当鼠标左键击中某个精灵对象时,通过分析主角与对方的阵营关系从而得出是否应该对其发动攻击,如果是则设置主角的攻击对象(Target)为该精灵,并向其冲去(RunTo)。
主角的这些逻辑全都在鼠标左键点击事件中实现,那么如果是非主角(比如怪物类)的精灵呢?它们该如何自动(非控)的产生类似的攻击意识和行为?这就要游戏将要涉及到的AI逻辑。
接下来,我为精灵再添上了3个关于攻击这方面的重要事件:
/// 环境侦测
/// </summary>
public event EventHandler Detect;
/// <summary>
/// 战斗侦测
/// </summary>
public event EventHandler FightDetect;
/// <summary>
/// 普通攻击
/// </summary>
public event EventHandler DoAttack;
它们所触发的位置如下:
//ObjectTracker.Track(this);
this.CacheMode = new BitmapCache();
this.Children.Add(fullName);
lifePanel.Children.Add(lifeBorder);
lifeBorder.Child = lifeRect;
this.Children.Add(lifePanel);
auxiliary.Interval = TimeSpan.FromMilliseconds(2000);
auxiliary.Tick += new EventHandler(auxiliaryTick);
auxiliary.Start();
}
Frame frame;
Frames frames;
Dictionary<string, Point2D> frameOffset = new Dictionary<string, Point2D>(); //各帧偏移
protected override void HeartTick(object sender, EventArgs e) {
string key = string.Format("{0}-{1}-{2}", (int)State, (int)Direction, frame.Current);
if (IsResReady) {
BodySource = Global.GetWebImage(string.Format("Sprite/{0}/{1}.png", Code, key));
BodyPosition = frameOffset[key];
} else {
BodySource = Global.GetProjectImage(string.Format("Model/Sprite/{0}/{1}.png", ModelCode, key));
BodyPosition = new Point2D(0, 0);
}
frame.Current++;
if (frame.Current > frame.Total) {
switch (State) {
case States.Attack:
if (DoAttack != null) { DoAttack(this, e); }
break;
case States.Injure:
case States.Casting:
Stand();
break;
}
frame.Current = 0;
}
//进行攻击侦测
if (FightDetect != null) { FightDetect(this, e); }
}
bool auxiliaryTicked = false;//测试用
void auxiliaryTick(object sender, EventArgs e) {
//间隔环境侦测
if (Detect != null) { Detect(this, e); }
auxiliaryTicked = true;
}
且听我一一道来。
首先是Detect事件,每隔2秒触发一次(实际开发中至少1秒用处会更广,比如中毒伤血处理等)。它的作用是实现精灵每隔一段时间对周围的环境进行侦测,包括判断自身是否已处于屏幕内,主角是否处于自己的视线范围中等等;从而作出相关反应,比如:隐藏掉自己(提升性能)、巡逻(原地随机走动)和追击。
/// 侦测
/// </summary>
void sprite_Detect(object sender, EventArgs e) {
Sprite sprite = sender as Sprite;
//异步判断精灵是否处于屏幕显示区域中,是的话且无目标则进行索敌判断:在视线范围内则追击,否则警戒
this.Dispatcher.BeginInvoke(() => {
sprite.IsVisible = sprite.InScreen(hero.Coordinate);
if (sprite.IsVisible && sprite.Target == null) {
//目前单机版怪物的敌人只有主角
if (sprite.InCircle(hero.Coordinate, sprite.SightDistance)) {
sprite.Target = hero;
sprite.RunTo(Scene.GetWindowCoordinate(hero.Coordinate));
} else {
sprite.GuardCenter = hero.Coordinate;
sprite.Guard();
}
}
});
}
当然,这些功能的实现仍然需要一些新属性的支持:
/// 警戒中心
/// </summary>
public Point GuardCenter { get; set; }
/// <summary>
/// 获取或设置警备距离
/// </summary>
public int GuardDistance { get; set; }
/// <summary>
/// 获取或设置视线距离(索敌距离)
/// </summary>
public int SightDistance { get; set; }
/// <summary>
/// 获取或设置物理攻击距离
/// </summary>
public int AttackDistance { get; set; }
/// <summary>
/// 获取或设置施法距离
/// </summary>
public int CastingDistance { get; set; }
然后是FightDetect事件,我把它放在精灵动作帧间隔里,即精灵每切换一张动作帧图片时触发一次。它在游戏中主要用作对正处于视线范围及攻击范围内的敌人进行相应的战斗处理。
/// 战斗侦测
/// </summary>
void sprite_FightDetect(object sender, EventArgs e) {
Sprite sprite = sender as Sprite;
//精灵处于屏幕中且存在目标则判断是否在攻击范围内进行攻击;否则追击;超出视线范围则停止
if (sprite.IsVisible && sprite.Target != null) {
if (sprite.InCircle(sprite.Target.Coordinate, sprite.AttackDistance)) {
sprite.TowardTo(sprite.Target);
sprite.Attack();
} else if (sprite.InCircle(sprite.Target.Coordinate, sprite.SightDistance)) {
sprite.RunTo(Scene.GetWindowCoordinate(sprite.Target.Coordinate));
} else {
sprite.Target = null;
sprite.Stand();
}
}
}
最后是DoAttack事件,当精灵攻击动作播放到起效帧时触发,根据精灵普通攻击类型进行相应的处理,比如近距离单体物理攻击则直接产生伤害;远距离单体物理攻击则发射一箭支飞向目标,到达后再对目标产生伤害:
/// 开始普通攻击
/// </summary>
void sprite_DoAttack(object sender, EventArgs e) {
Sprite attacker = sender as Sprite;
Sprite defencer = attacker.Target;
//攻击起效时如果在屏幕中且目标存在则对其进行伤害
if (attacker.IsVisible && defencer != null) {
attacker.TowardTo(defencer);
switch (attacker.AttackType) {
case AttackTypes.Close:
attacker.AttackToHurt(defencer);
break;
case AttackTypes.Distance:
Arraw arraw = new Arraw() {
Code = 0,
Z = defencer.Z,
Launcher = attacker,
Target = defencer,
};
EventHandler handler = null;
arraw.MoveCompleted += handler = delegate {
arraw.MoveCompleted -= handler;
if (arraw.Launcher != null && arraw.Target != null) {
arraw.Launcher.AttackToHurt(arraw.Target);
}
arraw.Launcher = null;
arraw.Target = null;
scene.RemoveUIElement(arraw);
};
scene.AddUIElement(arraw);
arraw.Move(new Point(attacker.Coordinate.X - 0.8, attacker.Coordinate.Y - 1.8), new Point(defencer.Coordinate.X - 1, defencer.Coordinate.Y - 1), 50, MoveModes.Normal);
break;
}
}
}
值得一提的是其中的箭支控件(由于暂时未做时时的碰撞检测(RPG中完全也没必要这么做),来源于网络的素材中心定位本身就不太准确,因此这里的箭支飞行大致是从身体手的部位发射出去,最后消失在被攻击者附近),该控件的呈现仅仅为一张头朝右的箭矢,根据发射者和攻击者之间的角度对其进行旋转从而实现360度全角度效果:
/// 剪支弹药
/// </summary>
public sealed class Arraw : MovableObject {
#region 构造
Image body = new Image() { Stretch = Stretch.None };
public Arraw() {
//ObjectTracker.Track(this);
this.IsHitTestVisible = false;
this.CacheMode = new BitmapCache();
this.Children.Add(body);
}
#endregion
#region 属性
public override int Code {
get { return base.Code; }
set { body.Source = Global.GetWebImage(string.Format("Icon/{0}.png", (base.Code = value))); }
}
/// <summary>
/// 发射者
/// </summary>
public Sprite Launcher { get; set; }
/// <summary>
/// 目标精灵
/// </summary>
public Sprite Target { get; set; }
#endregion
#region 方法
/// <summary>
/// 重载坐标改变方法
/// </summary>
/// <param name="coordinate">游戏坐标系坐标</param>
protected override void ChangeCoordinateTo(Point coordinate) {
base.ChangeCoordinateTo(Scene.GetWindowCoordinate(coordinate));
}
/// <summary>
/// 重载飞行方法
/// </summary>
/// <param name="from"></param>
/// <param name="to"></param>
/// <param name="speed"></param>
/// <param name="moveMode"></param>
public override void Move(Point from, Point to, double speed, MoveModes moveMode) {
Point p0 = Scene.GetWindowCoordinate(to);
Point p1 = Scene.GetWindowCoordinate(from);
double angle = Global.GetAngle(p0.Y - p1.Y, p0.X - p1.X);
this.RenderTransform = new RotateTransform() {
CenterX = 0,
CenterY = 0,
Angle = angle
};
base.Move(from, to, speed, moveMode);
}
#endregion
}
Arraw继承自MovableObject,游戏中一切可移动的对象都可继承自它,比如游戏中可拖动界面、特效、装饰、弹药、文字等等:
/// <summary>
/// 移动模式
/// </summary>
public enum MoveModes {
/// <summary>
/// 标准
/// </summary>
Normal = 0,
/// <summary>
/// 带透明度渐隐效果
/// </summary>
Opacity = 1,
}
#endregion
/// <summary>
/// 可运动物体
/// </summary>
public abstract class MovableObject : ObjectBase {
#region 事件
public event EventHandler MoveCompleted;
#endregion
#region 方法
/// <summary>
/// 从一点向另一点移动
/// </summary>
/// <param name="from">起点</param>
/// <param name="to">终点</param>
/// <param name="speed">速度系数</param>
/// <param name="moveMode">模式</param>
public virtual void Move(Point from, Point to, double speed, MoveModes moveMode) {
Duration duration = new Duration(TimeSpan.FromMilliseconds(Global.GetTwoPointDistance(from, to) * speed));
Storyboard storyboard = new Storyboard();
PointAnimation pointAnimation = new PointAnimation() {
From = from,
To = to,
Duration = duration,
};
Storyboard.SetTarget(pointAnimation, this);
Storyboard.SetTargetProperty(pointAnimation, new PropertyPath("Coordinate"));
storyboard.Children.Add(pointAnimation);
switch (moveMode) {
case MoveModes.Normal:
break;
case MoveModes.Opacity:
DoubleAnimation doubleAnimation = new DoubleAnimation() {
From = 1,
To = 0,
Duration = duration,
};
Storyboard.SetTarget(doubleAnimation, this);
Storyboard.SetTargetProperty(doubleAnimation, new PropertyPath("Opacity"));
storyboard.Children.Add(doubleAnimation);
break;
}
EventHandler handler = null;
storyboard.Completed += handler = (s, e) => {
storyboard.Completed -= handler;
storyboard.Stop();
storyboard = null;
Dispose();
if (MoveCompleted != null) { MoveCompleted(this, e); }
};
storyboard.Begin();
}
#endregion
}
综述,当以上3个事件配合上相应属性一同发挥作用时,战斗终于打响了。过程仅是绚烂的瞬间,包括我在内的所有玩家或许更关注最终产生伤害的那一刻attacker.AttackToHurt(defencer):
/// 普通攻击使对方造成伤害
/// </summary>
/// <param name="obj">对方精灵</param>
public void AttackToHurt(Sprite obj) {
int damage = this.ATK - obj.DEF < 0 ? 0 : this.ATK - obj.DEF + Global.random.Next(-30, 30);
//各种伤害情况判断(实际不这么写,需要真实的数值公式计算)
if (damage == 0 || Global.random.Next(100) < obj.DEX) {
obj.ChangeLife(0, ValueEffects.Failure);
} else if (Global.random.Next(100) >= 95) {
obj.ChangeLife(-damage * 2, ValueEffects.DoubleDecrease);
obj.Injure();
} else {
obj.ChangeLife(-damage, ValueEffects.Decrease);
}
//伤害后如果对象生命为0
if (obj.Life == 0) {
obj.ChangeLife(0, ValueEffects.Death);
this.Stand();
this.Target = null;
} else {
//附加被攻击特效
if (this.AttactEffect != -1) {
Animation animation = new Animation() { Code = this.AttactEffect, Coordinate = obj.Center };
EventHandler handler = null;
animation.Disposed += handler = delegate {
animation.Disposed -= handler;
obj.RemoveTemporaryEffect();
};
obj.AddTemporaryEffect(animation);
}
}
}
其实,真实的伤害剧本比这个要复杂百倍有余,该工作完全属于游戏数值策划师的范畴,并且大都以参数脚本的形式独立存在便于随时调试。
到此,RPG游戏的普通攻击系统简单实现告一段落,当您看见满天飞矢、战火连天的画面时,是否又一次勾起了那些封藏已久的往昔回忆?
最后是一些锦上添花,比如说受伤系统和动态障碍物系统。
受伤系统并不是所有RPG都具备或者说完整具备的,因为它涉及到多方面的原因和设计手法。在本节的示例游戏中,当精灵被一定几率暴击后将呈现受伤状态,此时的精灵所有动作将被打断,比如移动时失去目标而停止;攻击招式归零等。伤害系统对于规模性的群伤技能和动作感、打击感较强的ARPG类型游戏来说至关重要,常常者涉及到大量人体动作学及物理运动学等方面的相关知识,更神秘而有深度的领域等待您的参与一同去探索!
另外关于动态障碍物系统其实早在我的第一部教程中已有比较好的实现(效果完美但性能欠缺),本节中我以另外的形式进行了再一次的简单实现,不管方法如何,动态障碍物系统存在的目的仅有一个:不让精灵重叠。回合制RPG游戏似乎并不关心这方面的需求,然而却是即时战斗类型游戏不可或缺的环节之一,有了它游戏实景效果将更为贴近现实。当然,最完美的实现应该是包含坐标锁定与解锁的繁杂过程,同时很多处理都将被放上服务器,算法的好坏最终直接关系到运营成本的高低。
本课小结:每款游戏都有一个主题,一个背景,一个旋律,一个情节。成功的游戏设计大都是从众多已知的游戏特性中寻找共性,提炼出精华为己所用。战斗系统便是整个RPG游戏的精髓所在,也是游戏趣味性的重点环节,如果设计者能将更深层次的思考及优秀的策划融会贯通,使玩家得到酣畅淋漓、一气呵成之体验,实乃旷世之巨作。
本课源码:点击进入目录下载
参考资料:中游在线[WOWO世界] 之 Silverlight C# 游戏开发:游戏开发技术
教程Demo在线演示地址:http://silverfuture.cn