Unity卡牌Roguelike地图生成算法:分层有向图与加权采样实现

发布时间:2026/10/16 10:03:27

Unity卡牌Roguelike地图生成算法:分层有向图与加权采样实现 简介本资源是面向Unity开发者的杀戮尖塔式地图算法实现Demo基于Unity引擎与C#语言编写适合想学习Roguelike地图生成、房间布局与路线连接逻辑的中级开发者参考。项目核心包含两大部分地图房间生成规则与路线生成规则。房间生成方面起点层房间数量可动态配置中间层房间数量在最小值2与起点数量乘2减1之间浮动Boss层固定为1个房间房间位置采用平铺后叠加随机横向偏移纵向按层数乘以每层高度再加随机偏移。路线生成方面先获取当前房间最近的上层房间并存入上层对象列表再通过断路检索处理下层无对象的情况最后完成当前层与上层对象的链接。压缩包共144个文件以meta、png、asset、json、cs等为主涵盖场景资源、配置数据与核心脚本整体约10.3MB。目前已有1081人学习适合作为地图生成算法的实践参考。1. 从一张随机地图说起这套 Unity 算法 Demo 到底解决什么问题做过卡牌构筑类项目的人多半遇到过同一个尴尬战斗系统调通了卡牌逻辑跑顺了结果一进地图界面玩家看到的是一张每次重开都长得差不多、分支毫无意义的树状图。问题不在美术而在生成算法——节点怎么分层、路径怎么分叉、Boss 和宝箱该放在哪一层这些如果靠手写死数据项目稍微大一点就维护不动。这套 Unity 杀戮尖塔地图算法实现 Demo 要解决的正是这件事用一套可复现的随机地图生成逻辑把「分层、分叉、节点类型分配、路径连通性校验」全部交给算法处理让每次开局的地图既随机又保证可通关。它适合正在做卡牌 Roguelike 毕设或课设的同学也适合想把地图模块从硬编码里解放出来的独立开发者。下面按需求、设计、实现、避坑、验收的顺序把整套方案拆开讲清楚。2. 需求拆解与技术选型为什么用分层有向图而不是纯随机2.1 地图要满足的四条硬性约束在动手写代码之前先把「一张合格的地图」翻译成可验证的约束否则后面调参全靠感觉。这个 Demo 里我一般会锁定四条第一分层结构固定。地图纵向分成若干层通常 15 层左右每层横向排布若干节点玩家从底层入口出发逐层向上推进最终到达顶层的 Boss 节点。层数固定意味着游戏节奏可控不会出现「运气好三层就通关」的情况。第二路径必须连通且可通关。任意一个入口节点出发都要存在至少一条通往 Boss 的完整路径。这是随机生成最容易翻车的地方——随机连边很容易生成孤岛节点或者死胡同。第三节点类型按权重分配。战斗、精英、商店、宝箱、休息点、事件这几类节点在不同层出现的概率不一样。比如前几层不该刷精英休息点通常安排在 Boss 前一层这些规则要能配置而不是写死。第四同层节点不交叉连线。视觉上连线如果互相穿插玩家读图会很累。所以生成时要保证同层节点按横坐标排序后连边不产生交叉。把这四条写下来其实地图问题就变成了一个「带约束的分层有向无环图生成」问题。这也是为什么我不建议用纯随机——纯随机的边连接几乎必然违反第二、第四条。2.2 为什么选分层有向图 权重采样技术选型上核心数据结构用分层有向无环图DAG。每一层是一个节点列表边只从第 i 层指向第 i1 层天然无环也天然满足「逐层推进」的玩法。相比网格地图或纯树结构DAG 的好处是允许「多条路径汇聚到同一个节点」玩家能感受到路线选择的分量而不是走一条独木桥。节点类型分配用加权随机采样。给每类节点配一个基础权重再叠加「层数修正」——比如精英节点在第 3 层以下权重为 0第 4 层开始逐步升高。这样规则集中在一个配置表里策划改数值不用碰生成逻辑。连通性校验用反向可达性检查从 Boss 节点反向 BFS标记所有能到达 Boss 的节点凡是没被标记的节点直接剔除或重新连边。这一步是保证「可通关」的关键很多 Demo 生成的图看着漂亮实际跑起来有死路就是漏了这步。提示选型阶段别急着写生成器先把上面四条约束写成断言函数生成完立刻校验能省掉大量肉眼排查的时间。3. 总体架构设计三个模块与一次生成流程3.1 模块划分与职责边界整套方案拆成三个模块职责尽量单一方便单独测试模块职责输入输出MapGenerator生成分层节点与连边层数、每层节点数范围、随机种子节点列表 边列表NodeTypeAssigner按权重分配节点类型节点列表、权重配置表带类型的节点列表MapValidator校验连通性与约束完整地图数据校验结果 修正后的地图这样拆的好处是生成逻辑和类型分配解耦你可以先用固定类型调试连通性再单独调权重校验器独立出来还能当单元测试用。我见过不少项目把这三件事揉在一个几百行的 Generate() 里改一个参数就牵一发动全身血泪经验。3.2 一次完整生成的执行顺序生成流程按固定顺序走每一步的产物都是下一步的输入初始化随机种子可指定方便复现 bug。逐层创建节点每层节点数在配置范围内随机。连接相邻层为每个节点随机选 1~2 个上层节点连边。反向可达性校验剔除不可达节点并补边。分配节点类型此时结构已稳定。同层连线交叉检测必要时调整连边。输出地图数据交给渲染层。顺序不能乱。比如类型分配必须放在连通性校验之后否则你辛苦分配的 Boss 节点可能因为不可达被剔除白忙一场。3.3 数据结构定义节点和地图的数据结构直接决定后面代码好不好写。用 ScriptableObject 存配置用普通类存运行时数据// 单个地图节点 [System.Serializable] public class MapNode { public int id; // 唯一标识 public int layer; // 所在层从 0 开始 public int indexInLayer; // 层内序号用于排序和交叉检测 public NodeType type; // 节点类型 public Listint nextNodes; // 指向上一层节点的 id 列表 } // 节点类型枚举 public enum NodeType { Battle, // 普通战斗 Elite, // 精英 Shop, // 商店 Treasure, // 宝箱 Rest, // 休息点 Event, // 随机事件 Boss // 首领 } // 整张地图 public class MapData { public int seed; public ListMapNode nodes new ListMapNode(); public int layerCount; }indexInLayer这个字段容易被忽略但交叉检测和渲染排序都要靠它建议生成时就按横坐标顺序赋值后面省事。nextNodes存的是 id 而不是引用方便序列化和存档。4. 核心算法实现分层生成、连边与类型分配4.1 分层节点生成与连边策略先按层创建节点再连边。连边的核心思路是对第 i 层的每个节点从第 i1 层里挑 1~2 个节点连过去同时保证第 i1 层每个节点至少被连一次避免出现孤立节点。// 生成相邻层之间的连边 void ConnectLayers(ListMapNode lower, ListMapNode upper, System.Random rng) { // 第一步保证上层每个节点至少有一条入边 foreach (var up in upper) { var from lower[rng.Next(lower.Count)]; from.nextNodes.Add(up.id); } // 第二步给下层节点补充额外出边制造分叉 foreach (var low in lower) { int extra rng.Next(0, 2); // 0 或 1 条额外边 for (int i 0; i extra; i) { var target upper[rng.Next(upper.Count)]; if (!low.nextNodes.Contains(target.id)) low.nextNodes.Add(target.id); } } }第一步保证「上层不孤立」第二步保证「下层有分叉」。两步顺序不能反——如果先加额外边再补孤立节点可能出现重复连边。extra的取值范围控制地图的「茂密程度」想要路线选择更多就调大上限但别超过上层节点数否则连线会糊成一团。4.2 反向可达性校验与死路修复连边完成后从 Boss 反向 BFS标记所有能到达 Boss 的节点。没被标记的节点就是死路要么剔除要么补一条边把它接回可达区域。// 反向可达性校验返回可达节点 id 集合 HashSetint FindReachableToBoss(MapData map, int bossId) { // 先建反向邻接表 var reverseAdj new Dictionaryint, Listint(); foreach (var node in map.nodes) { foreach (var next in node.nextNodes) { if (!reverseAdj.ContainsKey(next)) reverseAdj[next] new Listint(); reverseAdj[next].Add(node.id); } } // 从 Boss 反向 BFS var reachable new HashSetint { bossId }; var queue new Queueint(); queue.Enqueue(bossId); while (queue.Count 0) { int cur queue.Dequeue(); if (!reverseAdj.ContainsKey(cur)) continue; foreach (var prev in reverseAdj[cur]) { if (reachable.Add(prev)) queue.Enqueue(prev); } } return reachable; }拿到reachable集合后遍历所有节点凡是不在集合里的从它的下层邻居里找一条边接过去。如果实在接不上比如整层都不可达就把这一层的节点整体剔除并重新连边。这一步是保证「可通关」的最后防线务必在类型分配之前跑完。4.3 节点类型加权分配类型分配用「基础权重 层数修正」的方式。配置表可以放在 ScriptableObject 里运行时读成字典。// 根据层数和权重配置决定节点类型 NodeType PickType(int layer, int totalLayers, System.Random rng) { // 每类节点的权重key 为类型value 为基础权重 var weights new DictionaryNodeType, float { { NodeType.Battle, 50f }, { NodeType.Event, 20f }, { NodeType.Shop, 10f }, { NodeType.Treasure, 10f }, { NodeType.Rest, 8f }, { NodeType.Elite, 2f }, }; // 层数修正前 3 层不出精英Boss 前一层强制休息点 if (layer 3) weights[NodeType.Elite] 0f; if (layer totalLayers - 2) return NodeType.Rest; // 加权随机 float total 0f; foreach (var w in weights.Values) total w; float roll (float)(rng.NextDouble() * total); float acc 0f; foreach (var kv in weights) { acc kv.Value; if (roll acc) return kv.Key; } return NodeType.Battle; // 兜底 }层数修正这段是整套逻辑里最值得花时间调的部分。前几层压掉精英是为了让新手期别被劝退Boss 前一层强制休息点是给玩家一个回血窗口这个设计在同类游戏里几乎是标配。权重数值没有标准答案建议先跑一百次生成统计各类型占比再微调。4.4 同层连线交叉检测交叉检测的判断依据是两条边 (a1→b1) 和 (a2→b2)如果 a1 在 a2 左边但 b1 在 b2 右边就产生了交叉。检测到交叉后交换其中一条边的目标节点即可消除。// 检测并修复同层连边交叉 void FixCrossing(ListMapNode lower, ListMapNode upper) { for (int i 0; i lower.Count; i) { for (int j i 1; j lower.Count; j) { foreach (var ti in lower[i].nextNodes) { foreach (var tj in lower[j].nextNodes) { int idxI upper.FindIndex(n n.id ti); int idxJ upper.FindIndex(n n.id tj); // i 在 j 左边但目标反了说明交叉 if (idxI idxJ) { // 交换目标消除交叉 lower[i].nextNodes.Remove(ti); lower[i].nextNodes.Add(tj); lower[j].nextNodes.Remove(tj); lower[j].nextNodes.Add(ti); return; // 一次修一处递归调用直到无交叉 } } } } } }这段是 O(n²) 的朴素实现节点数不多时够用。如果层内节点超过 20 个建议换成基于排序的扫描线算法。注意每次只修一处然后重新检测避免一次改太多导致新的交叉。5. 避坑与常见问题排查生成器最容易翻车的五个点5.1 地图出现无法到达 Boss 的死路现象玩家走到某个节点后发现上层没有任何可连的节点卡死。原因连边阶段只保证了「上层不孤立」没保证「下层每个节点都有出边」。某个下层节点可能一条出边都没分到。解决在连边第二步里先给每个下层节点强制分配至少一条出边再考虑额外分叉。或者干脆在可达性校验后统一补边。5.2 每次生成结果完全一样现象反复点「新游戏」地图长得一模一样。原因用了UnityEngine.Random但没在生成前重新播种或者用了固定种子却没把种子存下来。解决生成入口处显式rng new System.Random(seed)种子要么随机生成、要么从存档读取。用System.Random而不是UnityEngine.Random是因为前者可以实例化多个互不干扰的随机源方便复现。5.3 精英节点刷在开局第一层现象玩家刚进游戏就撞上精英直接被劝退。原因类型分配时忘了做层数修正或者修正条件写成了layer 3而不是layer 3。解决把层数修正规则单独抽成一个函数写单元测试覆盖边界层第 0 层、第 1 层、倒数第二层。5.4 连线视觉上糊成一团现象地图渲染出来线条互相穿插玩家看不清路径。原因交叉检测没跑或者跑了但只修了一处就退出。解决交叉检测要循环执行直到无交叉为止别用return提前退出。另外控制每层节点数上限节点太多再怎么修也乱。5.5 修改权重后地图类型分布没变化现象调了配置表里的权重生成结果却没区别。原因权重配置被缓存了或者 ScriptableObject 改了没保存。解决确认配置在生成时实时读取别在 Awake 里读一次就存成静态变量。改完配置记得手动保存资源文件。注意这五个坑里1 和 4 是结构性问题必须在生成阶段解决2、3、5 是配置和随机源问题靠单元测试和日志就能定位。建议给生成器加一个「调试模式」把每层节点数、边数、类型分布打到控制台出问题一眼就能看出是哪一步。6. 验收与答辩怎么证明这套地图算法真的能用验收这套方案别只看「跑起来没报错」要拿数据说话。我一般会准备三组验收指标答辩时直接摆出来。第一组是结构正确性。跑 1000 次生成统计「存在死路的地图比例」和「存在交叉连线的地图比例」两个指标都应该是 0。这个用自动化脚本跑把 MapValidator 的校验结果汇总成表格验收项合格标准实测方法死路地图比例0%1000 次生成统计可达性校验失败次数交叉连线比例0%1000 次生成统计交叉检测触发次数Boss 可达率100%每次生成后从入口 BFS 验证层数一致性100%校验实际层数等于配置层数第二组是分布合理性。统计各类型节点的实际占比和配置权重的偏差控制在合理范围内。比如战斗节点配置权重 50%实测占比应该在 45%~55% 之间。偏差过大说明加权采样实现有问题或者层数修正把某类节点压得太狠。第三组是性能。单次生成耗时应该控制在毫秒级。如果生成一张地图要几百毫秒玩家点「新游戏」会明显卡顿。用System.Diagnostics.Stopwatch打点重点看可达性校验和交叉检测这两步——它们是最容易拖慢生成的地方。答辩时我建议现场演示「改一个权重参数重新生成看分布变化」这个流程。这比放一堆截图有说服力因为它证明整套逻辑是参数化、可配置的而不是写死的。评委或导师最想看到的就是「你能不能控制随机性」而不是「你能不能生成一张图」。从那以后我每次做随机生成类模块都强制先写校验器再写生成器先定义「什么算合格」再动手实现。这个习惯帮我省掉了无数次返工。希望这套拆解能帮到你把地图这块硬骨头啃下来。本文还有配套的精品资源点击获取
延伸阅读

