Unity 2D飞行棋游戏开发实战:状态机、棋盘逻辑与UI交互

📅 2026/8/25 20:19:24
Unity 2D飞行棋游戏开发实战:状态机、棋盘逻辑与UI交互
在实际游戏开发中从零开始构建一个完整的项目是检验技术理解和工程能力的最佳方式。飞行棋作为一款经典的棋盘游戏规则清晰、交互明确非常适合作为 Unity 2D 开发的入门实战项目。上半部分我们可能已经搭建了基础场景、绘制了棋盘、实现了棋子的基本移动。下半部分我们将聚焦于游戏的核心逻辑闭环从玩家轮流掷骰子、棋子行走与碰撞判定、特殊格子效果如跳跃、暂停、返回起点到游戏胜负判定、UI交互反馈以及最终的打包发布。本文将手把手带你完成这些关键模块的开发让你不仅掌握 Unity 2D 的组件使用更能理解一个完整游戏项目的代码组织与逻辑架构。本文适合已经了解 Unity 界面基础操作和 C# 基本语法的开发者。通过本文你将能独立完成一个具备可玩性的飞行棋游戏并掌握处理游戏状态机、规则逻辑、动画与音效集成等核心技能。1. 核心游戏逻辑与状态管理一个可运行的飞行棋游戏其核心是清晰的状态流转。我们需要管理当前是哪个玩家的回合、游戏是否开始或结束、骰子是否已投掷、棋子是否在移动中等一系列状态。1.1 设计游戏管理器 (GameManager)游戏管理器应作为单例存在负责协调所有子系统。我们首先定义游戏的核心状态。// GameManager.cs using UnityEngine; using System.Collections.Generic; public class GameManager : MonoBehaviour { public static GameManager Instance; // 单例实例 public enum GameState { WaitingToStart, PlayerTurn, RollingDice, MovingPiece, ProcessingTileEffect, GameOver } public GameState CurrentState { get; private set; } public enum Player { Red, Blue, Green, Yellow } public Player CurrentPlayer { get; private set; } public int CurrentDiceValue { get; private set; } public ListPlayer playerOrder; // 玩家顺序 private int currentPlayerIndex 0; // 棋子预制件和起点位置 public GameObject piecePrefab; public Transform[] startPositions; // 对应红、蓝、绿、黄玩家的起点 void Awake() { if (Instance null) { Instance this; DontDestroyOnLoad(gameObject); // 跨场景保持 } else { Destroy(gameObject); } InitializeGame(); } void InitializeGame() { CurrentState GameState.WaitingToStart; playerOrder new ListPlayer { Player.Red, Player.Blue, Player.Green, Player.Yellow }; CurrentPlayer playerOrder[0]; // 初始化棋子等... } }这个GameManager定义了游戏的基本状态枚举和当前玩家。Awake方法确保它作为单例存在InitializeGame方法用于重置游戏数据。1.2 实现掷骰子与回合切换掷骰子是一个关键交互它触发状态从PlayerTurn切换到RollingDice再根据结果切换到MovingPiece。// GameManager.cs (续) public void RollDice() { if (CurrentState ! GameState.PlayerTurn) { Debug.LogWarning(现在不是投掷骰子的时机); return; } CurrentState GameState.RollingDice; // 模拟骰子动画这里用随机数代替 int diceResult Random.Range(1, 7); // 生成1-6的随机数 CurrentDiceValue diceResult; Debug.Log(${CurrentPlayer} 玩家掷出了 {diceResult} 点); // 假设骰子动画结束后调用此方法 OnDiceRollFinished(); } private void OnDiceRollFinished() { CurrentState GameState.MovingPiece; // 通知当前玩家的棋子开始移动 // 移动完成后会触发 ProcessTurnEnd } public void ProcessTurnEnd() { // 检查当前玩家是否所有棋子都已到达终点判断游戏是否结束 if (CheckPlayerWin(CurrentPlayer)) { CurrentState GameState.GameOver; Debug.Log($游戏结束获胜者是{CurrentPlayer}); // 触发UI显示 return; } // 切换到下一个玩家 currentPlayerIndex (currentPlayerIndex 1) % playerOrder.Count; CurrentPlayer playerOrder[currentPlayerIndex]; CurrentState GameState.PlayerTurn; Debug.Log($轮到 {CurrentPlayer} 玩家); }RollDice方法包含了状态检查和随机数生成。ProcessTurnEnd方法处理回合结束的逻辑包括胜负判定和玩家切换这是游戏循环的驱动核心。2. 棋子移动与棋盘格子系统棋子需要沿着预设的路径移动。我们需要一个棋盘数据管理器来定义路径并让棋子能够根据骰子点数前进。2.1 创建棋盘路径与格子数据在场景中我们可以使用空物体如Waypoint来标记路径点并按顺序组织。// BoardManager.cs using UnityEngine; using System.Collections.Generic; public class BoardManager : MonoBehaviour { public static BoardManager Instance; // 存储所有路径点的位置顺序即为行走顺序 public ListVector3 waypoints new ListVector3(); // 特殊格子字典格子索引 - 特殊效果类型 public Dictionaryint, SpecialTileEffect specialTiles new Dictionaryint, SpecialTileEffect(); public enum SpecialTileEffect { None, JumpForward, JumpBackward, StopOneTurn, BackToStart } void Awake() { if (Instance null) Instance this; // 初始化路径点可以手动在Inspector中赋值或通过子物体自动获取 InitializeWaypoints(); InitializeSpecialTiles(); } void InitializeWaypoints() { // 示例手动赋值或在Start中通过查找“Waypoint”标签的子物体获取位置 waypoints.Clear(); foreach (Transform child in transform) { if (child.CompareTag(Waypoint)) { waypoints.Add(child.position); } } Debug.Log($棋盘路径初始化完成共有 {waypoints.Count} 个路径点。); } void InitializeSpecialTiles() { // 预设一些特殊格子例如第5格跳跃到第10格第12格暂停一回合 specialTiles.Clear(); specialTiles.Add(5, SpecialTileEffect.JumpForward); specialTiles.Add(12, SpecialTileEffect.StopOneTurn); specialTiles.Add(18, SpecialTileEffect.BackToStart); // ... 可根据棋盘设计添加更多 } // 根据当前格子索引和骰子点数计算目标格子索引并处理特殊效果 public int GetTargetTileIndex(int currentIndex, int diceValue, out SpecialTileEffect effect) { int targetIndex currentIndex diceValue; effect SpecialTileEffect.None; // 如果目标索引超出路径范围说明已经到达或超过终点棋子不再移动或触发胜利逻辑 if (targetIndex waypoints.Count) { // 简单处理停留在最后一个格子 targetIndex waypoints.Count - 1; return targetIndex; } // 检查目标格子是否为特殊格子 if (specialTiles.TryGetValue(targetIndex, out SpecialTileEffect tileEffect)) { effect tileEffect; // 根据效果再次调整目标索引 switch (tileEffect) { case SpecialTileEffect.JumpForward: targetIndex 3; // 向前跳3格 break; case SpecialTileEffect.JumpBackward: targetIndex - 2; // 向后跳2格 break; case SpecialTileEffect.BackToStart: targetIndex 0; // 回到起点 break; // StopOneTurn 不需要改变索引但需要影响玩家状态 } } return targetIndex; } public Vector3 GetWaypointPosition(int index) { if (index 0 index waypoints.Count) return waypoints[index]; return Vector3.zero; } }BoardManager管理了棋盘的物理路径和逻辑规则。GetTargetTileIndex方法是核心它根据骰子点数和当前格子计算出考虑特殊效果后的最终目标格子。2.2 实现棋子移动组件每个棋子都需要知道自己的当前位置格子索引和归属玩家并能够平滑移动到目标点。// PieceController.cs using UnityEngine; using System.Collections; public class PieceController : MonoBehaviour { public GameManager.Player owner; // 这个棋子属于哪个玩家 public int currentTileIndex 0; // 当前所在的格子索引起点为0 public float moveSpeed 5.0f; private bool isMoving false; // 由GameManager调用传入骰子点数 public void MoveSteps(int steps) { if (isMoving) return; StartCoroutine(MoveAlongPath(steps)); } IEnumerator MoveAlongPath(int steps) { isMoving true; int startIndex currentTileIndex; BoardManager.SpecialTileEffect finalEffect; // 获取最终的目标格子索引和可能触发的特殊效果 int targetIndex BoardManager.Instance.GetTargetTileIndex(startIndex, steps, out finalEffect); // 一格一格地移动产生动画效果 for (int i startIndex 1; i targetIndex; i) { Vector3 targetPos BoardManager.Instance.GetWaypointPosition(i); while (Vector3.Distance(transform.position, targetPos) 0.01f) { transform.position Vector3.MoveTowards(transform.position, targetPos, moveSpeed * Time.deltaTime); yield return null; } // 每到达一格可以触发音效或粒子效果 yield return new WaitForSeconds(0.1f); // 短暂停顿让移动更清晰 } // 更新棋子逻辑位置 currentTileIndex targetIndex; isMoving false; // 移动结束处理特殊效果 ProcessTileEffect(finalEffect); // 通知GameManager这个棋子的移动动作完成了 // 这里需要判断是否所有可移动棋子都已完成动作简化处理单个棋子移动完即回合结束 GameManager.Instance.ProcessTurnEnd(); } void ProcessTileEffect(BoardManager.SpecialTileEffect effect) { switch (effect) { case BoardManager.SpecialTileEffect.StopOneTurn: Debug.Log(${owner}玩家的棋子在特殊格子上下一回合暂停); // 可以为玩家添加一个“暂停”状态标记 break; case BoardManager.SpecialTileEffect.BackToStart: Debug.Log(${owner}玩家的棋子被送回起点); StartCoroutine(MoveBackToStart()); break; // 其他效果... default: break; } } IEnumerator MoveBackToStart() { // 快速移动回起点 Vector3 startPos BoardManager.Instance.GetWaypointPosition(0); while (Vector3.Distance(transform.position, startPos) 0.01f) { transform.position Vector3.MoveTowards(transform.position, startPos, moveSpeed * 2 * Time.deltaTime); yield return null; } currentTileIndex 0; } }PieceController使用协程MoveAlongPath来实现棋子的平滑移动动画。它每帧向下一路径点移动直到到达最终格子。移动结束后会处理格子效果并通知游戏管理器回合结束。3. 用户界面 (UI) 与交互反馈游戏需要UI来显示状态、提供操作按钮并反馈信息。我们将使用 Unity 的 UGUI 系统。3.1 创建基础游戏UI创建一个 Canvas并添加以下基本UI元素状态文本 (TextMeshPro - StatusText)显示当前玩家和游戏状态。骰子按钮 (Button)点击触发掷骰子。骰子点数显示 (TextMeshPro - DiceValueText)显示掷出的点数。获胜提示面板 (Panel)游戏结束时显示获胜者。为UI创建控制器脚本// UIManager.cs using UnityEngine; using TMPro; using UnityEngine.UI; public class UIManager : MonoBehaviour { public static UIManager Instance; public TextMeshProUGUI statusText; public TextMeshProUGUI diceValueText; public Button rollDiceButton; public GameObject winPanel; public TextMeshProUGUI winText; void Awake() { if (Instance null) Instance this; } void Start() { // 绑定按钮事件 rollDiceButton.onClick.AddListener(OnRollDiceButtonClicked); winPanel.SetActive(false); UpdateUI(); } void OnRollDiceButtonClicked() { GameManager.Instance.RollDice(); // 按钮点击后暂时禁用等待移动结束 rollDiceButton.interactable false; } // 此方法由GameManager在状态变化时调用 public void UpdateUI() { var gm GameManager.Instance; statusText.text $当前玩家: {gm.CurrentPlayer}\n状态: {gm.CurrentState}; if (gm.CurrentState GameManager.GameState.PlayerTurn) { rollDiceButton.interactable true; diceValueText.text 准备掷骰子; } else if (gm.CurrentState GameManager.GameState.RollingDice) { rollDiceButton.interactable false; diceValueText.text 骰子转动中...; } else if (gm.CurrentState GameManager.GameState.MovingPiece) { rollDiceButton.interactable false; diceValueText.text $移动中... 点数: {gm.CurrentDiceValue}; } else if (gm.CurrentState GameManager.GameState.GameOver) { rollDiceButton.interactable false; diceValueText.text 游戏结束; } } public void ShowWinPanel(GameManager.Player winner) { winPanel.SetActive(true); winText.text $恭喜{winner} 玩家获胜; } }UI管理器负责根据游戏状态更新界面元素。按钮点击事件直接调用GameManager的公共方法。UpdateUI方法需要被集成到游戏状态变化的各个节点进行调用。3.2 集成UI更新到游戏流程修改GameManager在关键状态变化处通知UI更新。// GameManager.cs (补充) void Start() { UIManager.Instance.UpdateUI(); } private void OnDiceRollFinished() { CurrentState GameState.MovingPiece; UIManager.Instance.UpdateUI(); // 找到当前玩家的一个棋子并移动简化逻辑只移动第一个棋子 MoveCurrentPlayerPiece(); } private void MoveCurrentPlayerPiece() { // 这里需要根据你的棋子管理方式找到当前玩家的一个可移动棋子 // 假设我们有一个列表存储了所有棋子 // PieceController targetPiece ...; // targetPiece.MoveSteps(CurrentDiceValue); } public void ProcessTurnEnd() { if (CheckPlayerWin(CurrentPlayer)) { CurrentState GameState.GameOver; UIManager.Instance.ShowWinPanel(CurrentPlayer); UIManager.Instance.UpdateUI(); return; } currentPlayerIndex (currentPlayerIndex 1) % playerOrder.Count; CurrentPlayer playerOrder[currentPlayerIndex]; CurrentState GameState.PlayerTurn; UIManager.Instance.UpdateUI(); }这样游戏状态、UI显示和玩家操作就联动起来了。4. 动画、音效与游戏优化基础逻辑完成后加入视听反馈能极大提升游戏体验。4.1 添加骰子滚动动画我们可以创建一个简单的骰子模型或UI图片通过快速切换面来模拟滚动。// DiceAnimator.cs using UnityEngine; using UnityEngine.UI; using System.Collections; public class DiceAnimator : MonoBehaviour { public Sprite[] diceFaces; // 6个骰子面的Sprite索引0-5对应点数1-6 public Image diceImageUI; // 用于显示骰子的UI Image组件 public float rollDuration 1.0f; // 滚动持续时间 public IEnumerator RollDiceAnimation(int finalValue) { float elapsedTime 0f; float interval 0.1f; // 切换面的间隔 while (elapsedTime rollDuration) { // 在最终结果确定前随机显示一个面 int randomFace Random.Range(0, 6); diceImageUI.sprite diceFaces[randomFace]; yield return new WaitForSeconds(interval); elapsedTime interval; } // 动画结束显示最终点数对应的面 diceImageUI.sprite diceFaces[finalValue - 1]; // 点数1对应索引0 } }在GameManager.RollDice方法中启动这个动画协程并在动画结束后调用OnDiceRollFinished。4.2 添加音效在 Unity 中创建 AudioSource 组件并为不同事件掷骰子、移动、到达特殊格子、获胜分配 AudioClip。// SoundManager.cs using UnityEngine; public class SoundManager : MonoBehaviour { public static SoundManager Instance; public AudioSource audioSource; public AudioClip diceRollSound; public AudioClip pieceMoveSound; public AudioClip specialTileSound; public AudioClip winSound; void Awake() { if (Instance null) Instance this; } public void PlayDiceRoll() PlaySound(diceRollSound); public void PlayPieceMove() PlaySound(pieceMoveSound); public void PlaySpecialTile() PlaySound(specialTileSound); public void PlayWin() PlaySound(winSound); private void PlaySound(AudioClip clip) { if (clip ! null audioSource ! null) { audioSource.PlayOneShot(clip); } } }在GameManager和PieceController的相应位置调用SoundManager.Instance.PlayXXX()方法。4.3 性能与代码优化建议对象池管理棋子频繁实例化/销毁棋子会产生GC。可以在游戏初始化时为每个玩家创建好棋子如4个并放入对象池仅通过激活/失活来控制显示。使用 ScriptableObject 管理游戏数据将棋盘路径、特殊格子位置、玩家颜色等配置数据抽离到 ScriptableObject 中便于设计和平衡调整无需修改代码。状态机明确化当前GameState枚举可以进一步细化并使用专门的状态机模式如状态模式或 Unity Animator 状态机来管理使逻辑更清晰。输入控制除了UI按钮可以考虑增加键盘快捷键如空格键掷骰子并做好输入状态判断防止在动画播放时重复触发。5. 常见问题排查与调试在开发过程中你可能会遇到以下典型问题。5.1 棋子移动位置不正确问题现象可能原因检查方式处理建议棋子不移动或瞬移waypoints列表为空或顺序错误在BoardManager的Awake或Start中打印waypoints.Count并在Scene视图查看Waypoint物体的顺序。确保所有路径点都正确标记了“Waypoint”标签并且作为BoardManager游戏对象的子物体顺序即行走顺序。棋子移动终点偏移路径点位置是局部坐标而非世界坐标检查waypoints中存储的是transform.position世界坐标还是transform.localPosition。在InitializeWaypoints中应使用child.position世界坐标或根据你的棋盘设计决定。移动动画卡顿协程MoveAlongPath中的移动逻辑或等待时间有问题检查moveSpeed值是否过小或WaitForSeconds的间隔是否过长。调整moveSpeed如10减少中间等待时间。确保在while循环中使用Time.deltaTime乘以速度。5.2 游戏状态流转混乱问题现象可能原因检查方式处理建议玩家可以连续掷骰子RollDice方法的状态检查未生效或UI按钮未及时禁用在RollDice开始处添加Debug.Log打印当前状态。检查UIManager.UpdateUI中按钮交互状态的逻辑。确保CurrentState在MovingPiece和ProcessingTileEffect等状态时RollDice方法会提前返回。在状态切换后立即调用UIManager.Instance.UpdateUI()。回合结束后未切换玩家ProcessTurnEnd方法未被调用或玩家索引计算错误在PieceController.MoveAlongPath协程末尾的ProcessTurnEnd调用处打断点或打印日志。确保棋子移动协程结束后能正确调用GameManager.Instance.ProcessTurnEnd()。检查currentPlayerIndex的自增和取模逻辑。5.3 UI显示不更新问题现象可能原因检查方式处理建议状态文本始终不变UIManager.UpdateUI()未被调用在UpdateUI方法开始处添加Debug.Log。在GameManager中所有改变CurrentState或CurrentPlayer的地方都调用一次UIManager.Instance.UpdateUI()。获胜面板不显示winPanel未在Inspector中赋值或SetActive条件不满足检查UIManager脚本组件上winPanel变量是否关联。检查ShowWinPanel方法是否被调用。在Unity编辑器中拖拽赋值。在GameManager.ProcessTurnEnd的胜利分支中调用UIManager.Instance.ShowWinPanel(winner)。5.4 音效或动画不播放问题现象可能原因检查方式处理建议掷骰子没有声音AudioClip未赋值或AudioSource未配置检查SoundManager的Inspector面板确认diceRollSound等字段已拖入音频文件。确认audioSource组件所在物体未被禁用。为SoundManager所在的游戏对象添加AudioSource组件并将该组件拖拽到audioSource字段。在播放声音前可以添加Debug.Log确认方法被触发。骰子动画不显示最终点数动画协程提前结束或最终点数索引错误检查RollDiceAnimation协程中的finalValue参数是否正确1-6。检查diceFaces数组长度是否为6且顺序对应点数1-6。确保finalValue - 1作为数组索引是有效的。在协程结束后可以手动设置一次diceImageUI.sprite以确保无误。6. 项目构建与发布准备完成所有功能开发与测试后最后一步是构建可执行文件。场景管理确保所有游戏对象棋盘、棋子、UI Canvas、管理器等都放在一个或多个场景中。通常主游戏场景包含全部内容。构建设置点击菜单栏File - Build Settings。将当前主场景拖入Scenes In Build列表。选择目标平台如 PC, Mac Linux Standalone。点击Player Settings...可以设置公司名、产品名、图标、分辨率等。分辨率与UI适配在Player Settings的Resolution and Presentation中可以设置默认窗口大小。确保你的 Canvas 的Canvas Scaler组件设置正确例如Scale With Screen Size以适应不同分辨率。构建与运行在 Build Settings 窗口点击Build选择输出文件夹Unity 会生成可执行文件和相关数据文件。运行该可执行文件进行最终测试。通过以上步骤你已经完成了一个具备核心玩法的 Unity 2D 飞行棋游戏。这个项目涵盖了状态管理、物理移动、UI交互、动画音效等基础模块。要将其打磨得更完善可以考虑加入更多功能如多个棋子选择规则、网络对战、存档系统、更丰富的视觉特效等。最重要的是通过这个项目理解如何将复杂的游戏规则分解为可管理的代码模块这是独立游戏开发的关键能力。