首先我要对上一节中最后的ChangeAction()方法进行一些补充说明。该方法的作用之一是根据精灵的当前动作(Action)来设置精灵切图动画的起始帧和结束帧:
如上图,我们可以很清楚的看到精灵这5个动作所分别对应的CurrentStartFrame和CurrentEndFrame。这两个参数很重要是因为在精灵的生命线程中我们可以通过如下黄色区域代码来实现动态的更新精灵角色图片以形成连续动画:
//精灵线程间隔事件
private void Timer_Tick(object sender, EventArgs e) {
……
//动态更改精灵图片源以形成精灵连续动作
Body.Source = Source[(int)Direction, FrameCounter];
FrameCounter = FrameCounter == CurrentEndFrame ? CurrentStartFrame : FrameCounter + 1;
}
举个例子:当精灵在跑步的时候FrameCounter从5开始记数,然后以1为单位阶梯推进,目标是12,当到了12后再返回5继续重复前面的过程;当精灵在施法的时候,FrameCounter从20开始记数,然后以1为单位阶梯推进,目标是25,当到了25后再返回20重复前面过程。其他的以此类推……
充实了精灵动画原理后,我们再重新回到本节的主题上:如何使精灵在移动的时候表现出正确的朝向以及精确的定位与停止。
大家是否还记得我在第六节结尾的地方略有提到相关的实现方法,但是并未对之进行实现,也算留给大家的一个小思考吧。但是本节我既然起了完美精灵这个题目,就不打算辜负所有朋友们的期待,我们首先分析实现8方向精灵的步骤:
1、获取主角当前的坐标,这在第十五节中已经完美实现了,而且同样是定位到脚底的(Spirit.X,Spirit.Y)。
2、获取目标坐标,即鼠标左键(或右键)点击的点的坐标,该坐标我们可以通过鼠标左键(或右键)点击事件轻松得到,这在前面的章节里有大量的提及。
3、以以上两个坐标为参数,通过正切值计算公式计算出主角当前的朝向并返回一个数字代号(0-7分别对应8个方向)
具体如何操作,且看下图:
原理在上图右半部分的注释中描述很清楚;我依据此原理写了个通用判断朝向的方法,精华哦:
/// <summary>
/// 通过正切值获取精灵的朝向代号
/// </summary>
/// <param name="targetX">目标点的X值</param>
/// <param name="targetY">目标点的Y值</param>
/// <param name="currentX">当前点的X值</param>
/// <param name="currentY">当前点的Y值</param>
/// <returns>精灵朝向代号(以北为0顺时针依次1,2,3,4,5,6,7)</returns>
public static double GetDirectionByTan(double targetX, double targetY, double currentX, double currentY) {
double tan = (targetY - currentY) / (targetX - currentX);
if (Math.Abs(tan) >= Math.Tan(Math.PI * 3 / 8) && targetY <= currentY) {
return 0;
} else if (Math.Abs(tan) > Math.Tan(Math.PI / 8) && Math.Abs(tan) < Math.Tan(Math.PI * 3 / 8) && targetX > currentX && targetY < currentY) {
return 1;
} else if (Math.Abs(tan) <= Math.Tan(Math.PI / 8) && targetX >= currentX) {
return 2;
} else if (Math.Abs(tan) > Math.Tan(Math.PI / 8) && Math.Abs(tan) < Math.Tan(Math.PI * 3 / 8) && targetX > currentX && targetY > currentY) {
return 3;
} else if (Math.Abs(tan) >= Math.Tan(Math.PI * 3 / 8) && targetY >= currentY) {
return 4;
} else if (Math.Abs(tan) > Math.Tan(Math.PI / 8) && Math.Abs(tan) < Math.Tan(Math.PI * 3 / 8) && targetX < currentX && targetY > currentY) {
return 5;
} else if (Math.Abs(tan) <= Math.Tan(Math.PI / 8) && targetX <= currentX) {
return 6;
} else if (Math.Abs(tan) > Math.Tan(Math.PI / 8) && Math.Abs(tan) < Math.Tan(Math.PI * 3 / 8) && targetX < currentX && targetY < currentY) {
return 7;
} else {
return 0;
}
}
由于Math.Tan()函数的参数为弧度单位,因此需要将角度通过公式换算成弧度,并且至于之中的比较算法逻辑是否为最优,仍然那句老话:仁者见仁,智者见智。或许你写的算法更优秀呢?
有了该方法,接下来就是在鼠标左键点击事件中获取目标点,并且将主角的当前动作切换成跑步状态,并启动A*寻路:
private void Carrier_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) {
Point p = e.GetPosition(Map); //点击的地方在Map中的坐标点
Spirit.Action = Actions.Run; //主角动作切换成跑步状态
AStarMoveTo(p); //开始寻路
}
上两节的AstarMoveTo()方法中的Storyboard动画只创建X,Y序列点,而为了实现角色时时朝向,我们还需要创建对应的角色方向(Direction)序列点,因此我们还需要对本节中的AstarMoveTo()方法进行如下改进:
private void AStarMoveTo(Point p) {
……
//创建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"));
//创建朝向动画
DoubleAnimationUsingKeyFrames keyFramesAnimationDirection = new DoubleAnimationUsingKeyFrames();
keyFramesAnimationDirection.Duration = new Duration(TimeSpan.FromMilliseconds(path.Count * cost));
Storyboard.SetTarget(keyFramesAnimationDirection, Spirit);
Storyboard.SetTargetProperty(keyFramesAnimationDirection, new PropertyPath("Direction"));
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);
//加入朝向匀速关键帧
keyFrame = new LinearDoubleKeyFrame();
keyFrame.Value = i == framePosition.GetUpperBound(0)
? Super.GetDirectionByTan(framePosition[i].X, framePosition[i].Y, framePosition[i - 1].X, framePosition[i - 1].Y)
: Super.GetDirectionByTan(framePosition[i + 1].X, framePosition[i + 1].Y, framePosition[i].X, framePosition[i].Y)
;
keyFrame.KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(cost * i));
keyFramesAnimationDirection.KeyFrames.Add(keyFrame);
}
storyboard.Children.Add(keyFramesAnimationX);
storyboard.Children.Add(keyFramesAnimationY);
storyboard.Children.Add(keyFramesAnimationDirection);
……
}
该方法中的黄色部分即为新增加的内容,Direction动画创建与X,Y两个关联属性动画如出一辙,大家对比分析一下就可以轻松理解,原理大致是将寻路中得到的点序列进行后一个点与前一个点之间计算正切值以确定方向,再将这些方向序列值作为关键帧加入进Storyboard逐帧动画中。
一切就绪,让我们启动程序看看成果吧:
嘿嘿,角色可以显示正确的方向了呢!但是还存在一个小问题,当主角到达目的地后仍然保持着跑步状态而非站立。这里,我通过以下方法来判断并返回是否到达了目的地:
//判断是否移动到目的地
private bool ArriveTarget() {
return (storyboard != null && storyboard.GetCurrentProgress() == 1) ? true : false;
}
最后,我将ArriveTarget()方法加入到游戏窗口界面刷新主线程中,判断当主角到达目的地时,切换主角的动作为站立状态:
//游戏窗口刷新主线程间隔事件
private void Timer_Tick(object sender, EventArgs e) {
//判断主角是否移动到了目标,如果是则动作切换成停止
if (ArriveTarget()) {
Spirit.Action = Actions.Stop;
}
……
}
终于完成了,忽忽…用了3节才写完呢,真够累的。欣赏一下最终成果,犒劳犒劳一下自己吧:
神奇的虚拟世界还需要我们继续去锻造完善,下一节我将对前面19节的内容来个补遗及拓展,也算给本教程第一大部分(1-20节)做个小结吧,敬请关注。
出处:http://alamiye010.cnblogs.com
教程目录及源码下载:点击进入(欢迎加入WPF/Silverlight小组 WPF/Silverlight博客团队)
本文版权归作者和博客园共有,欢迎转载。但未经作者同意必须保留此段声明,且在文章页面显著位置给出原文连接,否则保留追究法律责任的权利。