更多相关文章

2026/10/16 10:03:27

需求管理实践指南:从文档驱动到软件产品待办列表管理化

很多团队一谈需求管理,第一反应就是写文档、建流程、开评审会。但真正跑过几个产品周期之后你会发现,团队卡住的地方往往不在文档写得够不够详细,而在需求被记录下来之后,从来没有一个可持续前进的推进机制。后来我们把整个需求管…

2026/10/16 10:03:27

Python列表从入门到避坑:第四章核心知识点与实战解析

1. 为什么第四章是Python新手的第一道分水岭1.1 从“看懂”到“写对”的认知跃迁很多人学Python卡在第四章,不是因为第四章有多难,而是因为前三章太顺了。前三章讲变量、字符串、数字、注释,基本上看一遍就能懂,照着敲一遍就能跑。…

2026/10/16 10:03:27

2026专科生AI论文写作工具TOP9:从选题到答辩全流程实战指南

每年三四月份,我的私信就会准时热闹起来。清一色是专科生毕业论文求助帖:选题选了一个月,开题报告改了四版,正文卡在第二章,查重率从32%降到29%就再也压不下去。说实话,我特别理解这种状态——专科生写论文…

2026/10/16 15:07:04

编程入门Day2:变量、循环与函数——新手必学的基础语法

