更新日期:2026年8月6日。 项目源码:获取源码。
索引
- 关卡生成算法
- 关卡生成种子
-
- 一、关卡中的地块分类
- 二、关卡的大小
- 三、关卡生成算法的流程
-
- 1.初始化算法数据
- 2.随机撒种子
- 3.计算地块的目标格子数
- 4.种子并行生长
- 5.边缘优化
- 6.生成玩家老巢
关卡生成算法
在序章中有一个夸张说法,我们的TankBattle联机坦克大战拥有多达2147483647个的超多关卡(int型的上限),事实上只是因为关卡生成算法采用了一个int型作为种子,就像我的世界那样,提供一个相同的生成种子,将生成一个完全一样的世界。
关卡生成种子
那么,在此之前必须通过关卡种子来定义一个随机器:
/// <summary>
/// 关卡地图的随机器(LevelSeed为关卡种子,比如第一关是1,第二关是2……)
/// </summary>
public System.Random MapRandom { get; private set; } = new System.Random(LevelSeed);
注意:在任何设备、任何环境下,通过这个种子生成出来的关卡都必须是一模一样的。
一、关卡中的地块分类
关卡中的地块共有7个类型:
| 空白 | 0 | 坦克和子弹可穿过 | ![]() |
| 砖墙 | 1 | 坦克不可穿过,子弹可销毁 | ![]() |
| 钢板 | 2 | 坦克不可穿过,子弹可销毁(需玩家坦克达到3星) | ![]() |
| 森林 | 3 | 坦克和子弹可穿过,但会被遮挡视野 | ![]() |
| 海水 | 4 | 坦克不可穿过,子弹可穿过 | ![]() |
| 雪地 | 5 | 坦克和子弹可穿过,但坦克会打滑 | ![]() |
| 玩家老巢 | 6 | 坦克不可穿过,子弹可销毁 | ![]() |
定义地块类型BlockType如下:
/// <summary>
/// 地块类型
/// </summary>
public enum BlockType
{
/// <summary>
/// 空白
/// </summary>
None = 0,
/// <summary>
/// 砖墙
/// </summary>
BrickWall = 1,
/// <summary>
/// 钢板
/// </summary>
SteelPlate = 2,
/// <summary>
/// 森林
/// </summary>
Forest = 3,
/// <summary>
/// 海水
/// </summary>
Seawater = 4,
/// <summary>
/// 雪地
/// </summary>
Snowfield = 5,
/// <summary>
/// 玩家老巢
/// </summary>
Lair = 6
}
二、关卡的大小
关卡的大小(地块数量)为13*13(保持跟原版一致),地块的索引为左下角(0,0),右上角(12,12):
/// <summary>
/// 地图行数
/// </summary>
public static readonly int Rows = 13;
/// <summary>
/// 地图列数
/// </summary>
public static readonly int Cols = 13;

