Godot 4.2 网格地图生成:从坐标原理到实战实现

Godot 4.2 网格地图生成:从坐标原理到实战实现

1. 项目概述:为什么我们需要代码生成网格地图?

在游戏开发里,地图系统是策略、战棋、模拟经营乃至RPG游戏的基石。很多开发者,尤其是刚接触Godot的朋友,可能会想:“我直接在编辑器里用TileMap节点拖拽不就行了吗?” 这话没错,对于小型、静态的地图,可视化编辑是最高效的。但当你面对的是动态生成的世界、需要复杂逻辑判断(如战棋游戏的移动范围高亮、寻路)、或者地图规模巨大时,纯手动编辑就力不从心了。

这时,用代码生成网格地图的优势就凸显出来了。它意味着程序化、可控制、可扩展。你可以用一个算法,瞬间生成成百上千个六边形或棋盘格,并精确地控制每个格子的属性(地形、高度、归属)。这对于制作随机地图、动态变化的战场、或者需要根据数据实时更新地图样式的游戏来说,是必不可少的技能。

这次,我们就聚焦在Godot 4.2上,抛开复杂的插件,从最基础的数学原理和代码逻辑出发,手把手实现两种最经典的网格:棋盘格(正方形)和六边形格。你会发现,理解了背后的坐标转换和几何关系,自己动手实现远比依赖黑盒插件要来得自由和强大。

2. 核心思路与数学基础:从像素到网格坐标

在动手写代码之前,我们必须先理清一个核心概念:世界坐标网格坐标的转换。这是所有网格地图系统的灵魂。

2.1 棋盘格(正方形网格)的坐标映射

棋盘格是最直观的。假设每个格子是边长为cell_size的正方形。

  • 网格坐标转世界坐标:一个位于网格(grid_x, grid_y)的格子,其中心点在世界中的位置通常是:world_position = Vector2(grid_x * cell_size.x, grid_y * cell_size.y)这里我们假设格子中心是对齐点。如果你想以格子左上角为锚点,公式需要调整。

  • 世界坐标转网格坐标:反之,给定一个世界中的点world_pos,它属于哪个格子?grid_x = floor(world_pos.x / cell_size.x)grid_y = floor(world_pos.y / cell_size.y)floor是向下取整,这确保了每个点都能唯一地映射到一个格子上。

注意:这里的cell_size是一个Vector2,意味着你可以支持非正方形的矩形格子(比如宽高比2:1的格子),这为制作特殊风格的地图提供了可能。

2.2 六边形网格的坐标系统与布局

六边形网格复杂一些,因为它有两种常见布局:平顶尖顶。这决定了六边形哪个边是水平的。

  • 平顶六边形:两个水平边在上下面。这是许多策略游戏(如《文明》系列)常用的布局,视觉上更稳定。
  • 尖顶六边形:一个顶点朝上。在某些战棋游戏中也有应用。

我们以更常见的平顶六边形为例进行推导。定义几个关键参数:

  • size: 可以理解为六边形的“半径”,即中心到顶点的距离,或者中心到边的垂直距离(内径)。通常我们用外接圆半径r
  • 六边形的宽度width = sqrt(3) * r
  • 六边形的高度height = 2 * r
  • 注意,在平顶布局中,相邻六边形在水平方向上的偏移是width,在垂直方向上的偏移是height * 0.75(因为它们是交错排列的)。

为了更优雅地处理六边形网格,我们通常会引入一个立方体坐标系统(Cube Coordinates)或轴向坐标系统(Axial Coordinates),而不是简单的二维行/列。这能极大简化六边形之间的距离计算、寻路等操作。不过为了入门直观,我们先从偏移坐标(Offset Coordinates)开始,理解其与像素坐标的转换。

偏移坐标转世界坐标(平顶,奇数行/列偏移): 假设我们使用“奇数行向右偏移”的布局。

func hex_to_pixel(hex: Vector2i, size: float) -> Vector2: var r = size var width = sqrt(3) * r var height = 2 * r var x = hex.x * width var y = hex.y * height * 0.75 # 如果是奇数行,向右偏移半个宽度 if int(hex.y) & 1: # 位运算判断奇偶,效率高 x += width * 0.5 return Vector2(x, y)

世界坐标转偏移坐标: 这个过程是上述的逆运算,需要先反推出一个近似的网格坐标,再通过计算距离中心点的偏差来校正。这是六边形网格编程中的第一个小难点。

func pixel_to_hex(point: Vector2, size: float) -> Vector2i: var r = size var width = sqrt(3) * r var height = 2 * r # 1. 先忽略偏移,计算一个粗略的网格坐标 var approx_y = point.y / (height * 0.75) var y = round(approx_y) # 2. 根据y的奇偶性,计算x的偏移 var x_offset = 0.0 if int(y) & 1: x_offset = width * 0.5 var approx_x = (point.x - x_offset) / width var x = round(approx_x) # 注意:这里简单的round可能在某些边界点产生误差,更健壮的方法是计算到周围几个候选六边形中心的距离,取最近的。 return Vector2i(x, y)

理解并亲手推导这些公式,是掌握代码生成网格地图的关键第一步。不要死记硬背,在白纸上画一画,标出r,width,height和偏移量,关系就一目了然了。

3. 实战准备:在Godot 4.2中建立项目结构

理论清晰后,我们开始在Godot中实践。我建议创建一个清晰的项目结构,这有利于代码管理和后续扩展。

  1. 创建新项目:使用Godot 4.2,渲染器选择兼容性强的(如Forward+),先确保基础环境没问题。

  2. 规划场景树

    • 我们通常会有一个主场景(如Main.tscn),里面包含一个Node2D作为根节点。
    • 在这个根节点下,我们会动态生成一个TileMap节点(用于棋盘格)或Node2D节点(作为六边形网格的容器)。
    • 为了更好的可视化和管理,我们可以创建单独的脚本用于生成不同类型的网格。
  3. 创建核心脚本

    • grid_generator.gd: 这是一个工具类static静态方法)或资源类Resource),专门存放我们上一节推导出的所有坐标转换函数。例如square_grid_to_world,world_to_square_grid,hex_to_pixel,pixel_to_hex等。这样做的好处是逻辑分离,在任何需要网格计算的地方都可以调用。
    • square_map.gd: 继承TileMapNode2D,负责棋盘格地图的生成、渲染和交互。
    • hex_map.gd: 继承Node2D,负责六边形地图的生成、渲染和交互。
  4. 资源准备

    • 棋盘格:准备一张简单的正方形地块纹理,或者直接使用Godot内置的TileMap的图块集(TileSet)功能。TileMap在渲染大量规则正方形格子时效率极高。
    • 六边形格:准备一张六边形地块纹理。这里有个关键技巧:为了确保六边形之间无缝拼接,你的纹理应该包含透明边缘,且六边形的有效图形部分必须完全包含在一个矩形内,这个矩形的尺寸要和我们代码里计算的widthheight匹配。通常我会在图像处理软件里先画一个精确尺寸的六边形,再导出为PNG。

4. 棋盘格地图的代码生成与优化

我们首先实现更简单的棋盘格。使用Godot的TileMap节点是最高效的方式。

4.1 使用TileMap节点生成

TileMap节点在Godot 4中功能非常强大,支持多层、多图块集、地形自动拼接等。但对于基础的代码生成,我们只用到它的核心功能。

# square_map.gd extends TileMap @export var grid_width: int = 20 # 网格宽度(格子数) @export var grid_height: int = 15 # 网格高度(格子数) @export var cell_size_px: Vector2i = Vector2i(64, 64) # 每个格子的像素尺寸 # 在_ready()或一个自定义的初始化函数中生成地图 func generate_map(): # 首先,清除所有已存在的格子 clear() # 假设我们已经设置好了一个TileSet,并且其中第一个图块的ID是0 var tile_id = 0 var atlas_coords = Vector2i(0, 0) # 在图块集中的坐标 var alternative_id = 0 for x in range(grid_width): for y in range(grid_height): # 设置格子。TileMap使用自己的网格坐标系统。 set_cell(0, Vector2i(x, y), tile_id, atlas_coords) # 可选:为不同的格子设置不同的图块,例如创建一条河流或山脉 # for x in range(5, 10): # set_cell(0, Vector2i(x, 3), 1, Vector2i(1,0)) # 使用ID为1的图块

关键点解析

  • set_cell(layer, coords, source_id, atlas_coords, alternative_id)是核心方法。在Godot 4.2中,TileMap的API更侧重于TileSet资源。你需要先创建一个TileSet资源并配置好图块源(如TileSetAtlasSource),然后通过source_idatlas_coords来指定具体的图块。
  • 循环遍历grid_widthgrid_height来填充所有格子,这是最直接的方法。

4.2 为格子添加逻辑数据

光有视觉表现还不够,我们通常需要为每个格子存储游戏逻辑数据,比如地形类型、移动消耗、是否可通行等。

方案一:使用二维数组这是最经典的方法。在SquareMap脚本中定义一个二维数组,其索引与网格坐标一一对应。

var grid_data: Array = [] # 将会是一个二维数组 func generate_map(): # ... 生成视觉格子 ... _init_grid_data() func _init_grid_data(): grid_data = [] grid_data.resize(grid_width) for x in range(grid_width): grid_data[x] = [] grid_data[x].resize(grid_height) for y in range(grid_height): # 初始化一个格子数据对象 grid_data[x][y] = { "terrain": "grass", # 地形 "movement_cost": 1, # 移动消耗 "is_passable": true, # 是否可通行 "occupant": null # 当前占据的单位 }

方案二:使用Dictionary和Vector2i作为Key如果你希望网格是稀疏的(比如只有部分格子有数据),或者网格非常大但有效数据少,用Dictionary可能更节省内存。键是Vector2i坐标,值是格子数据对象。

var grid_data_dict: Dictionary = {} func set_cell_data(cell_coords: Vector2i, data: Dictionary): grid_data_dict[cell_coords] = data func get_cell_data(cell_coords: Vector2i) -> Dictionary: return grid_data_dict.get(cell_coords, {}) # 返回空字典如果不存在

实操心得:对于大多数策略游戏,地图通常是稠密的,使用二维数组访问速度更快(O(1)),代码也更直观。Dictionary方案在需要动态增删格子或处理超大稀疏地图时更有优势。根据你的游戏类型做选择。

4.3 实现鼠标交互与格子高亮

交互是游戏地图的灵魂。我们需要实现:鼠标移动到某个格子上时高亮它,点击时触发事件。

# square_map.gd 继续 @onready var highlight_sprite: Sprite2D = $HighlightSprite # 场景中预先放好的一个高亮精灵 func _process(_delta): var mouse_pos = get_global_mouse_position() var cell_coords = local_to_map(mouse_pos) # TileMap的便捷方法! # 检查坐标是否在地图范围内 if _is_in_grid(cell_coords): # 将高亮精灵移动到该格子中心 var cell_center = map_to_local(cell_coords) highlight_sprite.global_position = cell_center highlight_sprite.visible = true # 点击检测 if Input.is_action_just_pressed("click"): # 需要在项目设置中定义"click"动作(如鼠标左键) _on_cell_clicked(cell_coords) else: highlight_sprite.visible = false func _is_in_grid(coords: Vector2i) -> bool: return coords.x >= 0 and coords.x < grid_width and coords.y >= 0 and coords.y < grid_height func _on_cell_clicked(coords: Vector2i): var data = grid_data[coords.x][coords.y] print("Clicked cell at %s, terrain: %s" % [coords, data.terrain]) # 在这里触发游戏逻辑,如移动单位、打开菜单等

避坑技巧

  • local_to_map()map_to_local()TileMap节点的两个神器,它们封装了之前我们手动推导的坐标转换公式,非常可靠。对于自定义的网格系统(如六边形),我们就需要自己实现类似的函数。
  • 高亮效果可以用一个半透明的彩色精灵,也可以动态修改TileMap中该格子的图块(比如切换到高亮状态的图块ID)。后者在性能上可能更好,但管理起来稍复杂。

5. 六边形网格地图的完整实现

六边形网格的实现比棋盘格更复杂,因为Godot没有内置的HexTileMap。我们需要完全自己管理渲染和交互。

5.1 构建六边形网格容器与数据层

我们不使用TileMap,而是创建一个普通的Node2D作为容器,然后为每个六边形格子实例化一个Sprite2D(或Polygon2D)作为其视觉表现。

# hex_map.gd extends Node2D @export var hex_radius: float = 32.0 # 六边形外接圆半径 @export var map_radius: int = 5 # 地图半径(从中心向外扩展的环数) @export var hex_texture: Texture2D # 六边形纹理 var hex_instances = {} # 用于存储生成的六边形实例,键为轴向坐标 (q, r) var hex_data = {} # 存储逻辑数据,键为轴向坐标 # 我们使用轴向坐标系统 (Axial Coordinates),它比偏移坐标更适合计算。 # 轴向坐标 (q, r) 与立方体坐标 (x, y, z) 的关系是: x = q, z = r, y = -x - z func _ready(): generate_hex_map() func generate_hex_map(): # 清除旧实例 for child in get_children(): child.queue_free() hex_instances.clear() hex_data.clear() # 遍历一个六边形环形区域内的所有坐标 for q in range(-map_radius, map_radius + 1): for r in range(-map_radius, map_radius + 1): # 在轴向坐标下,需要满足约束条件 |q| + |r| + |s| <= map_radius? # 更常见的做法是遍历立方体坐标然后转换。 # 这里我们采用一个简单方法:遍历一个大的矩形范围,然后过滤掉距离中心太远的点。 var s = -q - r # 计算“立方体距离”,即到中心(0,0,0)的最大坐标差绝对值 if max(abs(q), abs(r), abs(s)) <= map_radius: _create_hex_at(q, r) func _create_hex_at(q: int, r: int): # 1. 轴向坐标转世界像素坐标 var world_pos = axial_to_pixel(Vector2(q, r), hex_radius) # 2. 创建视觉实例 var sprite = Sprite2D.new() sprite.texture = hex_texture sprite.position = world_pos # 重要:设置纹理的中心点。如果纹理中六边形居中,则使用默认(0.5,0.5)。 # 如果你的纹理六边形顶部有空白,可能需要调整centered和offset属性。 sprite.centered = true add_child(sprite) hex_instances[Vector2i(q, r)] = sprite # 3. 初始化逻辑数据 hex_data[Vector2i(q, r)] = { "terrain": "plains", "movement_cost": 1, "is_passable": true, "occupant": null }

5.2 实现精确的坐标转换函数

这是六边形系统的核心。我们需要在工具脚本grid_generator.gd中完善这些函数。

# grid_generator.gd extends Node # 或直接是一个静态函数库 static func axial_to_pixel(hex: Vector2, size: float) -> Vector2: # hex.x 是 q, hex.y 是 r var x = size * (sqrt(3.0) * hex.x + sqrt(3.0)/2.0 * hex.y) var y = size * (3.0/2.0 * hex.y) return Vector2(x, y) static func pixel_to_axial(point: Vector2, size: float) -> Vector2: # 这是近似算法,更健壮的是先转到立方体坐标 var q = (sqrt(3.0)/3.0 * point.x - 1.0/3.0 * point.y) / size var r = (2.0/3.0 * point.y) / size # 得到的是浮点数坐标,需要四舍五入到最近的六边形中心 return _cube_to_axial(_cube_round(_axial_to_cube(Vector2(q, r)))) # 辅助函数:轴向坐标转立方体坐标 static func _axial_to_cube(hex: Vector2) -> Vector3: var x = hex.x var z = hex.y var y = -x - z return Vector3(x, y, z) # 辅助函数:立方体坐标转轴向坐标 static func _cube_to_axial(cube: Vector3) -> Vector2: return Vector2(cube.x, cube.z) # 辅助函数:立方体坐标取整(四舍五入到最近的整数立方体坐标) static func _cube_round(cube: Vector3) -> Vector3: var rx = round(cube.x) var ry = round(cube.y) var rz = round(cube.z) var x_diff = abs(rx - cube.x) var y_diff = abs(ry - cube.y) var z_diff = abs(rz - cube.z) # 由于立方体坐标 x+y+z=0 的约束,简单的四舍五入可能破坏这个约束。 # 需要校正:将差值最大的那个坐标重新计算。 if x_diff > y_diff and x_diff > z_diff: rx = -ry - rz elif y_diff > z_diff: ry = -rx - rz else: rz = -rx - ry return Vector3(rx, ry, rz)

5.3 处理六边形网格的交互与高亮

交互逻辑与棋盘格类似,但坐标转换需要使用我们自己的函数。

# hex_map.gd 继续 @onready var highlight_sprite: Sprite2D = $HighlightSprite func _process(_delta): var mouse_pos = get_global_mouse_position() # 注意:mouse_pos是世界坐标,需要转换到本地坐标(如果地图节点有位移/缩放) var local_mouse_pos = to_local(mouse_pos) var hex_coords_vec2 = GridGenerator.pixel_to_axial(local_mouse_pos, hex_radius) var hex_coords = Vector2i(int(hex_coords_vec2.x), int(hex_coords_vec2.y)) # 检查该坐标是否在我们生成的地图数据中 if hex_data.has(hex_coords): var world_pos = GridGenerator.axial_to_pixel(hex_coords_vec2, hex_radius) highlight_sprite.global_position = to_global(world_pos) # 注意坐标空间转换 highlight_sprite.visible = true if Input.is_action_just_pressed("click"): _on_hex_clicked(hex_coords) else: highlight_sprite.visible = false func _on_hex_clicked(coords: Vector2i): var data = hex_data.get(coords) if data: print("Clicked hex at %s, terrain: %s" % [coords, data.terrain]) # 可以在这里改变被点击格子的纹理,以示反馈 var sprite = hex_instances.get(coords) if sprite: sprite.modulate = Color(0.8, 0.8, 1.0) # 轻微变蓝

注意事项

  • 坐标空间:这是最容易出错的地方。mouse_pos是全局坐标,highlight_sprite.global_position也需要全局坐标。我们的axial_to_pixel计算出来的是相对于地图容器节点 (HexMap) 原点的本地坐标。因此需要用to_local()to_global()进行转换。务必理清你的节点树和坐标关系。
  • 性能考虑:如果地图上有成千上万个六边形,每个都是一个Sprite2D,可能会影响性能。对于非常大的静态地图,可以考虑使用MultiMeshInstance2D进行合批渲染,或者将整个地图烘焙成一张大纹理。但对于中小型地图或动态变化多的地图,Sprite2D方案更灵活。

6. 高级技巧与性能优化

当你的网格地图系统开始承载复杂的游戏逻辑时,以下几个高级技巧和优化点至关重要。

6.1 网格寻路算法集成

无论是A*(A星)还是Dijkstra算法,都需要一个“图”(Graph)的数据结构。我们的网格数据就是天然的图。

为棋盘格集成A*: Godot 4.2 内置了AStar2DAStarGrid2D类,后者专门为网格优化过,非常适合棋盘格。

# 在square_map.gd中 var astar_grid: AStarGrid2D func _setup_pathfinding(): astar_grid = AStarGrid2D.new() astar_grid.region = Rect2i(0, 0, grid_width, grid_height) astar_grid.cell_size = Vector2(cell_size_px) # 注意转换为Vector2 astar_grid.diagonal_mode = AStarGrid2D.DIAGONAL_MODE_NEVER # 战棋通常不允许斜走 astar_grid.update() # 根据grid_data设置不可通行区域 for x in range(grid_width): for y in range(grid_height): if not grid_data[x][y].is_passable: astar_grid.set_point_solid(Vector2i(x, y), true) func find_path(from_cell: Vector2i, to_cell: Vector2i) -> PackedVector2Array: if astar_grid: # 返回的是世界坐标路径点 return astar_grid.get_point_path(from_cell, to_cell) return PackedVector2Array()