直接扔给你一句“day2 基础语法”,听起来好像很轻松,但带过几批新人的同学心里都清楚:Day 1 装好环境、跑通 Hello World 时的兴奋感还在,Day 2 开始啃变量、数据类型、循环、函数这些硬骨头时,才是第一个真正的分水岭…

2026/10/16 15:07:04

从零构建Python爬虫工具箱:HTTP解析反爬工程化全解析

拿捏数据抓取:从零开始构建自己的Python爬虫工具箱爬虫这个词,很多人一听就觉得门槛高、容易被封、甚至带点灰色,但实际做过几个项目之后你会发现,它本质上就是一个“自动读取网页公开信息并整理成结构化数据”的程序。我最早接触…

2026/10/16 15:07:04

基因测序数据同步日志分析:关键字段与故障排查实战

在云平台上跑基因测序数据同步,平时最容易被忽视、一出问题又最让人抓狂的,就是日志分析。测序仪下机数据动辄TB级别,同步任务往往要跑几个小时甚至跨天,一旦任务失败或中断,盯着屏幕看“同步失败”四个字毫无意义——…

2026/10/16 15:07:04

中文分词实战:从词典匹配到二元语法与Viterbi算法

简介:本资源为南京邮电大学自然语言处理课程实验一的完整实验报告,面向修读NLP基础课程的高校学生及希望入门中文分词与句法分析的初学者。内容围绕词典分词与二元语法分词两大核心任务展开,涵盖HanLP工具的分词指令segment、句法分析指令par…

