☰
Unity五子棋人机对战:用打分法实现AI决策与完整代码解析
2026/10/17 10:53:36 网站建设 项目流程

说实话,Unity里写一个五子棋Demo,难倒绝大多数人的,不是棋盘绘制,不是点击交互,而是AI那一步该怎么想。很多人一上来就奔着Alpha-Beta剪枝去,结果写了一整天,AI还是又蠢又慢,最后连个能看的局面评估都没调出来。我这边的做法是先不去碰那些听起来很高级的搜索树,而是用一个足够直观、同样也足够耐打的“进攻/防守打分法”把五子棋人机对战在Unity里跑通,并且把整套程序代码整理成了可直接参考的版本。这篇就来拆一拆这个unity人机五子棋项目,从模块划分到核心算法,再到那些真正会卡住你的坑,顺带把可用的程序代码逻辑一并讲清楚。

1. 五子棋项目的第一刀:把需求拆成棋盘、规则、AI三个独立模块

五子棋看起来就是个“点击落子”的小游戏,但如果你直接把规则、UI、AI混在同一个脚本里写,后面调试绝对会痛不欲生。我在这边整理这个项目时的第一刀,就是把三个模块的边界先画清楚。

1.1 棋盘数据结构的选择:别用物体去找位置,用数组做唯一权威

Unity里通常有两种方式做五子棋棋盘:一种是直接在场景里摆225个棋子物体,用Physics或UI射线去判断点击了哪个;另一种是先维护一份int[15, 15]的二维数组,UI只负责“呈现”和“回传坐标”。

我强烈建议选第二种,而且这个是后续所有AI逻辑能顺利落地的前提。原因很简单:不管是胜负判定、AI打分还是悔棋回滚,它们本质上都是在对“棋盘状态”做计算。如果状态分散在一堆GameObject上,每次计算都要去遍历场景,效率低不说,还极易出现“视觉上有个棋子,逻辑里没有记录”这类幽灵Bug。

通常我建一个GameBoard类来统一管理状态,里面就四样东西:

