C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(十六) 牵引式地图移动模式②
精灵控件让游戏开发更美好!有了它,离完善牵引式地图移动模式可谓一步之遥。只剩下最后一个环节了,大家加油吧。
上一节,我在界面线程中通过时时设置Canvas.SetLeft(Spirit, Spirit.X + Canvas.GetLeft(Map) - SpiritCenterX * GridSize);和Canvas.SetTop(Spirit, Spirit.Y + Canvas.GetTop(Map) - SpiritCenterY * GridSize);来实现主角跟随着地图移动。从该公式我们可以分析出影响主角在窗口中显示位置的两个因素:第一个为地图图片(Image Map)相对于窗口的位置(Canvas.GetLeft(Map),Canvas.GetTop(Map)),它是在鼠标牵引地图移动的时候时时改变的,与主角在地图上的走动无关;第二个则为主角自身的X,Y坐标属性(Spirit.X,Spirit.Y),当主角在地图上走动时,它是时时更改的。由此可以得到一个结论:要实现主角在此模式地图上的移动,只需要在它走路的时候时时更新它的坐标Spirit.X和Spirit.Y即可,这样界面线程中会同步更新主角在窗口中的位置而达到完美的游戏动画衔接。
找到了切入点,那么实现起来就简单多了。
这里,我们首先需要对前面章节中的A*寻路方法进行一些改进。在前面的章节中,由于地图是固定死不动的,且尺寸相当于窗口大小,这样我们简单的将地图和窗口示为一体。因此,在A*寻路过程(AStarMove())中同时实现了主角相对于地图的移动,即基于对象关联属性为PropertyPath("Canvas.Left"), PropertyPath("Canvas.Top")的Storyboard动画。但是在牵引式地图移动模式中就不能这样做了,根据前面分析的原理,则必须改为基于对象关联属性为PropertyPath("X"),PropertyPath("Y")的Storyboard动画。此时的动画或许将之理解为从寻路得到的路径序列点中连续取出坐标的计时器更加贴切,因为它只负责改变Spirit的X,Y属性而不负责在界面中更新Spirit的位置实现动画。但是这已经足够了,因为它已经满足了原理中更新精灵坐标Spirit.X和Spirit.Y的目的(剩下的任务交由界面线程去做就好了,代码与上一节中的一样,我们不需要理会)。那好,接下来就看我如何对A*寻路再次进行改造(可别怕,目前的A*寻路Storyboard动画方法已经是很成熟的了,只需要对它的几个关节进行修改即可以达到不同的使用目的,其实在第九节、第十节中已经对其进行过修改了)。
接下来就是对A*寻路移动方法进行改造了:
private void AStarMoveTo(Point p) {
//进行坐标系缩小
int start_x = (int)(Spirit.X) / GridSize;
int start_y = (int)(Spirit.Y) / 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); //设置终点坐标
if (path == null) {
//MessageBox.Show("路径不存在!");
} else {
......
//创建X轴方向逐帧动画
DoubleAnimationUsingKeyFrames keyFramesAnimationX = new DoubleAnimationUsingKeyFrames();
//总共花费时间 = path.Count * cost
keyFramesAnimationX.Duration = new Duration(TimeSpan.FromMilliseconds(path.Count * cost));
Storyboard.SetTarget(keyFramesAnimationX, Spirit);
Storyboard.SetTargetProperty(keyFramesAnimationX, new PropertyPath("X"));
//创建Y轴方向逐帧动画
DoubleAnimationUsingKeyFrames keyFramesAnimationY = new DoubleAnimationUsingKeyFrames();
keyFramesAnimationY.Duration = new Duration(TimeSpan.FromMilliseconds(path.Count * cost));
Storyboard.SetTarget(keyFramesAnimationY, Spirit);
Storyboard.SetTargetProperty(keyFramesAnimationY, new PropertyPath("Y"));
for (int i = 0; i < framePosition.Count(); i++) {
//加入X轴方向的匀速关键帧
LinearDoubleKeyFrame keyFrame = new LinearDoubleKeyFrame();
//平滑衔接动画(将寻路坐标系中的坐标放大回地图坐标系中的坐标)
keyFrame.Value = i == 0 ? Spirit.X : framePosition[i].X * GridSize;
keyFrame.KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(cost * i));
keyFramesAnimationX.KeyFrames.Add(keyFrame);
//加入X轴方向的匀速关键帧
keyFrame = new LinearDoubleKeyFrame();
keyFrame.Value = i == 0 ? Spirit.Y : framePosition[i].Y * GridSize;
keyFrame.KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(cost * i));
keyFramesAnimationY.KeyFrames.Add(keyFrame);
}
......
}
}
以上代码中用黄色背景突出的即为需要修改的地方,其中Storyboard.SetTargetProperty(keyFramesAnimationX, new PropertyPath("X"));和Storyboard.SetTargetProperty(keyFramesAnimationY, new PropertyPath("Y"));这两句作用是将主角(Spirit)的X,Y属性值作为Storyboard动画的更新对象目标,值得注意的是keyFrame.Value = i == 0 ? Spirit.X : framePosition[i].X * GridSize;与keyFrame.Value = i == 0 ? Spirit.Y : framePosition[i].Y * GridSize;这两句话,它们的作用是将寻路后得到的所有路径点按从角色起点到终点这样的顺序依次做为Storyboard的关键帧添加进Storyboard动画中,并且首先舍弃寻路得到的第一个点而以坐标(Spirit.X,Spirit.Y)作为动画起点(…i == 0 ? Spirit.X ……i == 0 ? Spirit.Y…)从而起到平滑衔接动画的效果。千万别小看它,很多人往往忽略了它导致动画衔接粗糙(大家可以尝试将keyFrame.Value = i == 0 ? Spirit.X : framePosition[i].X * GridSize;替换成keyFrame.Value = framePosition[i].X * GridSize;将keyFrame.Value = i == 0 ? Spirit.Y : framePosition[i].Y * GridSize;替换成keyFrame.Value = framePosition[i].Y * GridSize;后再运行一下程序看看,当角色正在移动且还未到达终点的时候,此时你用鼠标再点击别的地方让主角向新的目的地移动,由于未采取平滑处理将导致角色会突然跳动一下的效果(起始坐标定位错误BUG)。为了配合大家更好的理解,我用张图来说明(图中的网格即为单位为20*20的单元格,即GridSize=20的效果):
A*寻路方法改造完成后,最后就是在鼠标左键点击事件中去启动它了:
private void Carrier_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) {
Point p1 = e.GetPosition(Map); //点击的地方在Map中的坐标点
//Point p2 = TranslatePoint(e.GetPosition(Carrier), Map); //此方法效果和上面一样
AStarMoveTo(p1);
}
这里的e.GetPosition(Map)我们可以理解为:在Map地图图片上点击,返回Map上该点的坐标。而我们同样的可以通过一个名为TranslatePoint的方法来达到相同的目的,即我们在Carrier画布上点击,返回Carrier上该点的坐标,接下来再将该点转换成Map地图图片上的位置点,因此返回的结果都是一样的。TranslatePoint()的形式很直观,虽然在此处使用效果不太好,但是将之用于地图边缘判断、物体与物体的碰撞与重叠判断、射程计算等问题上不失为一种优雅的解决方案。
OK,一切就绪,激动人心的时刻就要到了!按下CTRL+F5,尽情的在地图上随便点击吧,并且在任何的时候你用鼠标去牵引地图移动,主角和障碍物都会平滑的显示在正确位置上,什么叫完美?This is Perfect!Right?
后面的章节中,我将完善前十六节中一直遗留着的一个很多很多朋友迫切想要解决的大问题:如何实现精灵的八个方向?重磅炸弹即将粉墨登场,敬请关注。