为六边形网格集成A*: Godot没有内置的六边形A*,我们需要使用通用的AStar2D,并手动添加点和连接。

# 在hex_map.gd中 var astar: AStar2D var hex_to_astar_id = {} # 映射:六边形坐标 -> AStar点ID func _setup_hex_pathfinding(): astar = AStar2D.new() hex_to_astar_id.clear() var point_id = 0 # 1. 添加所有可通行的点 for hex_coords in hex_data.keys(): var data = hex_data[hex_coords] if data.is_passable: var world_pos = GridGenerator.axial_to_pixel(hex_coords, hex_radius) astar.add_point(point_id, world_pos) hex_to_astar_id[hex_coords] = point_id point_id += 1 # 2. 连接相邻的点(六边形有6个邻居) for hex_coords in hex_to_astar_id.keys(): var id = hex_to_astar_id[hex_coords] var neighbors = _get_hex_neighbors(hex_coords) # 需要实现这个函数 for neighbor_coords in neighbors: var neighbor_id = hex_to_astar_id.get(neighbor_coords) if neighbor_id != null: # 计算连接权重,可以基于地形移动消耗 var cost = hex_data[hex_coords].movement_cost astar.connect_points(id, neighbor_id, false) # 双向连接 # astar.set_point_weight_scale(neighbor_id, cost) // 如果需要不同权重 func _get_hex_neighbors(hex: Vector2i) -> Array[Vector2i]: # 六边形在轴向坐标下的六个方向向量 var directions = [ Vector2i(+1, 0), Vector2i(+1, -1), Vector2i(0, -1), Vector2i(-1, 0), Vector2i(-1, +1), Vector2i(0, +1) ] var neighbors: Array[Vector2i] = [] for dir in directions: var neighbor = hex + dir if hex_data.has(neighbor): # 只添加地图内存在的邻居 neighbors.append(neighbor) return neighbors

6.2 视野与迷雾系统

对于战争迷雾(FOV)系统,你需要为每个格子存储一个“是否可见”或“是否已探索”的状态。在渲染时,根据这个状态来调整格子的视觉表现(如变暗、半透明、显示问号图标)。

一个简单的实现思路:

  1. 在格子数据中添加visibility字段,如enum Visibility {HIDDEN, FOGGED, VISIBLE}
  2. 每个单位(如英雄)有一个视野范围(一个格子列表或一个半径)。
  3. 每帧或当单位移动时,计算该单位视野范围内的所有格子,将其visibility设为VISIBLE。同时,将上一帧视野范围内的格子(如果不在新范围内)设为FOGGED
  4. _process或一个专门的更新函数中,遍历所有格子,根据其visibility调整对应Sprite2Dmodulate(颜色/透明度)或切换纹理。

6.3 性能优化:实例化与合批

  • Sprite2D过多:如果地图格子数超过1000,成百上千个Sprite2D节点可能会成为性能瓶颈。考虑以下方案:

    • MultiMeshInstance2D:这是Godot中用于渲染大量相同网格和材质的最高效方式。你需要创建一个MultiMeshInstance2D,并预先计算好每个六边形的位置、旋转(如果需要)和颜色/自定义数据,然后一次性提交给GPU。这对于静态或变化不频繁的地图是终极优化方案。
    • 自定义绘制:在_draw()函数中,使用draw_texture()draw_colored_polygon()直接绘制所有六边形。这给了你最大的控制权,但需要手动管理所有绘制逻辑和状态更新。
    • 分块加载:对于超大地图,不要一次性生成所有格子。只生成玩家视野内及周边区域的格子(“区块”或“Chunk”),随着玩家移动动态加载和卸载区块。
  • 数据查询优化:频繁的grid_data[coord]访问是O(1),很快。但如果你需要频繁地“获取某点周围N格内的所有单位”,可以考虑使用空间分区数据结构,如网格本身、四叉树(Quadtree)或稀疏网格(Sparse Grid),来加速范围查询。

