资讯详情

Unity Roguelike随机地图生成:房间走廊与Tilemap实战

📅 2026/10/21 15:56:26 | 华诺云谱 👁 阅读
Unity Roguelike随机地图生成:房间走廊与Tilemap实战
1. 随机地图生成到底在解决什么问题做Roguelike最绕不开的一件事就是地图。玩家每开一局看到的房间布局、走廊走向、敌人分布、道具位置都得是新的否则重复感会瞬间劝退。Unity本身没有内置的Roguelike地图生成器Tilemap只是给你一块画布怎么在这块画布上“长出”一张合理的地图完全靠你自己写逻辑。我最早做第一个Roguelike原型的时候图省事直接手搓了十几张固定地图每局随机抽一张。结果测试阶段自己玩到第五局就腻了——路线记得比回家还熟。后来痛下决心研究程序化生成从最简单的随机撒点到房间走廊的经典结构再到带约束的BSP分割踩了不少坑也攒了一套还算稳定的方案。这篇内容适合谁看如果你已经会用Unity的Tilemap知道Tilemap.SetTile怎么调但一提到“随机生成”就不知道从哪下手那这篇就是写给你的。我会从最核心的思路讲起把每一步的参数选择、代码结构、常见翻车点都摊开说。代码用C#版本以Unity 2021 LTS以上为准2D项目Tilemap渲染。核心关键词就几个随机地图生成、房间布局、走廊连接、Tilemap、种子控制。把这几个搞明白一个能跑的Roguelike地图生成器就成型了。2. 整体设计思路与方案选型2.1 为什么选“房间走廊”而不是纯随机程序化地图生成的路子很多我列几个常见的说说为什么最后选了房间走廊这套。方案核心逻辑优点缺点适用场景纯随机撒点每个格子按概率决定是不是墙实现极简地图碎片化几乎不可玩几乎不用元胞自动机迭代平滑随机噪声洞穴感强难控制房间结构洞穴类RoguelikeBSP分割递归切分空间再挖房间房间分布均匀走廊连接较机械地牢类房间走廊先放房间再连走廊结构清晰可控需要处理重叠和连通大多数Roguelike迷宫算法完美迷宫生成路线复杂没有房间概念迷宫游戏我选房间走廊理由很直接可控。Roguelike需要房间来放战斗、放宝箱、放Boss纯洞穴或纯迷宫很难界定“一个房间”的边界。房间走廊的结构天然适配“进入房间-清怪-开箱-去下一个房间”这套循环。具体流程分四步先在网格上随机放置不重叠的矩形房间然后用走廊把房间连起来接着在房间和走廊上铺Tile最后根据房间类型刷敌人和道具。这四步里第一步和第二步是核心第三步是表现层第四步是玩法层。2.2 网格坐标系与数据结构设计动手写代码之前得先把数据结构定下来。我见过不少人上来就写生成逻辑写到一半发现房间重叠判断没法做又回头改结构很浪费时间。我的做法是定义一个GridMap类内部用一个二维数组存格子类型public enum CellType { Wall, // 墙 Floor, // 地板 Corridor, // 走廊 Door // 门可选 } public class GridMap { public int Width; public int Height; public CellType[,] Cells; public GridMap(int width, int height) { Width width; Height height; Cells new CellType[width, height]; // 默认全部填墙 for (int x 0; x width; x) for (int y 0; y height; y) Cells[x, y] CellType.Wall; } }房间用矩形表示存左下角坐标和宽高public struct Room { public int X, Y; // 左下角 public int Width, Height; public int CenterX X Width / 2; public int CenterY Y Height / 2; }这里有个细节房间中心点后面连走廊要用。用整数除法取中心对于偶数宽度的房间会偏左一格但影响不大走廊连的是大致位置。网格尺寸我一般设成80x60到120x80之间。太小了房间放不下几个太大了玩家跑图累。80x60大概能放8到15个房间一局节奏刚好。2.3 随机种子让“随机”变得可复现Roguelike的随机必须可控否则出了bug你连复现都做不到。Unity的Random是全局的状态不好管理我建议用System.Random自己实例化一个System.Random rng new System.Random(seed);seed可以来自玩家输入、时间戳或者固定值用于测试。每次生成前把seed记下来出问题直接拿seed重跑地图一模一样。这个习惯我强烈建议从第一天就养成后面调参数的时候能省大量时间。提示不要用UnityEngine.Random.Range做地图生成的核心随机它的状态和引擎其他系统共享容易互相干扰。用独立的System.Random实例干净。3. 核心细节解析与实操要点3.1 房间放置随机但不重叠的关键逻辑房间放置的核心矛盾是要随机分布又不能重叠。最朴素的做法是随机生成一个矩形然后遍历已有房间检查是否相交相交就丢弃重来。这个方法简单但有个隐患——当房间数量接近上限时随机尝试可能反复失败陷入死循环。我的处理是加一个最大尝试次数比如每个房间最多试200次试不出来就放弃这个房间继续下一个。这样最坏情况只是房间少几个不会卡死。ListRoom rooms new ListRoom(); int maxAttempts 200; int targetRoomCount 12; for (int i 0; i targetRoomCount; i) { bool placed false; for (int attempt 0; attempt maxAttempts; attempt) { int w rng.Next(minRoomSize, maxRoomSize 1); int h rng.Next(minRoomSize, maxRoomSize 1); int x rng.Next(1, mapWidth - w - 1); int y rng.Next(1, mapHeight - h - 1); Room newRoom new Room { X x, Y y, Width w, Height h }; if (!IsOverlap(newRoom, rooms, padding)) { rooms.Add(newRoom); placed true; break; } } }padding是房间之间的最小间隔我一般设2到3格。这个间隔很重要它保证了房间之间一定有墙走廊才有地方走。如果padding设0两个房间贴在一起视觉上会变成一个不规则大房间失去“独立房间”的感觉。minRoomSize和maxRoomSize我设5和12。小于5的房间放不下什么东西大于12的房间在80x60的图上放不了几个。这个范围可以根据你的地图尺寸和玩法调整。IsOverlap的判断要带上paddingbool IsOverlap(Room a, ListRoom others, int padding) { foreach (var b in others) { if (a.X - padding b.X b.Width a.X a.Width padding b.X a.Y - padding b.Y b.Height a.Y a.Height padding b.Y) return true; } return false; }这是标准的AABB相交检测加上padding就是把每个房间的碰撞盒向外扩一圈再判断。3.2 走廊连接怎么连才不会出现“孤岛”房间放好了接下来要把它们连起来。最直接的做法是每两个相邻房间的中心点之间画一条L形走廊。但“相邻”怎么定义如果按列表顺序两两相连可能出现房间A连B、B连C但A和C之间绕很远地图看起来像一条线。我试过几种连接策略顺序连接按房间生成顺序第i个连第i1个。简单但地图呈链状缺乏回路。最近邻连接每个房间连到离它最近的房间。会产生多个连通分量需要额外处理。MST最小生成树连接保证所有房间连通且总走廊最短。效果最好但实现稍复杂。MST额外边在MST基础上随机加几条边制造回路。Roguelike有回路才好玩玩家可以选择路线。我目前用的是简化版MST思路先把房间按中心点x坐标排序然后依次连接相邻的再随机补几条跨房间的走廊。这样既保证连通又有回路实现也不复杂。L形走廊的画法有两种先横后竖或先竖后横。随机选一种地图看起来更自然。void CarveCorridor(GridMap map, int x1, int y1, int x2, int y2) { if (rng.Next(2) 0) { // 先横后竖 for (int x Mathf.Min(x1, x2); x Mathf.Max(x1, x2); x) map.Cells[x, y1] CellType.Corridor; for (int y Mathf.Min(y1, y2); y Mathf.Max(y1, y2); y) map.Cells[x2, y] CellType.Corridor; } else { // 先竖后横 for (int y Mathf.Min(y1, y2); y Mathf.Max(y1, y2); y) map.Cells[x1, y] CellType.Corridor; for (int x Mathf.Min(x1, x2); x Mathf.Max(x1, x2); x) map.Cells[x, y2] CellType.Corridor; } }走廊宽度我设1格。有人喜欢3格宽的走廊看起来更气派但会占用更多空间而且玩家在窄走廊里的战斗体验其实更好——有压迫感。这个看个人喜好。注意走廊画完后一定要做一次连通性检查。用BFS或DFS从第一个房间的中心出发看能不能到达所有房间的中心。如果有房间到不了说明连接逻辑有漏洞需要补一条走廊。这个检查我建议每次都跑成本很低但能避免“玩家出生在孤岛”这种致命bug。3.3 房间类型分配让地图有节奏感所有房间都长一样玩起来会平淡。我会给房间分配类型出生房、普通战斗房、宝箱房、Boss房、商店房。分配规则基于房间的位置和大小。出生房选离地图中心最远的房间或者固定选第一个生成的房间。Boss房选离出生房最远的房间这样玩家要穿过整张地图才能到Boss流程长度有保证。宝箱房和商店房随机分配但数量固定比如各1到2个。// 找离出生房最远的房间作为Boss房 Room spawnRoom rooms[0]; Room bossRoom rooms.OrderByDescending(r Mathf.Abs(r.CenterX - spawnRoom.CenterX) Mathf.Abs(r.CenterY - spawnRoom.CenterY)).First();这里用的是曼哈顿距离比欧氏距离更贴合网格地图的实际移动距离。房间类型定好后刷敌人和道具的逻辑就有了依据。战斗房按难度刷怪宝箱房放宝箱Boss房放Boss。这部分属于玩法层后面单独开一篇讲这篇先把地图生成的地基打牢。4. 完整实操流程与核心代码实现4.1 从零搭建生成器的步骤拆解把前面的碎片拼起来一个完整的生成流程是这样的初始化GridMap全部填Wall。用System.Random实例化随机数生成器传入seed。循环尝试放置房间直到达到目标数量或尝试次数耗尽。把房间内部格子设为Floor。按连接策略生成走廊把走廊格子设为Corridor。做连通性检查补全缺失的连接。分配房间类型。把GridMap的数据刷到Unity的Tilemap上。在房间内按类型生成敌人、道具、玩家出生点。第8步是Unity特有的。我一般写一个MapRenderer脚本持有三个Tilemap地板层、墙壁层、装饰层。遍历GridMap.Cells根据CellType往对应Tilemap上SetTile。public class MapRenderer : MonoBehaviour { public Tilemap floorTilemap; public Tilemap wallTilemap; public TileBase floorTile; public TileBase wallTile; public void Render(GridMap map) { floorTilemap.ClearAllTiles(); wallTilemap.ClearAllTiles(); for (int x 0; x map.Width; x) { for (int y 0; y map.Height; y) { if (map.Cells[x, y] CellType.Wall) wallTilemap.SetTile(new Vector3Int(x, y, 0), wallTile); else floorTilemap.SetTile(new Vector3Int(x, y, 0), floorTile); } } } }墙壁的渲染有个小技巧只渲染与地板相邻的墙。地图外围大片纯墙不需要渲染省性能。判断方法是检查该墙格子的四邻域是否有非墙格子。4.2 参数调优让地图“好看”的几组数值代码跑通只是第一步地图好不好看、好不好玩全在参数上。我调了几十版下面这组数值在80x60的图上表现比较稳定参数取值说明地图宽度80格子数地图高度60格子数目标房间数12实际生成10-12个房间最小尺寸5小于5放不下东西房间最大尺寸12大于12地图放不下几个房间间隔padding2保证走廊有空间走廊宽度1窄走廊战斗体验好额外回路数3增加路线选择最大放置尝试200防止死循环房间数量不是越多越好。12个房间在80x60的图上平均每个房间占约400格加上走廊地图填充率大概30%到40%剩下的都是墙。这个密度玩起来不会太空旷也不会太拥挤。如果你想要更紧凑的地牢感可以把地图缩到60x45房间数降到8个。如果想要更开阔的探索感地图放到120x80房间数提到18个。但注意地图越大生成时间越长连通性检查的BFS开销也越大。120x80的图生成一次大概几十毫秒可以接受再大就要考虑分帧生成了。4.3 把生成结果落到Tilemap上Tilemap的渲染本身不复杂但有几个坑我踩过。第一个坑是Tilemap的坐标系。Unity的Tilemap以左下角为原点x向右y向上和我们的GridMap数组索引一致。但如果你用了Tilemap.SetTile之后发现地图上下颠倒检查一下是不是在遍历时把y的顺序搞反了。第二个坑是Tile的锚点。Unity的Tile默认锚点在格子中心但如果你自己切的图集锚点不对渲染出来会错位。用Tile资产的时候确保Sprite的Pivot设成Center。第三个坑是渲染顺序。地板、走廊、墙壁、装饰要分层用不同的Tilemap设置不同的Order in Layer。地板在最下面墙壁在上面装饰最上面。如果全塞一个Tilemap里后设的Tile会覆盖先设的墙壁可能被地板盖住。第四个坑是性能。SetTile逐个调用在80x60的图上要调4800次虽然不算慢但每局都调还是有点浪费。我的优化是只渲染“可见区域”或者用Tilemap.SetTiles批量设置。不过对于这个规模的地图逐个设置完全够用不用过早优化。4.4 生成结果的验证与调试可视化地图生成最怕的是“看起来没问题玩起来走不通”。我写了一个简单的调试工具在Scene视图里用Gizmos画出房间矩形和走廊中心线一眼就能看出连接对不对。void OnDrawGizmos() { if (rooms null) return; foreach (var room in rooms) { Gizmos.color Color.green; Gizmos.DrawWireCube( new Vector3(room.X room.Width / 2f, room.Y room.Height / 2f, 0), new Vector3(room.Width, room.Height, 0)); } }绿色框是房间红色线是走廊连接。如果看到某个房间没有红线连出去说明它是孤岛连接逻辑有问题。另外我会在生成完成后跑一次BFS统计从出生房能到达的格子数。如果这个数字明显小于总地板数说明有区域没连通。这个检查放在Start里生成完立刻跑有问题直接Debug.LogError。5. 常见问题与排查技巧实录5.1 房间重叠、走廊穿墙、出生点卡墙这三个是新手最常遇到的问题我逐个说排查思路。房间重叠九成是IsOverlap的padding没加对。检查你的相交判断有没有把padding算进去。另一个可能是房间坐标算错了比如x Width写成了x Width - 1导致边界判断差一格。我的建议是写个单元测试手动构造两个已知会重叠的矩形看函数返回是不是true。走廊穿墙走廊画的时候没有检查目标格子是不是已经是墙。如果走廊穿过一个房间会把房间的地板覆盖成走廊视觉上没问题但房间类型判断可能出错。我的做法是画走廊时只把Wall改成Corridor已经是Floor的格子不动。出生点卡墙出生点选在房间中心但房间中心可能正好是走廊穿过的位置或者被装饰物占了。生成玩家之前检查出生点格子是不是Floor不是的话在房间内找一个最近的Floor格子。5.2 地图不连通、房间成孤岛怎么办连通性问题分两种一种是生成时就没连上另一种是连上了但被后续操作破坏了。生成时没连上通常是连接策略的问题。如果你用的是“每个房间连最近的房间”当两个房间互为最近时它们只连彼此和其他房间断开。解决办法是改用MST或者在连接后做一次连通分量检查把不同的分量用额外走廊连起来。连通分量检查的代码不复杂ListListRoom GetConnectedComponents(ListRoom rooms, GridMap map) { // 用BFS从每个未访问的房间出发标记能到达的房间 // 返回所有连通分量 }如果发现超过一个连通分量就在分量之间找一对距离最近的中心点画一条走廊。重复直到只剩一个分量。被后续操作破坏的情况比如你在房间生成后加了“挖洞”或“炸墙”的效果可能把唯一的通道堵上。这种就要在破坏性操作后重新跑连通性检查。5.3 生成速度慢、卡顿的优化方向80x60的图生成一次大概10到30毫秒玩家感知不到。但如果你把地图开到200x150或者房间数上百生成时间可能到几百毫秒切换场景时会卡一下。优化方向有几个减少无效尝试房间放置的maxAttempts设太大失败时会空转。可以动态调整前期尝试次数多后期房间多了就减少。空间换时间用一个粗粒度的网格记录哪些区域已经被房间占用放置新房间时先查粗网格快速排除大部分不可能的位置。分帧生成把生成过程拆成协程每帧生成一部分避免单帧卡顿。适合超大地图。缓存结果如果seed固定生成结果可以缓存。同一局重开不用重新生成。我实测下来80x60的图用最朴素的实现生成加渲染总共不到50毫秒完全不需要优化。先把功能做对性能问题等真遇到了再说。5.4 常见问题速查表问题现象可能原因排查方法解决方案房间重叠padding未生效打印房间坐标检查修正IsOverlap的padding计算地图有孤岛连接策略缺陷BFS统计可达格子数补连通分量之间的走廊出生点卡墙中心点被占用检查出生点格子类型在房间内找最近的Floor走廊穿房间未检查格子类型打印走廊路径只把Wall改成Corridor地图上下颠倒遍历顺序错误对比GridMap和Tilemap坐标统一y轴方向墙壁被地板盖住渲染顺序错误检查Tilemap的Order in Layer墙壁层设更高的Order生成卡顿地图过大或尝试过多用Profiler看耗时减少尝试次数或分帧生成每次地图一样seed固定检查seed来源用时间戳或玩家输入做seed提示这张表建议打印出来贴在显示器旁边。我调地图生成的那两周至少有一半时间是在这几个问题之间反复横跳。6. 后续扩展方向与个人经验地图生成跑通之后能扩展的方向很多。比如给房间加形状变化不全是矩形加入L形、十字形房间比如走廊加宽度变化主走廊3格宽支路1格宽比如加入主题系统不同楼层用不同的Tile和装饰比如生成时考虑玩法节奏把难度高的房间放在离出生点远的位置。我个人在实际操作中的体会是地图生成最难的从来不是算法而是“调参”和“验证”。算法网上能找到一堆但每个参数对你的游戏体验有什么影响只能自己一局一局试。我建议你每调一组参数就实际玩三局记录一下“有没有迷路”“房间够不够用”“路线有没有选择”。这些主观感受比任何指标都重要。最后再分享一个小技巧把生成的地图导出成图片。写一个简单的脚本把GridMap的每个格子按类型涂色导出成PNG。这样你可以一次性生成100张地图快速浏览一眼就能看出哪些参数组合容易出问题。这个批量预览的方法帮我省了大量重复测试的时间。地图生成只是Roguelike的第一步后面还有敌人AI、道具系统、难度曲线一大堆事。但地基打好了后面盖楼才稳。下一篇我会讲怎么在生成好的地图上刷敌人和道具以及怎么控制难度曲线。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