Unity 2D飞行棋开发实战:从零构建回合制棋盘游戏
2026/8/25 18:41:17 网站建设 项目流程

在实际游戏开发中,很多开发者都是从 Unity 的 3D 项目开始学习,但 2D 游戏因其独特的艺术风格和相对简单的物理逻辑,往往是独立开发者和小团队的首选。飞行棋作为一款经典的棋盘类游戏,其规则清晰、逻辑明确,非常适合作为 2D 游戏开发的入门实战项目。通过这个项目,你不仅能掌握 Unity 2D 的核心模块,如 Sprite 管理、UI 系统、动画状态机和简单的游戏逻辑编程,还能理解一个完整游戏项目从场景搭建、角色控制到规则实现的完整流程。本文将以 C# 为主要脚本语言,带你从零开始,一步步构建一个可运行、可对战的 2D 飞行棋游戏。我们将聚焦于游戏的核心玩法实现,包括棋盘生成、棋子移动、回合制逻辑和胜负判定,为你后续添加更复杂的特效、音效或网络功能打下坚实基础。

1. 理解 2D 飞行棋的核心机制与 Unity 工程准备

在开始写代码之前,必须理清飞行棋的游戏规则,并将其转化为可编程的逻辑模块。这能避免在开发中期陷入逻辑混乱。

1.1 飞行棋规则拆解与数据建模

飞行棋通常包含 2-4 名玩家,每位玩家有 4 枚棋子。棋盘是一个闭合的路径,包含起点、终点、普通格子、安全格(如“机场”)、跳跃格(如“飞跃”)和特殊事件格。棋子从基地出发,根据骰子点数沿固定路径移动,所有棋子到达终点后该玩家获胜。

在 Unity 中,我们需要为这些概念建立数据模型:

  • 棋盘 (Board):一个GameObject,是所有格子的容器。其核心是一个List<Grid>或数组,按行走顺序存储所有格子信息。
  • 格子 (Grid):一个ScriptableObjectMonoBehaviour组件。它需要记录格子的类型(普通、起点、终点、安全区、跳跃、惩罚等)、位置、以及当前格子上停留的棋子列表。
  • 棋子 (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)。至少需要:

  1. 棋盘背景图:一张完整的棋盘布局图,或者由多个格子图块拼接而成。
  2. 棋子 Sprite:四种不同颜色的圆形或飞机形状图片,代表不同玩家。
  3. 格子指示器:用于高亮显示可移动目标或当前选中棋子的简单图形。
  4. 骰子 Sprite:6 张图片,分别显示 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 List<Piece> piecesOnGrid = new List<Piece>(); // 当前格上的棋子 // 可视化反馈,例如当棋子移动到此格时高亮 private SpriteRenderer spriteRenderer; private Color originalColor; void Awake() { spriteRenderer = GetComponent<SpriteRenderer>(); 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脚本,并手动设置其gridIndexgridType。这一步虽然繁琐,但建立了游戏最基础的逻辑地图。

完成后,将Grid物体拖入Assets/Prefabs文件夹生成预制体,方便复用。然后,创建一个BoardManager脚本来管理所有这些格子。

