程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> 關於C# >> C#開發WPF/Silverlight動畫及游戲系列教程(Game Course):(十九)

C#開發WPF/Silverlight動畫及游戲系列教程(Game Course):(十九)

編輯:關於C#

C#開發WPF/Silverlight動畫及游戲系列教程(Game Course):(十九) 完美精靈之八面玲珑(WPF Only)③

首先我要對C#開發WPF/Silverlight動畫及游戲系列教程(Game Course):(十八) 完美精靈之八面玲珑(WPF Only)②中最後的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節)做個小結吧,敬請關注。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved