C#開發WPF/Silverlight動畫及游戲系列教程(Game Course):(十三)牽引式地圖移動模式①
在前面諸多的章節裡,我就地圖構造的實現做了講解,至此還遺留著一個關鍵問題:在游戲中是角色在移動還是地圖在移動?它們之間的移動(位移)關系是如何實現的?
那麼在接下來的章節中我將圍繞這兩個問題進行詳細的分析解說。
首先,還得從游戲模式開始說起。目前2D俯視游戲中以即使戰略、SLG、RPG(ARPG)等類型的游戲為主流。在即時戰略、SLG大地圖中,地圖的移動原理是:當鼠標處於游戲窗口的8個邊緣時,地圖即開始移動,我暫且稱之為牽引式地圖移動模式。
如上圖,我們可以打這樣一個比方:將游戲窗口比做我們的攝象機(上圖中的Windows13窗口),在地圖世界裡不斷的取景,我們從攝象機中看的只有攝象機鏡頭(游戲窗口)中能看到的區域(其他虛的地圖部分窗口中是無法顯示的)。但是游戲窗口又相當於是用支架固定著不能移動的鏡頭,那該如何才能看到景色的各個部位呢?那當然只有去移動背景地圖圖片讓我們需要的景色部分呈現在窗口中。因此,根據上圖描述的原理,當鼠標進入這8個區域(藍色和棕色區域)時即觸發地圖的移動。
為更方便大家的理解,我以窗口中左邊那塊藍色區域為例:當我的鼠標在游戲窗口內往左移動快接近邊緣時,此時地圖圖片就開始反向向右進行一定速度或加速度移動;同樣的,地圖中的所有對象均跟著地圖圖片以同樣的方向與速度進行移動,這樣給我們視覺上產生一種感覺:我們在通過鼠標牽引游戲窗口向左方去探尋地圖及物體對象。有的朋友就要問了:如果一張地圖上有1000個對象,難道我每10毫秒都要去移動這1000個對象嗎?這樣性能上說是完全不科學的!對,在實際開發中如果一張地圖擁有大量物體對象的話,我們肯定不會這樣做(如果地圖是小地圖,或者物體不多,這樣做是完全可行的,並且更容易實現)。在理解了這個原理後,我們看看在WPF/Silverlight中是如何進行這些操作的。首先需要做的就在地圖移動的時候,根據地圖移動方向時時(在界面刷新線程CompositionTarget或間隔為10毫秒的DispatcherTimer中)通過Foreach高性能的對所有物體對象(Spirit)的X,Y坐標進行修改;而什麼時候才需要將這些物體對象顯示出來呢?判斷當前游戲窗口中心點對應的地圖坐標點;並以該點為中點(圓心)進行一個矩形范圍或半徑為R的圓形范圍(下文我簡稱為地圖中心范圍)搜索:如果某物體對象的X,Y坐標在此范圍內則動態將它的顯示實體加載它進入窗體畫布(Carrier.Children.Add(Spirit)),然後再將之布局到它對應的X,Y坐標位置上(Canvas.setLeft(Spirit,X);Canvas.setTop(Spirit,Y);),並且繼續根據地圖的移動而移動(時時修改Canvas.setLeft(),Canvas.setTop());同樣的,在地圖移動中地圖中心坐標是時時改變的,如果某些物體對象的X,Y坐標超出了地圖中心范圍,那麼我們就將之從窗體畫布中移除掉(Carrier.Children.Remove(Spirit)),此時這些物體對象相當於重新回到了等候顯示的狀態,它們的X,Y坐標同樣在後台線程中時時更改,只要某個時候當地圖中心再度出現在它們的附近時,它們又會重復以上的步驟再顯示出來。
大致原理有了,如何通過代碼來具體實現呢?
這裡我提供兩種方法:
第一種方法為通過載體來實現地圖移動。具體為首先向游戲窗體中添加8個完全透明的滾動介質(就好比圖中那8塊區域,其中4個藍的,4個棕的)分別布局在地圖邊緣的8個位置上(它們相對於游戲窗體來說永遠是不動的),然後在界面線程中時時判斷鼠標是否懸停在它們中的某個上從而進行相應的地圖移動。
這裡我以正下方的滾動介質為例,這樣來創建它:
int scrollspeed = 5; //定義滾動速度
Rectangle roller = new Rectangle(); //創建滾動介質
private void InitRoller() {
roller.Width = 800;
roller.Height = 20;
roller.Opacity = 0.3;
roller.Fill = new SolidColorBrush(Colors.Blue);
Carrier.Children.Add(roller);
Canvas.SetZIndex(roller, 10001);
Canvas.SetTop(roller, 490);
}
這裡為了演示需要,我將它的透明度暫且設置為0.3而不是0,目的是為方便大家可以看到它。接下來我們就需要在CompositionTarget的Timer_Tick事件中時時判斷鼠標是否在它上面:
private void Timer_Tick(object sender, EventArgs e) {
//時時判斷如果鼠標停留在了該滾動介質上,則地圖相應滾動
if (roller.IsMouseOver) {
Canvas.SetTop(Map, Canvas.GetTop(Map) - scrollspeed);
}
}
這樣就創建好了這8個區域中的其中一個(正下方區域),其他7個的創建和實現方法依次類推,很簡單就不累述了。好了,我們按CTRL+F5來測試一下,當鼠標停留在窗體正下方的那個藍色長方形區域時,地圖會非常平滑的向上移動,這樣就實現了我們視覺上的窗口向下取景效果。
從上圖可以看出,地圖向上移從而實現窗口向下取景的視覺效果,這就是地圖的相對移動原理。
通過實體介質來實現地圖移動的方式具有直觀、代碼簡單、邏輯不復雜的特性,但性能不好。
接下來看第二種方法,此方式不需要創建滾動介質,而是時時根據鼠標位置是否處於這8個區域中的任意一個進行對應的地圖移動。這種方法相對於上一種方法來說雖然不夠直觀且需要的邏輯代碼較多而繁,但它具有更高的性能與實用性,也是我推薦的方法。至於要如何實現它,我們首先需要寫一個方法,該方法用來判斷鼠標當前的位置並返回一個數字:
//根據鼠標的位置獲取鼠標所處的區域代號
//0代表正上方區域(即0點鐘位置)然後其他7個區域按順時針依次為1,2,3,4,5,6,7
int distance = 80; //定義距離邊緣多少即開始牽引地圖
private int getMouseArea() {
Point MousePosition = Mouse.GetPosition(Carrier); //獲取鼠標當前處於窗口中的位置
int result = -1;
//如果鼠標未超出窗口
if (MousePosition.X >= 0 && MousePosition.Y >= 0) {
//根據8種情況返回8個數字
if (MousePosition.X >= 190 && MousePosition.X <= 570) {
if (MousePosition.Y <= distance) {
result = 0;
} else if (MousePosition.Y >= 500 - distance) {
result = 4;
}
} else if (MousePosition.Y >= 125 && MousePosition.Y <= 375) {
if (MousePosition.X <= distance) {
result = 6;
} else if (MousePosition.X >= 760 - distance) {
result = 2;
}
} else if ((MousePosition.X < 190 && MousePosition.Y <= distance)
|| (MousePosition.Y < 125 && MousePosition.X <= distance)) {
result = 7;
} else if ((MousePosition.X > 570 && MousePosition.Y <= distance)
|| (MousePosition.Y < 125 && MousePosition.X >= 760 - distance)) {
result = 1;
} else if ((MousePosition.X > 570 && MousePosition.Y >= 500 - distance)
|| (MousePosition.Y > 375 && MousePosition.X >= 760 - distance)) {
result = 3;
} else if ((MousePosition.X < 190 && MousePosition.Y >= 500 - distance)
|| (MousePosition.Y > 375 && MousePosition.X <= distance)) {
result = 5;
}
}
return result;
}
然後我們通過這個數字就可以對應地圖邊緣的8個區域看是需要將地圖下移還是上移或是左上移動等等。這裡需要注意一個地方,當地圖已經移動到了某個方向的盡頭時,地圖是不能再移動的。所以綜合以上,我們在Timer_Tick事件中這樣來實現地圖滾動:
private void Timer_Tick(object sender, EventArgs e) {
//第二種方法
double mapleft = Canvas.GetLeft(Map);
double maptop = Canvas.GetTop(Map);
switch (getMouseArea()) {
case 0:
if (maptop < 0) {
Canvas.SetTop(Map, Canvas.GetTop(Map) + scrollspeed);
}
break;
case 1:
if (maptop < 0) {
Canvas.SetTop(Map, Canvas.GetTop(Map) + scrollspeed);
}
if (Map.Width + mapleft > this.ActualWidth) {
Canvas.SetLeft(Map, Canvas.GetLeft(Map) - scrollspeed);
}
break;
case 2:
if (Map.Width + mapleft > this.ActualWidth) {
Canvas.SetLeft(Map, Canvas.GetLeft(Map) - scrollspeed);
}
break;
case 3:
if (Map.Width + mapleft > this.ActualWidth) {
Canvas.SetLeft(Map, Canvas.GetLeft(Map) - scrollspeed);
}
if (Map.Height + maptop > this.ActualHeight) {
Canvas.SetTop(Map, Canvas.GetTop(Map) - scrollspeed);
}
break;
case 4:
if (Map.Height + maptop > this.ActualHeight) {
Canvas.SetTop(Map, Canvas.GetTop(Map) - scrollspeed);
}
break;
case 5:
if (Map.Height + maptop > this.ActualHeight) {
Canvas.SetTop(Map, Canvas.GetTop(Map) - scrollspeed);
}
if (mapleft < 0) {
Canvas.SetLeft(Map, Canvas.GetLeft(Map) + scrollspeed);
}
break;
case 6:
if (mapleft < 0) {
Canvas.SetLeft(Map, Canvas.GetLeft(Map) + scrollspeed);
}
break;
case 7:
if (maptop < 0) {
Canvas.SetTop(Map, Canvas.GetTop(Map) + scrollspeed);
}
if (mapleft < 0) {
Canvas.SetLeft(Map, Canvas.GetLeft(Map) + scrollspeed);
}
break;
}
}
以上的代碼主要就進行一些位置計算並判斷,重復的部分很多並不復雜。最後大家可以按下CTRL+F5,嘿嘿!地圖可以任意移動了。
效果上來說地圖是動了,可是主角還是始終處於窗口中的某一個位置保持不變(不管地圖怎麼移,它始終在窗口的左上角)。要實現跟隨移動效果,這就需要我們根據前面所說的原理,在地圖移動的同時對主角的X,Y坐標進行時時改變從而實現它的移動。關於主角如何在牽引式地圖移動模式中的地圖上移動及行走,我將在下一節進行詳細的講解,敬請關注。