2026/10/16 15:07:04

macOS下VirtualBox桥接模式IP获取失败的根因与修复

1. 这个问题不是VirtualBox的Bug,而是macOS网络栈的“礼貌性沉默” 我第一次在某高校实验室帮学生调试跨平台开发环境时,就撞上了这个经典问题:VirtualBox虚拟机设成桥接模式,启动后 ifconfig 一查,只有 lo0 &…

2026/10/16 15:02:03

Qt手绘流程图导出可解析XML:轻量级矢量图形建模方案

简介:本资源是一套基于Qt框架的图形绘制与XML序列化完整实践项目,面向C/Qt初学者及GUI开发进阶者,解决自定义图形界面构建、可视化元素持久化存储与跨平台数据交换等核心问题。项目涵盖从基础QPainter绘图(矩形、椭圆、路径等&…

2026/10/16 5:33:43

Python调用Gemini Structured Outputs实现工单路由门禁

客服工单最怕的不是模型“答错一句话”,而是它给出一段看起来合理的说明,程序却从中猜错优先级。通俗做法是:要求模型只交 JSON(JavaScript Object Notation,轻量数据格式),再让代码验证它。Gem…

2026/10/16 4:03:48

Spring Boot超市进销存系统毕设实战:从需求拆解到答辩通关

最近带的一个学生项目组里,有A同学跑来问我:选什么毕设题目最稳妥,既能让评审老师觉得工作量够,又不会在答辩时被问到语无伦次。我第一反应就是推荐基于Spring Boot的超市仓库管理系统——也就是超市进销存系统。这个题目乍一看平…

2026/10/15 13:42:16

Flutter StatefulWidget 生命周期核心解析

很多刚开始接触 Flutter 的朋友,在看完一堆“Hello World”和基础组件之后,大概率都会撞上同一堵墙:StatefulWidget 里那堆 initState、build、dispose 方法,到底什么时候被调用?为什么顺序是那样?在里面到…

2026/10/16 0:01:11

Cephadm实战指南:Linux系统工程师的生产级Ceph集群部署与排障

简介:本资源是一份完整的Ceph分布式存储系统中文技术文档合集,面向Linux系统运维工程师、云平台架构师及存储方向开发者,帮助读者系统掌握Ceph集群的部署、配置与运维核心能力。文档覆盖对象存储(RGW)、块设备&#xf…

2026/10/16 0:01:11

ITK中SpatialObject几何体系解析:坐标系、变换与管道集成

1. 空间对象的定位:为什么 ITK 需要一套几何描述体系很多刚开始接触 ITK 的朋友,包括我自己,最初都是被它的图像滤波、配准、分割这些“正统”功能吸引进来的。等真正上手做点实际项目,比如把分割出来的器官边界做量化分析、给血管…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