C#開發WPF/Silverlight動畫及游戲系列教程(Game Course):(八) 完美實現A*尋徑動態動畫
本節將緊接著上一節,在它的基礎上實現鼠標點擊動態創建完美的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 = new PathFinderFast(Matrix);
PathFinder.Formula = HeuristicFormula.Manhattan; //使用我個人覺得最快的曼哈頓A*算法
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游戲場景,敬請關注。