14. 地形与建筑物搭建
2026/4/14大约 12 分钟
地形与建筑物搭建
GridMap 节点介绍
上一章我们学会了用 CSG 搭建简单场景。现在让我们来学习更强大的工具——GridMap(网格地图)节点。
GridMap 就像一套巨大的乐高积木。你准备好各种积木块(墙壁、地板、屋顶等),然后在一个 3D 网格上把它们一块一块地摆放上去,就能快速搭建出城市、迷宫、建筑群等大型场景。
GridMap 的优势:
- 像画画一样简单:在编辑器里点击网格就能放置积木
- 自动对齐:所有积木都会对齐到网格上,不用担心摆放歪了
- 高效管理:用一个节点就能管理成千上万个积木实例
MeshLibrary 资源——积木库
在用 GridMap 搭建世界之前,你需要先准备好"积木库"——也就是 MeshLibrary 资源。
MeshLibrary 就像一个积木收纳箱,里面装着各种可用的积木块。每个积木块就是一个 3D 模型(Mesh),可以附带碰撞形状和导航信息。
创建 MeshLibrary 的步骤:
- 准备好各种 3D 模型(可以从 Blender 导入,也可以用 CSG 制作)
- 创建一个新的 MeshLibrary 资源(右键资源面板 → 新建资源 → 搜索 MeshLibrary)
- 把模型添加到 MeshLibrary 中
- 为每个模型设置碰撞形状
C#
// 通过代码创建简单的 MeshLibrary
using Godot;
public partial class MeshLibraryBuilder : MeshLibrary
{
public override void _Ready()
{
// 创建一个简单的方块积木
var boxMesh = new BoxMesh();
boxMesh.Size = new Vector3(2, 2, 2);
// 创建材质
var material = new StandardMaterial3D();
material.AlbedoColor = new Color(0.7f, 0.7f, 0.7f);
boxMesh.SurfaceSetMaterial(0, material);
// 创建碰撞形状
var shape = new BoxShape3D();
shape.Size = new Vector3(2, 2, 2);
// 将方块添加到 MeshLibrary 的第0个槽位
var itemMesh = new ArrayMesh();
// ... 在编辑器中操作更方便
GD.Print("MeshLibrary 创建提示:建议在编辑器中可视化操作");
}
}GDScript
# 通过代码创建简单的 MeshLibrary
extends MeshLibrary
func _ready():
# 创建一个简单的方块积木
var box_mesh = BoxMesh.new()
box_mesh.size = Vector3(2, 2, 2)
# 创建材质
var material = StandardMaterial3D.new()
material.albedo_color = Color(0.7, 0.7, 0.7)
box_mesh.surface_set_material(0, material)
# 创建碰撞形状
var shape = BoxShape3D.new()
shape.size = Vector3(2, 2, 2)
# 将方块添加到 MeshLibrary 的第0个槽位
# ... 在编辑器中操作更方便
print("MeshLibrary 创建提示:建议在编辑器中可视化操作")编辑器操作更高效
虽然可以用代码创建 MeshLibrary,但强烈建议在编辑器中可视化操作。选中 GridMap 节点后,编辑器顶部会出现专门的网格绘制工具栏,你可以像画画一样点击放置积木。
用 GridMap 搭建简单城市街区
C#
// 用 GridMap 代码搭建简单城市街区
using Godot;
public partial class CityBuilder : GridMap
{
// 积木类型的编号(对应 MeshLibrary 中的索引)
private const int FloorTile = 0;
private const int WallTile = 1;
private const int DoorTile = 2;
private const int RoofTile = 3;
private const int RoadTile = 4;
public override void _Ready()
{
// 加载 MeshLibrary
MeshLibrary = GD.Load<MeshLibrary>("res://resources/city_mesh_library.tres");
// 设置网格大小(每个格子2米)
CellSize = new Vector3I(2, 2, 2);
// 搭建地面
BuildGround();
// 搭建道路
BuildRoads();
// 搭建建筑
BuildBuilding(0, 0, 2, 3, 2, 3); // x, z位置,宽、高、深
BuildBuilding(8, 0, 2, 4, 2, 3);
BuildBuilding(-8, 0, 2, 2, 2, 4);
GD.Print("城市街区搭建完成!");
}
/// <summary>搭建地面</summary>
private void BuildGround()
{
for (int x = -10; x <= 10; x++)
{
for (int z = -10; z <= 10; z++)
{
SetCellItem(new Vector3I(x, -1, z), FloorTile);
}
}
}
/// <summary>搭建道路</summary>
private void BuildRoads()
{
// 东西向道路
for (int x = -10; x <= 10; x++)
{
SetCellItem(new Vector3I(x, -1, 0), RoadTile);
SetCellItem(new Vector3I(x, -1, 1), RoadTile);
}
// 南北向道路
for (int z = -10; z <= 10; z++)
{
SetCellItem(new Vector3I(0, -1, z), RoadTile);
SetCellItem(new Vector3I(1, -1, z), RoadTile);
}
}
/// <summary>搭建一栋建筑</summary>
private void BuildBuilding(int startX, int startZ, int width, int height, int floors, int depth)
{
for (int floor = 0; floor < floors; floor++)
{
int y = floor * height;
// 四面墙壁
for (int x = 0; x < width; x++)
{
for (int z = 0; z < depth; z++)
{
// 只在最外圈放置墙壁
bool isEdge = (x == 0 || x == width - 1 || z == 0 || z == depth - 1);
bool isDoor = (floor == 0 && x == width / 2 && z == 0);
if (isEdge)
{
int tileType = isDoor ? DoorTile : WallTile;
SetCellItem(new Vector3I(startX + x, y, startZ + z), tileType);
}
}
}
}
// 屋顶
int roofY = floors * height;
for (int x = 0; x < width; x++)
{
for (int z = 0; z < depth; z++)
{
SetCellItem(new Vector3I(startX + x, roofY, startZ + z), RoofTile);
}
}
}
}GDScript
# 用 GridMap 代码搭建简单城市街区
extends GridMap
# 积木类型的编号(对应 MeshLibrary 中的索引)
const FLOOR_TILE = 0
const WALL_TILE = 1
const DOOR_TILE = 2
const ROOF_TILE = 3
const ROAD_TILE = 4
func _ready():
# 加载 MeshLibrary
mesh_library = load("res://resources/city_mesh_library.tres")
# 设置网格大小(每个格子2米)
cell_size = Vector3i(2, 2, 2)
# 搭建地面
build_ground()
# 搭建道路
build_roads()
# 搭建建筑
build_building(0, 0, 3, 2, 3) # x, z位置,宽、高、深
build_building(8, 0, 3, 4, 3) # 更高的建筑
build_building(-8, 0, 4, 2, 4) # 更宽的建筑
print("城市街区搭建完成!")
## 搭建地面
func build_ground():
for x in range(-10, 11):
for z in range(-10, 11):
set_cell_item(Vector3i(x, -1, z), FLOOR_TILE)
## 搭建道路
func build_roads():
# 东西向道路
for x in range(-10, 11):
set_cell_item(Vector3i(x, -1, 0), ROAD_TILE)
set_cell_item(Vector3i(x, -1, 1), ROAD_TILE)
# 南北向道路
for z in range(-10, 11):
set_cell_item(Vector3i(0, -1, z), ROAD_TILE)
set_cell_item(Vector3i(1, -1, z), ROAD_TILE)
## 搭建一栋建筑
func build_building(start_x: int, start_z: int, width: int, height: int, depth: int):
for floor_idx in range(height):
var y = floor_idx
# 四面墙壁
for x in range(width):
for z in range(depth):
# 只在最外圈放置墙壁
var is_edge = (x == 0 or x == width - 1 or z == 0 or z == depth - 1)
var is_door = (floor_idx == 0 and x == width / 2 and z == 0)
if is_edge:
var tile_type = DOOR_TILE if is_door else WALL_TILE
set_cell_item(Vector3i(start_x + x, y, start_z + z), tile_type)
# 屋顶
var roof_y = height
for x in range(width):
for z in range(depth):
set_cell_item(Vector3i(start_x + x, roof_y, start_z + z), ROOF_TILE)CSG 组合模式搭建建筑
CSG 除了简单拼接外,还支持布尔运算——就像做雕塑一样,可以通过"加"和"减"来创造复杂形状。
CSG 支持 3 种布尔运算:
| 运算类型 | 英文名 | 效果 | 比喻 |
|---|---|---|---|
| 合并 | Union | 把两个形状融为一体 | 把两块橡皮泥捏在一起 |
| 减去 | Subtraction | 从一个形状中挖掉另一个 | 用模具在饼干上压出形状 |
| 相交 | Intersection | 只保留两个形状重叠的部分 | 两个交集部分的模具 |
CSG 建筑实例——带窗户和门洞的房屋
C#
// 用 CSG 布尔运算搭建一栋小房子
using Godot;
public partial class CSGHouse : Node3D
{
// 房屋尺寸参数
private const float Width = 6.0f;
private const float Height = 3.5f;
private const float Depth = 5.0f;
private const float WallThickness = 0.25f;
public override void _Ready()
{
BuildMainBody();
BuildDoor();
BuildWindows();
BuildRoof();
AddCollision();
GD.Print("CSG 房屋搭建完成!");
}
/// <summary>搭建房屋主体(四面墙壁)</summary>
private void BuildMainBody()
{
// 使用一个实心方块作为基础
var mainBlock = new CSGBox3D();
mainBlock.Name = "MainBody";
mainBlock.Size = new Vector3(Width, Height, Depth);
mainBlock.Position = new Vector3(0, Height / 2, 0);
mainBlock.Operation = CSGShape3D.OperationEnum.Union;
var mat = new StandardMaterial3D();
mat.AlbedoColor = new Color(0.85f, 0.8f, 0.72f); // 米黄色墙壁
mainBlock.Material = mat;
AddChild(mainBlock);
// 从内部挖空(让墙壁有厚度)
var hollow = new CSGBox3D();
hollow.Size = new Vector3(
Width - WallThickness * 2,
Height - WallThickness,
Depth - WallThickness * 2
);
hollow.Position = new Vector3(0, Height / 2 + WallThickness / 2, 0);
hollow.Operation = CSGShape3D.OperationEnum.Subtraction;
mainBlock.AddChild(hollow);
}
/// <summary>挖出门洞</summary>
private void BuildDoor()
{
var doorHole = new CSGBox3D();
doorHole.Size = new Vector3(1.2f, 2.4f, WallThickness + 0.2f);
doorHole.Position = new Vector3(-0.5f, 1.2f, Depth / 2);
doorHole.Operation = CSGShape3D.OperationEnum.Subtraction;
var body = GetNode<CSGBox3D>("MainBody");
body.AddChild(doorHole);
}
/// <summary>挖出窗户</summary>
private void BuildWindows()
{
var body = GetNode<CSGBox3D>("MainBody");
// 左侧窗户
var win1 = new CSGBox3D();
win1.Size = new Vector3(WallThickness + 0.2f, 1.0f, 1.2f);
win1.Position = new Vector3(-Width / 2, 2.0f, 0);
win1.Operation = CSGShape3D.OperationEnum.Subtraction;
body.AddChild(win1);
// 右侧窗户
var win2 = new CSGBox3D();
win2.Size = new Vector3(WallThickness + 0.2f, 1.0f, 1.2f);
win2.Position = new Vector3(Width / 2, 2.0f, 0);
win2.Operation = CSGShape3D.OperationEnum.Subtraction;
body.AddChild(win2);
}
/// <summary>搭建屋顶(用三角形棱柱)</summary>
private void BuildRoof()
{
// 用 CSGCylinder3D 的低细分近似三角棱柱
var roof = new CSGCylinder3D();
roof.Name = "Roof";
roof.Radius = Width / 2 + 0.3f;
roof.Height = Depth + 0.6f;
roof.Sides = 4; // 四边形截面
roof.Position = new Vector3(0, Height + 1.0f, 0);
roof.Rotation = new Vector3(0, Mathf.DegToRad(45), 0);
var mat = new StandardMaterial3D();
mat.AlbedoColor = new Color(0.6f, 0.3f, 0.2f); // 棕色屋顶
roof.Material = mat;
AddChild(roof);
}
/// <summary>添加碰撞</summary>
private void AddCollision()
{
var body = new StaticBody3D();
body.Name = "Collision";
// 简单的盒子碰撞覆盖整个建筑
var shape = new CollisionShape3D();
var box = new BoxShape3D();
box.Size = new Vector3(Width, Height, Depth);
shape.Shape = box;
shape.Position = new Vector3(0, Height / 2, 0);
body.AddChild(shape);
AddChild(body);
}
}GDScript
# 用 CSG 布尔运算搭建一栋小房子
extends Node3D
# 房屋尺寸参数
const WIDTH = 6.0
const HEIGHT = 3.5
const DEPTH = 5.0
const WALL_THICKNESS = 0.25
func _ready():
build_main_body()
build_door()
build_windows()
build_roof()
add_collision()
print("CSG 房屋搭建完成!")
## 搭建房屋主体(四面墙壁)
func build_main_body():
# 使用一个实心方块作为基础
var main_block = CSGBox3D.new()
main_block.name = "MainBody"
main_block.size = Vector3(WIDTH, HEIGHT, DEPTH)
main_block.position = Vector3(0, HEIGHT / 2.0, 0)
main_block.operation = CSGShape3D.OPERATION_UNION
var mat = StandardMaterial3D.new()
mat.albedo_color = Color(0.85, 0.8, 0.72) # 米黄色墙壁
main_block.material_override = mat
add_child(main_block)
# 从内部挖空(让墙壁有厚度)
var hollow = CSGBox3D.new()
hollow.size = Vector3(
WIDTH - WALL_THICKNESS * 2,
HEIGHT - WALL_THICKNESS,
DEPTH - WALL_THICKNESS * 2
)
hollow.position = Vector3(0, HEIGHT / 2.0 + WALL_THICKNESS / 2.0, 0)
hollow.operation = CSGShape3D.OPERATION_SUBTRACTION
main_block.add_child(hollow)
## 挖出门洞
func build_door():
var door_hole = CSGBox3D.new()
door_hole.size = Vector3(1.2, 2.4, WALL_THICKNESS + 0.2)
door_hole.position = Vector3(-0.5, 1.2, DEPTH / 2.0)
door_hole.operation = CSGShape3D.OPERATION_SUBTRACTION
var body = get_node("MainBody")
body.add_child(door_hole)
## 挖出窗户
func build_windows():
var body = get_node("MainBody")
# 左侧窗户
var win1 = CSGBox3D.new()
win1.size = Vector3(WALL_THICKNESS + 0.2, 1.0, 1.2)
win1.position = Vector3(-WIDTH / 2.0, 2.0, 0)
win1.operation = CSGShape3D.OPERATION_SUBTRACTION
body.add_child(win1)
# 右侧窗户
var win2 = CSGBox3D.new()
win2.size = Vector3(WALL_THICKNESS + 0.2, 1.0, 1.2)
win2.position = Vector3(WIDTH / 2.0, 2.0, 0)
win2.operation = CSGShape3D.OPERATION_SUBTRACTION
body.add_child(win2)
## 搭建屋顶(用四边形棱柱近似三角屋顶)
func build_roof():
var roof = CSGCylinder3D.new()
roof.name = "Roof"
roof.radius = WIDTH / 2.0 + 0.3
roof.height = DEPTH + 0.6
roof.sides = 4 # 四边形截面
roof.position = Vector3(0, HEIGHT + 1.0, 0)
roof.rotation = Vector3(0, deg_to_rad(45), 0)
var mat = StandardMaterial3D.new()
mat.albedo_color = Color(0.6, 0.3, 0.2) # 棕色屋顶
roof.material_override = mat
add_child(roof)
## 添加碰撞
func add_collision():
var body = StaticBody3D.new()
body.name = "Collision"
# 简单的盒子碰撞覆盖整个建筑
var shape = CollisionShape3D.new()
var box = BoxShape3D.new()
box.size = Vector3(WIDTH, HEIGHT, DEPTH)
shape.shape = box
shape.position = Vector3(0, HEIGHT / 2.0, 0)
body.add_child(shape)
add_child(body)程序化放置建筑物
当你需要在地图上放置大量建筑时,手动一个个放太慢了。可以用代码程序化地随机放置建筑物。
C#
// 程序化放置建筑物
using Godot;
using System.Collections.Generic;
public partial class ProceduralCityPlacer : Node3D
{
[Export] public PackedScene[] BuildingPrefabs { get; set; }
[Export] public int BuildingCount { get; set; } = 20;
[Export] public float CityRadius { get; set; } = 50.0f;
[Export] public float MinBuildingSpacing { get; set; } = 8.0f;
private readonly List<Vector3> _placedPositions = new();
private RandomNumberGenerator _rng;
public override void _Ready()
{
_rng = new RandomNumberGenerator();
PlaceBuildings();
}
private void PlaceBuildings()
{
int placed = 0;
int attempts = 0;
int maxAttempts = BuildingCount * 10;
while (placed < BuildingCount && attempts < maxAttempts)
{
attempts++;
// 在圆形区域内随机位置
float angle = _rng.RandfRange(0, Mathf.Tau);
float dist = _rng.RandfRange(10, CityRadius);
Vector3 candidatePos = new Vector3(
Mathf.Cos(angle) * dist,
0,
Mathf.Sin(angle) * dist
);
// 检查是否离已有建筑太近
if (!IsPositionValid(candidatePos)) continue;
// 随机选择建筑模板
int prefabIndex = _rng.RandiRange(0, BuildingPrefabs.Length - 1);
var building = BuildingPrefabs[prefabIndex].Instantiate<Node3D>();
building.Position = candidatePos;
// 随机旋转(只绕Y轴)
building.Rotation = new Vector3(0, _rng.RandfRange(0, Mathf.Tau), 0);
// 随机缩放(让建筑有大小差异)
float scale = _rng.RandfRange(0.8f, 1.5f);
building.Scale = Vector3.One * scale;
AddChild(building);
_placedPositions.Add(candidatePos);
placed++;
GD.Print($"建筑 {placed} 放置在 {candidatePos}");
}
GD.Print($"共放置 {placed} 栋建筑");
}
/// <summary>检查位置是否有效(不与其他建筑重叠)</summary>
private bool IsPositionValid(Vector3 pos)
{
foreach (var placedPos in _placedPositions)
{
float distance = pos.DistanceTo(placedPos);
if (distance < MinBuildingSpacing)
return false;
}
return true;
}
}GDScript
# 程序化放置建筑物
extends Node3D
@export var building_prefabs: Array[PackedScene] = []
@export var building_count: int = 20
@export var city_radius: float = 50.0
@export var min_building_spacing: float = 8.0
var _placed_positions: Array[Vector3] = []
var _rng: RandomNumberGenerator
func _ready():
_rng = RandomNumberGenerator.new()
place_buildings()
func place_buildings():
var placed = 0
var attempts = 0
var max_attempts = building_count * 10
while placed < building_count and attempts < max_attempts:
attempts += 1
# 在圆形区域内随机位置
var angle = _rng.randf_range(0, TAU)
var dist = _rng.randf_range(10, city_radius)
var candidate_pos = Vector3(
cos(angle) * dist,
0,
sin(angle) * dist
)
# 检查是否离已有建筑太近
if not is_position_valid(candidate_pos):
continue
# 随机选择建筑模板
var prefab_index = _rng.randi_range(0, building_prefabs.size() - 1)
var building = building_prefabs[prefab_index].instantiate()
building.position = candidate_pos
# 随机旋转(只绕Y轴)
building.rotation = Vector3(0, _rng.randf_range(0, TAU), 0)
# 随机缩放(让建筑有大小差异)
var scale = _rng.randf_range(0.8, 1.5)
building.scale = Vector3.ONE * scale
add_child(building)
_placed_positions.append(candidate_pos)
placed += 1
print("建筑 %d 放置在 %s" % [placed, candidate_pos])
print("共放置 %d 栋建筑" % placed)
## 检查位置是否有效(不与其他建筑重叠)
func is_position_valid(pos: Vector3) -> bool:
for placed_pos in _placed_positions:
var distance = pos.distance_to(placed_pos)
if distance < min_building_spacing:
return false
return true地形碰撞设置
有了地形和建筑的外观还不够,还需要设置碰撞,这样角色才不会掉到地里或穿墙而过。
C#
// 为地形添加碰撞
using Godot;
public partial class TerrainCollisionSetup : Node3D
{
public override void _Ready()
{
SetupGroundCollision();
SetupBuildingCollision();
}
/// <summary>为地面添加碰撞</summary>
private void SetupGroundCollision()
{
// 创建一个无限大的地面碰撞
var ground = new StaticBody3D();
ground.Name = "Ground";
var shape = new CollisionShape3D();
var plane = new WorldBoundaryShape3D();
shape.Shape = plane;
ground.AddChild(shape);
AddChild(ground);
GD.Print("地面碰撞已添加");
}
/// <summary>为建筑物添加碰撞</summary>
private void SetupBuildingCollision()
{
// 遍历所有建筑场景,为每个添加碰撞
foreach (var child in GetChildren())
{
if (child is Node3D building && building.Name.ToString().StartsWith("Building"))
{
// 检查是否已经有碰撞体
if (building.GetNodeOrNull<StaticBody3D>("Collision") == null)
{
var body = new StaticBody3D();
body.Name = "Collision";
var colShape = new CollisionShape3D();
var box = new BoxShape3D();
box.Size = new Vector3(4, 6, 4); // 根据建筑大小调整
colShape.Shape = box;
body.AddChild(colShape);
building.AddChild(body);
}
}
}
}
}GDScript
# 为地形添加碰撞
extends Node3D
func _ready():
setup_ground_collision()
setup_building_collision()
## 为地面添加碰撞
func setup_ground_collision():
# 创建一个无限大的地面碰撞
var ground = StaticBody3D.new()
ground.name = "Ground"
var shape = CollisionShape3D.new()
var plane = WorldBoundaryShape3D.new()
shape.shape = plane
ground.add_child(shape)
add_child(ground)
print("地面碰撞已添加")
## 为建筑物添加碰撞
func setup_building_collision():
# 遍历所有建筑场景,为每个添加碰撞
for child in get_children():
if child is Node3D and child.name.begins_with("Building"):
# 检查是否已经有碰撞体
if not child.has_node("Collision"):
var body = StaticBody3D.new()
body.name = "Collision"
var col_shape = CollisionShape3D.new()
var box = BoxShape3D.new()
box.size = Vector3(4, 6, 4) # 根据建筑大小调整
col_shape.shape = box
body.add_child(col_shape)
child.add_child(body)常见问题
GridMap 积木不显示
问题:GridMap 节点里画了积木但看不到。
解决方案:
- 确认已正确加载 MeshLibrary 资源
- 检查 MeshLibrary 中的模型是否正确导入
- 确认摄像机位置能看到积木区域
CSG 布尔运算结果不对
问题:减法操作没有正确挖洞。
解决方案:
- 确保子 CSG 节点的
Operation属性设为Subtraction - 减去的形状需要比目标区域的厚度更大(各方向多 0.1)
- 父子关系要正确:减去的 CSG 必须是被减 CSG 的子节点
碰撞体位置偏移
问题:角色在某些位置穿墙。
解决方案:
- 在编辑器中打开"可见碰撞形状"调试选项
- 检查 CollisionShape3D 的位置是否与视觉匹配
- 使用 Simple 碰撞检测模式确保精度
调试碰撞的方法
在 Godot 编辑器顶部菜单中,点击 Debug → Visible Collision Shapes,运行游戏时所有碰撞形状会用蓝色线框显示出来,方便你检查碰撞是否正确。
本章小结
本章我们学习了两种搭建大型场景的方法:
| 方法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| GridMap | 大型城市、迷宫、重复性地形 | 快速绘制、自动对齐 | 模型需要提前准备 |
| CSG 布尔运算 | 单体建筑、复杂形状原型 | 不需要建模软件、灵活 | 性能较差、不够精细 |
| 程序化放置 | 大量建筑随机分布 | 自动化、多样性 | 需要编写逻辑代码 |
通过组合这些方法,你可以快速搭建出丰富的游戏世界。
