本节将紧接着上一节,在它的基础上实现鼠标点击动态创建完美的A*寻路动画。(模拟游戏中人物的真实移动,这次可是有障碍物的,可以说基本上完成了人物移动引擎的一半了呢)
首先,在上一节的代码前部分加入一个叫做player的圆形作为我们将要控制的对象(模拟游戏中的主角,下文均称之为“主角”):
Ellipse player = new Ellipse(); //用一个圆来模拟目标对象
private void InitPlayer() {
player.Fill = new SolidColorBrush(Colors.Blue);
player.Width = GridSize;
player.Height = GridSize;
Carrier.Children.Add(player);
//开始位置(1,1)
Canvas.SetLeft(player, GridSize);
Canvas.SetTop(player, 5 * GridSize);
}
接下来,我们在窗体构造函数中加入InitPlayer()方法:
public Window8() {
InitializeComponent();
ResetMatrix(); //初始化二维矩阵
InitPlayer(); //初始化目标对象
}
如果大家对上一节的障碍物觉得还不过瘾,可以随便再添加,直到你觉得足够复杂来测试我们的A*动画,这里我也在上一节设定的障碍物基础上进行了一些改进,稍微复杂了些。那么我们直接进入本节的重点:如何实现鼠标点击窗体中任意点,实现主角从它当前位置移动到鼠标点击的点,并且幽雅平滑的通过A*用最短的路径越过所有的障碍物,这整个过程都是动态创建的,没有一点xaml的痕迹,嘿嘿,小得意了一下呢。当然讲解之前还是请各位朋友先熟悉前面章节的动画原理,否则还是比较难理解的。接下来看看代码:
private void Carrier_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) {
Point p = e.GetPosition(Carrier);
//进行坐标系缩小
int start_x = (int)Canvas.GetLeft(player) / GridSize;
int start_y = (int)Canvas.GetTop(player) / GridSize;
Start = new System.Drawing.Point(start_x, start_y); //设置起点坐标
int end_x = (int)p.X / GridSize;
int end_y = (int)p.Y / GridSize;
End = new System.Drawing.Point(end_x, end_y); //设置终点坐标
PathFinder.Formula = HeuristicFormula.Manhattan; //使用我个人觉得最快的曼哈顿A*算法
PathFinder.HeavyDiagonals = true; //使用对角线移动
PathFinder.HeuristicEstimate = 0;
List<PathFinderNode> path = PathFinder.FindPath(Start, End); //开始寻径
if (path == null) {
MessageBox.Show("路径不存在!");
} else {
Point[] framePosition = new Point[path.Count]; //定义关键帧坐标集
for (int i = path.Count - 1; i >= 0; i--) {
//从起点开始以GridSize为单位,顺序填充关键帧坐标集,并进行坐标系放大
framePosition[path.Count - 1 - i] = new Point(path[i].X * GridSize, path[i].Y * GridSize);
}
//创建故事板
Storyboard storyboard = new Storyboard();
int cost = 100; //每移动一个小方格(20*20)花费100毫秒
//创建X轴方向逐帧动画
DoubleAnimationUsingKeyFrames keyFramesAnimationX = new DoubleAnimationUsingKeyFrames();
//总共花费时间 = path.Count * cost
keyFramesAnimationX.Duration = new Duration(TimeSpan.FromMilliseconds(path.Count * cost));
Storyboard.SetTarget(keyFramesAnimationX, player);
Storyboard.SetTargetProperty(keyFramesAnimationX, new PropertyPath("(Canvas.Left)"));
//创建Y轴方向逐帧动画
DoubleAnimationUsingKeyFrames keyFramesAnimationY = new DoubleAnimationUsingKeyFrames();
keyFramesAnimationY.Duration = new Duration(TimeSpan.FromMilliseconds(path.Count * cost));
Storyboard.SetTarget(keyFramesAnimationY, player);
Storyboard.SetTargetProperty(keyFramesAnimationY, new PropertyPath("(Canvas.Top)"));
for (int i = 0; i < framePosition.Count(); i++) {
//加入X轴方向的匀速关键帧
LinearDoubleKeyFrame keyFrame = new LinearDoubleKeyFrame();
keyFrame.Value = i == 0 ? Canvas.GetLeft(player) : framePosition[i].X; //平滑衔接动画
keyFrame.KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(cost * i));
keyFramesAnimationX.KeyFrames.Add(keyFrame);
//加入X轴方向的匀速关键帧
keyFrame = new LinearDoubleKeyFrame();
keyFrame.Value = i == 0 ? Canvas.GetTop(player) : framePosition[i].Y;
keyFrame.KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(cost * i));
keyFramesAnimationY.KeyFrames.Add(keyFrame);
}
storyboard.Children.Add(keyFramesAnimationX);
storyboard.Children.Add(keyFramesAnimationY);
//添加进资源
if (!Resources.Contains("storyboard")) {
Resources.Add("storyboard", storyboard);
}
//故事板动画开始
storyboard.Begin();
//用白色点记录移动轨迹
for (int i = path.Count - 1; i >= 0; i--) {
rect = new Rectangle();
rect.Fill = new SolidColorBrush(Colors.Snow);
rect.Width = 5;
rect.Height = 5;
Carrier.Children.Add(rect);
Canvas.SetLeft(rect, path[i].X * GridSize);
Canvas.SetTop(rect, path[i].Y * GridSize);
}
}
}
上面的代码配有很详细的注释,这里除了前面章节里的动画知识外,新出现了动态创建关键帧动画的知识。首先我们来看第一小段,它的作用是将主角所处的位置定义为起点,将鼠标点击的位置定义为终点,然后就和上一节中讲解的一样开始通过A*寻径,最终得到路径点的集合List<PathFinderNode> path。因为根据A*原理算出的path是反向序列的,即由终点开始到起点的点集,但是我们需要得到的是正向的点集,这样在后面可以更方便调用。所以这里就用到了反向换算来计算出正向点集Point[] framePosition。万事具备后,我们分别开始创建X轴,Y轴的关键帧动画。具体关于WPF/Silverlight关键帧动画的知识这里不多说了,因为是高级教程嘛,有迷糊的朋友请先查阅相关资料,网络上有很多。这里要提出来特别讲解一下的是int cost这个变量,就如它的注释中讲的每移动一个小方格(20*20)花费100毫秒。有朋友就要问了:我移动到直线邻近方格的距离(假设为10)和移动到对角线邻近方格距离(则为14.14,根据三角函数计算)是不一样的,统一使用100来衡量是不是不够精确?这里我要特别说的是,如果您将GridSize(上一节有关于它的详细解说)定义得比较小(例如本例中定义为20),那么在程序实际运行中将完全感觉不到不同方向上移动速度的不同,所有方向上的动画感觉都是匀速且非常平滑的。但是如果GridSize定义的值越大(例如>50),那么斜线方向上的速度将明显慢过直线方向上的速度,这是因为Storyboard动画是基于时间轴形成的动画,初中物理学中就有讲解,在相同时间内行走不同长度的路程肯定会导致平均速度的不同。所以,如果想在此条件下进行真实情况模拟,就需要再进行一些数据计算及换算,这样将导致性能上打折扣。并且GridSize>50的情况在现实游戏开发中基本不存在(RPG类型游戏就不说了,GridSize是越小越好,从而得到更精确的定位,但同时带来的是更加复杂精细的地图布局工作。而显式使用格子的SLG类型游戏你有见哪款将每个格子定义为50*50像素的?如果有,800*600的屏幕显示不到10*10个格子,这是相当滑稽可笑的)。所以大家完全可以统一化,将直接和斜线的移动花费时间均统一成100毫秒,GridSize进行合理的设置,这样将大大降低程序的复杂度且性能上得到最佳效果。
回到代码上,在最后,我加入了一段代码用白色点来记录主角移动所经过的痕迹,其实就是Point[] framePosition,这样也可以非常方便大家去理解上面代码的功能作用。
完成以上代码后,我们来测试一下,运行程序我们随便乱点点看看,嘿嘿,主角可以幽雅的越过障碍物移动了呢,而且在移动的过程中你再点别的位置它将很平滑的重新向新的位置移动,可以说近乎完美的模拟了2D RPG游戏中的人物移动:
至此,我们已经实现了WPF/Silverlight游戏中人物的移动动画、越过障碍物、寻路等。那么后面的章节我将引入一个不可移动的地图作为背景并在地图中加入一些障碍物,最后结合第四章及第五章关于2D人物动画的知识模拟出一个RPG游戏场景,敬请关注。
出处:http://alamiye010.cnblogs.com/
教程目录及源码下载:点击进入(欢迎加入WPF/Silverlight小组 WPF/Silverlight博客团队)
本文版权归作者和博客园共有,欢迎转载。但未经作者同意必须保留此段声明,且在文章页面显著位置给出原文连接,否则保留追究法律责任的权利。