C#開發WPF/Silverlight動畫及游戲系列教程(Game Course):(十)斜度α地圖的構造及算法
在當前的網絡游戲中,地圖基本都是采取一定斜度的拼裝地圖,這其中存在兩種斜度地圖的構造方式:
第一種我稱之為偽斜度地圖:該類型地圖表現層圖片為斜度的,但地圖基底障礙物等的構造則實為正方形,如下圖:
其實最典型的例子就是上一節所演示的內容了,地圖是斜的,但是我們卻用垂直的障礙物對其進行基底布局,這就是典型的偽斜度地圖了。
這樣的地圖優點在於可以使用簡單直接的地圖構造算法(上一節中有詳細的講解),同樣也可以擁有漂亮的畫面。但是,當大家將之運用到實際游戲運行中將會發現人物在饒過不規則障礙物時會很別扭。當然,如果您能制作出優秀的地圖編輯器並且擁有與之默契匹配的地圖的話,這些或許不會成為大問題。
第二種即為真實的:斜度α地圖。下面我將就該類型地圖的構造基本原理及其在WPF/Silverlight中的基本實現及算法進行講解。
首先解釋一下關於α角度。通常來講,對局式或戰棋類回合制網絡游戲鐘愛於60度、45度角的地圖構造;而2D-MMORPG網絡游戲則無一定規律,可以是任意角度(根據地圖開發策劃設定進行統一的約束與規范)。下面我們先來看一張圖:
該圖以夢幻古龍對局戰斗時的場景為例進行了非常詳細的分析標注。首先我們要講解實際對應我們WPF窗口的坐標系W坐標系。圖中的W(x),W(y)即對應我們窗口坐標系的X軸(Canvas.LeftProperty)和Y軸(Canvas.TopProperty) (當然這其中有相對偏移量,我們後面會講到)。這兩軸是垂直的,也是我們最最常見的直角坐標系了,這很好理解。而該游戲的界面坐標系G坐標系,我在圖中用藍色的線進行了標識,其中G(x)正方向與G(y)負方向的夾角就是α了(在該游戲中為60度)。上圖我為了方便演示及說明,假設它的兩個坐標系均相交於一個點,這個點我將之定義為坐標原點(0,0)。大家回憶一下前兩節講解的關於障礙物數組Matrix[,]。該數組參數是無法有負值的,如Matrix[-1,5]、Matrix[6,-7]等,這些都是語法中非法的。所以假設按照坐標與障礙物等值對應原理(後面章節還會講到非等值對應—參數集體偏移量),如Matrix[5,5]對應G坐標系(5,5)、Matrix[8,9]對應G坐標系(8,9),那麼構建的地圖布局將如上圖:紅色和藍色的菱形均代表G坐標系下的坐標點(按照GridSize放大過的),菱形上方也有標識它們在G坐標系下的坐標。很清晰的可以看見,只要x或y值中有負值的,均為紅色,此區域為角色無法移動到的區域(在上圖中我用淺綠色區域進行標識)。而在其他正值區域中,菱形則均為藍色的。
如上圖,下部份那大片藍色的區域(G系正值區域)就是我們最終的游戲真實場景所在了,在斜度的游戲世界裡,所有人物角色的移動范圍均在其中。上一節中有講過,WPF窗口的左上角為原點(0,0)。但是上圖的W坐標系的原點(0,0)卻在中上部(已經標識出來,該點與左上角的x距離為a,y距離為b,圖中有標注)。如果我們需要在WPF窗口中構造出與上圖一模一樣的場景效果,就涉及到關於坐標偏移量的計算了。就拿這個例子來說,該游戲此場景中的W(0,0)其實就是WPF的(Canvas.Left(a),Canvas.Top(b));同理,點W(40,60)則為(Canvas.Left(a+40),Canvas.Top(b+60)),以此類推。這樣就很簡單了不是嗎?只要將所有的人物角色對象它們自身的坐標按以上方式進行換算,那麼就可以在WPF中實現以上的地圖坐標系構造了。這與上一節中講解到的關於將主角的坐標定位到它的腳底如出一轍。所以在大多數的游戲中都會存在一個關鍵點,比如MMORPG最典型了,主角始終處於屏幕的正中間(除非他位於地圖的8個邊緣,後面的章節會講到相關內容),顯而易見它的腳底坐標就是游戲的關鍵點,其他所有的物體都以之為參照物進行相對於它的位移。關於地圖和物體的移動問題需要大量的篇幅,相關內容我將放在後面的章節中再進行講解。那麼下面的內容就暫時以WPF窗口左上角為W系的(0,0)坐標原點,進行簡單演示在此基礎上構建的斜度α的地圖。
有了以上的基礎知識作鋪墊,後面的內容可謂小兒科了。
首要任務:構造W坐標系與G坐標系的換算公式。假設W坐標系下某點坐標為(W(x),W(y)),該點在G坐標系中的坐標為(G(x),G(y)),那麼它們之間的換算公式即為:
W(x)=(G(x)-G(y))*sinα
W(y)=(G(x)+G(y))*cosα
G(x)=(W(y)*sinα+W(x)*cosα)/2*sinα*cosα
G(y)=(W(y)*sinα-W(x)*cosα)/2*sinα*cosα
這乃本節之精華所在,好比上帝的右手,阿拉丁的神燈無所不能、天下無敵!汗一個。。。好了,有了該法寶,那麼我們開始練練手吧,看看一個斜度60的地圖是如何構造的。
首先我將該公式用代碼來表示寫成兩個方法,方法名很明確,它們的作用是分別獲取某點在G坐標系和W坐標系中的坐標:
//將窗口坐標系中的坐標換算成游戲坐標系中的坐標(縮小操作)
private Point getGamePosition(double x, double y) {
return new Point(
(int)((y + (x / 1.732)) / GridSize),
(int)((y - (x / 1.732)) / GridSize)
);
}
//將游戲坐標系中的坐標換算成窗口坐標系中的坐標(放大操作)
private Point getWindowPosition(double x, double y) {
return new Point(
(x - y) * 0.886 * GridSize,
(x + y) * 0.5 * GridSize
);
}
這裡我進行了簡單的正弦與余弦的取值,即sin60=1.732,cos60=0.5,那麼(sin60)/2=1.732/2=0.886。一張地圖中是不可能存在兩個α值的,所以本例在定義好α=60度後,我直接取它的正弦與余弦值這將有效的提高運算效率。
接下來就是構建障礙物了,只有通過它我們才能非常直觀的看到這個斜度α地圖的構造:
//構建障礙物
for (int x = 10; x < 20; x++) {
for (int y = 1; y < 10; y++) {
Matrix[x, y] = 0;
rect = new Rectangle();
//構建菱形
TransformGroup transformGroup = new TransformGroup();
SkewTransform skewTransform = new SkewTransform(-10, -25);
RotateTransform rotateTransform = new RotateTransform(54);
transformGroup.Children.Add(skewTransform);
transformGroup.Children.Add(rotateTransform);
rect.RenderTransform = transformGroup;
rect.Fill = new SolidColorBrush(Colors.GreenYellow);
rect.Opacity = 0.3;
rect.Stroke = new SolidColorBrush(Colors.Gray);
rect.Width = GridSize;
rect.Height = GridSize+2;
Carrier.Children.Add(rect);
Point p = getWindowPosition(x, y);
Canvas.SetLeft(rect, p.X);
Canvas.SetTop(rect, p.Y);
}
}
這裡我用菱形方塊真實的模擬障礙物視覺效果。接下來就是在上一節代碼的基礎上將窗口鼠標左鍵事件中相關的坐標值通過上面寫的兩個方法getGamePosition(double x, double y)和getWindowPosition(double x, double y)進行替換,實際上改動的地方不過4處,我用黃色背景色進行了標識(…….號表示該段代碼與上一節不變),具體如下:
private void Carrier_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) {
Point p = e.GetPosition(Carrier);
//進行坐標系縮小
Point start = getGamePosition(Canvas.GetLeft(Spirit) + SpiritCenterX,
Canvas.GetTop(Spirit) + SpiritCenterY);
Start = new System.Drawing.Point((int)start.X, (int)start.Y); //設置起點坐標
Point end = getGamePosition(p.X, p.Y);
End = new System.Drawing.Point((int)end.X, (int)end.Y); //設置終點坐標
…….
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] = getWindowPosition(path[i].X, path[i].Y);
}
…….
//用白色點記錄移動軌跡
for (int i = path.Count - 1; i >= 0; i--) {
rect = new Rectangle();
rect.Fill = new SolidColorBrush(Colors.Snow);
rect.Width = 4;
rect.Height = 4;
Carrier.Children.Add(rect);
Point target = getWindowPosition(path[i].X, path[i].Y);
Canvas.SetLeft(rect, target.X);
Canvas.SetTop(rect, target.Y);
}
}
}
如果大家能將上一節中講解的內容都吸收的話,那麼可以將修改的部分與上一節的代碼進行對比,再結合本節前部分內容的講解就會慢慢的理解了(請大家發散自己的思維吧)。
到這我們就完成了該斜度60的地圖構造。按Ctrl+F5看看我們的成果吧:
嘿嘿,A*尋路將我們的路徑描繪得非常明顯,顯然主角是沿著這樣一條斜度60的路線饒過這個片菱形障礙物區域的。而因為此例我將W(0,0)點和G(0,0)都定位在窗口的左上角,所以根據本節前部分關於G坐標系的講解,上圖中紅色的區域即為含有負值的區域,所以不被尋路方法所識別。您可以嘗試對該區域進行點擊,它將告訴您路徑不存在,從而也證明了我們這個坐標系的構建是成功的。
最後為了讓朋友們能更好的理解比較,我將本節例子中的障礙物代碼拷貝替換掉上一節的障礙物代碼,並將菱形換回成正方形,代碼如下:
//構建障礙物
for (int x = 10; x < 20; x++) {
for (int y = 0; y < 10; y++) {
Matrix[x, y] = 0;
rect = new Rectangle();
rect.Fill = new SolidColorBrush(Colors.GreenYellow);
rect.Opacity = 0.3;
rect.Stroke = new SolidColorBrush(Colors.Gray);
rect.Width = GridSize;
rect.Height = GridSize;
Carrier.Children.Add(rect);
Point p = getWindowPosition(x, y);
Canvas.SetLeft(rect, p.X);
Canvas.SetTop(rect, p.Y);
}
}
然後大家可以嘗試運行一下新的Window9.xaml,運行效果圖如下:
同樣的障礙物代碼在第九節的直角地圖坐標系中是垂直方型顯示的,而在本節中則為菱形方式顯示。同樣證明了本節斜度α地圖的成功構造!
Good idea!難道不是嗎?嘿嘿,比較復雜也是非常重要的一節。如果你能掌握它,想想A*尋路在不同模式地圖中可以完全忽略基本單元格的樣式(無論是正方形的,或是菱形的,甚至六邊形的)可謂無所不能,想想斜α地圖在實際游戲開發中的運用幾乎無處不在,這難道不是莫大的成就嗎?
至此,關於地圖表層的基礎知識基本都講解完了,地圖構造原理涉及的知識方方面面,有人就打這樣的比方:一個好的地圖編輯器決定著一款游戲的成功與否,這毫不為過。所以我們離真正完成它還有很長的路要走。下一節我將介紹如何實現地圖的遮罩效果,敬請關注。