8. 地图生成与探索
2026/4/13大约 11 分钟
地图生成与探索
大地图:更大的世界更多的敌人
想象你站在一个小房间里打蚊子——很快就会觉得无聊。但如果你站在一个无边无际的大草原上,面对从四面八方涌来的虫群——那感觉就完全不同了。
割草游戏需要一个比屏幕大很多的地图。角色在中间移动,地图跟着滚动(就像你拿着放大镜看报纸一样)。大地图不仅让游戏更有探索感,还给了大量敌人足够的"活动空间"。
地图设计原则
地图 vs 屏幕的关系
| 概念 | 说明 | 比喻 |
|---|---|---|
| 屏幕可视范围 | 摄像机能看到的区域 | 你透过窗户看到的风景 |
| 地图实际大小 | 整个游戏世界的面积 | 窗户外面的整个城市 |
| 摄像机跟随 | 摄像机跟着玩家移动 | 你拿着手电筒走路,光照到哪里你就看到哪里 |
地图大小建议
地图大小建议是屏幕可视范围的 10~20 倍。如果摄像机能看到 20x20 的范围,地图应该是 200x200 到 400x400。这样玩家有足够的空间移动,但又不会大到找不到敌人。
不同地形的特点
| 地形类型 | 视觉风格 | 特殊效果 | 适用阶段 |
|---|---|---|---|
| 草地 | 绿色平坦 | 无特殊效果 | 第1关(新手村) |
| 森林 | 深绿色,树木多 | 树木作为障碍物 | 第2关 |
| 墓地 | 灰暗色调,墓碑多 | 墓碑作为障碍物,氛围恐怖 | 第3关 |
| 沙漠 | 黄色,仙人掌 | 仙人掌障碍物 | 第4关 |
| 地狱 | 红色,熔岩 | 熔岩区域持续伤害 | 最终关 |
TileMap 地面生成
Godot 4 的 TileMap 系统非常适合生成大地图。我们可以用不同的"瓦片"(tile)铺满整个地面。
TileMap 基础概念
生活化比喻:TileMap 就像用乐高积木铺地板。每一块乐高积木就是一个"瓦片",你用不同颜色的积木拼出不同的地形。整个地图就是由成千上万块小积木拼成的。
| 概念 | 说明 |
|---|---|
| TileSet | 瓦片集合(所有可用的积木种类) |
| TileMap | 瓦片地图(用积木拼出来的地板) |
| Tile | 单个瓦片(一块积木) |
| Cell | 地图上的一个格子(放积木的位置) |
程序化生成地图
手动放置每个瓦片太累了。我们写代码来自动生成:
C
// MapGenerator.cs
// 程序化生成大地图
using Godot;
public partial class MapGenerator : Node3D
{
// === 地图配置 ===
[Export] public int MapWidth = 200; // 地图宽度(格子数)
[Export] public int MapHeight = 200; // 地图高度(格子数)
[Export] public float CellSize = 2f; // 每格大小(世界单位)
// 地形瓦片资源
[Export] public TileSetLayer TilesetLayer;
// 装饰物场景
[Export] public PackedScene TreeScene;
[Export] public PackedScene RockScene;
[Export] public PackedScene TombstoneScene;
// 装饰物密度
[Export] public float DecorationDensity = 0.05f; // 5%的格子有装饰物
// 地图边界
public float MapBoundX { get; private set; }
public float MapBoundZ { get; private set; }
// 装饰物容器
private Node3D _decorationsContainer;
private Node3D _obstaclesContainer;
// 随机数生成器(使用种子,保证每次生成一致)
private int _seed = 12345;
public override void _Ready()
{
// 创建容器
_decorationsContainer = new Node3D();
_decorationsContainer.Name = "Decorations";
AddChild(_decorationsContainer);
_obstaclesContainer = new Node3D();
_obstaclesContainer.Name = "Obstacles";
AddChild(_obstaclesContainer);
// 计算边界
MapBoundX = MapWidth * CellSize / 2f;
MapBoundZ = MapHeight * CellSize / 2f;
// 生成地图
GenerateGround();
GenerateDecorations();
GenerateBoundary();
}
/// <summary>
/// 生成地面
/// </summary>
private void GenerateGround()
{
// 创建一个大平面作为地面
var planeMesh = new PlaneMesh();
planeMesh.Size = new Vector2(
MapWidth * CellSize,
MapHeight * CellSize
);
// 地面材质(草绿色)
var material = new StandardMaterial3D();
material.AlbedoColor = new Color(0.22f, 0.38f, 0.15f);
planeMesh.SurfaceSetMaterial(0, material);
var groundNode = new MeshInstance3D();
groundNode.Name = "Ground";
groundNode.Mesh = planeMesh;
AddChild(groundNode);
}
/// <summary>
/// 生成随机装饰物(树、石头、墓碑等)
/// </summary>
private void GenerateDecorations()
{
// 使用简单的噪声算法生成自然分布的装饰物
var rng = new RandomNumberGenerator();
rng.Seed = (ulong)_seed;
for (int x = 0; x < MapWidth; x++)
{
for (int z = 0; z < MapHeight; z++)
{
// 跳过中心区域(玩家出生点,留空)
float distFromCenter = Mathf.Sqrt(
Mathf.Pow(x - MapWidth / 2f, 2)
+ Mathf.Pow(z - MapHeight / 2f, 2)
);
if (distFromCenter < 10) continue;
// 随机决定是否放置装饰物
if (rng.Randf() > DecorationDensity) continue;
// 转换为世界坐标
float worldX = (x - MapWidth / 2f) * CellSize;
float worldZ = (z - MapHeight / 2f) * CellSize;
// 随机选择装饰物类型
float roll = rng.Randf();
PackedScene scene;
if (roll < 0.5f)
scene = TreeScene; // 50%概率放树
else if (roll < 0.8f)
scene = RockScene; // 30%概率放石头
else
scene = TombstoneScene; // 20%概率放墓碑
if (scene == null) continue;
// 放置装饰物
PlaceDecoration(scene, worldX, worldZ, rng);
}
}
}
/// <summary>
/// 在指定位置放置装饰物
/// </summary>
private void PlaceDecoration(
PackedScene scene, float x, float z,
RandomNumberGenerator rng)
{
var decoration = scene.Instantiate<Node3D>();
decoration.Position = new Vector3(x, 0, z);
// 随机缩放和旋转,让场景看起来更自然
float scale = 0.8f + rng.Randf() * 0.4f; // 0.8~1.2倍
decoration.Scale = new Vector3(scale, scale, scale);
decoration.Rotation = new Vector3(
0,
rng.Randf() * Mathf.Tau,
0
);
// 大型装饰物(树)作为障碍物
if (scene == TreeScene)
{
_obstaclesContainer.AddChild(decoration);
}
else
{
_decorationsContainer.AddChild(decoration);
}
}
/// <summary>
/// 生成地图边界(不可越过的墙壁)
/// </summary>
private void GenerateBoundary()
{
// 使用 StaticBody3D 作为边界墙壁
var wallThickness = 1f;
// 四面墙
CreateBoundaryWall(
new Vector3(0, 0.5f, -MapBoundZ - wallThickness),
new Vector3(MapWidth * CellSize, 1, wallThickness)
);
CreateBoundaryWall(
new Vector3(0, 0.5f, MapBoundZ + wallThickness),
new Vector3(MapWidth * CellSize, 1, wallThickness)
);
CreateBoundaryWall(
new Vector3(-MapBoundX - wallThickness, 0.5f, 0),
new Vector3(wallThickness, 1, MapHeight * CellSize)
);
CreateBoundaryWall(
new Vector3(MapBoundX + wallThickness, 0.5f, 0),
new Vector3(wallThickness, 1, MapHeight * CellSize)
);
}
private void CreateBoundaryWall(
Vector3 position, Vector3 size)
{
var body = new StaticBody3D();
body.Position = position;
var collision = new CollisionShape3D();
var shape = new BoxShape3D();
shape.Size = size;
collision.Shape = shape;
body.AddChild(collision);
// 可选:添加视觉网格
var mesh = new MeshInstance3D();
var box = new BoxMesh();
box.Size = size;
var material = new StandardMaterial3D();
material.AlbedoColor = new Color(0.3f, 0.2f, 0.1f);
material.Transparency = BaseMaterial3D.TransparencyEnum.Alpha;
material.AlbedoColor = new Color(0.3f, 0.2f, 0.1f, 0.3f);
box.SurfaceSetMaterial(0, material);
mesh.Mesh = box;
body.AddChild(mesh);
AddChild(body);
}
}GDScript
# map_generator.gd
# 程序化生成大地图
extends Node3D
# === 地图配置 ===
@export var map_width: int = 200 # 地图宽度(格子数)
@export var map_height: int = 200 # 地图高度(格子数)
@export var cell_size: float = 2.0 # 每格大小(世界单位)
# 装饰物场景
@export var tree_scene: PackedScene
@export var rock_scene: PackedScene
@export var tombstone_scene: PackedScene
# 装饰物密度
@export var decoration_density: float = 0.05 # 5%的格子有装饰物
# 地图边界
var map_bound_x: float
var map_bound_z: float
# 装饰物容器
var _decorations_container: Node3D
var _obstacles_container: Node3D
# 随机数生成器
var _seed: int = 12345
func _ready():
# 创建容器
_decorations_container = Node3D.new()
_decorations_container.name = "Decorations"
add_child(_decorations_container)
_obstacles_container = Node3D.new()
_obstacles_container.name = "Obstacles"
add_child(_obstacles_container)
# 计算边界
map_bound_x = map_width * cell_size / 2.0
map_bound_z = map_height * cell_size / 2.0
# 生成地图
generate_ground()
generate_decorations()
generate_boundary()
## 生成地面
func generate_ground():
# 创建一个大平面作为地面
var plane_mesh = PlaneMesh.new()
plane_mesh.size = Vector2(
map_width * cell_size,
map_height * cell_size
)
# 地面材质(草绿色)
var material = StandardMaterial3D.new()
material.albedo_color = Color(0.22, 0.38, 0.15)
plane_mesh.surface_set_material(0, material)
var ground_node = MeshInstance3D.new()
ground_node.name = "Ground"
ground_node.mesh = plane_mesh
add_child(ground_node)
## 生成随机装饰物
func generate_decorations():
var rng = RandomNumberGenerator.new()
rng.seed = _seed
for x in range(map_width):
for z in range(map_height):
# 跳过中心区域(玩家出生点)
var dist_from_center = sqrt(
pow(x - map_width / 2.0, 2) \
+ pow(z - map_height / 2.0, 2))
if dist_from_center < 10:
continue
# 随机决定是否放置装饰物
if rng.randf() > decoration_density:
continue
# 转换为世界坐标
var world_x = (x - map_width / 2.0) * cell_size
var world_z = (z - map_height / 2.0) * cell_size
# 随机选择装饰物类型
var roll = rng.randf()
var scene: PackedScene
if roll < 0.5:
scene = tree_scene # 50%概率放树
elif roll < 0.8:
scene = rock_scene # 30%概率放石头
else:
scene = tombstone_scene # 20%概率放墓碑
if scene == null:
continue
# 放置装饰物
place_decoration(scene, world_x, world_z, rng)
## 在指定位置放置装饰物
func place_decoration(scene, x, z, rng):
var decoration = scene.instantiate()
decoration.position = Vector3(x, 0, z)
# 随机缩放和旋转
var scale_val = 0.8 + rng.randf() * 0.4 # 0.8~1.2倍
decoration.scale = Vector3(scale_val, scale_val, scale_val)
decoration.rotation = Vector3(0, rng.randf() * TAU, 0)
# 大型装饰物(树)作为障碍物
if scene == tree_scene:
_obstacles_container.add_child(decoration)
else:
_decorations_container.add_child(decoration)
## 生成地图边界
func generate_boundary():
var wall_thickness = 1.0
# 四面墙
create_boundary_wall(
Vector3(0, 0.5, -map_bound_z - wall_thickness),
Vector3(map_width * cell_size, 1, wall_thickness)
)
create_boundary_wall(
Vector3(0, 0.5, map_bound_z + wall_thickness),
Vector3(map_width * cell_size, 1, wall_thickness)
)
create_boundary_wall(
Vector3(-map_bound_x - wall_thickness, 0.5, 0),
Vector3(wall_thickness, 1, map_height * cell_size)
)
create_boundary_wall(
Vector3(map_bound_x + wall_thickness, 0.5, 0),
Vector3(wall_thickness, 1, map_height * cell_size)
)
func create_boundary_wall(pos, size):
var body = StaticBody3D.new()
body.position = pos
var collision = CollisionShape3D.new()
var shape = BoxShape3D.new()
shape.size = size
collision.shape = shape
body.add_child(collision)
# 可选:半透明视觉网格
var mesh = MeshInstance3D.new()
var box = BoxMesh.new()
box.size = size
var material = StandardMaterial3D.new()
material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
material.albedo_color = Color(0.3, 0.2, 0.1, 0.3)
box.surface_set_material(0, material)
mesh.mesh = box
body.add_child(mesh)
add_child(body)地图管理器
地图管理器负责处理地图相关的逻辑:限制玩家活动范围、管理不同区域的敌人类型等。
C
// MapManager.cs
// 地图管理器:限制玩家范围,管理区域
using Godot;
public partial class MapManager : Node3D
{
// 地图边界
[Export] public float MapBoundX = 200f;
[Export] public float MapBoundZ = 200f;
// 引用
private MapGenerator _mapGenerator;
private CharacterBody3D _player;
public override void _Ready()
{
_mapGenerator = GetNode<MapGenerator>("MapGenerator");
_player = GetParent().GetNode<CharacterBody3D>("Player");
if (_mapGenerator != null)
{
MapBoundX = _mapGenerator.MapBoundX;
MapBoundZ = _mapGenerator.MapBoundZ;
}
}
public override void _PhysicsProcess(double delta)
{
if (_player == null) return;
// 限制玩家在地图范围内
ClampPlayerPosition();
}
/// <summary>
/// 限制玩家位置在地图边界内
/// </summary>
private void ClampPlayerPosition()
{
Vector3 pos = _player.GlobalPosition;
pos.X = Mathf.Clamp(pos.X, -MapBoundX, MapBoundX);
pos.Z = Mathf.Clamp(pos.Z, -MapBoundZ, MapBoundZ);
_player.GlobalPosition = pos;
}
/// <summary>
/// 检查某个位置是否在地图内
/// </summary>
public bool IsInBounds(Vector3 position)
{
return Mathf.Abs(position.X) <= MapBoundX
&& Mathf.Abs(position.Z) <= MapBoundZ;
}
/// <summary>
/// 获取玩家在地图上的归一化位置(0~1)
/// 用于小地图显示
/// </summary>
public Vector2 GetPlayerMapPosition()
{
if (_player == null) return Vector2.Zero;
float x = (_player.GlobalPosition.X + MapBoundX)
/ (2 * MapBoundX);
float y = (_player.GlobalPosition.Z + MapBoundZ)
/ (2 * MapBoundZ);
return new Vector2(
Mathf.Clamp(x, 0, 1),
Mathf.Clamp(y, 0, 1)
);
}
}GDScript
# map_manager.gd
# 地图管理器:限制玩家范围,管理区域
extends Node3D
# 地图边界
@export var map_bound_x: float = 200.0
@export var map_bound_z: float = 200.0
# 引用
var _map_generator: Node
var _player: CharacterBody3D
func _ready():
_map_generator = get_node("MapGenerator")
_player = get_parent().get_node("Player")
if _map_generator:
map_bound_x = _map_generator.map_bound_x
map_bound_z = _map_generator.map_bound_z
func _physics_process(delta):
if _player == null:
return
clamp_player_position()
## 限制玩家位置在地图边界内
func clamp_player_position():
var pos = _player.global_position
pos.x = clampf(pos.x, -map_bound_x, map_bound_x)
pos.z = clampf(pos.z, -map_bound_z, map_bound_z)
_player.global_position = pos
## 检查某个位置是否在地图内
func is_in_bounds(position: Vector3) -> bool:
return absf(position.x) <= map_bound_x \
and absf(position.z) <= map_bound_z
## 获取玩家在地图上的归一化位置(0~1)
## 用于小地图显示
func get_player_map_position() -> Vector2:
if _player == null:
return Vector2.ZERO
var x = (_player.global_position.x + map_bound_x) \
/ (2.0 * map_bound_x)
var y = (_player.global_position.z + map_bound_z) \
/ (2.0 * map_bound_z)
return Vector2(
clampf(x, 0.0, 1.0),
clampf(y, 0.0, 1.0)
)关卡切换机制
关卡结构
| 关卡 | 地形 | 时长 | 结束条件 |
|---|---|---|---|
| 第1关 | 草地 | 5分钟 | 击败Boss |
| 第2关 | 森林 | 5分钟 | 击败Boss |
| 第3关 | 墓地 | 5分钟 | 击败Boss |
| 第4关 | 地狱 | 10分钟 | 击败最终Boss |
关卡切换方式
割草游戏通常不需要"加载画面"。可以在时间到达后,让Boss从地图边缘走进来。击败Boss后,画面渐黑,然后切换到下一关的地图。整个过渡过程不超过3秒。
Boss事件触发
C
// BossEvent.cs
// Boss事件管理器
using Godot;
public partial class BossEvent : Node
{
[Export] public float TriggerTime = 300f; // 5分钟后触发
[Export] public PackedScene BossScene;
[Export] public string BossWarningMessage = "警告:Boss来了!";
private bool _hasTriggered = false;
private float _gameTime = 0f;
[Signal] public delegate void BossSpawnedEventHandler();
public override void _Process(double delta)
{
if (_hasTriggered) return;
_gameTime += (float)delta;
// 检查是否到达触发时间
if (_gameTime >= TriggerTime)
{
TriggerBoss();
}
}
private void TriggerBoss()
{
_hasTriggered = true;
GD.Print(BossWarningMessage);
// 显示警告UI(红色闪烁)
ShowWarning();
// 延迟2秒后生成Boss
var timer = GetTree().CreateTimer(2.0);
timer.Timeout += SpawnBoss;
}
private void SpawnBoss()
{
if (BossScene == null) return;
var player = GetTree().CurrentScene
.GetNodeOrNull<Node3D>("Player");
if (player == null) return;
// 在屏幕外生成Boss
var boss = BossScene.Instantiate<Node3D>();
float spawnDistance = 25f;
boss.GlobalPosition = player.GlobalPosition
+ new Vector3(spawnDistance, 0, 0);
var enemies = GetTree().CurrentScene
.GetNode<Node3D>("Enemies");
enemies?.AddChild(boss);
EmitSignal(SignalName.BossSpawned);
GD.Print("Boss已出现!");
}
private void ShowWarning()
{
// 简单的打印警告
// 实际项目中应该显示UI警告效果
GD.Print("========== 警告 ==========");
GD.Print(" Boss 即将来袭!");
GD.Print("==========================");
}
}GDScript
# boss_event.gd
# Boss事件管理器
extends Node
@export var trigger_time: float = 300.0 # 5分钟后触发
@export var boss_scene: PackedScene
@export var boss_warning_message: String = "警告:Boss来了!"
var _has_triggered: bool = false
var _game_time: float = 0.0
signal boss_spawned()
func _process(delta):
if _has_triggered:
return
_game_time += delta
# 检查是否到达触发时间
if _game_time >= trigger_time:
trigger_boss()
func trigger_boss():
_has_triggered = true
print(boss_warning_message)
# 显示警告
show_warning()
# 延迟2秒后生成Boss
var timer = get_tree().create_timer(2.0)
timer.timeout.connect(spawn_boss)
func spawn_boss():
if boss_scene == null:
return
var player = get_tree().current_scene.get_node_or_null("Player")
if player == null:
return
# 在屏幕外生成Boss
var boss = boss_scene.instantiate()
var spawn_distance = 25.0
boss.global_position = player.global_position \
+ Vector3(spawn_distance, 0, 0)
var enemies = get_tree().current_scene.get_node("Enemies")
enemies.add_child(boss)
boss_spawned.emit()
print("Boss已出现!")
func show_warning():
print("========== 警告 ==========")
print(" Boss 即将来袭!")
print("==========================")性能考虑
大地图上有很多装饰物,如果不注意优化,帧率会明显下降。
优化策略
| 策略 | 原理 | 效果 |
|---|---|---|
| 距离剔除 | 远离摄像机的物体不渲染 | 减少 GPU 负担 |
| 合并网格 | 把多个静态物体合并成一个 | 减少 Draw Call |
| LOD(细节层次) | 远处的模型用低精度版本 | 减少顶点数 |
| 遮挡剔除 | 被墙壁挡住的物体不渲染 | 减少 GPU 负担 |
性能红线
如果地图上有超过 500 个装饰物(树、石头等),建议启用 Godot 的 VisibilityNotifier3D 来实现距离剔除。超过摄像机可视范围一定距离的装饰物可以隐藏。
总结
本章我们实现了:
- 程序化地图生成 — 用代码自动生成大地图
- 随机装饰物 — 树、石头、墓碑随机分布
- 地图边界 — 玩家不能走出地图
- 地图管理器 — 限制玩家范围,提供小地图坐标
- 关卡切换 — Boss事件触发和关卡过渡
大地图为游戏提供了探索空间和视觉多样性。下一章,我们要实现生存计时和难度递增系统。
