上一篇:C#开发WPF/Silverlight动画及游戏系列教程(Game Course):(十九) 完美精灵之八面玲珑③

写了20节,一路向追着鬼子打一样都没停过,索性也想暂时休息一下整理整理思绪好完成后面的第二部分更为精彩的内容:诸如跟随式地图移动模式、NPC & 怪物 与主角的互动、对象AI、攻击与魔法、各种类型伤害计算、完美的RPG游戏界面……等等等等,激动吗?讲实话:我很激动!

    读者声音:还没写就开始激动了,典型的傻子。

    ^_^||言归正传,本节就先来个承上启下的的小结吧,我打算分4个部分对前20节内容进行补充拓展:

一、完美的改进型A*寻路移动模式
 
    在上一节中,我们虽然实现了精灵的全方向与动作,但是细心的朋友就会发现,精灵在走路的时候一直使用着A*;这将导致两个问题:1、性能上的损失,每次移动不管中间是否有障碍物都启动寻路算法,造成资源的白白浪费;2、在第十二节的结尾我曾轻描淡写的叙述了如何实现改进型A*,虽然通过副本地图简单实现了,但是暂时并不完美。那么,下面我将向大家讲解通过地地道道的方法实现改进型完美A*移动模式。

    何谓改进型完美A*移动模式?即主角每次移动时,首先并不启动A*寻路而是直接建立两点间的直线移动;接下来即进行时时的障碍物判断,如果没有碰撞到任何障碍物或对象则将该直线移动保持到终点;但是中途一旦碰到障碍物,则以目的地为终点即时启动A*寻路。

    原理很简单,关键技术就是如何对碰撞进行检测?

    传统的方法有两种:

    第一种我且称之为坐标还原法:即时时记录精灵未碰撞障碍物时的坐标(Old_X,Old_Y),在精灵移动时一旦检测到精灵此时站到了障碍物上,则将精灵此时的坐标进行还原(X=Old_X,Y= Old_Y),然后启动A*寻路。此方法的优点是使用简单,不需要复杂的判断逻辑;缺点是效果不好,在画面上将造成精灵一瞬间被弹开的情况,虽然那一刻非常的短暂且距离微小,但是对于精灵移动动画平滑性的影响是严重的,因此我们最好不要采用此方法。

    第二种为启发式预测法:该方法的原理为时时对精灵前方的区域进行预测,一旦发现前方有障碍物,则即时启动A*寻路直到目的地。该方法可谓绝对皇室血统,一个字“正”,集所有优点之大成者;优点多相对的实现起来难度就大些。在WPF/Silverlight中如何实现之?先来看下图:


附件: 1.jpg



    上图中已经给了很详细的说明,即在直线移动过程中,精灵时时判断此时朝向前方的单元格是否为障碍物,如果是则启动A*寻路饶过它。充分理解了原理后,我们可以通过如下方法来返回精灵是否将要遇到障碍物了:
  1. //判断是否将要碰撞到障碍物(障碍物预测法)

  2. private bool WillCollide() {

  3.   switch ((int)Spirit.Direction) {

  4.     case 0:

  5.         return Matrix[(int)(Spirit.X / GridSize), (int)(Spirit.Y / GridSize) - 1] == 0 ? true : false;

  6.     case 1:

  7.         return Matrix[(int)(Spirit.X / GridSize) + 1, (int)(Spirit.Y / GridSize) - 1] == 0 ? true : false;

  8.     case 2:

  9.         return Matrix[(int)(Spirit.X / GridSize) + 1, (int)(Spirit.Y / GridSize)] == 0 ? true : false;

  10.     case 3:

  11.         return Matrix[(int)(Spirit.X / GridSize) + 1, (int)(Spirit.Y / GridSize) + 1] == 0 ? true : false;

  12.     case 4:

  13.         return Matrix[(int)(Spirit.X / GridSize), (int)(Spirit.Y / GridSize) + 1] == 0 ? true : false;

  14.     case 5:

  15.         return Matrix[(int)(Spirit.X / GridSize) - 1, (int)(Spirit.Y / GridSize) + 1] == 0 ? true : false;

  16.     case 6:

  17.         return Matrix[(int)(Spirit.X / GridSize) - 1, (int)(Spirit.Y / GridSize)] == 0 ? true : false;

  18.     case 7:

  19.         return Matrix[(int)(Spirit.X / GridSize) - 1, (int)(Spirit.Y / GridSize) - 1] == 0 ? true : false;

  20.     default:

  21.         return true;

  22.   }

  23. }
