Unity 2D飞行棋开发实战:从零构建回合制棋盘游戏

📅 2026/8/25 18:41:28
Unity 2D飞行棋开发实战:从零构建回合制棋盘游戏
在实际游戏开发中很多开发者都是从 Unity 的 3D 项目开始学习但 2D 游戏因其独特的艺术风格和相对简单的物理逻辑往往是独立开发者和小团队的首选。飞行棋作为一款经典的棋盘类游戏其规则清晰、逻辑明确非常适合作为 2D 游戏开发的入门实战项目。通过这个项目你不仅能掌握 Unity 2D 的核心模块如 Sprite 管理、UI 系统、动画状态机和简单的游戏逻辑编程还能理解一个完整游戏项目从场景搭建、角色控制到规则实现的完整流程。本文将以 C# 为主要脚本语言带你从零开始一步步构建一个可运行、可对战的 2D 飞行棋游戏。我们将聚焦于游戏的核心玩法实现包括棋盘生成、棋子移动、回合制逻辑和胜负判定为你后续添加更复杂的特效、音效或网络功能打下坚实基础。1. 理解 2D 飞行棋的核心机制与 Unity 工程准备在开始写代码之前必须理清飞行棋的游戏规则并将其转化为可编程的逻辑模块。这能避免在开发中期陷入逻辑混乱。1.1 飞行棋规则拆解与数据建模飞行棋通常包含 2-4 名玩家每位玩家有 4 枚棋子。棋盘是一个闭合的路径包含起点、终点、普通格子、安全格如“机场”、跳跃格如“飞跃”和特殊事件格。棋子从基地出发根据骰子点数沿固定路径移动所有棋子到达终点后该玩家获胜。在 Unity 中我们需要为这些概念建立数据模型棋盘 (Board)一个GameObject是所有格子的容器。其核心是一个ListGrid或数组按行走顺序存储所有格子信息。格子 (Grid)一个ScriptableObject或MonoBehaviour组件。它需要记录格子的类型普通、起点、终点、安全区、跳跃、惩罚等、位置、以及当前格子上停留的棋子列表。棋子 (Piece)一个MonoBehaviour组件挂载在代表棋子的 Sprite 上。它需要知道自己的所属玩家、当前所在的格子索引、是否在基地、是否已到达终点等状态。玩家 (Player)一个管理类。它持有该玩家的所有棋子引用、颜色信息、当前回合状态等。游戏管理器 (GameManager)单例模式的核心控制器。负责管理游戏状态如当前玩家、骰子点数、回合阶段、处理输入、驱动游戏流程和判定胜负。1.2 Unity 项目初始化与 2D 环境设置启动 Unity Hub创建一个新的 2D 项目命名为“FlightChess2D”。确保项目模板选择“2D”。创建清晰的项目文件夹结构是良好工程习惯的开始Assets/ ├── Scripts/ │ ├── Managers/ // 存放 GameManager, UIManager 等 │ ├── Gameplay/ // 存放 Player, Piece, Grid, Dice 等核心逻辑 │ └── Utilities/ // 存放单例、扩展方法等工具类 ├── Sprites/ // 存放所有图片资源如棋盘、棋子、骰子面 ├── Prefabs/ // 存放预制体如棋子、格子 ├── Scenes/ // 存放游戏场景 └── Settings/ // 存放 ScriptableObject 资产如游戏配置进入Edit - Project Settings - Editor将Version Control模式设置为Visible Meta Files将Asset Serialization模式设置为Force Text。这有利于版本管理如 Git和合并。2. 构建游戏世界棋盘与角色的可视化实现游戏的可视化部分是玩家最直接的感受。我们将先搭建静态场景再注入动态逻辑。2.1 设计并导入棋盘与棋子 Sprite你需要准备或绘制一些基本的 2D 精灵图Sprite。至少需要棋盘背景图一张完整的棋盘布局图或者由多个格子图块拼接而成。棋子 Sprite四种不同颜色的圆形或飞机形状图片代表不同玩家。格子指示器用于高亮显示可移动目标或当前选中棋子的简单图形。骰子 Sprite6 张图片分别显示 1 到 6 点。将图片文件拖入Assets/Sprites文件夹。在 Inspector 窗口中确保它们的Texture Type设置为Sprite (2D and UI)并根据像素风格调整Filter Mode通常为Point (no filter)和Pixels Per Unit例如 100。2.2 创建棋盘网格逻辑与预制体我们不直接使用一张完整的棋盘图片作为逻辑依据而是创建逻辑格子来管理游戏状态。首先创建Grid脚本它主要是一个数据容器// Scripts/Gameplay/Grid.cs using UnityEngine; using System.Collections.Generic; public class Grid : MonoBehaviour { public enum GridType { Normal, Start, Safe, Jump, Penalty, Final } public GridType gridType GridType.Normal; public int gridIndex; // 在棋盘路径上的顺序 public ListPiece piecesOnGrid new ListPiece(); // 当前格上的棋子 // 可视化反馈例如当棋子移动到此格时高亮 private SpriteRenderer spriteRenderer; private Color originalColor; void Awake() { spriteRenderer GetComponentSpriteRenderer(); if(spriteRenderer ! null) originalColor spriteRenderer.color; } public void Highlight(bool isOn) { if(spriteRenderer ! null) spriteRenderer.color isOn ? Color.yellow : originalColor; } }接下来在场景中创建空物体Board。然后根据你的棋盘布局在Board下创建一系列空物体作为格子并重命名为Grid_0,Grid_1... 为每个格子物体挂载Grid脚本并手动设置其gridIndex和gridType。这一步虽然繁琐但建立了游戏最基础的逻辑地图。完成后将Grid物体拖入Assets/Prefabs文件夹生成预制体方便复用。然后创建一个BoardManager脚本来管理所有这些格子。// Scripts/Managers/BoardManager.cs using UnityEngine; using System.Collections.Generic; public class BoardManager : MonoBehaviour { public static BoardManager Instance; // 简单单例便于访问 public ListGrid allGrids new ListGrid(); // 按顺序存储所有格子 void Awake() { if (Instance null) Instance this; else Destroy(gameObject); // 初始化时可以手动将格子拖入这个列表或者用代码查找子物体 // 这里假设格子都是 Board 的直接子物体并且已按顺序排好 InitializeGrids(); } void InitializeGrids() { allGrids.Clear(); foreach (Transform child in transform) { Grid grid child.GetComponentGrid(); if (grid ! null) { allGrids.Add(grid); // 确保 gridIndex 与列表索引一致可选但建议 grid.gridIndex allGrids.Count - 1; } } Debug.Log($棋盘初始化完成共有 {allGrids.Count} 个格子。); } // 根据索引获取格子 public Grid GetGridByIndex(int index) { if (index 0 index allGrids.Count) return allGrids[index]; return null; } }2.3 制作棋子与玩家逻辑创建Piece脚本并将其挂载到棋子 Sprite 上。// Scripts/Gameplay/Piece.cs using UnityEngine; public class Piece : MonoBehaviour { public Player owner; // 所属玩家 public int currentGridIndex -1; // -1 表示在基地-2 表示已到达终点 public bool isInBase true; public bool isFinished false; private Grid currentGrid; // 当前所在的逻辑格引用 // 移动到目标格子 public void MoveToGrid(int targetGridIndex) { if (targetGridIndex 0) return; // 处理基地和终点的逻辑 Grid targetGrid BoardManager.Instance.GetGridByIndex(targetGridIndex); if (targetGrid null) return; // 离开当前格子 if (currentGrid ! null) { currentGrid.piecesOnGrid.Remove(this); } // 更新状态 currentGridIndex targetGridIndex; currentGrid targetGrid; currentGrid.piecesOnGrid.Add(this); isInBase false; // 更新视觉位置可以加入动画 transform.position targetGrid.transform.position; // 处理格子特殊事件如跳跃、暂停 HandleGridSpecialEffect(targetGrid); } void HandleGridSpecialEffect(Grid grid) { switch (grid.gridType) { case Grid.GridType.Jump: // 例如跳到前方第5格 int jumpTarget currentGridIndex 5; MoveToGrid(jumpTarget); break; case Grid.GridType.Penalty: // 暂停一回合由 GameManager 处理 break; // ... 其他类型 } } }创建Player类它不一定是MonoBehaviour而是一个纯数据和管理类。// Scripts/Gameplay/Player.cs using System.Collections.Generic; using UnityEngine; [System.Serializable] public class Player { public int playerId; public Color playerColor; public ListPiece pieces new ListPiece(4); // 每位玩家4枚棋子 public bool isActive false; // 检查是否所有棋子都已到达终点 public bool HasAllPiecesFinished() { foreach (var piece in pieces) { if (!piece.isFinished) return false; } return true; } // 获取还在基地或可移动的棋子 public ListPiece GetMovablePieces(int diceValue) { ListPiece movablePieces new ListPiece(); foreach (var piece in pieces) { if (piece.isFinished) continue; // 简单规则在基地且掷出6点或不在基地即可移动 if ((piece.isInBase diceValue 6) || !piece.isInBase) { movablePieces.Add(piece); } } return movablePieces; } }3. 驱动游戏进程回合制逻辑与用户交互游戏的核心循环是“掷骰子 - 选棋子 - 移动棋子”。我们需要一个强大的GameManager来协调这一切。3.1 实现游戏管理器与状态机GameManager将作为游戏的大脑管理回合状态。// Scripts/Managers/GameManager.cs using System.Collections.Generic; using UnityEngine; public class GameManager : MonoBehaviour { public static GameManager Instance; public ListPlayer players new ListPlayer(); public int currentPlayerIndex 0; public int currentDiceValue 0; private enum GameState { WaitForRoll, WaitForPieceSelection, ProcessingMove } private GameState currentState GameState.WaitForRoll; public UIManager uiManager; // 需要在 Inspector 中关联 void Awake() { if (Instance null) Instance this; else Destroy(gameObject); InitializePlayers(); } void InitializePlayers() { // 示例初始化2名玩家 for (int i 0; i 2; i) { Player p new Player(); p.playerId i; p.playerColor (i 0) ? Color.red : Color.blue; // 这里需要关联已经实例化在场景中的棋子 Prefab // 通常通过 FindObjectsOfTypePiece() 然后根据 owner 或位置来分配 players.Add(p); } StartPlayerTurn(0); // 玩家0开始 } // 开始一个玩家的回合 public void StartPlayerTurn(int playerIndex) { currentPlayerIndex playerIndex; players[currentPlayerIndex].isActive true; currentState GameState.WaitForRoll; uiManager.UpdateStatusText($玩家 {currentPlayerIndex1} 的回合请掷骰子。); uiManager.SetDiceButtonInteractable(true); } // 由UI按钮调用 public void OnRollDiceButtonClicked() { if (currentState ! GameState.WaitForRoll) return; currentDiceValue Random.Range(1, 7); // 生成1-6随机数 uiManager.ShowDiceFace(currentDiceValue); uiManager.SetDiceButtonInteractable(false); // 检查当前玩家是否有可移动的棋子 ListPiece movablePieces players[currentPlayerIndex].GetMovablePieces(currentDiceValue); if (movablePieces.Count 0) { uiManager.UpdateStatusText($玩家 {currentPlayerIndex1} 没有可移动的棋子回合结束。); Invoke(nameof(EndCurrentTurn), 1.5f); // 延迟1.5秒后结束回合 } else { currentState GameState.WaitForPieceSelection; uiManager.UpdateStatusText($掷出 {currentDiceValue} 点请选择要移动的棋子。); HighlightMovablePieces(movablePieces); // 高亮可移动的棋子 } } void HighlightMovablePieces(ListPiece pieces) { // 清除所有高亮 foreach (var player in players) foreach (var piece in player.pieces) piece.GetComponentSpriteRenderer().color piece.owner.playerColor; // 高亮可移动棋子 foreach (var piece in pieces) { piece.GetComponentSpriteRenderer().color Color.green; } } // 由棋子被点击时调用 public void OnPieceSelected(Piece selectedPiece) { if (currentState ! GameState.WaitForPieceSelection) return; if (selectedPiece.owner ! players[currentPlayerIndex]) return; ListPiece movablePieces players[currentPlayerIndex].GetMovablePieces(currentDiceValue); if (!movablePieces.Contains(selectedPiece)) return; // 开始移动 currentState GameState.ProcessingMove; MovePiece(selectedPiece, currentDiceValue); } void MovePiece(Piece piece, int steps) { int targetIndex piece.currentGridIndex steps; // 这里需要处理棋盘边界、终点逻辑等 if (targetIndex BoardManager.Instance.allGrids.Count) { targetIndex BoardManager.Instance.allGrids.Count - 1; // 简单处理实际应处理终点逻辑 } piece.MoveToGrid(targetIndex); // 检查是否到达终点 // ... 省略终点判定逻辑 // 移动完成后结束回合 Invoke(nameof(EndCurrentTurn), 0.5f); } void EndCurrentTurn() { players[currentPlayerIndex].isActive false; ClearAllHighlights(); // 检查胜负 if (players[currentPlayerIndex].HasAllPiecesFinished()) { uiManager.ShowGameOverPanel($玩家 {currentPlayerIndex1} 获胜); return; } // 切换到下一个玩家 int nextPlayerIndex (currentPlayerIndex 1) % players.Count; StartPlayerTurn(nextPlayerIndex); } void ClearAllHighlights() { // 清除棋子和格子的高亮 // ... 实现略 } }3.2 创建用户界面与骰子系统UI 是玩家交互的窗口。我们创建一个简单的 UI 管理器。在场景中创建 Canvas。在 Canvas 下创建Button(掷骰子按钮)Text(状态显示文本)Image(用于显示骰子点数的图片)另一个Text或Panel(游戏结束提示)创建UIManager脚本// Scripts/Managers/UIManager.cs using UnityEngine; using UnityEngine.UI; public class UIManager : MonoBehaviour { public Button rollDiceButton; public Text statusText; public Image diceImage; public Sprite[] diceFaces; // 在 Inspector 中按顺序拖入6个骰子面Sprite public GameObject gameOverPanel; public Text gameOverText; void Start() { rollDiceButton.onClick.AddListener(() GameManager.Instance.OnRollDiceButtonClicked()); gameOverPanel.SetActive(false); } public void UpdateStatusText(string message) { statusText.text message; } public void ShowDiceFace(int value) { if (value 1 value 6 diceFaces[value-1] ! null) { diceImage.sprite diceFaces[value-1]; } } public void SetDiceButtonInteractable(bool interactable) { rollDiceButton.interactable interactable; } public void ShowGameOverPanel(string message) { gameOverPanel.SetActive(true); gameOverText.text message; } }别忘了为棋子添加点击事件。在Piece脚本的Start方法中可以添加碰撞器并处理点击对于 2D使用Collider2D。// 在 Piece.cs 中添加 private void OnMouseDown() { if (GameManager.Instance ! null) { GameManager.Instance.OnPieceSelected(this); } }确保棋子的 GameObject 有Collider2D如Box Collider 2D组件。4. 运行测试、问题排查与优化方向完成以上步骤后将GameManager、BoardManager、UIManager脚本分别挂载到场景中空的 GameObject 上并在 Inspector 面板中完成必要的关联如将 UI 元素拖到UIManager的对应字段将骰子面 Sprite 数组赋值。运行游戏点击掷骰子按钮然后点击高亮的棋子观察棋子是否按点数移动。4.1 常见问题与排查清单在开发过程中你可能会遇到以下典型问题问题现象可能原因检查与解决方案点击掷骰子按钮无反应1. 按钮事件未绑定。2.GameManager.Instance为空。3. 当前游戏状态不是WaitForRoll。1. 检查UIManager的Start方法中是否添加了监听。2. 检查GameManager脚本是否挂载Awake 方法是否执行。3. 在OnRollDiceButtonClicked开始处添加Debug.Log打印当前状态。棋子无法被点击选中1. 棋子没有Collider2D。2. 棋子上层有 UI 元素遮挡。3.OnMouseDown方法未被调用。1. 为棋子 GameObject 添加Box Collider 2D。2. 检查 Canvas 的渲染模式或调整棋子与 UI 的 Sorting Layer。3. 确认Piece脚本已挂载且GameManager.Instance不为空。棋子移动后位置不对1.BoardManager.allGrids顺序与棋盘逻辑顺序不一致。2.MoveToGrid中计算目标索引的逻辑错误。3. 格子预制体的位置未对齐。1. 在BoardManager.InitializeGrids后打印所有格子的索引和位置进行核对。2. 单步调试MovePiece方法检查targetIndex计算。3. 在场景视图中仔细检查每个Grid物体的 Transform 位置。游戏状态逻辑混乱如连续掷骰GameState状态机转换有遗漏或条件判断不严。在每个改变状态的地方如OnRollDiceButtonClicked,OnPieceSelected,EndCurrentTurn打印日志梳理状态流转路径。确保每个操作前都检查了currentState。玩家棋子列表为空导致空引用InitializePlayers方法中没有正确找到并分配棋子给玩家。在InitializePlayers中编写代码查找场景中所有Piece并根据其初始位置或预设的 Tag 分配给对应玩家。或者使用预制体在代码中实例化并分配。4.2 核心优化与扩展实践当前实现是一个最小可运行版本。要使其更完整、健壮需要考虑以下方面棋子移动动画不要直接修改transform.position。使用Vector3.Lerp或DOTween插件实现平滑移动移动完成后才更新逻辑状态。完整的棋盘规则实现真正的终点区域逻辑棋子必须精确掷出点数进入终点、迭子撞击规则将对手棋子送回基地、以及更多特殊格子如暂停一回合、后退等。输入与反馈优化为可移动棋子的高亮添加更明显的效果如缩放、发光。在棋子移动路径上显示预览。添加音效掷骰子、移动、撞击、胜利。数据与配置分离将棋盘布局、玩家数量、颜色等配置数据抽离到ScriptableObject如GameConfig中使设计人员无需修改代码即可调整游戏参数。代码结构优化使用事件Action或UnityEvent来解耦模块。例如GameManager抛出“回合开始”、“骰子掷出”、“游戏结束”等事件UIManager和音效管理器监听这些事件并作出反应。将棋子的移动规则如如何处理跳跃格从Piece脚本中抽离放到BoardManager或专门的RuleManager中使逻辑更清晰。异常处理与日志在关键函数开始和结束处添加日志输出便于调试。对可能出现的数组越界、空引用进行防御性检查。通过这个项目你不仅学会了如何用 Unity 和 C# 构建一个 2D 棋盘游戏的核心循环更重要的是掌握了将游戏规则转化为代码逻辑、管理复杂游戏状态以及组织项目结构的基本方法。接下来你可以尝试为游戏添加 AI 对手、联网对战功能或者用 Tilemap 来构建更复杂的棋盘这些都是在现有框架上自然的延伸。