第二十章:TileMap与地图编辑
"TileMap是构建游戏世界的画布,让关卡设计变得高效且有趣。"
TileMap是2D游戏中创建关卡、地形的核心工具。本章将全面讲解Godot 4的TileMap系统,包括TileSet创建、图层管理、自动图块、程序化生成等内容。
20.1 TileMap概述
20.1.1 Godot 4 TileMap新特性
Godot 4 TileMap改进:
├── 多图层支持(Layer)
├── 地形系统(Terrain)
├── 物理层(Physics Layer)
├── 导航层(Navigation Layer)
├── 场景图块(Scene Tiles)
├── 动画图块改进
└── 编辑器工具优化
20.1.2 基本概念
TileMap系统组成:
├── TileMap节点 - 管理图块的放置和渲染
├── TileSet资源 - 定义可用的图块
│ ├── 图块源(TileSetSource)
│ │ ├── TileSetAtlasSource - 图集图块
│ │ └── TileSetScenesCollectionSource - 场景图块
│ └── 图块数据(Tile Data)
├── 图层(Layer)- 多层结构
└── 地形(Terrain)- 自动图块规则
20.2 创建TileSet
20.2.1 图集图块
# 在编辑器中创建:
# 1. 选择TileMap节点
# 2. 在检查器中创建新的TileSet
# 3. 在底部面板的TileSet编辑器中添加图块源
# 代码创建TileSet
func create_tileset() -> TileSet:
var tileset = TileSet.new()
# 设置图块大小
tileset.tile_size = Vector2i(16, 16)
# 添加图集源
var source = TileSetAtlasSource.new()
source.texture = preload("res://tiles/tileset.png")
source.texture_region_size = Vector2i(16, 16)
# 创建图块(自动从图集创建)
source.create_tile(Vector2i(0, 0)) # 第一个图块
source.create_tile(Vector2i(1, 0)) # 第二个图块
# 添加到TileSet
tileset.add_source(source, 0) # source_id = 0
return tileset
20.2.2 场景图块
# 场景图块允许使用完整的场景作为图块
func create_scene_tiles() -> TileSet:
var tileset = TileSet.new()
tileset.tile_size = Vector2i(32, 32)
# 创建场景集合源
var scenes_source = TileSetScenesCollectionSource.new()
# 添加场景
var torch_scene = preload("res://objects/torch.tscn")
scenes_source.create_scene_tile(torch_scene, 0) # id = 0
var chest_scene = preload("res://objects/chest.tscn")
scenes_source.create_scene_tile(chest_scene, 1) # id = 1
tileset.add_source(scenes_source, 1) # source_id = 1
return tileset
20.2.3 图块属性
# 设置图块的自定义数据
func setup_tile_data(tileset: TileSet, source: TileSetAtlasSource):
# 添加自定义数据层
tileset.add_custom_data_layer()
tileset.set_custom_data_layer_name(0, "walkable")
tileset.set_custom_data_layer_type(0, TYPE_BOOL)
tileset.add_custom_data_layer()
tileset.set_custom_data_layer_name(1, "damage")
tileset.set_custom_data_layer_type(1, TYPE_INT)
# 获取图块数据并设置
var tile_data = source.get_tile_data(Vector2i(0, 0), 0)
tile_data.set_custom_data("walkable", true)
tile_data.set_custom_data("damage", 0)
# 设置碰撞
# 首先在TileSet中添加物理层
tileset.add_physics_layer()
tileset.set_physics_layer_collision_layer(0, 1)
tileset.set_physics_layer_collision_mask(0, 1)
20.3 TileMap图层
20.3.1 多图层管理
extends TileMap
func _ready():
# 图层操作
add_layer(1) # 添加图层
set_layer_name(0, "Ground")
set_layer_name(1, "Decoration")
# 图层属性
set_layer_enabled(1, true)
set_layer_modulate(1, Color(1, 1, 1, 0.8))
set_layer_y_sort_enabled(1, true)
set_layer_y_sort_origin(1, 8)
set_layer_z_index(1, 1)
# 在不同图层绘制
func draw_tiles():
# 在地面层绘制
set_cell(0, Vector2i(0, 0), 0, Vector2i(0, 0))
# 在装饰层绘制
set_cell(1, Vector2i(0, 0), 0, Vector2i(1, 0))
20.3.2 图层用途示例
推荐的图层结构:
Layer 0: Background - 背景(远景)
Layer 1: Ground - 地面(主要碰撞)
Layer 2: Decoration - 装饰(草、花)
Layer 3: Objects - 物件(可交互)
Layer 4: Foreground - 前景(遮挡玩家)
20.4 地形系统
20.4.1 设置地形
# 地形系统用于自动图块(Auto-tiling)
func setup_terrain(tileset: TileSet):
# 添加地形集
tileset.add_terrain_set(0)
tileset.set_terrain_set_mode(0, TileSet.TERRAIN_MODE_MATCH_CORNERS_AND_SIDES)
# TERRAIN_MODE_MATCH_CORNERS_AND_SIDES - 47图块
# TERRAIN_MODE_MATCH_CORNERS - 16图块
# TERRAIN_MODE_MATCH_SIDES - 16图块
# 添加地形
tileset.add_terrain(0, 0) # terrain_set_id, terrain_id
tileset.set_terrain_name(0, 0, "Grass")
tileset.set_terrain_color(0, 0, Color.GREEN)
# 设置图块的地形
func setup_tile_terrain(source: TileSetAtlasSource, atlas_coords: Vector2i):
var tile_data = source.get_tile_data(atlas_coords, 0)
# 设置地形所属
tile_data.terrain_set = 0
tile_data.terrain = 0
# 设置各个角和边的地形
# 使用地形掩码
tile_data.set_terrain_peering_bit(TileSet.CELL_NEIGHBOR_TOP_LEFT_CORNER, 0)
tile_data.set_terrain_peering_bit(TileSet.CELL_NEIGHBOR_TOP_SIDE, 0)
# ... 其他方向
20.4.2 使用地形绘制
extends TileMap
func draw_terrain_line(from: Vector2i, to: Vector2i, terrain_set: int, terrain: int):
var cells = []
# 收集路径上的单元格
var diff = to - from
var steps = max(abs(diff.x), abs(diff.y))
for i in range(steps + 1):
var t = float(i) / steps if steps > 0 else 0
var cell = Vector2i(
from.x + int(diff.x * t),
from.y + int(diff.y * t)
)
cells.append(cell)
# 使用地形绘制
set_cells_terrain_connect(0, cells, terrain_set, terrain)
func draw_terrain_rect(rect: Rect2i, terrain_set: int, terrain: int):
var cells = []
for x in range(rect.position.x, rect.position.x + rect.size.x):
for y in range(rect.position.y, rect.position.y + rect.size.y):
cells.append(Vector2i(x, y))
set_cells_terrain_connect(0, cells, terrain_set, terrain)
20.5 碰撞与导航
20.5.1 物理碰撞
# 设置TileSet的物理层
func setup_physics(tileset: TileSet):
# 添加物理层
tileset.add_physics_layer()
tileset.set_physics_layer_collision_layer(0, 1) # 在第1层
tileset.set_physics_layer_collision_mask(0, 0) # 不检测任何层
# 为图块设置碰撞形状
func setup_tile_collision(source: TileSetAtlasSource, atlas_coords: Vector2i):
var tile_data = source.get_tile_data(atlas_coords, 0)
# 创建碰撞多边形
var polygon = PackedVector2Array([
Vector2(-8, -8),
Vector2(8, -8),
Vector2(8, 8),
Vector2(-8, 8)
])
tile_data.set_collision_polygons_count(0, 1) # physics_layer, count
tile_data.set_collision_polygon_points(0, 0, polygon) # physics_layer, polygon_index
# 查询图块碰撞
func get_collision_at(world_pos: Vector2) -> bool:
var tile_pos = local_to_map(world_pos)
var tile_data = get_cell_tile_data(0, tile_pos)
if tile_data:
return tile_data.get_collision_polygons_count(0) > 0
return false
20.5.2 导航网格
# 设置导航层
func setup_navigation(tileset: TileSet):
tileset.add_navigation_layer()
tileset.set_navigation_layer_layers(0, 1)
# 为图块设置导航多边形
func setup_tile_navigation(source: TileSetAtlasSource, atlas_coords: Vector2i):
var tile_data = source.get_tile_data(atlas_coords, 0)
var nav_polygon = NavigationPolygon.new()
nav_polygon.add_outline(PackedVector2Array([
Vector2(-8, -8),
Vector2(8, -8),
Vector2(8, 8),
Vector2(-8, 8)
]))
nav_polygon.make_polygons_from_outlines()
tile_data.set_navigation_polygon(0, nav_polygon)
# 烘焙导航网格
func bake_navigation():
# TileMap会自动生成导航网格
# 需要在场景中添加NavigationRegion2D
pass
20.6 代码操作TileMap
20.6.1 放置和移除图块
extends TileMap
func place_tile(layer: int, pos: Vector2i, source_id: int, atlas_coords: Vector2i):
set_cell(layer, pos, source_id, atlas_coords)
func remove_tile(layer: int, pos: Vector2i):
erase_cell(layer, pos)
func clear_layer(layer: int):
clear_layer(layer)
func clear_all():
clear()
# 批量操作
func fill_rect(layer: int, rect: Rect2i, source_id: int, atlas_coords: Vector2i):
for x in range(rect.position.x, rect.position.x + rect.size.x):
for y in range(rect.position.y, rect.position.y + rect.size.y):
set_cell(layer, Vector2i(x, y), source_id, atlas_coords)
20.6.2 查询图块
extends TileMap
func get_tile_info(layer: int, pos: Vector2i) -> Dictionary:
var info = {}
info["source_id"] = get_cell_source_id(layer, pos)
info["atlas_coords"] = get_cell_atlas_coords(layer, pos)
info["alternative_tile"] = get_cell_alternative_tile(layer, pos)
var tile_data = get_cell_tile_data(layer, pos)
if tile_data:
# 获取自定义数据
info["walkable"] = tile_data.get_custom_data("walkable")
info["damage"] = tile_data.get_custom_data("damage")
return info
func get_used_cells_in_layer(layer: int) -> Array[Vector2i]:
return get_used_cells(layer)
func get_used_rect_in_layer() -> Rect2i:
return get_used_rect()
20.6.3 坐标转换
extends TileMap
func world_to_tile(world_pos: Vector2) -> Vector2i:
return local_to_map(to_local(world_pos))
func tile_to_world(tile_pos: Vector2i) -> Vector2:
return to_global(map_to_local(tile_pos))
func get_tile_center(tile_pos: Vector2i) -> Vector2:
return to_global(map_to_local(tile_pos))
# 获取鼠标所在的图块
func get_mouse_tile() -> Vector2i:
var mouse_pos = get_global_mouse_position()
return local_to_map(to_local(mouse_pos))
20.7 程序化生成
20.7.1 简单地形生成
extends TileMap
@export var width: int = 50
@export var height: int = 30
@export var ground_level: int = 20
func generate_simple_terrain():
clear()
# 生成地面
for x in range(width):
# 使用噪声调整地面高度
var noise_height = int(sin(x * 0.3) * 3)
var ground_y = ground_level + noise_height
# 填充地面以下
for y in range(ground_y, height):
if y == ground_y:
# 草地表面
set_cell(0, Vector2i(x, y), 0, Vector2i(0, 0))
else:
# 泥土
set_cell(0, Vector2i(x, y), 0, Vector2i(1, 0))
20.7.2 使用噪声生成
extends TileMap
var noise: FastNoiseLite
func _ready():
noise = FastNoiseLite.new()
noise.seed = randi()
noise.noise_type = FastNoiseLite.TYPE_PERLIN
noise.frequency = 0.05
func generate_cave():
clear()
var width = 100
var height = 60
var threshold = 0.0
for x in range(width):
for y in range(height):
var noise_value = noise.get_noise_2d(x, y)
if noise_value > threshold:
# 墙壁
set_cell(0, Vector2i(x, y), 0, Vector2i(0, 0))
# else: 空地
# 可选:应用细胞自动机平滑
smooth_cave(3)
func smooth_cave(iterations: int):
for i in range(iterations):
var changes = []
for cell in get_used_cells(0):
var neighbors = count_neighbors(cell)
if neighbors < 4:
changes.append({"pos": cell, "remove": true})
# 检查空单元格
var rect = get_used_rect()
for x in range(rect.position.x, rect.end.x):
for y in range(rect.position.y, rect.end.y):
var pos = Vector2i(x, y)
if get_cell_source_id(0, pos) == -1:
if count_neighbors(pos) > 4:
changes.append({"pos": pos, "remove": false})
# 应用更改
for change in changes:
if change.remove:
erase_cell(0, change.pos)
else:
set_cell(0, change.pos, 0, Vector2i(0, 0))
func count_neighbors(pos: Vector2i) -> int:
var count = 0
for dx in range(-1, 2):
for dy in range(-1, 2):
if dx == 0 and dy == 0:
continue
var neighbor = pos + Vector2i(dx, dy)
if get_cell_source_id(0, neighbor) != -1:
count += 1
return count
20.7.3 房间生成器
# room_generator.gd
class_name RoomGenerator
extends RefCounted
var tilemap: TileMap
var rooms: Array[Rect2i] = []
func generate(map: TileMap, room_count: int, map_size: Vector2i):
tilemap = map
rooms.clear()
# 填充墙壁
_fill_with_walls(map_size)
# 生成房间
for i in range(room_count):
_try_place_room(map_size)
# 连接房间
_connect_rooms()
func _fill_with_walls(size: Vector2i):
for x in range(size.x):
for y in range(size.y):
tilemap.set_cell(0, Vector2i(x, y), 0, Vector2i(0, 0))
func _try_place_room(map_size: Vector2i, attempts: int = 30):
for i in range(attempts):
var width = randi_range(5, 12)
var height = randi_range(5, 10)
var x = randi_range(1, map_size.x - width - 1)
var y = randi_range(1, map_size.y - height - 1)
var room = Rect2i(x, y, width, height)
if _can_place_room(room):
_carve_room(room)
rooms.append(room)
return
func _can_place_room(room: Rect2i) -> bool:
var expanded = room.grow(2)
for existing in rooms:
if expanded.intersects(existing):
return false
return true
func _carve_room(room: Rect2i):
for x in range(room.position.x, room.end.x):
for y in range(room.position.y, room.end.y):
tilemap.erase_cell(0, Vector2i(x, y))
func _connect_rooms():
for i in range(rooms.size() - 1):
var room_a = rooms[i]
var room_b = rooms[i + 1]
var center_a = room_a.get_center()
var center_b = room_b.get_center()
# L形走廊
if randi() % 2 == 0:
_carve_horizontal_tunnel(center_a.x, center_b.x, center_a.y)
_carve_vertical_tunnel(center_a.y, center_b.y, center_b.x)
else:
_carve_vertical_tunnel(center_a.y, center_b.y, center_a.x)
_carve_horizontal_tunnel(center_a.x, center_b.x, center_b.y)
func _carve_horizontal_tunnel(x1: int, x2: int, y: int):
for x in range(min(x1, x2), max(x1, x2) + 1):
tilemap.erase_cell(0, Vector2i(x, y))
func _carve_vertical_tunnel(y1: int, y2: int, x: int):
for y in range(min(y1, y2), max(y1, y2) + 1):
tilemap.erase_cell(0, Vector2i(x, y))
20.8 实际案例
20.8.1 关卡编辑器
# level_editor.gd
extends Node2D
@onready var tilemap: TileMap = $TileMap
@onready var cursor: Sprite2D = $Cursor
var current_layer: int = 0
var current_source: int = 0
var current_tile: Vector2i = Vector2i.ZERO
var is_erasing: bool = false
func _process(delta: float):
# 更新光标位置
var mouse_pos = get_global_mouse_position()
var tile_pos = tilemap.local_to_map(tilemap.to_local(mouse_pos))
cursor.global_position = tilemap.to_global(tilemap.map_to_local(tile_pos))
func _unhandled_input(event: InputEvent):
if event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_LEFT:
if event.pressed:
_paint_tile()
elif event.button_index == MOUSE_BUTTON_RIGHT:
if event.pressed:
_erase_tile()
elif event is InputEventMouseMotion:
if Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT):
_paint_tile()
elif Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT):
_erase_tile()
func _paint_tile():
var tile_pos = _get_mouse_tile_pos()
tilemap.set_cell(current_layer, tile_pos, current_source, current_tile)
func _erase_tile():
var tile_pos = _get_mouse_tile_pos()
tilemap.erase_cell(current_layer, tile_pos)
func _get_mouse_tile_pos() -> Vector2i:
var mouse_pos = get_global_mouse_position()
return tilemap.local_to_map(tilemap.to_local(mouse_pos))
func save_level(path: String):
var data = {
"layers": []
}
for layer in range(tilemap.get_layers_count()):
var layer_data = []
for cell in tilemap.get_used_cells(layer):
layer_data.append({
"pos": [cell.x, cell.y],
"source": tilemap.get_cell_source_id(layer, cell),
"atlas": [
tilemap.get_cell_atlas_coords(layer, cell).x,
tilemap.get_cell_atlas_coords(layer, cell).y
]
})
data.layers.append(layer_data)
var file = FileAccess.open(path, FileAccess.WRITE)
file.store_string(JSON.stringify(data))
file.close()
func load_level(path: String):
var file = FileAccess.open(path, FileAccess.READ)
var data = JSON.parse_string(file.get_as_text())
file.close()
tilemap.clear()
for layer_idx in range(data.layers.size()):
for cell in data.layers[layer_idx]:
var pos = Vector2i(cell.pos[0], cell.pos[1])
var atlas = Vector2i(cell.atlas[0], cell.atlas[1])
tilemap.set_cell(layer_idx, pos, cell.source, atlas)
本章小结
本章全面学习了Godot的TileMap系统:
- TileMap概述:新特性、基本概念
- 创建TileSet:图集图块、场景图块、属性设置
- TileMap图层:多图层管理、图层用途
- 地形系统:自动图块设置、地形绘制
- 碰撞与导航:物理碰撞、导航网格
- 代码操作:放置/移除图块、查询、坐标转换
- 程序化生成:简单地形、噪声生成、房间生成器
- 实际案例:关卡编辑器
下一章将学习2D相机系统。
上一章:2D动画系统
下一章:2D相机系统