Unity棋类AI实战:从Minimax到Alpha-Beta剪枝与MCTS的搜索引擎

发布时间:2026/10/22 3:01:51
Unity棋类AI实战:从Minimax到Alpha-Beta剪枝与MCTS的搜索引擎 在 Unity 里做棋类游戏最让我上瘾的往往是那个“怎么都下不赢”的 AI 对手。智能搜索算法这个词听起来很唬人但说白了就是让电脑在落子之前把未来几步甚至几十步的变化都算一遍然后挑一个胜率最高的走法。这篇文章不是给你堆公式而是围绕五子棋这个最容易上手的棋种把极大极小搜索、Alpha-Beta 剪枝、蒙特卡洛树搜索这些算法在 Unity 里的落地过程完整拆一遍。适合那些已经把 Unity 基础操作摸熟、想让游戏 AI 水平更上一层楼的开发者也适合准备用棋类 AI 做毕业设计或课程项目的同学。1. 博弈树与算法选型做棋类 AI 之前先想清楚的问题1.1 为什么棋类 AI 的核心是搜索而不是“记住棋谱”很多第一次接触棋类 AI 的人会下意识觉得难点在于“知识”也就是让程序记住大量棋谱和定式。这个想法不能说错但在 Unity 的实时游戏里硬背棋谱的路子明显走不通。棋类对局的状态空间实在太大五子棋的棋盘 15x15理论上的局面数就足以让你怀疑人生。就算只记录常见开局后期的分支变化也完全覆盖不过来。真正的核心其实是搜索。棋类游戏天然可以建模成一棵博弈树当前棋局是根节点你落一个子就进入下一层对方应对一个子又进入下一层。电脑要做的就是从这棵巨大的树上找到一个最有利于自己的分支。搜索算法的本质就是在有限的时间和空间里尽可能找到最优的那个分支。换句话说棋类 AI 的水平上限取决于你“看”得够不够远、剪枝剪得够不够准而不是你背了多少棋谱。这也是我在做某跨平台棋类项目时最大的感悟一开始老想着给 AI 塞各种开局库结果效率极低。后来换成纯搜索框架加上一个还算靠谱的评估函数AI 的水平反而一下子上来了。所以做 Unity 棋类 AI第一件事就是接受“搜索为王”这个前提。1.2 Minimax、Alpha-Beta、MCTS 到底该选哪个先把最常见的三种算法摆在一起对比心里有个底再动手。算法核心思想适合棋种难点Minimax假设双方都下最优取最小最大收益井字棋、五子棋、国际象棋层数深了组合爆炸Alpha-Beta 剪枝在 Minimax 基础上忽略无用分支同上剪枝顺序影响效率MCTS用随机模拟统计胜率逐步精化搜索树围棋、不完美信息游戏模拟质量和收敛速度Minimax 是最朴素的思路我方要最大化自己的收益对方要最小化我们的收益。Alpha-Beta 剪枝则是在这个基础上通过记录上下界直接砍掉一部分不用看的分支让同样的计算量能多搜索几层。这两个算法有一个共同特点严重依赖评估函数。评估函数写得好不好基本决定了 AI 是“会下棋”还是“乱下棋”。MCTS 的思路完全不一样。它不追求把整棵博弈树算完而是每局对局都从当前节点开始不断做随机模拟然后统计每个走法的胜率。模拟的局数越多统计结果越可靠树也会慢慢往最有希望的分支长。对于评估函数很难写的围棋来说MCTS 这种“用算力换知识”的方式特别合适。如果你在 Unity 里做的是五子棋、黑白棋这类规则相对简单的棋类我建议从 Minimax 加 Alpha-Beta 剪枝入手因为实现直观、调试方便而且 AI 强度足够符合大多数休闲游戏的需求。如果做的是围棋或者想要一个更“通用”的 AI 引擎MCTS 是更好的基础。我自己做五子棋时先用 Alpha-Beta 撑起骨架后面为了对比效果又实现了 MCTS两套放在同一个项目里做 AB 测试才能直观感受到各自的天花板。2. 手写 Minimax 与 Alpha-Beta 剪枝五子棋案例实战2.1 数据结构棋盘、落子、胜负判定在 Unity 里写棋类搜索第一步不是算法而是把棋盘数据结构设计好。别小看这一步数据结构选对了后面评估函数、搜索展开都会很顺手。我习惯用二维数组int[,] board表示棋盘0 表示空位1 表示我方黑棋2 表示对方白棋。棋盘大小用一个常量控制比如五子棋用 15。搜索过程中为了不反复拷贝整个二维数组通常会用一个“落子栈”记录每次落子的位置搜索回溯时直接回退即可。这里有个经验如果你图省事在递归里用board.Clone()复制棋盘深度一到 6 层以上GC 压力会让 Unity 编辑器直接卡到让人抓狂。胜负判定不要贪图一次写全四方向连通检测是最稳的。每落一个子只需要以这个子为起点向水平、垂直、两个斜方向分别数连续同色棋子数一旦达到 5 就判胜。代码很简单但要注意边界判断尤其斜方向用数组偏移量时要小心索引越界。private bool CheckWin(int[,] board, int row, int col, int player) { int[] dr { 0, 1, 1, 1 }; int[] dc { 1, 0, 1, -1 }; for (int i 0; i 4; i) { int count 1; for (int step 1; step 5; step) { int nr row dr[i] * step; int nc col dc[i] * step; if (nr 0 || nr size || nc 0 || nc size) break; if (board[nr, nc] ! player) break; count; } for (int step 1; step 5; step) { int nr row - dr[i] * step; int nc col - dc[i] * step; if (nr 0 || nr size || nc 0 || nc size) break; if (board[nr, nc] ! player) break; count; } if (count 5) return true; } return false; }这段代码里我用了反向偏移量做双向延伸属于常见实践。建议把它封装成纯 C# 静态方法不要直接放在 MonoBehaviour 里方便后续在单元测试里调用。2.2 评估函数怎么设计才靠谱评估函数是整个搜索算法的“品味担当”。如果评估函数只会数剩余棋子数AI 就是个憨憨。五子棋里通常用“棋型打分”的方式把棋子按连续长度、堵口情况拆成活四、冲四、活三、眠三、活二等棋型每种棋型给一个分数然后累加得到局面分数。这里必须提醒一个新手常踩的坑评估函数不是统计己方棋型分减去对方棋型分就完事了。如果只算当前棋盘上的静态棋型搜索出来的 AI 会显得很“近视”因为它看不到下一步的威胁。更合理的方式是把“我方进攻分”和“对方防守分”同时算出来并且给进攻分和防守分设置不同权重。实战里我一般用 1.2 的进攻系数让 AI 稍微偏向主动进攻否则它会经常变成一个只防守的沙袋。一个最简可用的棋型打分的思路是扫描每个空位周围的八个方向尝试把该空位假设为某方落子后的连子情况再返回价值。这么做虽然粗略但和 Alpha-Beta 配合起来已经能做出像样的 AI。如果想进一步精细化可以考虑把每一条线上的连续同色棋子形成的模式直接编码成整数查表返回分数。查表法的优点是速度快、可调性强缺点是前期特征工程麻烦。如果追求的是“让 AI 有个性”评估函数里还可以加入位置权重比如五子棋棋盘中央位置权重略高因为中心区域的棋型发展空间更大。不过权重不宜设太大否则 AI 会忽略局势只往中间挤。我一般会把位置权重控制在总分的 5% 以内只做引导不做主导。2.3 Alpha-Beta 剪枝原理与核心代码Minimax 的原始写法很直观每一层要么取最大值要么取最小值。但直接这么写搜索深度一旦到 6 层节点的数量就是指数级增长通常几十秒都出不了结果。Alpha-Beta 剪枝的原理并不复杂既然我们知道了当前已经搜出来的最好结果那就可以给搜索设定一个上下界一旦发现某个分支不可能比已有结果更好就直接停止搜索这个分支。Alpha 代表当前我方能保证的最小收益Beta 代表对方能接受的最大损失。随着搜索深入这个区间不断收窄最终大量分支被剪掉。剪枝的效果和节点排序非常相关如果每次都能先搜到最好的走法剪枝效率惊人甚至可以比原始 Minimax 多搜索一倍深度。因此在展开子节点之前对候选落子点做一个粗略排序是很值的事情。下面是一个经典的负极大值版本代码量比正负两个函数来回调用的版本少很多逻辑也更清晰。负极大值玩了一个技巧当前节点站在当前玩家视角评估递归时对子节点取负值这样极大和极小就统一成“尽量取最大值”了。private int NegaMax(int[,] board, int depth, int alpha, int beta, int currentPlayer) { int winner CheckWinner(board); if (winner currentPlayer) return 9999 depth; if (winner Opponent(currentPlayer)) return -9999 - depth; if (depth 0) return Evaluate(board); ListVector2Int moves GenerateMoves(board, currentPlayer); SortMoves(board, moves, currentPlayer); int best int.MinValue; foreach (var move in moves) { board[move.x, move.y] currentPlayer; int val -NegaMax(board, depth - 1, -beta, -alpha, Opponent(currentPlayer)); board[move.x, move.y] Empty; if (val best) best val; if (val alpha) alpha val; if (alpha beta) break; } return best; }注意递归层里alpha和beta的交换写法这是负极大值和普通 Minimax 最大的不同。很多刚上手的人容易在这里把边界写错导致 AI 返回的走法偶尔非常蠢。我的经验是用一个 3x3 井字棋场景先验证剪枝逻辑再挪到五子棋棋盘上定位问题会容易得多。2.4 搜索深度、时间预算与用户体验搜索深度不是越大越好Unity 里每一帧能留给 AI 的时间非常有限。如果你的游戏是回合制在玩家落子后等待几百毫秒是没问题的。但如果是实时对战或带有动画播报的流程AI 思考时间过长会让玩家以为游戏卡死了。通常我会给 AI 设置一个思考时间上限比如 300 毫秒。搜索过程中先按固定深度去搜如果时间到了还没搜完就使用当前已经确认的最佳走法。实现方式是在递归里不断检查stopwatch.ElapsedMilliseconds超时则抛出一个专门的信号量中断搜索。注意在递归里中断需要小心处理棋盘状态回退否则会导致后续落子错乱。另一个实用的做法是加深迭代先从深度 2 开始搜搜完保存结果如果时间允许继续搜深度 4然后再用新结果替换旧结果。这样即使时间被截断手里也永远有一个“上一步的最佳走法”。这个思路很像游戏渲染里的渐进式加载用户体验会好很多。我在某五子棋 Demo 中把深度从 4 调到 6再配合 300ms 的思考预算玩家普遍反馈“电脑变聪明了但又不会让人等得烦躁”。3. 用蒙特卡洛树搜索增强棋类 AI3.1 MCTS 四个阶段详解Alpha-Beta 剪枝虽然效率不错但一旦需要搜索 10 层以上或者评估函数写不清楚它就有点力不从心。MCTS 走的是另一条路它通过反复模拟完整对局用胜率来决定哪个走法更值得深挖。MCTS 的每轮迭代都由四个阶段组成选择、扩展、模拟、回溯。选择阶段从根节点出发沿着孩子节点不断往下走。选择的标准一般用 UCB1 公式既要考虑这个孩子节点目前的平均胜率高不高也要考虑这个节点被访问的次数够不够多。这样能避免 AI 只盯着当前看起来最好的走法忽略了那些还没被充分探索但可能更好的分支。扩展阶段是在选中的节点上创建新的子节点。大多数实现里每个节点对应一个“落子操作”。到了扩展这一步我们就把这个落子加入搜索树作为新的局面节点。模拟阶段则是从扩展出来的节点开始用随机策略把棋下到终局或者达到一个最大步数上限然后统计胜负。回溯阶段把模拟得到的胜负结果从新节点一直向上累加到根节点更新每个节点的访问次数和胜利次数。这四个阶段听起来多但写成代码其实核心数据结构就是一个树节点类加一个循环。关键在于模拟阶段不要每次都真的用完整规则模拟到终局那样太慢了。常用做法是设定一个模拟步数上限比如 30 步如果还没分胜负就调用一个轻量评估函数给分。这个折中方案在实际项目里非常有效既保留了 MCTS 的统计特性又避免了随机模拟时间过长。3.2 UCB1/UCT 公式与参数UCB1 公式是 MCTS 选择阶段的灵魂。它长这样UCB1 winRate C * sqrt(log(parentVisits) / visits)其中winRate是当前节点胜率visits是当前节点访问次数parentVisits是父节点的访问次数C是探索常数。这个公式第一项鼓励 AI 选择胜率高的节点第二项鼓励选择访问次数少的节点是一种典型的“利用与探索”权衡。C的取值直接影响 AI 风格。C 设得太大AI 会特别爱试各种没试过的走法导致收敛慢C 设得太小AI 会过早锁定一个局部最优的走法错过更好的选择。我的实际经验是 C 取 1.4 到 2.0 之间比较合适。如果你做的是休闲棋类游戏不需要 AI 有超高棋力把 C 调小一点可以让它显得更“果断”。反过来需要 AI 去破解某种固定套路时调大 C 反而有效。sqrt(log(parentVisits) / visits)这部分的衰减速度比1 / visits慢得多所以就算某个节点已经被访问了很多次探索项仍然会缓慢递减。这个设计很巧妙它保证了 MCTS 在理论上能无限持续提升搜索质量而不会像贪心算法一样早早停滞。3.3 在 Unity 协程中实现 MCTS避免主线程卡死MCTS 最大的问题在于它的迭代次数通常很大想要效果好几千次模拟是常事。如果把这些模拟全部塞在Update里一次性跑完Unity 编辑器铁定卡到转圈圈。解决方案有很多我比较推荐协程。协程的思路是把 MCTS 的每一次迭代拆成一个时间片每次执行一定数量的迭代后yield return null把控制权交还给主线程下一帧继续。这样玩家能明显感觉到 AI 在“思考”而不是整个游戏冻结了。代码结构大致这样IEnumerator RunMCTS() { int iterationsPerFrame 200; for (int i 0; i totalIterations; i) { mctsRoot new TreeNode(currentBoard, currentPlayer); for (int j 0; j iterationsPerFrame; j) { mctsRoot.RunIteration(board, player); } yield return null; } OnMCTSFinished(mctsRoot.GetBestMove()); }这里有个需要注意的点TreeNode里不能持有棋盘引用否则迭代过程中棋盘状态会乱。正确做法是每次RunIteration里使用棋盘对象的一份快照或者在还原状态后继续。为了性能快照方式可以用“链表式落子记录 撤销”来实现比一次性Clone快得多。迭代次数和每帧迭代数的配比需要根据目标平台的性能来调。我在编辑器里用每帧 200 次迭代总 5000 次玩家体感是大约 1 秒出结果。如果放到低端手机上这个量要打折到 1000 次左右。强烈建议把这个数值做成可调参数方便做不同平台性能适配。3.4 随机模拟 vs 轻量评估MCTS 的模拟阶段如果完全随机落子AI 的决策会很震荡。尤其是五子棋这种需要细腻棋型的游戏5 步之内随机模拟基本看不出局势好坏。所以在模拟阶段我做了一个折中先用一个很简单的贪心函数计算每个候选点的基础棋型价值然后按概率比例随机选择。这样既保留了随机性又不至于让模拟结果过于离谱。如果你是做棋盘复杂度较高的棋种比如黑白棋纯随机模拟的噪声还会更大。此时更聪明的做法是让模拟阶段不过度追求“终局胜负”而是跑到固定深度后直接调用评估函数。这么做本质上是把“纯统计”的 MCTS 变成了“半统计半启发式”的混合算法。混合方案通常比纯 MCTS 在相同迭代次数下表现更好代价是实现复杂度稍高。还需要小心一件事如果评估函数过于倾向某一方模拟结果会让 MCTS 的胜率统计失去参考价值。我一般先把评估函数归一化到 0 到 1 之间再把它折算成模拟阶段的胜负输入。这一步细节容易被忽略但恰恰是让 MCTS 稳定的关键。4. Unity 集成中的工程化细节4.1 MonoBehaviour vs 独立纯 C# 类很多初学者习惯把 AI 搜索逻辑直接写在 MonoBehaviour 里因为这样访问 Transform、Input 都很方便。但棋类搜索算法往往是纯逻辑模块放在 MonoBehaviour 里会带来两个问题一是生命周期和场景绑定过强场景卸载时容易丢状态二是难以做单元测试和性能基准测试。我的习惯是把搜索算法封装成独立的纯 C# 类比如BoardEngine、MinimaxSearcher、MCTSSearcher不继承 MonoBehaviour。MonoBehaviour 只保留一个AIController来负责调度它通过公有方法调用纯 C# 类的接口再把结果解析成坐标变化应用到棋盘对象上。这样的解耦让整个项目清晰很多也方便我在多个项目之间复用同一套 AI 代码。做独立类还有一个好处可以利用 C# 的异步和 Task甚至借助 Unity 的 Job System 跑并行搜索而不会被生命周期问题绊住脚。如果你打算做多线程搜索这一点会非常重要。4.2 多线程与 Job System 的取舍协程虽然能防止卡死但并没有真正利用多核 CPU。MCTS 这种大量随机模拟的任务天生适合并行化。Unity 的 Job System 配合 Burst Compiler 是首选方案但说实话写起来比普通 C# 要绕不少因为原生容器不支持引用类型树节点。我做过的折中方案是“并行根节点搜索”假设当前有 10 个候选走法就把 10 个候选走法分别作为独立根开 10 个线程并行跑各自的 MCTS 模拟最后在主线程比较各个根节点的胜率。这种做法天然适合 C# 的Task.Run因为各个根节点之间完全隔离不需要加锁。唯一的坑是线程里不能访问 UnityEngine 的 API所以棋盘数据必须用结构体或数组传进去。如果你不想引入多线程用协程已经够了。多线程带来的棋力提升在休闲游戏里未必能感知到但复杂度提升是实打实的。我的建议是项目先上协程版本等瓶颈确实出现在搜索速度上再考虑多线程优化。4.3 动态难度搜索深度调节与思考时间棋类游戏不能只有一个固定水平。玩家刚上手时需要 AI 犯错玩熟了又希望 AI 发狠。动态难度一般通过三个旋钮控制搜索深度、MCTS 迭代次数、评估函数中的人为随机误差。太简单的方法是随机让 AI 在 20% 概率下故意走一个次优解这个方法做起来快但玩家很快会识破感觉 AI 在人工放水。更好的方法是对候选走法分数添加一个噪声项。噪声大小随难度变化难度低时噪声大AI 经常“看不准”哪个走法更好从而表现出人类会犯的错误。这个方案不会让 AI 突然走出特别弱智的废棋但又不是每一手都完美。另一个实用做法是根据玩家连胜或当前关卡动态调整思考时间。AI 思考时间短搜索层数就低棋力自然下降。把这个参数放在游戏配置里通过调难度接口来改能够非常平滑地控制 AI 强度。4.4 可视化调试搜索路径、评估分数与热度图调试棋类 AI 最痛苦的地方在于看不见“它怎么想的”。你只知道 AI 走了一步棋但不知道它为什么这么走。后来我花了不少时间做可视化调试发现这比调任何参数都效率都高。最简单的调试 UI 是在每个候选落子点旁边显示它的评估分数。我通过 Gizmos 在 Scene 视图里绘制分数文本能直接看出来 AI 是否因为某个棋型权重不对而高估了一个很差的点。另一个很有用的工具是搜索热度图在 MCTS 迭代结束后把每个节点的访问次数映射为红蓝颜色深度绘制到棋盘上。访问次数高的区域就是 AI 认为争夺最激烈的区域一眼就能看懂它的关注点。可视化这块最好做成独立的 Debug 开关只在编辑器下启用。发布版本里可以剔除避免不必要的性能开销。调试功能虽然不直接影响游戏体验但对于持续调优 AI 棋力来说价值巨大。5. 常见问题与排查技巧实录5.1 AI 总走废棋先怀疑评估函数如果 Alpha-Beta 搜索深度已经到 6 层AI 仍然下出明显不合理的棋问题大概率不在搜索逻辑而在评估函数。一个典型症状是AI 在一端堵住对手活四以后突然跑到棋盘角落下了一手无关紧要的闲棋。这通常是因为评估函数对威胁棋型的价值没有加权或者对方棋型只计算了当前连子长度没有计算“还差一子就能赢”的严重性。排查方法很简单把当前局面下所有候选点的评估分数打出来看看最高分的位置是否合理。如果分数都偏高说明评估函数的量纲可能有问题。我遇过一个项目评估函数里把活三打成了 1000 分冲四却只有 500 分结果 AI 放着快赢的冲四不走老是去造活三。单独看每个分数都觉得合理一对比才发现冲四分数严重偏低。5.2 搜索太慢导致一帧卡顿另一个高发问题是玩家落子后主线程明显卡了一小会儿。原因多半是搜索逻辑没做时间分片或者搜索深度设得过高。解决办法参考前面提到的迭代加深和思考时间上限。如果时间上限已经到了还是卡就要检查是不是候选走法生成太慢。候选走法生成是很容易被忽略的性能瓶颈。满盘 225 个点全部生成的话每层都是 225 次评估压力非常大。优化方面只生成“已有棋子周围 2 格范围内”的空位即可。这个优化能把搜索空间缩小一个数量级棋力损失却很小是我强烈推荐的基础优化。5.3 Alpha-Beta 剪枝后结果不稳定剪枝输出结果偶尔和没剪枝版本不一致这种问题几乎都出在alpha和beta参数传递或者走法排序的不稳定性上。一个隐蔽的错误是在负极大值递归里把-beta和-alpha写反了导致某些分支被错误剪掉。另有一个情况是走法排序时依赖的评估分数和搜索内使用的评估函数不一致导致排序结果在不同搜索阶段抖动。处理这种问题最好用小型棋盘做回归测试。我用井字棋构造几个已知局面对照如果剪枝版本和非剪枝版本选出的走法不一致说明边界条件有误。优先检查递归返回值的取负逻辑再检查alpha beta中断条件。5.4 MCTS 越到后期越慢胜率统计失真MCTS 常见的问题是随着搜索树变大选择阶段的深度越来越深导致单次迭代的平均耗时变长。加上模拟阶段用到了复杂评估函数几千次迭代之后耗时暴涨。规避思路是限制树的最大深度或者模拟步数上限超过之后直接返回评估分不再继续展开子节点。胜率统计失真通常和模拟策略的偏向性有关。如果随机模拟采用“偏向进攻”的贪心策略黑棋先手优势会被过度放大。解决方法是让模拟策略对双方保持对称或者干脆用纯随机 固定步数后再评估的策略。这样做统计更干净AI 决策也更稳定。5.5 常见问题速查表问题现象可能原因排查方向解决建议AI 下无关闲棋评估函数未覆盖威胁棋型打印候选点评估分增加冲四、活三的高权重主线程卡顿搜索未分帧检查思考时间上限使用协程分片或迭代加深剪枝后结果不同alpha/beta 传参错误用井字棋回归测试检查负极大值取负逻辑走法排序抖动排序评分与评估不一致对比两处评分函数统一评估函数入口MCTS 越跑越慢树深度无限制查看节点访问深度设置模拟步数上限最后再分享一个我自己的习惯每次调整评估函数参数之后都让 AI 和上一个版本自对弈几十局把胜率记录下来。棋类 AI 调优最怕“感觉变强了”的错觉只有用数据对比说话才能确认改动到底有没有用。搜索算法这块坑不少但当你看到 AI 终于会主动做双杀陷阱时那种成就感绝对值得你折腾一整晚。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询