public class GameBoard { public const int BoardSize = 15; public int[,] grid = new int[BoardSize, BoardSize]; public int currentPlayer = 1; // 1=黑,2=白 public int moveCount = 0; public bool IsEmpty(int x, int y) { if (x < 0 || x >= BoardSize || y < 0 || y >= BoardSize) return false; return grid[x, y] == 0; } public bool Place(int x, int y) { if (!IsEmpty(x, y)) return false; grid[x, y] = currentPlayer; moveCount++; return true; } }

注意,这里棋盘下标我统一用x, y而不是row, col,因为我希望UI层坐标和AI逻辑坐标完全一致,避免转换混乱。示例中currentPlayer第一次默认为黑棋,也就是玩家先手,AI持白后手,这是最常见的人机五子棋设定。

1.2 UI层的正确姿势:GridLayoutGroup批量生成格子

不要手动拖225个Image进场景,那是纯体力活。用Unity UGUI的GridLayoutGroup,配合一个循环生成子物体,不到三十行代码就能完成棋盘渲染。

我在这边的实现习惯是:每个格子一个Button组件,点击时通过Button.onClick绑同一个监听方法,再把格子的行列坐标存到按钮自身的transform上或者用一个Cell脚本记录。关键点在于,棋局逻辑完全不在Button里,而是统一转发到GameManager处理。

private void BuildBoard() { for (int x = 0; x < GameBoard.BoardSize; x++) { for (int y = 0; y < GameBoard.BoardSize; y++) { GameObject cell = Instantiate(cellPrefab, gridContainer); int row = x, col = y; // 闭包陷阱,必须用局部变量 cell.GetComponent<Button>().onClick.AddListener(() => OnCellClick(row, col)); } } }

这里有个大多数新手都会踩的坑:在循环里给事件添加监听时,lambda表达式捕获的循环变量会变成同一个值。所以在Unity 5.0以后都强制要求先复制一份局部变量。上面代码里的row、col就是干这个事的,我这边写过太多次旧式写法导致所有格子点击后都落到最后一个格子,所以这个细节一定要记住。

2. AI方案选型:为什么先做打分法,而不是一上来就上搜索树

关于五子棋AI,网上你一搜,清一色就是Min-Max加Alpha-Beta剪枝,有的还挂上蒙特卡洛树。这些方法不是不好,而是对一个从零起步的项目来说,它们把“AI智能程度”和“算法复杂度”捆绑得太紧,调试成本太高。我在整理这个unity人机五子棋项目时,先把核心AI实现为“打分法”,然后用一个开关可以切换体验。

2.1 搜索树的代价:你先得有一个能用的评估函数

很多人没想明白一件事:就算你写了Alpha-Beta剪枝,剪枝本身不会下棋,真正决定棋力的是叶子节点上那个“评估函数”。如果你的评估函数很粗糙,搜索再深,AI也只是在一个错误的方向上看得更远而已。

所以AI设计的第一步,永远是先把“评估函数”做对,也就是“这个位置如果落子,有多好”这个分数。打分法本质上就是一种深度为1的搜索:只评估当前这一步的收益,不做递归推演。由于五子棋是零和游戏,评估一个点只需要做两件事:

  1. 评估“如果我(AI)下在这里”的进攻得分。
  2. 评估“如果对手下在这里”的防守得分。

最终这个点的价值是两者加权和。这种方案在实现上很轻量,只需要对空位做一次扫描,不需要递归,运行速度极快,也足够让AI表现出“会堵、会冲、会组织双路”的基础棋感。

2.2 打分表设计:所有棋型都必须显式定义

打分法好不好用,核心在于棋型分值的定义。我在这个项目里直接用一张常量表,把所有常见棋型按威胁程度排好。

棋型分值
己方成五1000000
对方成五999999
活四500000
冲四30000
活三20000
眠三3000
活二2000
眠二300
单子20

注意,表格里同时包含己方和对方成五,是为了让AI在检测到“对方就差一步赢”时,能优先去堵截而不是继续做自己的棋型。五子棋里“防守优先”的思想不是靠复杂逻辑,而是靠这个分值排序实现的。

棋型识别算法我后面在代码部分会展开讲,这时候只需要记住一个原则:同一局面,用连续棋子数和两端开口情况去判定棋型,而不是只看数量。比如同样是三个子,活三和眠三的威胁天壤之别,这个区分如果在纸面上没做足,AI就会出现“看着像在下棋,实际在乱走”的观感。

2.3 进攻与防守的加权策略

打分法有个自由度很高的地方,就是在totalScore = attackScore + defendScore * ratio里,ratio的取值直接影响AI性格。

我这边测试下来,defenseRatio取2.0是刚起步时的相对稳妥值。也就是说,AI优先封堵对手的活三/冲四,其次才考虑自己搭棋型。取1.5会偏激进,AI自己的攻击欲望更高,但对对手的连续手段有时反应慢半拍;取3.0则过于保守,AI会一直在应对,缺少自己主动取胜的能力。

实践中常见的问题反而是AI“宁愿挡不住也要进攻”。因为你只做了“这步棋对我的进攻分 + 对对手的防守分”的评估,却没考虑“我不挡的话对手下一步会不会连成五”。于是我在打分法里额外做了个特判:如果某空位存在对手成五点,直接把这个点的比分拉满,保证AI绝对不会漏掉这个必救的位置。这个特判效率极高,相当于给AI装了一个“最短路”保险。

3. 核心程序代码解析:从落子到胜负判断再到AI决策

接下来是这篇unity人机五子棋文章最有价值的部分,程序代码的逻辑骨架。我会按数据流顺序,把“点击落子 → 胜负判断 → AI决策 → AI落子”一条线完整拆开讲。

3.1 落子与刷新:状态变化只有一条路

我特意设计了一个GameManager,所有操作在它下面统一处理。玩家点击某个格子后,先对棋盘状态做校验,再更新UI,如果未分胜负且轮到AI,就启动协程等一小段时间再让AI走棋。

public void OnCellClick(int x, int y) { if (isGameOver || isAiThinking) return; if (board.currentPlayer != 1) return; // 仅玩家回合 if (!board.Place(x, y)) return; Cell cell = cells[x, y]; cell.SetVisual(1); if (CheckWin(x, y, 1)) { EndGame(1); return; } board.currentPlayer = 2; isAiThinking = true; StartCoroutine(AIPlayRoutine()); }

这里的关键是CheckWin调用时机。初学者最容易犯的错误是:在落子完成后立刻判断AI或玩家胜利,结果总会出现“最后一步已经五连了,游戏却不结束”的诡异现象。正确的顺序一定是:落子 → 判断当前玩家是否获胜 → 若无胜者,则切换当前玩家。

UI和逻辑分离的好处这时候也体现出来了:棋盘数组里改变了状态,界面只做同步,任何逻辑代码都不需要依赖某个具体图片是否被设置,永远以数组为准。

3.2 胜负判断:用方向数组替代八方向枚举

四子棋、五子棋这类判断连续棋型,最容易判断错的就是方向遗漏。这里我用两个定长数组一次性覆盖四个直线方向:

private readonly int[] dx = { 1, 0, 1, -1 }; private readonly int[] dy = { 0, 1, 1, 1 }; public bool CheckWin(int x, int y, int player) { if (board.grid[x, y] != player) return false; for (int d = 0; d < 4; d++) { int count = 1; for (int step = 1; step < 5; step++) { int nx = x + dx[d] * step; int ny = y + dy[d] * step; if (!InBoard(nx, ny) || board.grid[nx, ny] != player) break; count++; } for (int step = 1; step < 5; step++) { int nx = x - dx[d] * step; int ny = y - dy[d] * step; if (!InBoard(nx, ny) || board.grid[nx, ny] != player) break; count++; } if (count >= 5) return true; } return false; }

dx, dy这组方向数组的含义,从左到右依次是:水平方向、垂直方向、主对角线方向、副对角线方向。从落子点向正负两个方向延伸统计同类棋子数量,只要总数达到5就算赢。不需要去判断“长连”之类的情况,因为普通五子棋规则下,超过5个同样算赢。

3.3 AI打分函数:扫描每个空位的进攻与防守价值

AI决策的核心在EvaluatePoint,这个函数返回某个位置对指定玩家的价值。它考虑的是:如果该玩家把子落在这里,四个方向上能形成什么样的棋型。

public int EvaluatePoint(int x, int y, int player) { int totalScore = 0; for (int d = 0; d < 4; d++) { int count = 1; int openEnds = 0; // 正向延伸 int nx = x + dx[d]; int ny = y + dy[d]; while (InBoard(nx, ny) && board.grid[nx, ny] == player) { count++; nx += dx[d]; ny += dy[d]; } if (InBoard(nx, ny) && board.grid[nx, ny] == 0) openEnds++; // 反向延伸 nx = x - dx[d]; ny = y - dy[d]; while (InBoard(nx, ny) && board.grid[nx, ny] == player) { count++; nx -= dx[d]; ny -= dy[d]; } if (InBoard(nx, ny) && board.grid[nx, ny] == 0) openEnds++; totalScore += GetPatternScore(count, openEnds); } return totalScore; }

GetPatternScore就是把前面分值表实例化的地方:

private int GetPatternScore(int count, int openEnds) { if (count >= 5) return 1000000; if (count == 4) { if (openEnds == 2) return 500000; // 活四 if (openEnds == 1) return 30000; // 冲四 return 0; } if (count == 3) { if (openEnds == 2) return 20000; // 活三 if (openEnds == 1) return 3000; // 眠三 return 0; } if (count == 2) { if (openEnds == 2) return 2000; // 活二 if (openEnds == 1) return 300; // 眠二 return 0; } if (count == 1 && openEnds == 2) return 20; return 0; }

这个识别方式朴实但有效。需要强调一点:对于限制长度超过5的情况(比如你连续数出了6个,此时两端几乎必然被封死),GetPatternScore会返回0,这在多数五子棋规则里是合理的,因为长连虽然会造成胜利或特殊规则判定,但在常规禁手规则里并不应该被推荐。

3.4 AI选点逻辑:算攻、算防、再决策

得到EvaluatePoint能力后,找最佳落点就非常自然了:

private Vector2Int FindBestMove() { int aiPlayer = 2; int humanPlayer = 1; int bestScore = -999999; Vector2Int bestMove = new Vector2Int(-1, -1); // 开局第一步直接下天元附近,省去大量无意义扫描 if (board.moveCount == 1) { return new Vector2Int(7, 7); } for (int x = 0; x < GameBoard.BoardSize; x++) { for (int y = 0; y < GameBoard.BoardSize; y++) { if (!board.IsEmpty(x, y)) continue; int attackScore = EvaluatePoint(x, y, aiPlayer); int defendScore = EvaluatePoint(x, y, humanPlayer); // 若对手当前局面存在成五可能,立即封堵 if (defendScore >= 1000000) { return new Vector2Int(x, y); } int totalScore = attackScore + defendScore * 2; if (totalScore > bestScore) { bestScore = totalScore; bestMove = new Vector2Int(x, y); } } } return bestMove; }

一个非常实用的细节是:开局时黑棋第一手下在什么位置,直接决定AI要不要做全盘扫描。我这边统一处理为:如果AI先手且棋盘为空,直接落天元;如果玩家先手第一手,则AI优先从天元附近的八个候选点里选。这能避免AI一开局就把棋子下到角落里,完全是“经验值”的体现。

3.5 用协程模拟AI思考,而不是直接卡死主线程

AI计算量并不大,15×15的棋盘全部扫描一遍,225个空位,每个空位评估8个方向,耗时通常在一毫秒以内。但如果你在玩家落子后立刻同步调用AI选点,UI会有一瞬间的卡顿,表现为“点击没反应,然后棋子突然跳出来”。为了手感,我把AI执行装进协程,人为加一个0.25秒的延迟:

private IEnumerator AIPlayRoutine() { yield return new WaitForSeconds(0.25f); Vector2Int move = FindBestMove(); if (move.x >= 0 && move.y >= 0) { board.Place(move.x, move.y); cells[move.x, move.y].SetVisual(2); if (CheckWin(move.x, move.y, 2)) { EndGame(2); yield break; } board.currentPlayer = 1; isAiThinking = false; } }

这段代码同时解决了一个隐藏问题:如果AI因为某种原因没有找到任何可落子的点,游戏不会直接卡死在“轮到AI”的状态。把棋局状态判断放在每次轮换的出口处,永远保证currentPlayer与UI提示一致。

4. 实测踩坑记录:这套代码真正会让你头痛的三个意外情况

完整的代码框架在上面已经能给到一个很清晰的参考了。但说实话,代码写出来是一回事,能在项目里跑顺是另一回事。下面这几个问题是我在整理unity人机五子棋程序代码时,真实卡过较久的地方。

4.1 局面结算时机:为什么AI会多走一步棋

刚开始把AI写进去后,我测试时发现一个很迷惑的现象:玩家已经五连了,AI还在思考并落下最后一子。查了半天才发现,问题出在玩家落子后调用CheckWin用的是临时落子状态,但由于board.currentPlayer在前一步已经切换成了AI,UI回传的时候玩家点击事件被拦截,但AI侧的控制逻辑却被启动,导致AI在游戏结束后又跑了一次。

解决起来不算难,但要注意顺序。我的建议是:在EndGame里同时设置isGameOver = true,并且在GetComponent<Button>()那里判断一次isGameOver。所有交互入口、AI协程入口,都要在开头做一句“游戏是否已结束”的检查。这类问题不会在你第一次测试时出现,通常都是对局到中后期、玩家被人机AI压制时,因为手速太快点了好几个格子才暴露。

4.2 长连与禁手规则:不要默认所有人都下无禁手规则

五子棋有“无禁手”和“有禁手”两种规则,禁手规则主要限制黑棋的“双三、双四、长连”等获胜方式。很多并不可靠的AI示例里,只做“连成五就赢”的判断,导致黑棋长连也判胜利。在我们这个人机五子棋示例里,默认是无禁手规则,所以不需要考虑太多。

但如果有人想扩展成有禁手,那就要多写一个禁手检测函数,并且必须在胜负判定之前调用。我在实测过程中,遇到过将“禁手”误实现成“所有超过5个的连子都判负”,结果AI自己长连被判负,观感非常奇怪。所以,要么简化处理,要么严格定义规则,不要在两种之间摇摆。

4.3 打分法在“双三”面前会犯的糊涂

打分法有个天生的盲区,就是它对“两步或者三步之后才会形成强威胁”的复合棋型感知很弱。比如AI某个点如果同时形成两个活二,那它对局面的威胁其实远大于一个活三。但打分法只看单点形成的四方向棋型,不会去叠加评估组合效果。

我在实际对局中发现AI有时会错过“双活二”这种重要走法,因为它把两个方向的分值简单相加时,仍低于一个“活三”的分值。这个问题可以通过给组合加分来解决,比如判断某点是否同时让多个方向分值都高于活二,就额外加一个“组合加成”。虽然不是最优解,但能明显提升AI表现,让对局更有压迫感。

5. 从“能玩”到“好玩”:手感和表现力的那些细节

人机五子棋做到“能玩”很容易,但要做成“能分享给别人玩、而且觉得还不错的”,UI的反馈细节和规则完整度就变得重要了。

5.1 落子动画与格子状态反馈

对于大多数Unity项目,给落子加一个“从无到有,再落到棋盘上”的简单动画,体验感会好非常多。我这边是用LocalScale从0到1,通过一个短缓动来实现。格子本身可以设置Button的过渡色,让玩家点击时有明确的按压反馈。

但注意不要让动画阻塞逻辑。比如在动画播放期间,玩家快速点击另一个格子,此时可能会导致棋盘状态被修改两次。我的处理是:在动画期间把isAiThinking设为true,禁止所有新的点击;动画结束后再开放交互。

5.2 悔棋功能的实现思路

五子棋这种回合制游戏,悔棋几乎是标配。实现的关键点在于“回滚状态”,而不仅仅是“把最后一个棋子变透明”。因为AI落子和玩家落子各占一步,悔棋至少应该回滚两步:玩家最后一步、AI最后一步。

我建议用保存棋盘快照的方式实现:

public class BoardSnapshot { public int[,] grid; public int currentPlayer; public int moveCount; }

在每次落子前,把当前GameBoard完整快照压入栈中。悔棋时弹出栈顶快照,并把UI按照快照的状态全部重绘一遍。这个过程对15×15的棋盘来说,代价极小,但能完美避免“AI和玩家各自悔一步导致状态错乱”的坑。

5.3 给AI增加难度档位

由于打分法本身没有深度搜索,要让AI变弱,最直接的做法是给它添加“随机扰动”。我在项目里暴露了一个int aiLevel字段,当AI水平较低时,它只在得分最高的前N个落点里随机选一个。N越大,AI越弱。

这个方案远比“让AI故意不下最优点”要自然,因为它保留了AI的“棋型逻辑”,只是决策不稳定,玩家很难察觉是在放水。我实测下来,五子棋入门玩家和2级AI对战,胜率大约能稳定在六成以上,这个调参思路很值得参考。

6. 下一步:当你觉得打分法不够强时该怎么加深度

打分法最大的优点是可控、简单、适合教学,但它的天花板也很明显:没有前瞻能力。如果你希望自己的unity人机五子棋项目能再进一步,我建议按下面顺序升级。

  1. 增加两步搜索:在打分法基础上,先假设AI选点X,再假设玩家最优应对点Y,最后回到AI选点Z,比较这一条路径的总分。这个搜索深度只有2到3层,但已经能覆盖“双三”这类复合威胁。
  2. 引入Alpha-Beta剪枝:把打分函数作为叶节点评估函数,再加上标准的极小化极大搜索。此时你的AI棋力会获得质的提升,但要小心里面的排序优化,遍历顺序决定剪枝效率。
  3. 做索引候选点压缩:不要扫描全部225个点。只扫描现有棋子周围两格范围内的空位,这个剪枝技巧能让你把搜索深度从2层提升到4层而性能几乎不变。

我在这边建议各位在动手前,先把这篇里的打分法调熟。打分法做扎实了,你后面的搜索树才有可靠的评估地基。不然直接套Alpha-Beta,你会发现AI既缓慢又看不出哪里在下棋,调试体验会非常崩溃。

最后再分享一个我在实际使用中发现的小技巧:所有棋型分值表里的数字,不要写成魔法值散落在代码各处,最好集中放在一个静态配置类里。这样你在后期调参时,只需要改一张表,所有模块的AI行为都会同步变化,省掉了很多零散的重新编译时间。五子棋的核心魅力在于“规则极其简单,但策略层次极深”,希望这份unity人机五子棋程序代码能帮你快速搭出一个基础版本,再慢慢走出自己的棋力进阶路线。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询