Unity游戏开发实战:四邻域连通算法在网格占领游戏中的应用

📅 2026/8/8 10:44:39
Unity游戏开发实战:四邻域连通算法在网格占领游戏中的应用
1. 项目概述从经典派对游戏到算法实战最近在重温《马里奥派对》系列时里面一个叫“选面积大作战”的小游戏让我眼前一亮。它的规则很简单屏幕上有一张被分割成不同颜色区块的网格地图玩家需要轮流选择一种颜色所有与该颜色相连的区块就会归你所有。最终谁占领的总面积最大谁就获胜。这个看似简单的游戏背后却藏着非常巧妙的算法设计——如何快速、准确地计算一片颜色区域的面积如何判断两个格子是否“相连”这让我萌生了一个想法能不能在Unity里用代码完整地复刻这个玩法并且把核心的“四邻域连通算法”讲透这不只是一个简单的游戏克隆更是一次绝佳的游戏开发算法实战。对于Unity开发者尤其是刚入门不久、想深入理解游戏逻辑与算法结合的朋友来说这个项目涵盖了网格系统处理、用户交互、状态管理和一个经典图论算法的应用。网上虽然有很多算法教程但往往脱离具体的游戏场景而这个项目能让你看到算法是如何“驱动”一个完整、可玩的游戏机制的。今天我就把自己实现这个项目的完整思路、代码细节以及过程中踩过的坑和优化技巧毫无保留地分享出来。文末也会提供完整的C#源码你可以直接拿去研究、修改甚至扩展成你自己的游戏。2. 核心玩法与算法设计思路拆解在动手写代码之前我们必须把游戏规则和背后的计算逻辑彻底想清楚。这能避免在开发中期陷入逻辑混乱也是写出优雅代码的前提。2.1 玩法规则的精确定义《马里奥派对》里的“选面积大作战”规则可以抽象为以下几点游戏场地一个M x N的矩形网格例如8x810x10。每个网格单元Cell被随机分配一种颜色代表一种“领地类型”。玩家回合2-4名玩家轮流进行。在当前玩家的回合他需要从场地中现存的所有颜色类型中选择一种。占领逻辑玩家选择颜色后所有属于该颜色且相互连通的网格单元以及所有与这些单元相邻四邻域方向的其他颜色单元都会被该玩家占领。注意这里有一个关键点占领会引发“连锁反应”。新占领的格子如果旁边有其他颜色的格子而这些格子颜色相同且连通那么这些格子也会被一并占领。这个过程会持续到没有新的格子可以被占领为止。胜负判定当所有网格单元都被玩家占领或者剩余的未占领颜色已无法被任何玩家有效选择时游戏结束。统计每位玩家占领的格子总数多者胜。这里最核心的难点在于第3点如何高效、无误地计算一次颜色选择后最终所有会被占领的格子。这直接引出了我们的核心算法。2.2 为什么是“四邻域连通算法”“连通区域”是计算机视觉和图像处理中的经典问题。在我们这个二维网格世界里判断两个格子是否属于同一区域就看它们是否通过相同颜色的格子路径连接起来。连接的方式通常有两种定义四邻域 (4-neighborhood)只考虑上下左右四个方向的相邻格子。八邻域 (8-neighborhood)考虑上下左右以及对角线方向共八个相邻格子。《马里奥派对》原版游戏采用的是四邻域规则。为什么这主要是出于游戏性和公平性的考虑。八邻域连接性太强对角线上的格子也算连通这会导致区域形状更加复杂连锁反应更容易发生可能使游戏局势变化过于剧烈和随机削弱了玩家的策略性思考比如预判连锁效果。四邻域规则更清晰、更可控也更容易被玩家直观理解。因此我们的算法核心就是给定一个起始网格找出所有通过四邻域路径可达的、颜色相同的网格集合。这本质上是一个无向图的连通分量搜索问题。每个格子是图的一个节点如果两个格子颜色相同且四邻域相邻则它们之间有一条边。2.3 算法选型DFS vs BFS搜索连通分量深度优先搜索DFS和广度优先搜索BFS都是可行的。在这个特定场景下我选择了深度优先搜索DFS的递归实现。理由如下代码简洁直观递归形式的DFS代码非常简短逻辑清晰易于理解和调试。对于网格规模不大比如10x10以内的游戏来说完全够用。自然契合“蔓延”感觉递归的调用栈模拟了占领行为一层层向外扩散的过程在概念上很贴切。性能考量对于最大几百个格子的搜索DFS和BFS的时间复杂度都是O(N)其中N是连通区域的格子数。递归带来的额外函数调用开销在可接受范围内。如果网格非常大比如100x100则需要警惕递归深度过大可能导致栈溢出的风险那时可以考虑显式栈的DFS或BFS。但对我们这个游戏递归DFS是首选。当然BFS也有其优点例如使用队列可以避免递归深度问题并且搜索顺序是“一圈圈”扩散有时更符合需求。在文末的源码中我也会提供BFS版本的备选实现供你参考。3. Unity项目搭建与核心组件解析理论清晰了现在开始在Unity中搭建项目。我们的目标是创建一个结构清晰、易于扩展的系统。3.1 场景与网格生成首先我们需要一个可视化的网格。这里不采用Tilemap而是用更基础的GameObject来动态生成以便更灵活地控制每个格子的逻辑状态。1. 创建基础Prefab创建一个Cube或Quad命名为GridCell。为其添加一个SpriteRenderer如果使用2D Sprite或MeshRenderer如果使用3D Cube来显示颜色。再挂载一个我们即将编写的C#脚本比如CellData.cs。将这个GameObject做成Prefab。2. 网格生成管理器创建一个空GameObject命名为GameBoardManager并挂载脚本BoardManager.cs。这个脚本负责定义网格的行数(rows)和列数(cols)。定义几种用于随机分配的颜色。在Start()或一个初始化方法中根据行列数在指定范围内如以(0,0)为中心实例化GridCellPrefab排列成网格。为每个生成的Cell随机分配一种颜色并记录其行列索引。// BoardManager.cs 部分代码示例 public class BoardManager : MonoBehaviour { public GameObject cellPrefab; public int rows 8; public int cols 8; public Color[] availableColors; public float cellSpacing 1.1f; private CellData[,] grid; // 核心数据网格 void Start() { InitializeGrid(); } void InitializeGrid() { grid new CellData[rows, cols]; Vector2 startPos new Vector2(-(cols-1)*cellSpacing/2, -(rows-1)*cellSpacing/2); for (int r 0; r rows; r) { for (int c 0; c cols; c) { Vector3 spawnPos new Vector3(startPos.x c * cellSpacing, startPos.y r * cellSpacing, 0); GameObject cellObj Instantiate(cellPrefab, spawnPos, Quaternion.identity, this.transform); CellData cellData cellObj.GetComponentCellData(); // 初始化格子数据 Color randomColor availableColors[Random.Range(0, availableColors.Length)]; cellData.Initialize(r, c, randomColor); grid[r, c] cellData; } } } }3. 格子数据脚本CellData.cs脚本附着在每个格子物体上是数据模型Model层。// CellData.cs public class CellData : MonoBehaviour { public int rowIndex; public int colIndex; public Color cellColor; public Player owner; // 当前占领的玩家初始为null private SpriteRenderer spriteRenderer; void Awake() { spriteRenderer GetComponentSpriteRenderer(); } public void Initialize(int row, int col, Color color) { rowIndex row; colIndex col; cellColor color; UpdateVisual(); } public void SetOwner(Player newOwner) { owner newOwner; UpdateVisual(); // 被占领后视觉上可能需要变化如加上玩家标志色边框 } void UpdateVisual() { if (spriteRenderer ! null) { spriteRenderer.color cellColor; // 可以在这里根据owner添加边框效果 } } }注意这里将CellData作为组件挂在每个格子上方便通过GameObject直接获取数据。grid[r, c]数组也持有这些组件的引用形成了数据访问的双向通道。这是一种在小型项目中常见的简单架构。3.2 玩家系统与回合管理我们需要一个GameManager作为游戏总控制器以及一个Player类来代表玩家。1. Player类这是一个纯C#类不一定是MonoBehaviour用于存储玩家信息。// Player.cs [System.Serializable] public class Player { public string playerName; public Color playerColor; // 用于UI显示或格子边框 public int score 0; // 占领的格子数 public Player(string name, Color color) { playerName name; playerColor color; } public void AddScore(int amount) { score amount; // 可以在这里触发UI更新事件 } }2. GameManager单例模式使用单例模式方便全局访问。它负责管理玩家列表和当前回合玩家索引。持有BoardManager的引用。处理玩家的颜色选择输入。调用核心算法计算占领区域。更新分数并判断游戏结束。// GameManager.cs public class GameManager : MonoBehaviour { public static GameManager Instance; public ListPlayer players; private int currentPlayerIndex 0; public BoardManager boardManager; void Awake() { if (Instance null) Instance this; else Destroy(gameObject); } void Start() { // 初始化玩家例如players new ListPlayer{ new Player(P1, Color.red), new Player(P2, Color.blue) }; StartPlayerTurn(players[currentPlayerIndex]); } // 开始一个玩家的回合例如高亮可选择的颜色 void StartPlayerTurn(Player player) { Debug.Log($轮到 {player.playerName} 行动); // 这里可以触发UI显示当前玩家和可操作提示 } // 这个函数由UI按钮触发传入玩家选择的颜色 public void OnPlayerSelectColor(Color selectedColor) { Player currentPlayer players[currentPlayerIndex]; // 调用核心算法计算并占领区域 ListCellData cellsToCapture CalculateAreaToCapture(selectedColor); if (cellsToCapture.Count 0) { CaptureCells(currentPlayer, cellsToCapture); EndCurrentTurn(); } else { Debug.Log(选择无效该颜色已无连通区域可占领。); } } void EndCurrentTurn() { // 检查游戏是否结束... // 切换到下一个玩家 currentPlayerIndex (currentPlayerIndex 1) % players.Count; StartPlayerTurn(players[currentPlayerIndex]); } // 核心算法函数下一节详解 ListCellData CalculateAreaToCapture(Color selectedColor) { ... } void CaptureCells(Player player, ListCellData cells) { ... } }4. 四邻域连通算法的C#实现与优化这是整个项目的灵魂。我们将实现CalculateAreaToCapture函数。4.1 算法步骤详解给定一个玩家选择的颜色selectedColor我们需要找出场上所有未被占领的、颜色为selectedColor的格子并通过四邻域连通性找出所有会被连锁占领的格子集合。注意初始的“种子”格子是所有该颜色的未占领格子而不仅仅是某一个。算法步骤如下初始化一个HashSetCellData用于存储最终要占领的所有格子cellsToCapture一个QueueCellData或递归栈用于搜索。遍历整个网格将所有颜色为selectedColor且owner null的格子加入一个“待搜索种子”列表。遍历这个种子列表对每一个种子如果它还没有被加入到cellsToCapture中则以其为起点进行四邻域连通区域搜索将这个连通区域内的所有格子加入cellsToCapture。搜索过程中对于当前格子检查其上下左右四个邻居注意边界判断。如果邻居格子满足a) 索引有效b) 未被占领(owner null)c) 颜色与selectedColor相同d) 还未被加入cellsToCapture则将其作为新的搜索起点。完成所有种子格子的搜索后cellsToCapture包含了所有直接连通的颜色区域。但根据规则占领这些格子后如果它们旁边有其他颜色的格子且这些其他颜色的格子因此变得与同色格子连通也需要被占领。这需要迭代处理。我们需要不断检查cellsToCapture中所有格子的四邻域。对于每一个邻居格子如果它未被占领且颜色不同则找到这个邻居格子所属的整个同色连通区域。如果这个区域的所有格子都至少有一个邻居在cellsToCapture中即被新占领的格子包围或接触那么这个区域的格子也要被加入cellsToCapture。重复步骤6直到在一次完整的检查中没有新的格子被加入cellsToCapture为止。此时cellsToCapture就是本次操作最终要占领的所有格子。4.2 递归DFS核心代码实现以下是GameManager中核心算法的简化版实现重点展示递归DFS搜索连通分量// GameManager.cs 内部 private ListCellData CalculateAreaToCapture(Color selectedColor) { HashSetCellData totalCaptureSet new HashSetCellData(); CellData[,] grid boardManager.grid; int rows boardManager.rows; int cols boardManager.cols; // 步骤1 2: 找到所有该颜色的未占领种子格子 ListCellData seedCells new ListCellData(); for (int r 0; r rows; r) { for (int c 0; c cols; c) { CellData cell grid[r, c]; if (cell.owner null ColorsApproximatelyEqual(cell.cellColor, selectedColor)) { seedCells.Add(cell); } } } // 步骤3: 对每个种子进行连通区域搜索 HashSetCellData visitedForColor new HashSetCellData(); foreach (CellData seed in seedCells) { if (!visitedForColor.Contains(seed)) { HashSetCellData colorRegion new HashSetCellData(); DFSFindConnectedRegion(seed, selectedColor, colorRegion, grid, rows, cols); // 将这个颜色区域加入总集合 foreach (var cell in colorRegion) { totalCaptureSet.Add(cell); visitedForColor.Add(cell); } } } // 如果初始颜色区域都没有直接返回空 if (totalCaptureSet.Count 0) return new ListCellData(totalCaptureSet); // 步骤6 7: 处理连锁反应不同颜色区域 bool changed; do { changed false; // 收集当前totalCaptureSet所有格子的所有未占领的、颜色不同的邻居 HashSetCellData potentialNewColorNeighbors new HashSetCellData(); foreach (CellData capturedCell in totalCaptureSet) { ListCellData neighbors GetFourNeighbors(capturedCell, grid, rows, cols); foreach (var neighbor in neighbors) { if (neighbor.owner null !ColorsApproximatelyEqual(neighbor.cellColor, selectedColor) !totalCaptureSet.Contains(neighbor)) { potentialNewColorNeighbors.Add(neighbor); } } } // 检查每个潜在邻居所在的同色区域是否可被占领 HashSetCellData visitedOtherColors new HashSetCellData(); foreach (CellData neighborSeed in potentialNewColorNeighbors) { if (visitedOtherColors.Contains(neighborSeed)) continue; HashSetCellData otherColorRegion new HashSetCellData(); DFSFindConnectedRegion(neighborSeed, neighborSeed.cellColor, otherColorRegion, grid, rows, cols); // 判断该区域是否所有格子都与totalCaptureSet相邻 bool allCellsAdjacentToCaptured true; foreach (var cell in otherColorRegion) { if (!IsCellAdjacentToSet(cell, totalCaptureSet, grid, rows, cols)) { allCellsAdjacentToCaptured false; break; } } // 如果该区域所有格子都与已占领集相邻则占领整个区域 if (allCellsAdjacentToCaptured) { foreach (var cell in otherColorRegion) { if (totalCaptureSet.Add(cell)) // 如果成功添加即之前没有 { changed true; } } } visitedOtherColors.UnionWith(otherColorRegion); } } while (changed); // 只要本轮有新增就继续循环检查 return new ListCellData(totalCaptureSet); } // 递归DFS函数 private void DFSFindConnectedRegion(CellData startCell, Color targetColor, HashSetCellData resultSet, CellData[,] grid, int rows, int cols) { // 递归终止条件越界、已访问、已被占领、颜色不匹配 if (startCell null || resultSet.Contains(startCell) || startCell.owner ! null || !ColorsApproximatelyEqual(startCell.cellColor, targetColor)) return; resultSet.Add(startCell); // 四邻域递归搜索 int[][] directions new int[][] { new int[] { -1, 0 }, new int[] { 1, 0 }, new int[] { 0, -1 }, new int[] { 0, 1 } }; foreach (var dir in directions) { int newR startCell.rowIndex dir[0]; int newC startCell.colIndex dir[1]; if (newR 0 newR rows newC 0 newC cols) { DFSFindConnectedRegion(grid[newR, newC], targetColor, resultSet, grid, rows, cols); } } } // 辅助函数获取四邻域格子 private ListCellData GetFourNeighbors(CellData cell, CellData[,] grid, int rows, int cols) { ... } // 辅助函数判断格子是否与集合中任一格子相邻 private bool IsCellAdjacentToSet(CellData cell, HashSetCellData set, CellData[,] grid, int rows, int cols) { ... } // 辅助函数比较颜色Unity Color是浮点数直接可能因精度问题出错 private bool ColorsApproximatelyEqual(Color a, Color b, float threshold 0.01f) { ... }4.3 关键细节与优化点颜色比较Unity的Color是结构体包含r, g, b, a四个float。直接使用运算符可能会因为浮点数精度问题导致误判。建议自己写一个近似比较函数ColorsApproximatelyEqual判断每个分量的差值是否小于一个很小的阈值如0.01f。使用HashSet在搜索和去重时HashSetT的Contains和Add操作平均时间复杂度是O(1)比List快得多。大量使用HashSet来存储已访问节点和结果集合是性能关键。递归深度对于8x8的网格最大的连通区域理论上可达64格递归深度为64。这在Unity中是安全的默认栈空间足够。但如果网格扩大到50x50最大深度可能达到2500这就有栈溢出风险。这时需要将递归DFS改为显式栈Stack的DFS或队列Queue的BFS。下面是BFS版本的连通区域搜索示例private void BFSFindConnectedRegion(CellData startCell, Color targetColor, HashSetCellData resultSet, CellData[,] grid, int rows, int cols) { if (startCell.owner ! null || !ColorsApproximatelyEqual(startCell.cellColor, targetColor)) return; QueueCellData queue new QueueCellData(); queue.Enqueue(startCell); resultSet.Add(startCell); int[][] directions { new int[] { -1, 0 }, new int[] { 1, 0 }, new int[] { 0, -1 }, new int[] { 0, 1 } }; while (queue.Count 0) { CellData current queue.Dequeue(); foreach (var dir in directions) { int newR current.rowIndex dir[0]; int newC current.colIndex dir[1]; if (newR 0 newR rows newC 0 newC cols) { CellData neighbor grid[newR, newC]; if (neighbor.owner null ColorsApproximatelyEqual(neighbor.cellColor, targetColor) resultSet.Add(neighbor)) { queue.Enqueue(neighbor); } } } } }连锁反应迭代算法中最复杂的部分是连锁反应。do...while循环确保了能处理多层嵌套的连锁。IsCellAdjacentToSet函数需要高效因为它会被频繁调用。可以预先计算每个格子的邻居列表并缓存但在这个规模下实时计算也可以接受。5. 游戏逻辑完善与用户体验打磨核心算法跑通后我们需要完善游戏循环和交互让它成为一个真正可玩的游戏。5.1 回合流程与状态管理在GameManager中我们需要一个更健壮的回合状态机。例如定义几个状态public enum GameState { Init, PlayerTurn, Processing, GameOver } private GameState currentState;Init: 初始化棋盘和玩家。PlayerTurn: 等待玩家操作点击UI按钮选择颜色。此时应高亮当前玩家并可能在地图上用半透明色显示选择每种颜色后预计能占领的区域这需要预先计算是一个高级功能。Processing: 玩家做出选择后开始执行占领计算、播放占领动画格子颜色渐变、粒子效果等、更新分数。这个状态期间应锁定玩家输入。GameOver: 显示胜利者提供重新开始按钮。状态之间的转换要清晰避免在动画播放中途被输入打断。5.2 视觉反馈与动画干巴巴的格子变色很无趣好的视觉反馈能极大提升游戏体验。选中预览当鼠标悬停在某种颜色的UI按钮上时可以在地图上用半透明高亮显示所有该颜色的未占领区域让玩家提前预判。占领动画不要瞬间变色。可以为每个CellData添加一个协程Coroutine实现颜色从原始色渐变到玩家色的效果。也可以让被占领的格子依次“弹出”一个缩放动画形成波浪式的蔓延感。// 在CellData.cs中 public IEnumerator CaptureAnimation(Color targetColor, float duration) { float elapsed 0f; Color startColor spriteRenderer.color; while (elapsed duration) { elapsed Time.deltaTime; float t elapsed / duration; spriteRenderer.color Color.Lerp(startColor, targetColor, t); // 可以同时混合一些缩放动画 transform.localScale Vector3.Lerp(Vector3.one, Vector3.one * 1.2f, Mathf.Sin(t * Mathf.PI)); // 脉冲效果 yield return null; } spriteRenderer.color targetColor; transform.localScale Vector3.one; }在GameManager的CaptureCells函数中可以StartCoroutine启动每个格子的动画并用WaitForSeconds或等待所有协程结束来控制回合节奏。玩家标识格子被占领后除了变色还可以在格子中央显示一个代表玩家的小图标或改变边框颜色。5.3 胜负判定与策略提示游戏结束条件需要仔细实现无地可占遍历所有未占领格子检查其颜色是否至少被一位玩家“有效”选择即选择后能占领至少一个格子。如果所有未占领颜色的任何选择都无法占领新格子可能因为该颜色区域被已占领格子完全包围但包围者不是同一个人这里规则需要明确则游戏立即结束。原版规则似乎是直到所有格子被占领为止。提前结束也可以实现当某玩家面积超过一半时直接获胜加快游戏节奏。在PlayerTurn状态可以添加简单的AI或策略提示。例如计算如果选择每种颜色能获得多少面积并在UI上显示这个数字帮助玩家决策。这只需要调用CalculateAreaToCapture并返回列表的Count即可但要注意性能可以在后台异步计算。6. 性能优化与常见问题排查当网格变大、颜色变多时一些性能问题和逻辑Bug就会暴露出来。6.1 性能瓶颈分析与优化频繁的网格遍历CalculateAreaToCapture中最耗时的部分是遍历整个网格寻找种子格子O(N)以及连锁反应中的多次邻居检查。对于10x10的网格这完全不是问题。但如果做到20x20或更大就需要优化。优化思路1维护未占领格子列表。用一个ListCellData动态记录所有owner null的格子。当格子被占领后从列表中移除。这样寻找种子格子时只需遍历这个列表而非整个网格。优化思路2缓存连通区域。在游戏开始时为整个网格计算一次所有颜色的连通区域并缓存起来。当格子被占领后更新受影响的缓存区域。这比较复杂但能极大提升后续计算速度适合追求极致性能或超大地图。颜色比较与HashSet操作ColorsApproximatelyEqual函数和HashSet的Add/Contains是内层循环高频操作。确保颜色比较函数尽量简单比如比较Color32而不是Color或者将颜色映射为整数ID。HashSet的容量可以预先设定一个合理值以减少扩容开销。动画协程开销同时为几十上百个格子播放协程动画可能会有开销。可以考虑使用对象池管理动画效果或者使用更高效的DOTween插件来处理补间动画。6.2 常见Bug与调试技巧连锁反应死循环或漏算这是最容易出错的地方。务必确保你的“判断区域是否可被占领”的逻辑IsCellAdjacentToSet是正确的。关键规则一个其他颜色的区域只有当其每一个格子都与当前totalCaptureSet中的至少一个格子四邻域相邻时才能被占领。如果这个区域中有一个格子是“孤岛”不与任何已占领格相邻那么整个区域都不能被占领。我的算法中通过allCellsAdjacentToCaptured布尔变量来确保这一点。调试时可以打印出每次迭代中totalCaptureSet的数量和新增的格子观察其增长是否符合预期。颜色误判因为浮点数精度两个视觉上一样的颜色可能被判为不同。除了使用阈值比较更稳妥的方法是在初始化时为每种颜色分配一个唯一的整数ColorId格子只存储ColorId。比较时直接比较整数又快又准。边界条件处理在GetFourNeighbors函数中一定要严格检查数组索引r 0 r rows c 0 c cols否则会引发IndexOutOfRangeException。游戏状态不同步确保在Processing状态时玩家无法再次点击选择颜色。可以通过禁用UI按钮组或检查currentState来实现。内存泄漏如果频繁创建List或HashSet注意在函数内部使用后如果不再需要可以将引用置为null对于局部变量函数结束会自动回收。对于需要重用的集合可以考虑在类级别声明并Clear()后重用避免频繁的GC垃圾回收开销。7. 功能扩展与创意发散一个基础版本实现后你可以尝试很多有趣的扩展让这个项目成为你作品集里的亮点。多种地图与地形引入“障碍物”格子永远不能被占领、“一次性格子”占领后消失、“传送门格子”等增加策略深度。道具系统玩家回合除了选颜色还可以使用道具。例如“炸弹”移除一个格子及其相邻格子“颜色转换器”将一小片区域转换成指定颜色“面积加倍”使本轮占领的格子分数翻倍。AI对手实现不同难度的AI。简单AI随机选择中等AI选择当前能占领面积最大的颜色高级AI会模拟未来几步考虑封锁对手或为自己创造更大的后续机会。网络对战使用Unity的Netcode或第三方SDK将游戏改造成在线多人对战模式。这涉及到状态同步、输入命令、随机种子同步等更复杂的知识。数据持久化与回放记录每一步的操作实现游戏回放功能或者将精彩对局保存下来。这个项目虽然源于一个小游戏但它串联起了Unity开发中的多个核心技能点Prefab动态生成、数据模型与视图分离、递归/迭代算法、协程动画、简单状态机、基础AI。无论你是用来巩固算法还是作为面试作品都很有价值。最后所有的完整C#源码包括BoardManager,CellData,GameManager,Player以及几个算法辅助类我已经整理好。你可以直接复制这些代码到你的Unity项目中记得创建对应的脚本文件并挂载到正确的GameObject上然后根据文中的步骤设置场景和Prefab就能运行起来。希望这篇长文和配套源码能帮助你彻底掌握这个有趣的游戏开发案例。在实际动手实现的过程中你一定会遇到我文中没提到的小问题这正是学习提升的最好机会。