基于此,我们先创建一个可以代表整个关卡地图的二维数组:
//初始化空白地图
BlockType[,] map = new BlockType[Rows, Cols];
for (int r = 0; r < Rows; r++)
{
for (int c = 0; c < Cols; c++)
{
map[r, c] = BlockType.None;
}
}
三、关卡生成算法的流程
我们的关卡生成算法流程如下:
1.初始化算法数据 2.随机撒种子 3.计算地块的目标格子数 4.种子并行生长 5.边缘优化 6.生成玩家老巢
1.初始化算法数据
定义关卡生成算法所需要用到的数据和字段:
/// <summary>
/// 聚集因子:控制地形成片程度(40~80之间,值越大越聚集)
/// </summary>
private const int ClusterFactor = 65;
/// <summary>
/// 地块的权重(数值越大,该地块越多)
/// </summary>
private readonly Dictionary<BlockType, int> BlockWeights = new Dictionary<BlockType, int>();
/// <summary>
/// 生成地图的例外区域(玩家出生点、敌人出生点、玩家老巢等)
/// </summary>
private readonly HashSet<(int row, int col)> ExceptionZones = new HashSet<(int, int)>();
/// <summary>
/// 所有地块的种子数量
/// </summary>
private Dictionary<BlockType, Queue<(int row, int col)>> _blockSeeds = new Dictionary<BlockType, Queue<(int row, int col)>>();
/// <summary>
/// 所有地块的目标格子数
/// </summary>
private Dictionary<BlockType, int> _blockTargetCount = new Dictionary<BlockType, int>();
/// <summary>
/// 所有地块的当前格子数
/// </summary>
private Dictionary<BlockType, int> _blockCurrentCount = new Dictionary<BlockType, int>();
/// <summary>
/// 邻居地块缓存队列
/// </summary>
private List<(int row, int col)> _neighborCache = new List<(int row, int col)>();
定义各个地块的权重(砖墙最多,钢板和森林次之):
//地块的权重(数值越大,该地块越多)
BlockWeights.Add(BlockType.BrickWall, 40);
BlockWeights.Add(BlockType.SteelPlate, 15);
BlockWeights.Add(BlockType.Forest, 15);
BlockWeights.Add(BlockType.Seawater, 10);
BlockWeights.Add(BlockType.Snowfield, 5);
定义例外区域(玩家老巢、玩家出生点、敌人出生点为例外区域,不会在这些区域生成地块):
//玩家老巢及周边保护区域
ExceptionZones.Add((5, 0));
ExceptionZones.Add((5, 1));
ExceptionZones.Add((6, 0));
ExceptionZones.Add((6, 1));
ExceptionZones.Add((7, 0));
ExceptionZones.Add((7, 1));
//玩家出生点
ExceptionZones.Add((4, 0));
ExceptionZones.Add((8, 0));
//敌人出生点
ExceptionZones.Add((0, 12));
ExceptionZones.Add((6, 12));
ExceptionZones.Add((12, 12));
然后,在每一轮关卡生成前,初始化相关数据:
//重置地图数据
_blockSeeds[BlockType.BrickWall].Clear();
_blockSeeds[BlockType.SteelPlate].Clear();
_blockSeeds[BlockType.Forest].Clear();
_blockSeeds[BlockType.Seawater].Clear();
_blockSeeds[BlockType.Snowfield].Clear();
_blockTargetCount[BlockType.BrickWall] = 0;
_blockTargetCount[BlockType.SteelPlate] = 0;
_blockTargetCount[BlockType.Forest] = 0;
_blockTargetCount[BlockType.Seawater] = 0;
_blockTargetCount[BlockType.Snowfield] = 0;
_blockCurrentCount[BlockType.BrickWall] = 0;
_blockCurrentCount[BlockType.SteelPlate] = 0;
_blockCurrentCount[BlockType.Forest] = 0;
_blockCurrentCount[BlockType.Seawater] = 0;
_blockCurrentCount[BlockType.Snowfield] = 0;
_neighborCache.Clear();
2.随机撒种子
先根据权重值在地图上随机撒种子,权重值越高,种子越多:
/// <summary>
/// 随机撒种子
/// </summary>
/// <param name="map">地图</param>
/// <param name="blockType">种子地块类型</param>
private void Seeding(BlockType[,] map, BlockType blockType)
{
//根据权重决定种子数量(至少1颗)
int numSeeds = Math.Max(1, BlockWeights[blockType] / 5);
//播种所有种子
for (int m = 0; m < numSeeds; m++)
{
//每一个种子尝试播种200次
for (int attempt = 0; attempt < 200; attempt++)
{
//使用关卡种子随机器MapRandom
int r = MapRandom.Next(0, Rows);
int c = MapRandom.Next(0, Cols);
//排除非空地块
if (map[r, c] != BlockType.None)
continue;
//排除例外区域
if (ExceptionZones.Contains((r, c)))
continue;
//排除距离例外区域太近的(距离小于等于1格)
if (IsTooCloseOfExceptionZones(r, c))
continue;
//种下种子
map[r, c] = blockType;
_blockSeeds[blockType].Enqueue((r, c));
break;
}
}
}
将5种类型的地块种子都撒下:
//随机撒种子
Seeding(map, BlockType.BrickWall);
Seeding(map, BlockType.SteelPlate);
Seeding(map, BlockType.Forest);
Seeding(map, BlockType.Seawater);
Seeding(map, BlockType.Snowfield);
撒下种子后,大概就是如下这样:

3.计算地块的目标格子数
在种子生长前,我们先计算所有地块的最终数目(以控制种子生长上限):
/// <summary>
/// 计算地块的目标格子数
/// </summary>
/// <param name="blockType">种子地块类型</param>
private void CalculateTargetCount(BlockType blockType)
{
//总的可生成地块数量(预留10个空白)
int totalBlock = Rows * Cols – ExceptionZones.Count – 10;
//填充比例
double fillRatio = 0.55;
//总的地块权重
int sumWeights = BlockWeights.Values.Sum();
//计算地块的目标格子数(保底4个)
int target = (int)(totalBlock * (BlockWeights[blockType] / (double)sumWeights) * fillRatio);
_blockTargetCount[blockType] = Math.Max(4, target);
}
计算5种类型的地块最终数目:
//计算地块的目标格子数
CalculateTargetCount(BlockType.BrickWall);
CalculateTargetCount(BlockType.SteelPlate);
CalculateTargetCount(BlockType.Forest);
CalculateTargetCount(BlockType.Seawater);
CalculateTargetCount(BlockType.Snowfield);
4.种子并行生长
最核心的环节就是让所有种子并行生长(向邻居地块生长):
/// <summary>
/// 地块种子生长
/// </summary>
/// <param name="map">地图</param>
/// <param name="blockType">种子地块类型</param>
/// <returns>是否存在任意一个地块生长成功</returns>
private bool SeedGrowth(BlockType[,] map, BlockType blockType)
{
bool anyGrowth = false;
//如果已生长到目标数量,则不再生长
if (_blockCurrentCount[blockType] >= _blockTargetCount[blockType])
return anyGrowth;
//如果种子数量为零,则不再生长
if (_blockSeeds[blockType].Count <= 0)
return anyGrowth;
//取出一个种子
(int row, int col) seed = _blockSeeds[blockType].Dequeue();
//获取其邻居地块
_neighborCache.Clear();
_neighborCache.Add((seed.row – 1, seed.col – 1));
_neighborCache.Add((seed.row – 1, seed.col));
_neighborCache.Add((seed.row – 1, seed.col + 1));
_neighborCache.Add((seed.row, seed.col – 1));
_neighborCache.Add((seed.row, seed.col + 1));
_neighborCache.Add((seed.row + 1, seed.col – 1));
_neighborCache.Add((seed.row + 1, seed.col));
_neighborCache.Add((seed.row + 1, seed.col + 1));
//向邻居地块生长
for (int i = 0; i < _neighborCache.Count; i++)
{
//如果已生长到目标数量,则不再生长
if (_blockCurrentCount[blockType] >= _blockTargetCount[blockType])
break;
int r = _neighborCache[i].row;
int c = _neighborCache[i].col;
//生长位置为有效区域
if (r >= 0 && r < Rows && c >= 0 && c < Cols)
{
//排除非空地块
if (map[r, c] != BlockType.None)
continue;
//排除例外区域
if (ExceptionZones.Contains((r, c)))
continue;
//以ClusterFactor的概率扩散
if (MapRandom.IsTriggerProbability(ClusterFactor))
{
//生长成功
map[r, c] = blockType;
_blockCurrentCount[blockType]++;
_blockSeeds[blockType].Enqueue((r, c));
anyGrowth = true;
}
}
}
return anyGrowth;
}
所有种子共生长2000次,这个值不固定,只是我根据情况微调的结果:
//地块种子并行生长(生长2000次)
_blockCurrentCount[BlockType.BrickWall] = _blockSeeds[BlockType.BrickWall].Count;
_blockCurrentCount[BlockType.SteelPlate] = _blockSeeds[BlockType.SteelPlate].Count;
_blockCurrentCount[BlockType.Forest] = _blockSeeds[BlockType.Forest].Count;
_blockCurrentCount[BlockType.Seawater] = _blockSeeds[BlockType.Seawater].Count;
_blockCurrentCount[BlockType.Snowfield] = _blockSeeds[BlockType.Snowfield].Count;
int numGrowth = 2000;
for (int i = 0; i < numGrowth; i++)
{
bool brickWallGrowth = SeedGrowth(map, BlockType.BrickWall);
bool steelPlateGrowth = SeedGrowth(map, BlockType.SteelPlate);
bool forestGrowth = SeedGrowth(map, BlockType.Forest);
bool seawaterGrowth = SeedGrowth(map, BlockType.Seawater);
bool snowfieldGrowth = SeedGrowth(map, BlockType.Snowfield);
//如果没有任何生长发生,则提前结束
if (!brickWallGrowth && !steelPlateGrowth && !forestGrowth && !seawaterGrowth && !snowfieldGrowth)
{
break;
}
}
为了让各个地块区域(砖墙区、海水区等)看起来更连贯、更成片(而不是抛玉米般分散一地),所以各个类型的地块种子是并行生长的(你生长一次,我生长一次),由聚集因子决定了生长的成功率,一次完整的生长后,地图变成了如下这样:

看起来比纯粹的随机(每一个地块随机一下自己的类型)更有可玩性。
5.边缘优化
生长完成后,补充一个边缘优化算法,旨在将边缘的海水和钢板转为空白(40%几率转化),提高可玩性:
//边缘优化(边缘的海水和钢板转为空白,提高可玩性)
for (int r = 0; r < Rows; r++)
{
if (map[r, 0] == BlockType.Seawater || map[r, 0] == BlockType.SteelPlate)
{
if (MapRandom.IsTriggerProbability(40)) map[r, 0] = BlockType.None;
}
if (map[r, Cols – 1] == BlockType.Seawater || map[r, Cols – 1] == BlockType.SteelPlate)
{
if (MapRandom.IsTriggerProbability(40)) map[r, Cols – 1] = BlockType.None;
}
}
for (int c = 0; c < Cols; c++)
{
if (map[0, c] == BlockType.Seawater || map[0, c] == BlockType.SteelPlate)
{
if (MapRandom.IsTriggerProbability(40)) map[0, c] = BlockType.None;
}
if (map[Rows – 1, c] == BlockType.Seawater || map[Rows – 1, c] == BlockType.SteelPlate)
{
if (MapRandom.IsTriggerProbability(40)) map[Rows – 1, c] = BlockType.None;
}
}
因为边缘是敌方坦克的主要移动区域,所以其应该更通畅。
6.生成玩家老巢
最后,生成玩家的老巢区域就大功告成了:
//生成玩家老巢及保护地块
map[5, 0] = BlockType.BrickWall;
map[5, 1] = BlockType.BrickWall;
map[6, 0] = BlockType.Lair;
map[6, 1] = BlockType.BrickWall;
map[7, 0] = BlockType.BrickWall;
map[7, 1] = BlockType.BrickWall;

当然,我们会发现问题所在,玩家老巢的保护地块应该都是半砖,不过这与关卡生成算法无关,其涉及到地块的真实创建与分片,后续再来处理它吧。









