18. 自定义搭建复杂建筑与导出
2026/4/14大约 20 分钟
自定义搭建复杂建筑与导出
自定义建筑核心思路
搭建复杂的游戏建筑就像玩乐高——你不需要一块巨大的积木,而是用很多小积木块拼出想要的形状。这种方法叫做模块化搭建。
模块化搭建的核心思路:
- 拆分:把复杂建筑拆成小的、可重复使用的模块(墙壁、地板、窗户、门、楼梯等)
- 制作:为每个模块创建独立的场景文件
- 组合:在编辑器或代码中把模块像积木一样拼起来
- 参数化:通过参数控制生成不同大小、样式的建筑
这样做的好处:
- 复用:一面墙的模块可以在几十栋建筑里反复使用
- 高效:修改一个模块,所有使用该模块的建筑自动更新
- 灵活:改变参数就能生成不同的建筑变体
模块化建筑组件设计
每种模块都是一个独立的 .tscn 场景文件。下面我们来设计四种基础模块。
墙壁模块
墙壁是最基本的建筑模块。一面标准墙壁通常高 3 米、宽 3 米、厚 0.2 米。
C#
// 墙壁模块场景脚本
using Godot;
public partial class WallModule : StaticBody3D
{
[Export] public float Width { get; set; } = 3.0f;
[Export] public float Height { get; set; } = 3.0f;
[Export] public float Thickness { get; set; } = 0.2f;
[Export] public Color WallColor { get; set; } = new Color(0.85f, 0.82f, 0.78f);
private CSGBox3D _wallMesh;
private CollisionShape3D _collision;
public override void _Ready()
{
BuildWall();
}
public void BuildWall()
{
// 创建视觉效果
_wallMesh = new CSGBox3D();
_wallMesh.Name = "WallMesh";
_wallMesh.Size = new Vector3(Width, Height, Thickness);
var mat = new StandardMaterial3D();
mat.AlbedoColor = WallColor;
_wallMesh.Material = mat;
AddChild(_wallMesh);
// 创建碰撞
_collision = new CollisionShape3D();
_collision.Name = "WallCollision";
var box = new BoxShape3D();
box.Size = new Vector3(Width, Height, Thickness);
_collision.Shape = box;
AddChild(_collision);
}
/// <summary>设置墙壁大小</summary>
public void SetSize(float width, float height)
{
Width = width;
Height = height;
if (_wallMesh != null)
{
_wallMesh.Size = new Vector3(Width, Height, Thickness);
}
if (_collision != null)
{
((BoxShape3D)_collision.Shape).Size = new Vector3(Width, Height, Thickness);
}
}
}GDScript
# 墙壁模块场景脚本
extends StaticBody3D
@export var width: float = 3.0
@export var height: float = 3.0
@export var thickness: float = 0.2
@export var wall_color: Color = Color(0.85, 0.82, 0.78)
var _wall_mesh: CSGBox3D
var _collision: CollisionShape3D
func _ready():
build_wall()
func build_wall():
# 创建视觉效果
_wall_mesh = CSGBox3D.new()
_wall_mesh.name = "WallMesh"
_wall_mesh.size = Vector3(width, height, thickness)
var mat = StandardMaterial3D.new()
mat.albedo_color = wall_color
_wall_mesh.material_override = mat
add_child(_wall_mesh)
# 创建碰撞
_collision = CollisionShape3D.new()
_collision.name = "WallCollision"
var box = BoxShape3D.new()
box.size = Vector3(width, height, thickness)
_collision.shape = box
add_child(_collision)
## 设置墙壁大小
func set_size(new_width: float, new_height: float):
width = new_width
height = new_height
if _wall_mesh:
_wall_mesh.size = Vector3(width, height, thickness)
if _collision:
_collision.shape.size = Vector3(width, height, thickness)地板模块
C#
// 地板模块场景脚本
using Godot;
public partial class FloorModule : StaticBody3D
{
[Export] public float Width { get; set; } = 3.0f;
[Export] public float Depth { get; set; } = 3.0f;
[Export] public float Thickness { get; set; } = 0.15f;
[Export] public Color FloorColor { get; set; } = new Color(0.6f, 0.5f, 0.4f);
public override void _Ready()
{
// 创建地板视觉
var mesh = new CSGBox3D();
mesh.Name = "FloorMesh";
mesh.Size = new Vector3(Width, Thickness, Depth);
var mat = new StandardMaterial3D();
mat.AlbedoColor = FloorColor;
mesh.Material = mat;
AddChild(mesh);
// 创建碰撞
var collision = new CollisionShape3D();
var box = new BoxShape3D();
box.Size = new Vector3(Width, Thickness, Depth);
collision.Shape = box;
AddChild(collision);
}
}GDScript
# 地板模块场景脚本
extends StaticBody3D
@export var width: float = 3.0
@export var depth: float = 3.0
@export var thickness: float = 0.15
@export var floor_color: Color = Color(0.6, 0.5, 0.4)
func _ready():
# 创建地板视觉
var mesh = CSGBox3D.new()
mesh.name = "FloorMesh"
mesh.size = Vector3(width, thickness, depth)
var mat = StandardMaterial3D.new()
mat.albedo_color = floor_color
mesh.material_override = mat
add_child(mesh)
# 创建碰撞
var collision = CollisionShape3D.new()
var box = BoxShape3D.new()
box.size = Vector3(width, thickness, depth)
collision.shape = box
add_child(collision)屋顶模块
C#
// 屋顶模块场景脚本
using Godot;
public partial class RoofModule : StaticBody3D
{
[Export] public float Width { get; set; } = 3.2f; // 稍微比墙壁宽一点,形成屋檐
[Export] public float Depth { get; set; } = 3.2f;
[Export] public float Thickness { get; set; } = 0.2f;
[Export] public Color RoofColor { get; set; } = new Color(0.6f, 0.3f, 0.2f);
public override void _Ready()
{
// 创建平屋顶
var mesh = new CSGBox3D();
mesh.Name = "RoofMesh";
mesh.Size = new Vector3(Width, Thickness, Depth);
var mat = new StandardMaterial3D();
mat.AlbedoColor = RoofColor;
mesh.Material = mat;
AddChild(mesh);
// 碰撞
var collision = new CollisionShape3D();
var box = new BoxShape3D();
box.Size = new Vector3(Width, Thickness, Depth);
collision.Shape = box;
AddChild(collision);
}
}GDScript
# 屋顶模块场景脚本
extends StaticBody3D
@export var width: float = 3.2
@export var depth: float = 3.2
@export var thickness: float = 0.2
@export var roof_color: Color = Color(0.6, 0.3, 0.2)
func _ready():
# 创建平屋顶
var mesh = CSGBox3D.new()
mesh.name = "RoofMesh"
mesh.size = Vector3(width, thickness, depth)
var mat = StandardMaterial3D.new()
mat.albedo_color = roof_color
mesh.material_override = mat
add_child(mesh)
# 碰撞
var collision = CollisionShape3D.new()
var box = BoxShape3D.new()
box.size = Vector3(width, thickness, depth)
collision.shape = box
add_child(collision)楼梯模块
C#
// 楼梯模块场景脚本
using Godot;
public partial class StairModule : StaticBody3D
{
[Export] public float Width { get; set; } = 1.5f;
[Export] public float StepHeight { get; set; } = 0.2f;
[Export] public float StepDepth { get; set; } = 0.3f;
[Export] public int StepCount { get; set; } = 10;
public override void _Ready()
{
BuildStairs();
}
private void BuildStairs()
{
var mat = new StandardMaterial3D();
mat.AlbedoColor = new Color(0.7f, 0.65f, 0.6f);
for (int i = 0; i < StepCount; i++)
{
// 每一级台阶
var step = new CSGBox3D();
step.Name = $"Step_{i}";
step.Size = new Vector3(Width, StepHeight, StepDepth);
// 位置:每级向上和向前偏移
float y = StepHeight / 2 + i * StepHeight;
float z = -(StepDepth / 2 + i * StepDepth);
step.Position = new Vector3(0, y, z);
step.Material = mat;
AddChild(step);
}
// 整体碰撞(用斜面简化)
var collision = new CollisionShape3D();
collision.Name = "StairCollision";
collision.Position = new Vector3(0, StepCount * StepHeight / 2, -StepCount * StepDepth / 2);
var box = new BoxShape3D();
box.Size = new Vector3(Width, StepCount * StepHeight, StepCount * StepDepth);
collision.Shape = box;
collision.Rotation = new Vector3(
Mathf.Atan2(StepHeight, StepDepth), 0, 0
);
AddChild(collision);
}
}GDScript
# 楼梯模块场景脚本
extends StaticBody3D
@export var width: float = 1.5
@export var step_height: float = 0.2
@export var step_depth: float = 0.3
@export var step_count: int = 10
func _ready():
build_stairs()
func build_stairs():
var mat = StandardMaterial3D.new()
mat.albedo_color = Color(0.7, 0.65, 0.6)
for i in range(step_count):
# 每一级台阶
var step = CSGBox3D.new()
step.name = "Step_%d" % i
step.size = Vector3(width, step_height, step_depth)
# 位置:每级向上和向前偏移
var y = step_height / 2.0 + i * step_height
var z = -(step_depth / 2.0 + i * step_depth)
step.position = Vector3(0, y, z)
step.material_override = mat
add_child(step)
# 整体碰撞(用斜面简化)
var collision = CollisionShape3D.new()
collision.name = "StairCollision"
collision.position = Vector3(0, step_count * step_height / 2.0, -step_count * step_depth / 2.0)
var box = BoxShape3D.new()
box.size = Vector3(width, step_count * step_height, step_count * step_depth)
collision.shape = box
collision.rotation = Vector3(atan2(step_height, step_depth), 0, 0)
add_child(collision)用场景实例化组合复杂建筑
现在我们有了各种模块,可以像搭积木一样组合出一栋完整的建筑。
C#
// 用模块化组件组合一栋两层小楼
using Godot;
public partial class ModularBuilding : Node3D
{
[Export] public PackedScene WallPrefab { get; set; }
[Export] public PackedScene FloorPrefab { get; set; }
[Export] public PackedScene RoofPrefab { get; set; }
[Export] public PackedScene StairPrefab { get; set; }
private const float ModuleSize = 3.0f;
private const float WallHeight = 3.0f;
public override void _Ready()
{
// 如果没有在编辑器中指定,从默认路径加载
WallPrefab ??= GD.Load<PackedScene>("res://modules/wall_module.tscn");
FloorPrefab ??= GD.Load<PackedScene>("res://modules/floor_module.tscn");
RoofPrefab ??= GD.Load<PackedScene>("res://modules/roof_module.tscn");
StairPrefab ??= GD.Load<PackedScene>("res://modules/stair_module.tscn");
BuildHouse(2, 3); // 2层、3格宽
GD.Print("模块化建筑搭建完成!");
}
/// <summary>
/// 搭建一栋房屋
/// </summary>
/// <param name="floors">楼层数</param>
/// <param name="widthModules">宽度方向的模块数量</param>
public void BuildHouse(int floors, int widthModules)
{
float buildingWidth = widthModules * ModuleSize;
float buildingDepth = ModuleSize;
for (int floor = 0; floor < floors; floor++)
{
float baseY = floor * WallHeight;
// === 地板 ===
for (int w = 0; w < widthModules; w++)
{
var floorModule = FloorPrefab.Instantiate<Node3D>();
floorModule.Position = new Vector3(
w * ModuleSize - buildingWidth / 2 + ModuleSize / 2,
baseY,
0
);
AddChild(floorModule);
}
// === 前墙 ===
for (int w = 0; w < widthModules; w++)
{
var wall = WallPrefab.Instantiate<Node3D>();
wall.Position = new Vector3(
w * ModuleSize - buildingWidth / 2 + ModuleSize / 2,
baseY + WallHeight / 2,
buildingDepth / 2
);
AddChild(wall);
}
// === 后墙 ===
for (int w = 0; w < widthModules; w++)
{
var wall = WallPrefab.Instantiate<Node3D>();
wall.Position = new Vector3(
w * ModuleSize - buildingWidth / 2 + ModuleSize / 2,
baseY + WallHeight / 2,
-buildingDepth / 2
);
AddChild(wall);
}
// === 左右墙 ===
var leftWall = WallPrefab.Instantiate<Node3D>();
leftWall.Position = new Vector3(
-buildingWidth / 2,
baseY + WallHeight / 2,
0
);
leftWall.Rotation = new Vector3(0, Mathf.DegToRad(90), 0);
AddChild(leftWall);
var rightWall = WallPrefab.Instantiate<Node3D>();
rightWall.Position = new Vector3(
buildingWidth / 2,
baseY + WallHeight / 2,
0
);
rightWall.Rotation = new Vector3(0, Mathf.DegToRad(90), 0);
AddChild(rightWall);
// === 楼梯(从第二层开始) ===
if (floor > 0)
{
var stair = StairPrefab.Instantiate<Node3D>();
stair.Position = new Vector3(
buildingWidth / 2 - ModuleSize / 2,
(floor - 1) * WallHeight,
0
);
AddChild(stair);
}
}
// === 屋顶 ===
float roofY = floors * WallHeight;
for (int w = 0; w < widthModules; w++)
{
var roof = RoofPrefab.Instantiate<Node3D>();
roof.Position = new Vector3(
w * ModuleSize - buildingWidth / 2 + ModuleSize / 2,
roofY,
0
);
AddChild(roof);
}
}
}GDScript
# 用模块化组件组合一栋两层小楼
extends Node3D
@export var wall_prefab: PackedScene
@export var floor_prefab: PackedScene
@export var roof_prefab: PackedScene
@export var stair_prefab: PackedScene
const MODULE_SIZE = 3.0
const WALL_HEIGHT = 3.0
func _ready():
# 如果没有在编辑器中指定,从默认路径加载
if not wall_prefab:
wall_prefab = load("res://modules/wall_module.tscn")
if not floor_prefab:
floor_prefab = load("res://modules/floor_module.tscn")
if not roof_prefab:
roof_prefab = load("res://modules/roof_module.tscn")
if not stair_prefab:
stair_prefab = load("res://modules/stair_module.tscn")
build_house(2, 3) # 2层、3格宽
print("模块化建筑搭建完成!")
## 搭建一栋房屋
## floors: 楼层数
## width_modules: 宽度方向的模块数量
func build_house(floors: int, width_modules: int):
var building_width = width_modules * MODULE_SIZE
var building_depth = MODULE_SIZE
for floor_idx in range(floors):
var base_y = floor_idx * WALL_HEIGHT
# === 地板 ===
for w in range(width_modules):
var floor_module = floor_prefab.instantiate()
floor_module.position = Vector3(
w * MODULE_SIZE - building_width / 2.0 + MODULE_SIZE / 2.0,
base_y, 0
)
add_child(floor_module)
# === 前墙 ===
for w in range(width_modules):
var wall = wall_prefab.instantiate()
wall.position = Vector3(
w * MODULE_SIZE - building_width / 2.0 + MODULE_SIZE / 2.0,
base_y + WALL_HEIGHT / 2.0,
building_depth / 2.0
)
add_child(wall)
# === 后墙 ===
for w in range(width_modules):
var wall = wall_prefab.instantiate()
wall.position = Vector3(
w * MODULE_SIZE - building_width / 2.0 + MODULE_SIZE / 2.0,
base_y + WALL_HEIGHT / 2.0,
-building_depth / 2.0
)
add_child(wall)
# === 左右墙 ===
var left_wall = wall_prefab.instantiate()
left_wall.position = Vector3(
-building_width / 2.0,
base_y + WALL_HEIGHT / 2.0, 0
)
left_wall.rotation = Vector3(0, deg_to_rad(90), 0)
add_child(left_wall)
var right_wall = wall_prefab.instantiate()
right_wall.position = Vector3(
building_width / 2.0,
base_y + WALL_HEIGHT / 2.0, 0
)
right_wall.rotation = Vector3(0, deg_to_rad(90), 0)
add_child(right_wall)
# === 楼梯(从第二层开始) ===
if floor_idx > 0:
var stair = stair_prefab.instantiate()
stair.position = Vector3(
building_width / 2.0 - MODULE_SIZE / 2.0,
(floor_idx - 1) * WALL_HEIGHT, 0
)
add_child(stair)
# === 屋顶 ===
var roof_y = floors * WALL_HEIGHT
for w in range(width_modules):
var roof = roof_prefab.instantiate()
roof.position = Vector3(
w * MODULE_SIZE - building_width / 2.0 + MODULE_SIZE / 2.0,
roof_y, 0
)
add_child(roof)参数化建筑生成
参数化生成就是通过改变几个参数(比如楼层数、宽度),自动生成不同的建筑。就像工厂里调整机器参数就能生产不同规格的产品。
C#
// 参数化建筑生成器
using Godot;
[GlobalClass]
public partial class BuildingGenerator : Node3D
{
[ExportGroup("Building Parameters")]
[Export] public int Floors { get; set; } = 3;
[Export] public int WidthModules { get; set; } = 4;
[Export] public int DepthModules { get; set; } = 2;
[Export] public float FloorHeight { get; set; } = 3.0f;
[Export] public float ModuleSize { get; set; } = 3.0f;
[Export] public bool HasStairs { get; set; } = true;
[Export] public bool HasRoofOverhang { get; set; } = true;
[ExportGroup("Module Prefabs")]
[Export] public PackedScene WallPrefab { get; set; }
[Export] public PackedScene FloorPrefab { get; set; }
[Export] public PackedScene RoofPrefab { get; set; }
[Export] public PackedScene StairPrefab { get; set; }
[ExportGroup("Colors")]
[Export] public Color WallColor { get; set; } = new Color(0.85f, 0.82f, 0.78f);
[Export] public Color FloorColor { get; set; } = new Color(0.6f, 0.5f, 0.4f);
[Export] public Color RoofColor { get; set; } = new Color(0.6f, 0.3f, 0.2f);
/// <summary>重新生成建筑(在编辑器中调用)</summary>
public void Regenerate()
{
// 清除旧的子节点
foreach (var child in GetChildren())
{
child.QueueFree();
}
Generate();
}
private void Generate()
{
float halfWidth = WidthModules * ModuleSize / 2.0f;
float halfDepth = DepthModules * ModuleSize / 2.0f;
for (int floor = 0; floor < Floors; floor++)
{
float baseY = floor * FloorHeight;
// 地板
for (int x = 0; x < WidthModules; x++)
{
for (int z = 0; z < DepthModules; z++)
{
PlaceModule(FloorPrefab, new Vector3(
x * ModuleSize - halfWidth + ModuleSize / 2,
baseY,
z * ModuleSize - halfDepth + ModuleSize / 2
));
}
}
// 四面墙
for (int x = 0; x < WidthModules; x++)
{
// 前墙
PlaceModule(WallPrefab, new Vector3(
x * ModuleSize - halfWidth + ModuleSize / 2,
baseY + FloorHeight / 2,
halfDepth
));
// 后墙
PlaceModule(WallPrefab, new Vector3(
x * ModuleSize - halfWidth + ModuleSize / 2,
baseY + FloorHeight / 2,
-halfDepth
));
}
for (int z = 0; z < DepthModules; z++)
{
// 左墙
var leftWall = PlaceModule(WallPrefab, new Vector3(
-halfWidth,
baseY + FloorHeight / 2,
z * ModuleSize - halfDepth + ModuleSize / 2
));
leftWall.Rotation = new Vector3(0, Mathf.DegToRad(90), 0);
// 右墙
var rightWall = PlaceModule(WallPrefab, new Vector3(
halfWidth,
baseY + FloorHeight / 2,
z * ModuleSize - halfDepth + ModuleSize / 2
));
rightWall.Rotation = new Vector3(0, Mathf.DegToRad(90), 0);
}
// 楼梯
if (HasStairs && floor > 0)
{
PlaceModule(StairPrefab, new Vector3(
halfWidth - ModuleSize,
(floor - 1) * FloorHeight,
0
));
}
}
// 屋顶
float roofY = Floors * FloorHeight;
float roofOverhang = HasRoofOverhang ? 0.3f : 0;
for (int x = 0; x < WidthModules; x++)
{
for (int z = 0; z < DepthModules; z++)
{
var roof = PlaceModule(RoofPrefab, new Vector3(
x * ModuleSize - halfWidth + ModuleSize / 2,
roofY,
z * ModuleSize - halfDepth + ModuleSize / 2
));
}
}
GD.Print($"建筑生成完成:{Floors}层 x {WidthModules}格宽 x {DepthModules}格深");
}
private Node3D PlaceModule(PackedScene prefab, Vector3 position)
{
var instance = prefab.Instantiate<Node3D>();
instance.Position = position;
AddChild(instance);
return instance;
}
}GDScript
# 参数化建筑生成器
class_name BuildingGenerator
extends Node3D
@export_group("Building Parameters")
@export var floors: int = 3
@export var width_modules: int = 4
@export var depth_modules: int = 2
@export var floor_height: float = 3.0
@export var module_size: float = 3.0
@export var has_stairs: bool = true
@export var has_roof_overhang: bool = true
@export_group("Module Prefabs")
@export var wall_prefab: PackedScene
@export var floor_prefab: PackedScene
@export var roof_prefab: PackedScene
@export var stair_prefab: PackedScene
@export_group("Colors")
@export var wall_color: Color = Color(0.85, 0.82, 0.78)
@export var floor_color: Color = Color(0.6, 0.5, 0.4)
@export var roof_color: Color = Color(0.6, 0.3, 0.2)
## 重新生成建筑(在编辑器中调用)
func regenerate():
# 清除旧的子节点
for child in get_children():
child.queue_free()
generate()
func generate():
var half_width = width_modules * module_size / 2.0
var half_depth = depth_modules * module_size / 2.0
for floor_idx in range(floors):
var base_y = floor_idx * floor_height
# 地板
for x in range(width_modules):
for z in range(depth_modules):
place_module(floor_prefab, Vector3(
x * module_size - half_width + module_size / 2.0,
base_y,
z * module_size - half_depth + module_size / 2.0
))
# 四面墙
for x in range(width_modules):
# 前墙
place_module(wall_prefab, Vector3(
x * module_size - half_width + module_size / 2.0,
base_y + floor_height / 2.0,
half_depth
))
# 后墙
place_module(wall_prefab, Vector3(
x * module_size - half_width + module_size / 2.0,
base_y + floor_height / 2.0,
-half_depth
))
for z in range(depth_modules):
# 左墙
var left_wall = place_module(wall_prefab, Vector3(
-half_width,
base_y + floor_height / 2.0,
z * module_size - half_depth + module_size / 2.0
))
left_wall.rotation = Vector3(0, deg_to_rad(90), 0)
# 右墙
var right_wall = place_module(wall_prefab, Vector3(
half_width,
base_y + floor_height / 2.0,
z * module_size - half_depth + module_size / 2.0
))
right_wall.rotation = Vector3(0, deg_to_rad(90), 0)
# 楼梯
if has_stairs and floor_idx > 0:
place_module(stair_prefab, Vector3(
half_width - module_size,
(floor_idx - 1) * floor_height, 0
))
# 屋顶
var roof_y = floors * floor_height
for x in range(width_modules):
for z in range(depth_modules):
place_module(roof_prefab, Vector3(
x * module_size - half_width + module_size / 2.0,
roof_y,
z * module_size - half_depth + module_size / 2.0
))
print("建筑生成完成:%d层 x %d格宽 x %d格深" % [floors, width_modules, depth_modules])
func place_module(prefab: PackedScene, pos: Vector3) -> Node3D:
var instance = prefab.instantiate()
instance.position = pos
add_child(instance)
return instance建筑放置系统
建筑放置系统允许玩家在游戏中选择模块、通过鼠标点击来放置建筑物。这在模拟建造类游戏中非常常见。
C#
// 建筑放置系统
using Godot;
public partial class BuildingPlacementSystem : Node3D
{
[Export] public Camera3D Camera { get; set; }
[Export] public float GridSize { get; set; } = 3.0f;
[Export] public float RotationStep { get; set; } = 90.0f;
private PackedScene[] _availableModules;
private int _selectedModuleIndex;
private Node3D _previewInstance;
private float _currentRotation;
private bool _isPlacing;
public override void _Ready()
{
_availableModules = new PackedScene[]
{
GD.Load<PackedScene>("res://modules/wall_module.tscn"),
GD.Load<PackedScene>("res://modules/floor_module.tscn"),
GD.Load<PackedScene>("res://modules/roof_module.tscn"),
GD.Load<PackedScene>("res://modules/stair_module.tscn")
};
_selectedModuleIndex = 0;
_isPlacing = false;
}
public override void _Process(double delta)
{
if (!_isPlacing) return;
// 鼠标位置 → 3D 世界位置
Vector3? worldPos = GetMouseWorldPosition();
if (worldPos == null) return;
// 吸附到网格
Vector3 snappedPos = SnapToGrid(worldPos.Value);
// 更新预览位置
if (_previewInstance != null)
{
_previewInstance.Position = snappedPos;
_previewInstance.RotationDegrees = new Vector3(0, _currentRotation, 0);
}
}
public override void _UnhandledInput(InputEvent @event)
{
if (!_isPlacing) return;
// 左键放置
if (@event is InputEventMouseButton mouseBtn && mouseBtn.Pressed)
{
if (mouseBtn.ButtonIndex == MouseButton.Left)
{
PlaceModule();
}
else if (mouseBtn.ButtonIndex == MouseButton.Right)
{
CancelPlacement();
}
}
// R 键旋转
if (@event.IsActionPressed("rotate_building"))
{
_currentRotation += RotationStep;
if (_currentRotation >= 360) _currentRotation = 0;
GD.Print($"旋转到 {_currentRotation} 度");
}
// 数字键切换模块
for (int i = 0; i < _availableModules.Length; i++)
{
if (@event.IsActionPressed($"select_module_{i + 1}"))
{
SelectModule(i);
}
}
// ESC 取消
if (@event.IsActionPressed("ui_cancel"))
{
CancelPlacement();
}
}
/// <summary>开始放置模式</summary>
public void StartPlacement(int moduleIndex = 0)
{
_isPlacing = true;
_currentRotation = 0;
SelectModule(moduleIndex);
GD.Print("进入建筑放置模式");
}
/// <summary>选择模块</summary>
private void SelectModule(int index)
{
if (index < 0 || index >= _availableModules.Length) return;
// 移除旧预览
if (_previewInstance != null)
{
_previewInstance.QueueFree();
}
_selectedModuleIndex = index;
// 创建新预览(半透明显示)
_previewInstance = _availableModules[index].Instantiate<Node3D>();
_previewInstance.Name = "Preview";
// 设置半透明效果
SetPreviewTransparency(_previewInstance, 0.5f);
AddChild(_previewInstance);
GD.Print($"选择模块 {index + 1}");
}
/// <summary>确认放置</summary>
private void PlaceModule()
{
if (_previewInstance == null) return;
// 创建正式的实例(替换半透明预览)
Vector3 pos = _previewInstance.Position;
float rot = _currentRotation;
// 移除预览
_previewInstance.QueueFree();
// 创建正式模块
var placed = _availableModules[_selectedModuleIndex].Instantiate<Node3D>();
placed.Position = pos;
placed.RotationDegrees = new Vector3(0, rot, 0);
AddChild(placed);
// 创建新的预览继续放置
_previewInstance = _availableModules[_selectedModuleIndex].Instantiate<Node3D>();
_previewInstance.Position = pos;
SetPreviewTransparency(_previewInstance, 0.5f);
AddChild(_previewInstance);
GD.Print($"放置模块在 {pos}");
}
/// <summary>取消放置</summary>
private void CancelPlacement()
{
_isPlacing = false;
if (_previewInstance != null)
{
_previewInstance.QueueFree();
_previewInstance = null;
}
GD.Print("退出建筑放置模式");
}
/// <summary>将鼠标位置转换为3D世界坐标</summary>
private Vector3? GetMouseWorldPosition()
{
Vector2 mousePos = GetViewport().GetMousePosition();
var rayLength = 1000;
var from = Camera.ProjectRayOrigin(mousePos);
var to = from + Camera.ProjectRayNormal(mousePos) * rayLength;
var spaceState = GetWorld3D().DirectSpaceState;
var query = PhysicsRayQueryParameters3D.Create(from, to);
query.CollideWithAreas = false;
query.CollideWithBodies = true;
var result = spaceState.IntersectRay(query);
if (result != null && result.ContainsKey("position"))
{
return (Vector3)result["position"];
}
return null;
}
/// <summary>吸附到网格</summary>
private Vector3 SnapToGrid(Vector3 pos)
{
float x = Mathf.Snapped(pos.X, GridSize);
float y = Mathf.Snapped(pos.Y, GridSize);
float z = Mathf.Snapped(pos.Z, GridSize);
return new Vector3(x, y, z);
}
/// <summary>设置预览节点的透明度</summary>
private void SetPreviewTransparency(Node node, float alpha)
{
if (node is CSGBox3D csg)
{
var mat = csg.Material as StandardMaterial3D;
if (mat == null)
{
mat = new StandardMaterial3D();
csg.Material = mat;
}
mat.Transparency = BaseMaterial3D.TransparencyEnum.Alpha;
mat.AlbedoColor = new Color(mat.AlbedoColor, alpha);
}
foreach (var child in node.GetChildren())
{
SetPreviewTransparency(child, alpha);
}
}
}GDScript
# 建筑放置系统
extends Node3D
@export var camera: Camera3D
@export var grid_size: float = 3.0
@export var rotation_step: float = 90.0
var _available_modules: Array[PackedScene] = []
var _selected_module_index: int = 0
var _preview_instance: Node3D
var _current_rotation: float = 0.0
var _is_placing: bool = false
func _ready():
_available_modules = [
load("res://modules/wall_module.tscn"),
load("res://modules/floor_module.tscn"),
load("res://modules/roof_module.tscn"),
load("res://modules/stair_module.tscn")
]
_is_placing = false
func _process(delta: float):
if not _is_placing:
return
# 鼠标位置 → 3D 世界位置
var world_pos = get_mouse_world_position()
if world_pos == null:
return
# 吸附到网格
var snapped_pos = snap_to_grid(world_pos)
# 更新预览位置
if _preview_instance:
_preview_instance.position = snapped_pos
_preview_instance.rotation_degrees = Vector3(0, _current_rotation, 0)
func _unhandled_input(event: InputEvent):
if not _is_placing:
return
# 左键放置
if event is InputEventMouseButton and event.pressed:
var mouse_btn = event as InputEventMouseButton
if mouse_btn.button_index == MOUSE_BUTTON_LEFT:
place_module()
elif mouse_btn.button_index == MOUSE_BUTTON_RIGHT:
cancel_placement()
# R 键旋转
if event.is_action_pressed("rotate_building"):
_current_rotation += rotation_step
if _current_rotation >= 360:
_current_rotation = 0
print("旋转到 %.0f 度" % _current_rotation)
# ESC 取消
if event.is_action_pressed("ui_cancel"):
cancel_placement()
## 开始放置模式
func start_placement(module_index: int = 0):
_is_placing = true
_current_rotation = 0
select_module(module_index)
print("进入建筑放置模式")
## 选择模块
func select_module(index: int):
if index < 0 or index >= _available_modules.size():
return
# 移除旧预览
if _preview_instance:
_preview_instance.queue_free()
_selected_module_index = index
# 创建新预览(半透明显示)
_preview_instance = _available_modules[index].instantiate()
_preview_instance.name = "Preview"
set_preview_transparency(_preview_instance, 0.5)
add_child(_preview_instance)
print("选择模块 %d" % (index + 1))
## 确认放置
func place_module():
if not _preview_instance:
return
# 记录位置和旋转
var pos = _preview_instance.position
var rot = _current_rotation
# 移除预览
_preview_instance.queue_free()
# 创建正式模块
var placed = _available_modules[_selected_module_index].instantiate()
placed.position = pos
placed.rotation_degrees = Vector3(0, rot, 0)
add_child(placed)
# 创建新的预览继续放置
_preview_instance = _available_modules[_selected_module_index].instantiate()
_preview_instance.position = pos
set_preview_transparency(_preview_instance, 0.5)
add_child(_preview_instance)
print("放置模块在 %s" % pos)
## 取消放置
func cancel_placement():
_is_placing = false
if _preview_instance:
_preview_instance.queue_free()
_preview_instance = null
print("退出建筑放置模式")
## 将鼠标位置转换为3D世界坐标
func get_mouse_world_position() -> Variant:
var mouse_pos = get_viewport().get_mouse_position()
var ray_length = 1000
var from = camera.project_ray_origin(mouse_pos)
var to = from + camera.project_ray_normal(mouse_pos) * ray_length
var space_state = get_world_3d().direct_space_state
var query = PhysicsRayQueryParameters3D.create(from, to)
query.collide_with_areas = false
query.collide_with_bodies = true
var result = space_state.intersect_ray(query)
if result.size() > 0 and result.has("position"):
return result["position"]
return null
## 吸附到网格
func snap_to_grid(pos: Vector3) -> Vector3:
var x = snappedf(pos.x, grid_size)
var y = snappedf(pos.y, grid_size)
var z = snappedf(pos.z, grid_size)
return Vector3(x, y, z)
## 设置预览节点的透明度
func set_preview_transparency(node: Node, alpha: float):
if node is CSGBox3D:
var csg = node as CSGBox3D
var mat = csg.material_override as StandardMaterial3D
if not mat:
mat = StandardMaterial3D.new()
csg.material_override = mat
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
mat.albedo_color = Color(mat.albedo_color, alpha)
for child in node.get_children():
set_preview_transparency(child, alpha)建筑碰撞自动生成
当建筑比较复杂时,手动添加碰撞很麻烦。可以通过代码自动生成碰撞。
C#
// 自动生成建筑碰撞
using Godot;
public static class CollisionAutoGenerator
{
/// <summary>为建筑节点自动生成简化碰撞</summary>
public static void GenerateCollisions(Node3D building)
{
var bounds = CalculateBounds(building);
// 创建一个大的静态碰撞体包裹整个建筑
var body = new StaticBody3D();
body.Name = "AutoCollision";
var shape = new CollisionShape3D();
var box = new BoxShape3D();
box.Size = bounds.Size;
shape.Shape = box;
shape.Position = bounds.Position + bounds.Size / 2;
body.AddChild(shape);
building.AddChild(body);
GD.Print($"自动碰撞生成:大小 {box.Size},位置 {shape.Position}");
}
/// <summary>计算节点的包围盒</summary>
private static AABB CalculateBounds(Node3D node)
{
var bounds = new AABB();
bool first = true;
TraverseForBounds(node, ref bounds, ref first);
return bounds;
}
private static void TraverseForBounds(Node node, ref AABB bounds, ref bool first)
{
if (node is CSGShape3D csg)
{
var aabb = csg.GetAabb();
if (first)
{
bounds = aabb;
first = false;
}
else
{
bounds = bounds.Merge(aabb);
}
}
foreach (var child in node.GetChildren())
{
TraverseForBounds(child, ref bounds, ref first);
}
}
}GDScript
# 自动生成建筑碰撞
class_name CollisionAutoGenerator
## 为建筑节点自动生成简化碰撞
static func generate_collisions(building: Node3D):
var bounds = calculate_bounds(building)
# 创建一个大的静态碰撞体包裹整个建筑
var body = StaticBody3D.new()
body.name = "AutoCollision"
var shape = CollisionShape3D.new()
var box = BoxShape3D.new()
box.size = bounds.size
shape.shape = box
shape.position = bounds.position + bounds.size / 2.0
body.add_child(shape)
building.add_child(body)
print("自动碰撞生成:大小 %s,位置 %s" % [box.size, shape.position])
## 计算节点的包围盒
static func calculate_bounds(node: Node3D) -> AABB:
var bounds = AABB.new()
var first = true
_traverse_for_bounds(node, bounds, first)
return bounds
static func _traverse_for_bounds(node: Node, bounds: AABB, first: bool):
if node is CSGShape3D:
var csg = node as CSGShape3D
var aabb = csg.get_aabb()
if first:
bounds = aabb
first = false
else:
bounds = bounds.merge(aabb)
for child in node.get_children():
_traverse_for_bounds(child, bounds, first)导出为独立场景
搭建好的建筑可以保存为独立的 .tscn 场景文件,方便在其他地方复用。
C#
// 场景保存工具
using Godot;
public static class SceneExportHelper
{
/// <summary>将节点保存为场景文件</summary>
public static bool SaveAsScene(Node node, string path)
{
var scene = new PackedScene();
var result = scene.Pack(node);
if (result == Error.Ok)
{
result = ResourceSaver.Save(scene, path);
if (result == Error.Ok)
{
GD.Print($"场景已保存到:{path}");
return true;
}
}
GD.PrintErr($"保存场景失败:{result}");
return false;
}
/// <summary>加载已保存的场景</summary>
public static Node3D LoadScene(string path)
{
var scene = GD.Load<PackedScene>(path);
if (scene != null)
{
return scene.Instantiate<Node3D>();
}
GD.PrintErr($"加载场景失败:{path}");
return null;
}
}
// 使用示例
// SceneExportHelper.SaveAsScene(buildingNode, "res://buildings/my_house.tscn");
// var house = SceneExportHelper.LoadScene("res://buildings/my_house.tscn");GDScript
# 场景保存工具
class_name SceneExportHelper
## 将节点保存为场景文件
static func save_as_scene(node: Node, path: String) -> bool:
var scene = PackedScene.new()
var result = scene.pack(node)
if result == OK:
result = ResourceSaver.save(scene, path)
if result == OK:
print("场景已保存到:%s" % path)
return true
push_error("保存场景失败:%s" % result)
return false
## 加载已保存的场景
static func load_scene(path: String) -> Node3D:
var scene = load(path) as PackedScene
if scene:
return scene.instantiate()
push_error("加载场景失败:%s" % path)
return null
# 使用示例
# SceneExportHelper.save_as_scene(building_node, "res://buildings/my_house.tscn")
# var house = SceneExportHelper.load_scene("res://buildings/my_house.tscn")建筑性能优化
当场景中有大量建筑时,性能可能会下降。以下是几种优化方法:
静态合批
将多个静态物体合并为一个,减少绘制调用(Draw Call)。
C#
// 静态合批优化
using Godot;
public partial class StaticBatchOptimizer : Node
{
public override void _Ready()
{
OptimizeBuilding(GetParent() as Node3D);
}
/// <summary>优化建筑:合并同类材质的网格</summary>
private void OptimizeBuilding(Node3D building)
{
// 将所有 CSG 替换为 MeshInstance3D 并合并
var meshInstances = new Godot.Collections.Array<MeshInstance3D>();
foreach (var child in building.GetChildren())
{
if (child is CSGBox3D csgBox)
{
// CSG 转换为 MeshInstance3D
var meshInst = new MeshInstance3D();
var boxMesh = new BoxMesh();
boxMesh.Size = csgBox.Size;
meshInst.Mesh = boxMesh;
meshInst.Position = csgBox.Position;
meshInst.Rotation = csgBox.Rotation;
// 复制材质
if (csgBox.Material != null)
{
meshInst.MaterialOverride = csgBox.Material as Material;
}
meshInst.Name = csgBox.Name + "_Optimized";
building.AddChild(meshInst);
meshInstances.Add(meshInst);
// 移除 CSG
csgBox.QueueFree();
}
}
GD.Print($"优化完成:{meshInstances.Count} 个网格已转换");
}
}GDScript
# 静态合批优化
extends Node
func _ready():
optimize_building(get_parent() as Node3D)
## 优化建筑:合并同类材质的网格
func optimize_building(building: Node3D):
var mesh_instances: Array[MeshInstance3D] = []
for child in building.get_children():
if child is CSGBox3D:
var csg_box = child as CSGBox3D
# CSG 转换为 MeshInstance3D
var mesh_inst = MeshInstance3D.new()
var box_mesh = BoxMesh.new()
box_mesh.size = csg_box.size
mesh_inst.mesh = box_mesh
mesh_inst.position = csg_box.position
mesh_inst.rotation = csg_box.rotation
# 复制材质
if csg_box.material_override:
mesh_inst.material_override = csg_box.material_override
mesh_inst.name = csg_box.name + "_Optimized"
building.add_child(mesh_inst)
mesh_instances.append(mesh_inst)
# 移除 CSG
csg_box.queue_free()
print("优化完成:%d 个网格已转换" % mesh_instances.size())LOD(Level of Detail)
LOD 就像看地图时的"缩放"——远处的建筑用低精度的模型,近处的用高精度的模型,节省性能。
C#
// 简单的 LOD 系统
using Godot;
public partial class SimpleLOD : Node3D
{
[Export] public MeshInstance3D HighDetailMesh { get; set; }
[Export] public MeshInstance3D LowDetailMesh { get; set; }
[Export] public float LodDistance { get; set; } = 30.0f;
private Camera3D _camera;
public override void _Ready()
{
_camera = GetViewport().GetCamera3D();
}
public override void _Process(double delta)
{
if (_camera == null) return;
float distance = GlobalPosition.DistanceTo(_camera.GlobalPosition);
if (distance > LodDistance)
{
// 远处:低精度
HighDetailMesh.Visible = false;
LowDetailMesh.Visible = true;
}
else
{
// 近处:高精度
HighDetailMesh.Visible = true;
LowDetailMesh.Visible = false;
}
}
}GDScript
# 简单的 LOD 系统
extends Node3D
@export var high_detail_mesh: MeshInstance3D
@export var low_detail_mesh: MeshInstance3D
@export var lod_distance: float = 30.0
var _camera: Camera3D
func _ready():
_camera = get_viewport().get_camera_3d()
func _process(delta: float):
if not _camera:
return
var distance = global_position.distance_to(_camera.global_position)
if distance > lod_distance:
# 远处:低精度
high_detail_mesh.visible = false
low_detail_mesh.visible = true
else:
# 近处:高精度
high_detail_mesh.visible = true
low_detail_mesh.visible = false常见问题
模块之间有缝隙
问题:模块组合后,墙壁和地板之间出现可见的缝隙。
解决方案:
- 确保所有模块的尺寸是统一的(比如都是 3 米的倍数)
- 使用
SnapToGrid函数确保位置精确对齐 - 模块之间稍微重叠 0.01 米来消除缝隙
放置时预览与实际位置不一致
问题:半透明预览和实际放置的位置有偏差。
解决方案:
- 确保预览实例和正式实例使用相同的场景
- 检查碰撞检测的偏移设置
- 确认网格吸附的大小与模块大小匹配
导出场景后碰撞丢失
问题:保存为 .tscn 文件后重新加载,碰撞不见了。
解决方案:
- 确保碰撞节点是场景的子节点(而非外部引用)
- 使用
ResourceSaver.Save时检查返回值 - 在保存前调用
PackedScene.Pack确认打包成功
建筑设计的最佳实践
- 保持模块尺寸统一:所有模块使用相同的基准尺寸(如 3x3x3 米),方便对齐
- 建立模块库:创建更多的模块变体(带窗户的墙、带门的墙、拐角墙等)
- 从简单开始:先用最基本的模块验证系统,再逐步增加复杂度
- 性能意识:每栋建筑的模块数量不要太多,CSG 适合原型,正式版应该用优化后的 Mesh
本章小结
本章我们学习了自定义搭建复杂建筑的完整流程:
| 知识点 | 说明 |
|---|---|
| 模块化设计 | 把复杂建筑拆分为可复用的小模块 |
| 墙壁/地板/屋顶/楼梯模块 | 四种基础建筑模块的创建 |
| 场景实例化组合 | 用模块像积木一样拼出完整建筑 |
| 参数化生成 | 通过参数控制自动生成不同建筑 |
| 建筑放置系统 | 鼠标选择+网格吸附+旋转放置 |
| 碰撞自动生成 | 自动计算包围盒生成碰撞 |
| 场景导出 | 保存为 .tscn 文件复用 |
| 静态合批 | CSG 转 Mesh 优化性能 |
| LOD | 远近切换不同精度的模型 |
掌握了这些技术,你就能像真正的建筑师一样在游戏里搭建出各种复杂的建筑了。