// Scripts/Managers/BoardManager.cs using UnityEngine; using System.Collections.Generic; public class BoardManager : MonoBehaviour { public static BoardManager Instance; // 简单单例,便于访问 public List<Grid> allGrids = new List<Grid>(); // 按顺序存储所有格子 void Awake() { if (Instance == null) Instance = this; else Destroy(gameObject); // 初始化时,可以手动将格子拖入这个列表,或者用代码查找子物体 // 这里假设格子都是 Board 的直接子物体,并且已按顺序排好 InitializeGrids(); } void InitializeGrids() { allGrids.Clear(); foreach (Transform child in transform) { Grid grid = child.GetComponent<Grid>(); 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 List<Piece> pieces = new List<Piece>(4); // 每位玩家4枚棋子 public bool isActive = false; // 检查是否所有棋子都已到达终点 public bool HasAllPiecesFinished() { foreach (var piece in pieces) { if (!piece.isFinished) return false; } return true; } // 获取还在基地或可移动的棋子 public List<Piece> GetMovablePieces(int diceValue) { List<Piece> movablePieces = new List<Piece>(); 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 List<Player> players = new List<Player>(); 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 // 通常通过 FindObjectsOfType<Piece>() 然后根据 owner 或位置来分配 players.Add(p); } StartPlayerTurn(0); // 玩家0开始 } // 开始一个玩家的回合 public void StartPlayerTurn(int playerIndex) { currentPlayerIndex = playerIndex; players[currentPlayerIndex].isActive = true; currentState = GameState.WaitForRoll; uiManager.UpdateStatusText($"玩家 {currentPlayerIndex+1} 的回合,请掷骰子。"); 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); // 检查当前玩家是否有可移动的棋子 List<Piece> movablePieces = players[currentPlayerIndex].GetMovablePieces(currentDiceValue); if (movablePieces.Count == 0) { uiManager.UpdateStatusText($"玩家 {currentPlayerIndex+1} 没有可移动的棋子,回合结束。"); Invoke(nameof(EndCurrentTurn), 1.5f); // 延迟1.5秒后结束回合 } else { currentState = GameState.WaitForPieceSelection; uiManager.UpdateStatusText($"掷出 {currentDiceValue} 点,请选择要移动的棋子。"); HighlightMovablePieces(movablePieces); // 高亮可移动的棋子 } } void HighlightMovablePieces(List<Piece> pieces) { // 清除所有高亮 foreach (var player in players) foreach (var piece in player.pieces) piece.GetComponent<SpriteRenderer>().color = piece.owner.playerColor; // 高亮可移动棋子 foreach (var piece in pieces) { piece.GetComponent<SpriteRenderer>().color = Color.green; } } // 由棋子被点击时调用 public void OnPieceSelected(Piece selectedPiece) { if (currentState != GameState.WaitForPieceSelection) return; if (selectedPiece.owner != players[currentPlayerIndex]) return; List<Piece> 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($"玩家 {currentPlayerIndex+1} 获胜!"); return; } // 切换到下一个玩家 int nextPlayerIndex = (currentPlayerIndex + 1) % players.Count; StartPlayerTurn(nextPlayerIndex); } void ClearAllHighlights() { // 清除棋子和格子的高亮 // ... 实现略 } }

3.2 创建用户界面与骰子系统

UI 是玩家交互的窗口。我们创建一个简单的 UI 管理器。

  1. 在场景中创建 Canvas。
  2. 在 Canvas 下创建:
    • Button(掷骰子按钮)
    • Text(状态显示文本)
    • Image(用于显示骰子点数的图片)
    • 另一个TextPanel(游戏结束提示)

创建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. 运行测试、问题排查与优化方向

完成以上步骤后,将GameManagerBoardManagerUIManager脚本分别挂载到场景中空的 GameObject 上,并在 Inspector 面板中完成必要的关联(如将 UI 元素拖到UIManager的对应字段,将骰子面 Sprite 数组赋值)。运行游戏,点击掷骰子按钮,然后点击高亮的棋子,观察棋子是否按点数移动。

4.1 常见问题与排查清单

在开发过程中,你可能会遇到以下典型问题:

问题现象可能原因检查与解决方案
点击掷骰子按钮无反应1. 按钮事件未绑定。
2.GameManager.Instance为空。
3. 当前游戏状态不是WaitForRoll
1. 检查UIManagerStart方法中是否添加了监听。
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 核心优化与扩展实践

当前实现是一个最小可运行版本。要使其更完整、健壮,需要考虑以下方面:

  1. 棋子移动动画:不要直接修改transform.position。使用Vector3.LerpDOTween插件实现平滑移动,移动完成后才更新逻辑状态。
  2. 完整的棋盘规则:实现真正的终点区域逻辑(棋子必须精确掷出点数进入终点)、迭子(撞击)规则(将对手棋子送回基地)、以及更多特殊格子(如暂停一回合、后退等)。
  3. 输入与反馈优化
    • 为可移动棋子的高亮添加更明显的效果(如缩放、发光)。
    • 在棋子移动路径上显示预览。
    • 添加音效(掷骰子、移动、撞击、胜利)。
  4. 数据与配置分离:将棋盘布局、玩家数量、颜色等配置数据抽离到ScriptableObject(如GameConfig)中,使设计人员无需修改代码即可调整游戏参数。
  5. 代码结构优化
    • 使用事件(ActionUnityEvent)来解耦模块。例如,GameManager抛出“回合开始”、“骰子掷出”、“游戏结束”等事件,UIManager和音效管理器监听这些事件并作出反应。
    • 将棋子的移动规则(如如何处理跳跃格)从Piece脚本中抽离,放到BoardManager或专门的RuleManager中,使逻辑更清晰。
  6. 异常处理与日志:在关键函数开始和结束处添加日志输出,便于调试。对可能出现的数组越界、空引用进行防御性检查。

通过这个项目,你不仅学会了如何用 Unity 和 C# 构建一个 2D 棋盘游戏的核心循环,更重要的是掌握了将游戏规则转化为代码逻辑、管理复杂游戏状态以及组织项目结构的基本方法。接下来,你可以尝试为游戏添加 AI 对手、联网对战功能,或者用 Tilemap 来构建更复杂的棋盘,这些都是在现有框架上自然的延伸。

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

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

立即咨询