复制代码
WillCollide()方法依据精灵的朝向判断精灵前方是否为障碍物(即判断障碍物数组Matrix[,]此时是否为0)。

    有了它以后,我们同样还需要像第十二节一样建立一个名为NormalMoveTo()的方法用于精灵直线移动,此时我们只需要在第十二节代码的基础上增加精灵朝向部分即可:
  1.         //直线移动

  2.         private void NormalMoveTo(Point p) {

  3.             //总的移动花费

  4.             int totalcost = (int)Math.Sqrt(Math.Pow(p.X - Spirit.X, 2) + Math.Pow(p.Y - Spirit.Y, 2)) / GridSize * UnitMoveCost;

  5.             ……

  6.             //创建主角朝向属性动画

  7.             double direction = Super.GetDirectionByTan(p.X, p.Y, Spirit.X, Spirit.Y);

  8.             doubleAnimation = new DoubleAnimation(

  9.               direction,

  10.               direction,

  11.               new Duration(TimeSpan.FromMilliseconds(totalcost))

  12.             );

  13.             Storyboard.SetTarget(doubleAnimation, Spirit);

  14.             Storyboard.SetTargetProperty(doubleAnimation, new PropertyPath("Direction"));

  15.             storyboard.Children.Add(doubleAnimation);

  16.             //动画播放

  17.             storyboard.Begin();

  18.         }
复制代码
这里要特别注意的是我用黄色背景注明的totalcost这个变量,它的值代表精灵在两点间移动所需要花费的时间,计算它的目的是因为Storyboard动画是基于时间轴的动画(即在一个规定时间内完成指定动画),第一节中也有相应的说明。因此,为了让精灵在全角度(不仅仅是8个方向,是360度全方位)的任意两点间直线移动时均使用统一速度(每移动一个单元格固定花费UnitMoveCost毫秒),这样不论两点间是30度、40度、55度、76.3度、87.6度等等随意多少角度,精灵均能进行平滑的均速移动。

    OK,一切就绪,接下来就是在游戏窗口中的鼠标左键点击事件中启动精灵的直线移动:
  1.         private void Carrier_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) {

  2.             Point p = e.GetPosition(Map); //点击的地方在Map中的坐标点

  3.             //假如点击的地点不是障碍物

  4.             if (Matrix[(int)p.X / GridSize, (int)p.Y / GridSize] != 0) {

  5.                 Spirit.Destination = p; //设置主角的最终移动目的地

  6.                 Spirit.Action = Actions.Run; //主角动作切换成跑步状态

  7.                 Spirit.IsAStarMoving = false; //非寻路模式

  8.                 NormalMoveTo(p); //两点间建立直线移动

  9.             }

  10.         }
复制代码
看完上面代码有朋友就要问了:IsAStarMoving是什么东西?简单讲,它是精灵直线移动与A*寻路移动的枢纽。虽然我们实现了NormalMoveTo()和AStarMoveTo()这两种移动方式,但是如何在逻辑中对其进行很好的衔接,这里就必须加入IsAStarMoving这个精灵属性,有了它,我们就可以在窗口刷新事件中这样写:
  1.         //游戏窗口刷新主线程间隔事件

  2.         private void Timer_Tick(object sender, EventArgs e) {

  3.             ……

  4.             //判断主角是否移动到了目标,如果是则动作切换成停止

  5.             if (ArriveTarget()) {

  6.                 Spirit.Action = Actions.Stop;

  7.             } else if (!Spirit.IsAStarMoving && WillCollide()) {

  8.                 //在寻路移动模式中,主角100%会饶过障碍物的,

  9.                 //因此只有在非寻路模式中才需要时时判断主角是否将要碰撞障碍物

  10.                 AStarMoveTo(Spirit.Destination);

  11.                 Spirit.IsAStarMoving = true;

  12.             }

  13.         }
复制代码
通过黄色背景代码部分的逻辑我们可以轻松实现精灵的直线移动与A*移动的转换。即精灵首先进行直线移动,在它没有到达目的地之前(ArriveTarget()==false)我们需要时时判断它是否将要碰撞到障碍物(判断WillCollide()是否==True),并且前提是精灵在此移动中还没启动过A*寻路(IsAStarMoving==false),因为一旦在直线移动中启动过A*寻路,结果100%会引导精灵饶过障碍物到达终点,A*寻路过程中不需要额外再判断是否还会碰撞到障碍物,那是多此一举(如果出现偶然,不要怪别人,怪自己没把A*算法写正确)。如果此两个条件都符合了,则以精灵的移动目标(Destination)为终点启动A*寻路模式,这样就顺利的由直线移动转入到A*寻路移动,完美的衔接与枢纽。

附件: 2.jpg
TOP