3. 地图编辑器
2026/4/14大约 15 分钟
3. 坦克大战——地图编辑器
简介
地图编辑器就像一个数字版的乐高拼图板。你有一块13x13的网格板,旁边放着各种类型的"积木块"——砖墙、钢墙、草地、水面等。你只需要用鼠标点一点,就能把积木放到板子上,拼出你想要的战场布局。
做完之后,你可以保存这个地图,下次玩游戏时就能在你自己设计的战场上打坦克了。甚至可以把地图文件分享给朋友,让他们也来挑战你设计的关卡。
为什么需要地图编辑器?
有了地图编辑器,你的游戏就不再只是"打完固定关卡就没意思了"。玩家可以:
- 创造无限关卡:13x13的格子,每格7种选择,组合起来有天文数字种可能
- 分享乐趣:把地图分享给朋友,互相挑战
- 学习游戏设计:通过设计地图,理解什么是"好玩的关卡"
网格地图基础
什么是网格地图?
想象你在一张方格纸上画画。这张纸被分成了很多小格子,每个格子你可以涂不同的颜色。坦克大战的地图就是这样的——13行13列,一共169个格子。
| 概念 | 说明 | 数值 |
|---|---|---|
| 网格大小 | 地图有多少行和列 | 13行 x 13列 |
| 格子大小 | 每个格子的像素尺寸 | 48 x 48 像素 |
| 总格子数 | 地图上一共有多少个格子 | 169个 |
| 地形类型 | 每个格子可以放什么 | 7种(空地、砖墙、钢墙、草地、水面、冰面、基地) |
网格坐标与世界坐标的转换
地图编辑器需要频繁在两种坐标系之间转换:
- 网格坐标:用"第几行第几列"表示,比如 (3, 5) 表示第3行第5列
- 世界坐标:用像素位置表示,比如 (240, 144) 表示屏幕上某个具体位置
转换公式很简单,就像把"厘米"换算成"毫米"一样:
网格坐标 → 世界坐标:worldX = 列号 x 格子大小, worldY = 行号 x 格子大小
世界坐标 → 网格坐标:列号 = worldX / 格子大小, 行号 = worldY / 格子大小// MapEditor.cs - 地图编辑器核心逻辑
using Godot;
using System.Collections.Generic;
public partial class MapEditor : Control
{
// ========== 常量 ==========
private const int GRID_COLS = 13;
private const int GRID_ROWS = 13;
private const int TILE_SIZE = 48;
// ========== 编辑器状态 ==========
// 当前选中的画笔类型(要画什么地形)
private int _currentTileType = 0;
// 地图数据(二维数组)
private int[,] _mapData = new int[GRID_ROWS, GRID_COLS];
// 编辑器的网格线是否显示
private bool _showGrid = true;
// 是否正在拖拽绘制(按住鼠标左键不放连续绘制)
private bool _isDrawing = false;
// ========== 编辑器UI节点引用 ==========
[Export] public PanelContainer EditorPanel { get; set; }
[Export] public TextureRect MapDisplay { get; set; }
[Export] public HBoxContainer ToolBar { get; set; }
// 画笔按钮列表
private List<Button> _toolButtons = new();
// ========== 初始化 ==========
public override void _Ready()
{
// 初始化地图为全空
ClearMap();
// 创建工具栏按钮
CreateToolBar();
// 设置默认画笔为空地(橡皮擦)
SelectTool(0);
}
// 清空地图
private void ClearMap()
{
for (int row = 0; row < GRID_ROWS; row++)
{
for (int col = 0; col < GRID_COLS; col++)
{
_mapData[row, col] = 0;
}
}
RenderMap();
}
// 创建工具栏
private void CreateToolBar()
{
// 地形名称和对应的类型编号
var tools = new (string name, int type)[]
{
("空地", 0),
("砖墙", 1),
("钢墙", 2),
("草地", 3),
("水面", 4),
("冰面", 5),
("基地", 9)
};
foreach (var (name, type) in tools)
{
var button = new Button { Text = name };
button.Pressed += () => SelectTool(type);
ToolBar.AddChild(button);
_toolButtons.Add(button);
}
}
// 选择画笔工具
private void SelectTool(int type)
{
_currentTileType = type;
// 高亮当前选中的按钮
for (int i = 0; i < _toolButtons.Count; i++)
{
var isMatch = (i == type || (i > 5 && type == 9));
_toolButtons[i].Modulate = isMatch
? new Color(1, 1, 0) // 黄色高亮
: new Color(1, 1, 1); // 默认白色
}
}
// ========== 坐标转换 ==========
// 鼠标位置转网格坐标
private Vector2I ScreenToGrid(Vector2 mousePos)
{
// 需要考虑地图显示区域的偏移
var localPos = MapDisplay.GetLocalMousePosition();
int col = (int)(localPos.X / TILE_SIZE);
int row = (int)(localPos.Y / TILE_SIZE);
// 确保不超出边界
col = Mathf.Clamp(col, 0, GRID_COLS - 1);
row = Mathf.Clamp(row, 0, GRID_ROWS - 1);
return new Vector2I(row, col);
}
// ========== 绘制逻辑 ==========
public override void _GuiInput(InputEvent @event)
{
// 鼠标左键按下:开始绘制
if (@event is InputEventMouseButton mouseDown
&& mouseDown.ButtonIndex == MouseButton.Left
&& mouseDown.Pressed)
{
_isDrawing = true;
PlaceTile(ScreenToGrid(mouseDown.Position));
}
// 鼠标左键松开:停止绘制
if (@event is InputEventMouseButton mouseUp
&& mouseUp.ButtonIndex == MouseButton.Left
&& !mouseUp.Pressed)
{
_isDrawing = false;
}
// 鼠标移动 + 按住左键:连续绘制
if (@event is InputEventMouseMotion mouseMove && _isDrawing)
{
PlaceTile(ScreenToGrid(mouseMove.Position));
}
}
// 在指定网格位置放置地形
private void PlaceTile(Vector2I gridPos)
{
// 基地只能放在最底部中间
if (_currentTileType == 9 && gridPos != new Vector2I(12, 6))
{
GD.Print("基地只能放在地图最底部中间!");
return;
}
_mapData[gridPos.X, gridPos.Y] = _currentTileType;
RenderTile(gridPos.X, gridPos.Y);
}
// ========== 渲染地图 ==========
// 渲染整个地图
private void RenderMap()
{
// 清除旧的渲染
foreach (var child in MapDisplay.GetChildren())
{
child.QueueFree();
}
// 逐格渲染
for (int row = 0; row < GRID_ROWS; row++)
{
for (int col = 0; col < GRID_COLS; col++)
{
RenderTile(row, col);
}
}
// 画网格线
if (_showGrid) RenderGridLines();
}
// 渲染单个格子
private void RenderTile(int row, int col)
{
// 使用不同颜色的矩形表示不同地形
var colorRect = new ColorRect
{
Size = new Vector2(TILE_SIZE, TILE_SIZE),
Position = new Vector2(col * TILE_SIZE, row * TILE_SIZE),
Color = GetTileColor(_mapData[row, col])
};
MapDisplay.AddChild(colorRect);
}
// 根据地形类型返回颜色
private Color GetTileColor(int tileType)
{
return tileType switch
{
0 => new Color("2d2d2d"), // 空地 - 深灰色
1 => new Color("b5651d"), // 砖墙 - 棕色
2 => new Color("808080"), // 钢墙 - 银灰色
3 => new Color("228b22"), // 草地 - 绿色
4 => new Color("4169e1"), // 水面 - 蓝色
5 => new Color("e0ffff"), // 冰面 - 浅蓝色
9 => new Color("ffd700"), // 基地 - 金色
_ => new Color("2d2d2d") // 默认空地
};
}
// 画网格线
private void RenderGridLines()
{
var lineWidth = 1;
var gridColor = new Color(1, 1, 1, 0.3f); // 半透明白色
// 画竖线
for (int col = 0; col <= GRID_COLS; col++)
{
var line = new Line2D
{
Width = lineWidth,
DefaultColor = gridColor
};
line.AddPoint(new Vector2(col * TILE_SIZE, 0));
line.AddPoint(new Vector2(col * TILE_SIZE, GRID_ROWS * TILE_SIZE));
MapDisplay.AddChild(line);
}
// 画横线
for (int row = 0; row <= GRID_ROWS; row++)
{
var line = new Line2D
{
Width = lineWidth,
DefaultColor = gridColor
};
line.AddPoint(new Vector2(0, row * TILE_SIZE));
line.AddPoint(new Vector2(GRID_COLS * TILE_SIZE, row * TILE_SIZE));
MapDisplay.AddChild(line);
}
}
}# map_editor.gd - 地图编辑器核心逻辑
extends Control
# ========== 常量 ==========
const GRID_COLS: int = 13
const GRID_ROWS: int = 13
const TILE_SIZE: int = 48
# ========== 编辑器状态 ==========
# 当前选中的画笔类型(要画什么地形)
var current_tile_type: int = 0
# 地图数据(二维数组)
var map_data: Array = []
# 编辑器的网格线是否显示
var show_grid: bool = true
# 是否正在拖拽绘制
var is_drawing: bool = false
# ========== 编辑器UI节点引用 ==========
@export var editor_panel: PanelContainer
@export var map_display: TextureRect
@export var tool_bar: HBoxContainer
# 画笔按钮列表
var tool_buttons: Array[Button] = []
# ========== 初始化 ==========
func _ready() -> void:
# 初始化地图为全空
clear_map()
# 创建工具栏按钮
create_tool_bar()
# 设置默认画笔为空地(橡皮擦)
select_tool(0)
## 清空地图
func clear_map() -> void:
map_data = []
for row in GRID_ROWS:
var row_data: Array = []
for col in GRID_COLS:
row_data.append(0)
map_data.append(row_data)
render_map()
## 创建工具栏
func create_tool_bar() -> void:
# 地形名称和对应的类型编号
var tools: Array = [
{"name": "空地", "type": 0},
{"name": "砖墙", "type": 1},
{"name": "钢墙", "type": 2},
{"name": "草地", "type": 3},
{"name": "水面", "type": 4},
{"name": "冰面", "type": 5},
{"name": "基地", "type": 9}
]
for tool in tools:
var button = Button.new()
button.text = tool.name
button.pressed.connect(func(): select_tool(tool.type))
tool_bar.add_child(button)
tool_buttons.append(button)
## 选择画笔工具
func select_tool(type: int) -> void:
current_tile_type = type
# 高亮当前选中的按钮
for i in range(tool_buttons.size()):
var is_match = (i == type or (i > 5 and type == 9))
tool_buttons[i].modulate = Color.YELLOW if is_match else Color.WHITE
# ========== 坐标转换 ==========
## 鼠标位置转网格坐标
func screen_to_grid(mouse_pos: Vector2) -> Vector2i:
var local_pos = map_display.get_local_mouse_position()
var col: int = int(local_pos.x / TILE_SIZE)
var row: int = int(local_pos.y / TILE_SIZE)
# 确保不超出边界
col = clampi(col, 0, GRID_COLS - 1)
row = clampi(row, 0, GRID_ROWS - 1)
return Vector2i(row, col)
# ========== 绘制逻辑 ==========
func _gui_input(event: InputEvent) -> void:
# 鼠标左键按下:开始绘制
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
is_drawing = true
place_tile(screen_to_grid(event.position))
# 鼠标左键松开:停止绘制
elif event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and not event.pressed:
is_drawing = false
# 鼠标移动 + 按住左键:连续绘制
elif event is InputEventMouseMotion and is_drawing:
place_tile(screen_to_grid(event.position))
## 在指定网格位置放置地形
func place_tile(grid_pos: Vector2i) -> void:
# 基地只能放在最底部中间
if current_tile_type == 9 and grid_pos != Vector2i(12, 6):
print("基地只能放在地图最底部中间!")
return
map_data[grid_pos.x][grid_pos.y] = current_tile_type
render_tile(grid_pos.x, grid_pos.y)
# ========== 渲染地图 ==========
## 渲染整个地图
func render_map() -> void:
# 清除旧的渲染
for child in map_display.get_children():
child.queue_free()
# 逐格渲染
for row in GRID_ROWS:
for col in GRID_COLS:
render_tile(row, col)
# 画网格线
if show_grid:
render_grid_lines()
## 渲染单个格子
func render_tile(row: int, col: int) -> void:
var color_rect = ColorRect.new()
color_rect.size = Vector2(TILE_SIZE, TILE_SIZE)
color_rect.position = Vector2(col * TILE_SIZE, row * TILE_SIZE)
color_rect.color = get_tile_color(map_data[row][col])
map_display.add_child(color_rect)
## 根据地形类型返回颜色
func get_tile_color(tile_type: int) -> Color:
match tile_type:
0: return Color("2d2d2d") # 空地 - 深灰色
1: return Color("b5651d") # 砖墙 - 棕色
2: return Color("808080") # 钢墙 - 银灰色
3: return Color("228b22") # 草地 - 绿色
4: return Color("4169e1") # 水面 - 蓝色
5: return Color("e0ffff") # 冰面 - 浅蓝色
9: return Color("ffd700") # 基地 - 金色
_: return Color("2d2d2d") # 默认空地
## 画网格线
func render_grid_lines() -> void:
var line_width: float = 1.0
var grid_color = Color(1, 1, 1, 0.3) # 半透明白色
# 画竖线
for col in range(GRID_COLS + 1):
var line = Line2D.new()
line.width = line_width
line.default_color = grid_color
line.add_point(Vector2(col * TILE_SIZE, 0))
line.add_point(Vector2(col * TILE_SIZE, GRID_ROWS * TILE_SIZE))
map_display.add_child(line)
# 画横线
for row in range(GRID_ROWS + 1):
var line = Line2D.new()
line.width = line_width
line.default_color = grid_color
line.add_point(Vector2(0, row * TILE_SIZE))
line.add_point(Vector2(GRID_COLS * TILE_SIZE, row * TILE_SIZE))
map_display.add_child(line)保存与加载地图
保存地图
当你设计好一张地图后,需要把它保存成文件。我们使用 JSON 格式保存,因为它既人类可读,又方便程序解析。
// MapFileManager.cs - 地图文件管理
using Godot;
public partial class MapFileManager : Node
{
// ========== 保存地图 ==========
// 把编辑器里的地图数据保存成 JSON 文件
public static void SaveMap(int[,] mapData, string mapName, int stageNumber = -1)
{
var data = new Godot.Collections.Dictionary
{
{ "name", mapName },
{ "version", "1.0" },
{ "cols", mapData.GetLength(1) },
{ "rows", mapData.GetLength(0) },
{ "map", Convert2DArrayToList(mapData) }
};
string fileName;
if (stageNumber >= 0)
{
// 预设关卡,保存到 levels 文件夹
fileName = $"user://levels/level_{stageNumber}.json";
}
else
{
// 自定义关卡,保存到 custom_levels 文件夹
fileName = $"user://custom_levels/{mapName}.json";
}
// 确保目录存在
var dir = DirAccess.Open("user://");
if (!dir.DirExists("custom_levels"))
{
dir.MakeDir("custom_levels");
}
if (!dir.DirExists("levels"))
{
dir.MakeDir("levels");
}
// 写入文件
using var file = FileAccess.Open(fileName, FileAccess.ModeFlags.Write);
if (file != null)
{
file.StoreString(Json.Stringify(data, "\t")); // 用 Tab 缩进,方便阅读
GD.Print($"地图已保存: {fileName}");
}
else
{
GD.PrintErr($"保存失败: {fileName}");
}
}
// ========== 加载地图 ==========
public static int[,] LoadMap(string filePath)
{
if (!FileAccess.FileExists(filePath))
{
GD.PrintErr($"地图文件不存在: {filePath}");
return null;
}
using var file = FileAccess.Open(filePath, FileAccess.ModeFlags.Read);
var json = new Json();
var error = json.Parse(file.GetAsText());
if (error != Error.Ok)
{
GD.PrintErr($"JSON 解析失败: {error}");
return null;
}
var data = json.Data.AsGodotDictionary();
var mapArray = (Godot.Collections.Array)data["map"];
int rows = mapArray.Count;
int cols = ((Godot.Collections.Array)mapArray[0]).Count;
// 把列表转换回二维数组
var result = new int[rows, cols];
for (int row = 0; row < rows; row++)
{
var rowData = (Godot.Collections.Array)mapArray[row];
for (int col = 0; col < cols; col++)
{
result[row, col] = (int)rowData[col];
}
}
GD.Print($"地图已加载: {filePath} ({rows}x{cols})");
return result;
}
// ========== 获取自定义关卡列表 ==========
public static string[] GetCustomLevelList()
{
var dir = DirAccess.Open("user://custom_levels");
if (dir == null) return System.Array.Empty<string>();
var files = dir.GetFiles();
var result = new System.Collections.Generic.List<string>();
foreach (var file in files)
{
if (file.EndsWith(".json"))
{
result.Add(file.Replace(".json", ""));
}
}
return result.ToArray();
}
// ========== 删除自定义关卡 ==========
public static void DeleteCustomLevel(string mapName)
{
var filePath = $"user://custom_levels/{mapName}.json";
if (FileAccess.FileExists(filePath))
{
DirAccess.RemoveAbsolute(filePath);
GD.Print($"已删除地图: {mapName}");
}
}
// ========== 工具方法 ==========
// 把 C# 二维数组转成 Godot 可以序列化的列表
private static Godot.Collections.Array Convert2DArrayToList(int[,] array)
{
var result = new Godot.Collections.Array();
int rows = array.GetLength(0);
int cols = array.GetLength(1);
for (int row = 0; row < rows; row++)
{
var rowData = new Godot.Collections.Array();
for (int col = 0; col < cols; col++)
{
rowData.Add(array[row, col]);
}
result.Add(rowData);
}
return result;
}
}# map_file_manager.gd - 地图文件管理
extends Node
# ========== 保存地图 ==========
# 把编辑器里的地图数据保存成 JSON 文件
static func save_map(map_data: Array, map_name: String, stage_number: int = -1) -> void:
var data = {
"name": map_name,
"version": "1.0",
"cols": map_data[0].size(),
"rows": map_data.size(),
"map": map_data
}
var file_name: String
if stage_number >= 0:
# 预设关卡,保存到 levels 文件夹
file_name = "user://levels/level_%d.json" % stage_number
else:
# 自定义关卡,保存到 custom_levels 文件夹
file_name = "user://custom_levels/%s.json" % map_name
# 确保目录存在
var dir = DirAccess.open("user://")
if not dir.dir_exists("custom_levels"):
dir.make_dir("custom_levels")
if not dir.dir_exists("levels"):
dir.make_dir("levels")
# 写入文件
var file = FileAccess.open(file_name, FileAccess.WRITE)
if file != null:
file.store_string(JSON.stringify(data, "\t")) # 用 Tab 缩进,方便阅读
print("地图已保存: %s" % file_name)
else:
push_error("保存失败: %s" % file_name)
# ========== 加载地图 ==========
static func load_map(file_path: String) -> Variant:
if not FileAccess.file_exists(file_path):
push_error("地图文件不存在: %s" % file_path)
return null
var file = FileAccess.open(file_path, FileAccess.READ)
var json = JSON.new()
var error = json.parse(file.get_as_text())
if error != OK:
push_error("JSON 解析失败: %s" % error)
return null
var data = json.data
var map_array = data.map
var rows = map_array.size()
var cols = map_array[0].size()
print("地图已加载: %s (%dx%d)" % [file_path, rows, cols])
return map_array
# ========== 获取自定义关卡列表 ==========
static func get_custom_level_list() -> Array:
var dir = DirAccess.open("user://custom_levels")
if dir == null:
return []
var files = dir.get_files()
var result: Array = []
for file in files:
if file.ends_with(".json"):
result.append(file.replace(".json", ""))
return result
# ========== 删除自定义关卡 ==========
static func delete_custom_level(map_name: String) -> void:
var file_path = "user://custom_levels/%s.json" % map_name
if FileAccess.file_exists(file_path):
DirAccess.remove_absolute(file_path)
print("已删除地图: %s" % map_name)预设关卡
游戏自带5个预设关卡,每个关卡有不同的难度和地形布局:
| 关卡 | 名称 | 难度 | 特点 |
|---|---|---|---|
| 第1关 | 新手村 | 简单 | 砖墙多、钢墙少,路径宽敞 |
| 第2关 | 钢铁要塞 | 普通 | 钢墙增加,需要绕路 |
| 第3关 | 水上战场 | 中等 | 水面多,通行路线受限 |
| 第4关 | 冰上迷踪 | 困难 | 冰面多,坦克容易打滑 |
| 第5关 | 最终防线 | 极难 | 基地周围只有少量砖墙保护 |
第1关地图数据
{
"name": "新手村",
"stage": 1,
"map": [
[0,0,0,0,0,0,0,0,0,0,0,0,0],
[0,1,0,1,0,1,0,1,0,1,0,1,0],
[0,1,0,1,0,1,0,1,0,1,0,1,0],
[0,1,0,1,0,0,0,0,0,1,0,1,0],
[0,0,0,0,0,1,0,1,0,0,0,0,0],
[0,1,0,1,0,1,0,1,0,1,0,1,0],
[0,1,0,0,0,0,0,0,0,0,0,1,0],
[0,1,0,1,0,1,0,1,0,1,0,1,0],
[0,0,0,0,0,1,0,1,0,0,0,0,0],
[0,1,0,1,0,0,0,0,0,1,0,1,0],
[0,1,0,1,0,1,0,1,0,1,0,1,0],
[0,0,0,0,0,1,1,1,0,0,0,0,0],
[0,0,0,0,0,1,9,1,0,0,0,0,0]
]
}编辑器UI设计
地图编辑器的界面应该直观易用,就像画画软件一样:
┌──────────────────────────────────────────────┐
│ 地图编辑器 [关闭] │
├──────────────────────────────────────────────┤
│ │
│ ┌────────────────────────┐ ┌────────────┐ │
│ │ │ │ 画笔工具 │ │
│ │ │ │ │ │
│ │ 13x13 的网格 │ │ [空地] │ │
│ │ 编辑区域 │ │ [砖墙] │ │
│ │ │ │ [钢墙] │ │
│ │ │ │ [草地] │ │
│ │ │ │ [水面] │ │
│ │ │ │ [冰面] │ │
│ │ │ │ [基地] │ │
│ └────────────────────────┘ │ │ │
│ │ 地图名称 │ │
│ │ [________] │ │
│ │ │ │
│ │ [清空] │ │
│ │ [保存] │ │
│ │ [测试] │ │
│ └────────────┘ │
└──────────────────────────────────────────────┘编辑器场景脚本
// MapEditorUI.cs - 编辑器UI控制
using Godot;
public partial class MapEditorUI : Control
{
[Export] public LineEdit MapNameInput { get; set; }
[Export] public MapEditor Editor { get; set; }
[Export] public Label StatusLabel { get; set; }
// 清空地图
private void OnClearButtonPressed()
{
Editor.ClearMap();
StatusLabel.Text = "地图已清空";
}
// 保存地图
private void OnSaveButtonPressed()
{
var mapName = MapNameInput.Text.Trim();
if (string.IsNullOrEmpty(mapName))
{
StatusLabel.Text = "请输入地图名称!";
return;
}
// 检查地图是否有基地
if (!Editor.HasBase())
{
StatusLabel.Text = "地图必须有基地!";
return;
}
// 保存
MapFileManager.SaveMap(Editor.GetMapData(), mapName);
StatusLabel.Text = $"地图 \"{mapName}\" 保存成功!";
}
// 测试地图(直接进入游戏)
private void OnTestButtonPressed()
{
// 把编辑器里的地图数据传给 GameManager
GameManager.Instance.CustomMapData = Editor.GetMapData();
GameManager.Instance.CurrentState = GameManager.GameState.Playing;
}
// 关闭编辑器
private void OnCloseButtonPressed()
{
GameManager.Instance.CurrentState = GameManager.GameState.MainMenu;
}
}# map_editor_ui.gd - 编辑器UI控制
extends Control
@export var map_name_input: LineEdit
@export var editor: Node # MapEditor 节点
@export var status_label: Label
## 清空地图
func _on_clear_button_pressed() -> void:
editor.clear_map()
status_label.text = "地图已清空"
## 保存地图
func _on_save_button_pressed() -> void:
var map_name = map_name_input.text.strip_edges()
if map_name.is_empty():
status_label.text = "请输入地图名称!"
return
# 检查地图是否有基地
if not editor.has_base():
status_label.text = "地图必须有基地!"
return
# 保存
MapFileManager.save_map(editor.get_map_data(), map_name)
status_label.text = "地图 \"%s\" 保存成功!" % map_name
## 测试地图(直接进入游戏)
func _on_test_button_pressed() -> void:
# 把编辑器里的地图数据传给 GameManager
GameManager.custom_map_data = editor.get_map_data()
GameManager.current_state = GameManager.GameState.PLAYING
## 关闭编辑器
func _on_close_button_pressed() -> void:
GameManager.current_state = GameManager.GameState.MAIN_MENU地图验证规则
保存地图时,需要检查地图是否符合规则。就像老师批改作业一样,要检查几个关键条件:
| 检查项 | 说明 | 不通过时的提示 |
|---|---|---|
| 必须有基地 | 地图上必须有且只有一个基地 | "地图必须有基地!" |
| 基地位置正确 | 基地只能在最底部中间 (12, 6) | "基地位置不正确!" |
| 玩家出生点畅通 | 玩家出生位置 (12, 4) 和 (12, 6) 不能有墙 | "出生点被堵住了!" |
| 敌人出生点畅通 | 顶部三个出生点不能有墙 | "敌人出生点被堵住了!" |
| 不能全空 | 地图不能一个墙都没有 | "请至少放一些墙壁!" |
// 地图验证器
public static class MapValidator
{
// 验证地图是否合法
public static (bool isValid, string errorMessage) Validate(int[,] mapData)
{
int rows = mapData.GetLength(0);
int cols = mapData.GetLength(1);
// 1. 检查地图大小
if (rows != 13 || cols != 13)
{
return (false, "地图大小必须是13x13!");
}
// 2. 检查是否有基地
bool hasBase = false;
int baseCount = 0;
for (int r = 0; r < rows; r++)
{
for (int c = 0; c < cols; c++)
{
if (mapData[r, c] == 9)
{
hasBase = true;
baseCount++;
// 检查基地位置
if (r != 12 || c != 6)
{
return (false, "基地只能放在最底部中间!");
}
}
}
}
if (!hasBase) return (false, "地图必须有基地!");
if (baseCount > 1) return (false, "地图只能有一个基地!");
// 3. 检查玩家出生点(第12行第4列和第6列)
if (mapData[12, 4] != 0) return (false, "玩家1出生点被堵住了!");
if (mapData[12, 6] != 0 && mapData[12, 6] != 9) return (false, "出生点附近被堵住了!");
// 4. 检查敌人出生点(第0行第0、6、12列)
if (mapData[0, 0] != 0) return (false, "敌人出生点被堵住了!");
if (mapData[0, 6] != 0) return (false, "敌人出生点被堵住了!");
if (mapData[0, 12] != 0) return (false, "敌人出生点被堵住了!");
// 5. 检查是否有至少一面墙
bool hasWall = false;
for (int r = 0; r < rows && !hasWall; r++)
{
for (int c = 0; c < cols && !hasWall; c++)
{
if (mapData[r, c] >= 1 && mapData[r, c] <= 5)
{
hasWall = true;
}
}
}
if (!hasWall) return (false, "请至少放一些墙壁!");
return (true, "地图验证通过!");
}
}# 地图验证器
class_name MapValidator
## 验证地图是否合法
static func validate(map_data: Array) -> Dictionary:
var rows = map_data.size()
var cols = map_data[0].size()
# 1. 检查地图大小
if rows != 13 or cols != 13:
return {"valid": false, "error": "地图大小必须是13x13!"}
# 2. 检查是否有基地
var has_base = false
var base_count = 0
for r in rows:
for c in cols:
if map_data[r][c] == 9:
has_base = true
base_count += 1
# 检查基地位置
if r != 12 or c != 6:
return {"valid": false, "error": "基地只能放在最底部中间!"}
if not has_base:
return {"valid": false, "error": "地图必须有基地!"}
if base_count > 1:
return {"valid": false, "error": "地图只能有一个基地!"}
# 3. 检查玩家出生点
if map_data[12][4] != 0:
return {"valid": false, "error": "玩家1出生点被堵住了!"}
if map_data[12][6] != 0 and map_data[12][6] != 9:
return {"valid": false, "error": "出生点附近被堵住了!"}
# 4. 检查敌人出生点
if map_data[0][0] != 0:
return {"valid": false, "error": "敌人出生点被堵住了!"}
if map_data[0][6] != 0:
return {"valid": false, "error": "敌人出生点被堵住了!"}
if map_data[0][12] != 0:
return {"valid": false, "error": "敌人出生点被堵住了!"}
# 5. 检查是否有至少一面墙
var has_wall = false
for r in rows:
for c in cols:
if map_data[r][c] >= 1 and map_data[r][c] <= 5:
has_wall = true
break
if has_wall:
break
if not has_wall:
return {"valid": false, "error": "请至少放一些墙壁!"}
return {"valid": true, "error": "地图验证通过!"}下一章预告
地图编辑器完成了!现在玩家可以自己设计关卡。下一章我们将实现坦克的移动和碰撞——让玩家和敌人坦克能在地图上跑起来,并且正确地撞到墙壁停住。
