C#開發WPF/Silverlight動畫及游戲系列教程(Game Course):(三十四)地圖編輯器誕生啦!
到目前為止,教程示例游戲中雖然實現了A*,但是還無法輕松的為地圖設置障礙物;並且游戲所有地圖均為一張整的大圖片,主角的移動會導致窗體對地圖的不停切割,越大的地圖帶來的負面性能損耗越明顯。對地圖進行切片處理則可達到性能的最大優化:載入的時候按需加載,地圖根據主角的位置僅顯示特定部份;並且如果還能配上任意勾勒的遮擋物,那麼這一切的一切將更能完美的诠釋我們的游戲。開發制作地圖編輯器已迫在眉睫。
那麼本節我將為大家講解如何制作一款基於Grid的即易用又強大的地圖編輯器,並首先實現障礙物設定功能及A*尋路模擬。
第一步:設計布局
通用型的編輯器必須能夠適應所有尺寸的地圖,因此我選擇ScrollViewer作為地圖的承載容器,並通過設置它的HorizontalScrollBarVisibility與VerticalScrollBarVisibility均為auto使之能自適應地圖尺寸,即當地圖超出窗體尺寸的情況下出現滾動條。由於一個ScrollViewer只能承載一個Content,因此為了可以在其上面能夠進行障礙物的繪制及擦除操作,我們還必須添加另外一個背景透明的ScrollViewer用於承載障礙物網格(Grid):
如描述上說的,這兩個ScrollViewer必須保持協動一致,即兩者的尺寸、滾動條時刻所處的位置等均表現一致:
//滾動窗體二級協動
private void ObstructionViewer_ScrollChanged(object sender, ScrollChangedEventArgs e) {
ScrollViewer scrollViewer = sender as ScrollViewer;
MapViewer.ScrollToVerticalOffset(scrollViewer.VerticalOffset);
MapViewer.ScrollToHorizontalOffset(scrollViewer.HorizontalOffset);
}
第二步,設計功能
1)載入地圖:
通過OpenFileDialog來開啟一個文件選擇對話框,並通過文件選擇過濾器Filter限制加載圖片的類型為*.jpg和*.png:
//載入地圖
private void LoadMap_Click(object sender, RoutedEventArgs e) {
OpenFileDialog loadMap = new OpenFileDialog() {
CheckFileExists = true,
CheckPathExists = true,
Multiselect = false,
Filter = "圖像文件(*.jpg,*.png)|*.jpg;*.png",
};
loadMap.FileOk += new System.ComponentModel.CancelEventHandler(loadMap_FileOk);
loadMap.ShowDialog();
}
2)布局網格:
為了可以適應不同GridSize尺寸的需要,我們需要更加靈活的網格模型,因此選擇Grid做為主體,當加載完地圖後,我們可以根據該地圖的相關數據自動在畫板ScrollViewer中動態添加Grid(包括行數和列數):
……
grid = new Grid() {
ShowGridLines = ShowGrid.IsChecked.Value,
Width = width,
Height = height,
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top,
};
GridWidth.Text = gridWidth.ToString();
GridHeight.Text = gridHeight.ToString();
for (int x = 0; x < grid.Width / gridWidth; x++) {
ColumnDefinition col = new ColumnDefinition() {
Width = new GridLength(gridWidth),
};
grid.ColumnDefinitions.Add(col);
}
for (int y = 0; y < grid.Height / gridHeight; y++) {
RowDefinition row = new RowDefinition() {
Height = new GridLength(gridHeight),
};
grid.RowDefinitions.Add(row);
}
scrollViewer.Content = grid;
……
Grid的ShowGridLines參數非常有意思,通過將之設置為True即可顯示出Grid所有單元格的邊框:
遺憾的是,一旦啟動了網格邊框顯示,將嚴重影響界面線程的性能,仿佛有些雞肋了,有時間我還打算嘗試其他的方式來高效的設置單元格邊框。
3)設置障礙物:
通過為畫板ScrollViewer注冊鼠標左鍵點擊事件及鼠標移動事件並配合一定的邏輯來實現障礙物的繪制於擦除:
…
if (grid == null) { return; }
Point p = e.GetPosition(ObstructionViewer);
if (p.X < 738 && p.Y < 551) {
p = e.GetPosition(Map);
test.Text = string.Format("當前坐標 x:{0} y:{1}", (int)p.X, (int)p.Y);
SetObstructionMatrix((int)(p.X / GridWidthSlider.Value), (int)(p.Y / GridHeightSlider.Value), 0);
}
}
…
4)模擬A*尋路:
在畫板上描繪完障礙物後,再通過自行繪制起點與終點,並將教程中的A*尋路dll引用到本編輯器中即可以實現A*尋路模擬:
IPathFinder pathFinder;
List<Rectangle> pathRect = new List<Rectangle>();
//模擬A*尋路
private void FindPath_Click(object sender, RoutedEventArgs e) {
if (grid == null || start == "" || end == "") { return; }
string[] str = start.Split('_');
int start_x = Convert.ToInt32(str[1]);
int start_y = Convert.ToInt32(str[2]);
str = end.Split('_');
int end_x = Convert.ToInt32(str[1]);
int end_y = Convert.ToInt32(str[2]);
pathFinder = new PathFinderFast(ObstructionMatrix);
List<PathFinderNode> path = pathFinder.FindPath(new Point(start_x, start_y), new Point(end_x, end_y));
if (path == null) {
MessageBox.Show("路徑不存在!");
} else {
textBlock3.Text = string.Format("耗時:{0}秒", Math.Round(pathFinder.CompletedTime, 8).ToString());
string result = "";
RemoveRect();
for (int i = 0; i < path.Count; i++) {
result += string.Format("{0}_{1}", path[i].X, path[i].Y);
SetRect(new SolidColorBrush(Colors.White), new SolidColorBrush(Colors.Black), GridWidthSlider.Value * 2 / 3, GridHeightSlider.Value * 2 / 3, GridWidthSlider.Value * 2 / 3, GridHeightSlider.Value * 2 / 3, path[i].X, path[i].Y);
}
}
}
4)障礙物數組的導出與導入:
我們可以事先制作好一個xml模板用於存放地圖中的障礙物信息:
<?xml version="1.0" encoding="utf-8" ?>
<Item ID="Obstruction" Value="" />
當繪制出滿意的地圖障礙物並通過A*模擬測試無誤後即可將此時的障礙物數組信息進行導出保存:
//導出障礙物信息文件
private void outputMatrix_FileOk(object sender, CancelEventArgs e) {
SaveFileDialog outputMatrix = sender as SaveFileDialog;
string result = "";
for (int y = 0; y <= ObstructionMatrix.GetUpperBound(1); y++) {
for (int x = 0; x <= ObstructionMatrix.GetUpperBound(0); x++) {
if (ObstructionMatrix[x, y] == 0) {
result = string.Format("{0}{1}", result, string.Format("{0}_{1},", x, y));
}
}
}
SetXmlValue(Data, "Item", "ID", "Obstruction", "Value", result.TrimEnd(','));
Data.Save(outputMatrix.FileName);
MessageBox.Show("導出成功!");
}
以上圖為例,該圖中的障礙物信息導出後的文件內容如下:
這些障礙物數據以x_y的形式命名,並以,號間隔,因此對其重新載入也是非常容易的事:
//導入障礙物信息文件
private void loadMatrix_FileOk(object sender, CancelEventArgs e) {
OpenFileDialog loadMatrix = sender as OpenFileDialog;
try{
XElement xml = XElement.Load(string.Format(@"{0}", loadMatrix.FileName));
if (xml.HasAttributes) {
ClearGrid();
RemoveRect();
string[] matrix = GetXmlValue(xml, "Item", "ID", "Obstruction", "Value").Split(',');
for (int i = 0; i < matrix.Count(); i++) {
SetRect(string.Format("Rect_{0}", matrix[i]), new SolidColorBrush(Colors.Yellow), new SolidColorBrush(Colors.Black), GridWidthSlider.Value, GridHeightSlider.Value, 0, 0, Convert.ToInt32(matrix[i].Split('_')[0]), Convert.ToInt32(matrix[i].Split('_')[1]));
}
}
} catch {
MessageBox.Show("導入失敗!請檢文件是否匹配");
e.Cancel = true;
}
}
至於這些障礙物數據該如何才能為本教程示例游戲所用?嘿嘿~且聽下回分解。
地圖編輯器通過以上的構造及功能設置已初具雛形,但是離真正完整功能的編輯器還是有著非常大的距離。後續教程中我會根據需要,在此編輯器的基礎上不斷添加新功能,目的只有一個:使游戲設計更輕松,更快速。一定要關注哦!