7. 常见问题与调试技巧实录

在实际开发中,你几乎一定会遇到下面这些问题。这里是我踩过坑后的经验总结。

问题1:六边形纹理拼接处有缝隙或重叠。

  • 原因:纹理尺寸与代码中计算的widthheight不匹配,或者纹理的透明边缘(Padding)不够。
  • 解决
    1. 确保你的六边形纹理尺寸是精确的。例如,如果hex_radius是32,那么纹理的宽度应该是ceil(sqrt(3) * 32) ≈ 56像素,高度是2 * 32 = 64像素。在图像软件中创建画布时就用这个尺寸。
    2. 在Godot中导入纹理时,检查导入设置。确保“拉伸模式”不是“拉伸”,并且“重复”模式是“禁用”(除非你需要平铺)。对于像素艺术,记得关闭“过滤”。
    3. 在代码中设置Sprite2Dcentered属性为true,确保精灵的中心点对准纹理中心,也就是六边形的几何中心。

问题2:鼠标点击检测不准确,尤其是六边形边缘。

  • 原因pixel_to_axial函数使用的是近似算法,或者在判断“格子是否存在”时逻辑有误。
  • 解决
    1. 实现更健壮的pixel_to_hex函数。标准做法是:先将像素坐标转换为立方体坐标(浮点数),然后对这个浮点立方体坐标进行取整(cube_round),最后再转回轴向坐标。这能确保总是得到最近的、正确的六边形坐标。上文提供的GridGenerator._cube_round函数就是做这个的。
    2. 对于棋盘格,使用TileMaplocal_to_map()是最准确的。
    3. 可以在调试时,在_process中把当前鼠标悬停的格子坐标实时打印出来,并高亮显示,帮助你观察判断逻辑是否正确。

问题3:地图生成或单位移动时出现卡顿。

  • 原因:在_process_physics_process中执行了过重的操作,比如每帧都重新生成整个地图,或者进行了复杂的寻路计算。
  • 解决
    1. 性能分析:使用Godot编辑器的“调试器”面板中的“性能”监视器,查看CPU和GPU的使用情况,定位瓶颈。
    2. 避免每帧重复计算:地图生成、A*图构建,这些都应该只在初始化或地图变化时执行一次,然后缓存结果。
    3. 分帧处理:如果必须在一帧内处理大量格子(比如更新大片区域的视野),可以考虑将任务分摊到多帧完成。使用await get_tree().process_frame或者自定义一个状态机来逐步处理。
    4. 使用正确的数据结构:确保你的grid_data是数组或字典,访问速度快。避免在循环中频繁进行复杂的查找。

问题4:如何保存和加载生成的地图?

  • 方案:你不能直接保存节点实例,但可以保存生成地图的参数逻辑数据
    • 参数grid_width,grid_height,hex_radius,map_radius, 随机种子等。
    • 逻辑数据:序列化grid_datahex_data数组/字典。Godot提供了ResourceResourceSaver/ResourceLoader来做这件事。你可以创建一个自定义的MapData资源类来存储这些信息。
    • 加载时,用保存的参数重新调用generate_map()函数,然后再用保存的逻辑数据覆盖新生成的grid_data,并根据数据重新设置每个格子的视觉状态(纹理、颜色等)。

从棋盘格到六边形格,从简单的显示到复杂的交互和寻路,代码生成网格地图是一个层层递进的过程。最关键的不是记住每一行代码,而是理解坐标系转换这个核心思想,以及将视觉表现与逻辑数据分离的设计模式。一旦掌握了这个框架,你就能轻松应对各种网格类型(三角形、菱形等)和复杂的游戏逻辑需求。