C#開發WPF/Silverlight動畫及游戲系列教程(Game Course):(十六) 牽引式地圖移動模式②
精靈控件讓游戲開發更美好!有了它,離完善牽引式地圖移動模式可謂一步之遙。只剩下最後一個環節了,大家加油吧。
上一節,(C#開發WPF/Silverlight動畫及游戲系列教程(Game Course):(十五) 精靈控件橫空出世!② )我在界面線程中通過時時設置Canvas.SetLeft(Spirit, mapleft + Spirit.X);和Canvas.SetTop(Spirit, maptop + Spirit.Y);來實現主角跟隨著地圖移動。從該公式我們可以分析出影響主角在窗口中顯示位置的兩個因素:第一個為地圖圖片(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動畫方法已經是很成熟的了,只需要對它的幾個關節進行修改即可以達到不同的使用目的,其實在第九節、第十節中已經對其進行過修改了)。
首先定義兩個函數SpiritGameX、SpiritGameY用於將主角在地圖坐標系中的坐標換算成游戲尋路坐標系中的坐標:
int GridSize = 20; //單位格子大小
int SpiritCenterX = 4; //主角腳底離主角圖片左邊的距離(尋路坐標系中)
int SpiritCenterY = 5; //主角腳底離主角頂部的距離(尋路坐標系中)
//將主角的坐標換算成游戲坐標系中的坐標(縮小操作)
int GameSystemX {
get { return ((int)(Spirit.X) / GridSize) + SpiritCenterX; }
}
int GameSystemY {
get { return ((int)(Spirit.Y) / GridSize) + SpiritCenterY; }
}
接下來就是對A*尋路移動方法進行改造了:
private void AStarMoveTo(Point p) {
//進行坐標系縮小
int start_x = GameSystemX;
int start_y = GameSystemY;
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 - SpiritCenterX) * 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 - SpiritCenterY) * 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 - SpiritCenterX) * GridSize;與keyFrame.Value = i == 0 ? Spirit.Y : (framePosition[i].Y - SpiritCenterY) * GridSize;這兩句話,它們的作用是將尋路後得到的所有路徑點按從角色起點到終點這樣的順序依次做為Storyboard的關鍵幀添加進Storyboard動畫中,並且首先捨棄尋路得到的第一個點而以坐標(Spirit.X,Spirit.Y)作為動畫起點(…i == 0 ? Spirit.X ……i == 0 ? Spirit.Y…)從而起到平滑銜接動畫的效果。千萬別小看它,很多人往往忽略了它導致動畫銜接粗糙(大家可以嘗試將keyFrame.Value = i == 0 ? Spirit.X : (framePosition[i].X - SpiritCenterX) * GridSize;替換成keyFrame.Value = (framePosition[i].X - SpiritCenterX) * GridSize;將keyFrame.Value = i == 0 ? Spirit.Y : (framePosition[i].Y - SpiritCenterY) * GridSize;替換成keyFrame.Value = (framePosition[i].Y - SpiritCenterY) * 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?
後面的章節中,我將對前幾節的知識來個匯總,架設一個基於單元格GridSize=30,且斜度為60度的回合制戰棋類游戲戰斗場景,讓大家真真切切體驗到WPF/Silverlight開發游戲的高效、快速與簡單。在這之前,我將首先完善前十六節中一直遺留著的一個很多很多朋友迫切想要解決的大問題:如何實現精靈的八個方向?兩枚重磅炸彈即將粉墨登場,敬請關注。