在上一篇文章中我们已经完成了飞行棋游戏的核心框架搭建包括棋盘生成、棋子移动逻辑、玩家回合切换以及基础的UI交互。本篇我们将深入项目实战的下半部分聚焦于游戏规则的完善、动画效果的添加、音效集成、游戏状态管理以及最终的打包发布。通过本篇的学习你将掌握如何将一个功能性的原型打磨成一个体验完整、可供发布的2D游戏项目。1. 完善游戏核心规则与逻辑一个完整的飞行棋游戏不仅需要棋子能走还需要处理丰富的游戏规则例如起飞、跳跃、撞击、终点判定等。这些规则是游戏趣味性的核心。1.1 实现“起飞”与“掷骰规则”在飞行棋中棋子通常需要掷出特定的点数如6点才能从基地“起飞”到起点。同时掷出6点通常可以获得额外一次掷骰机会。首先我们修改GameManager中的掷骰逻辑。// 文件路径Assets/Scripts/Managers/GameManager.cs using System.Collections; using UnityEngine; using UnityEngine.UI; public class GameManager : MonoBehaviour { // ... 其他变量声明 ... [Header(掷骰规则)] public int diceNumberForTakeOff 6; // 起飞所需骰子点数 private bool hasExtraTurn false; // 是否获得额外回合 // 修改后的掷骰方法 public void RollDice() { if (!isPlayerTurn || isMoving) return; int roll Random.Range(1, 7); // 生成1-6的随机数 diceResultText.text 点数: roll; Debug.Log($玩家{currentPlayerIndex}掷出: {roll}); // 处理掷骰结果 ProcessDiceRoll(roll); } private void ProcessDiceRoll(int roll) { // 检查当前玩家是否有棋子尚未起飞 bool hasPieceInBase CheckIfPlayerHasPieceInBase(currentPlayerIndex); if (hasPieceInBase) { // 有棋子未起飞检查是否满足起飞条件 if (roll diceNumberForTakeOff) { // 满足起飞条件让玩家选择一枚棋子起飞 UIManager.Instance.ShowMessage($掷出{roll}点请选择一枚棋子起飞。); // 这里可以触发一个UI让玩家点击基地中的棋子我们简化处理自动选择第一枚未起飞的棋子 TryTakeOffFirstPiece(currentPlayerIndex); hasExtraTurn true; // 掷出6点获得额外回合 } else { // 不满足起飞条件回合结束 UIManager.Instance.ShowMessage($需要掷出{diceNumberForTakeOff}点才能起飞。); hasExtraTurn false; EndTurn(); } } else { // 所有棋子均已起飞正常移动 pendingMoveSteps roll; // 检查是否有棋子可以移动例如不在终点格且移动后不超出范围 if (CanAnyPieceMove(currentPlayerIndex, roll)) { UIManager.Instance.ShowMessage($移动{roll}步。); // 等待玩家选择棋子通过棋子上的脚本触发 } else { UIManager.Instance.ShowMessage($没有棋子可以移动{roll}步。); hasExtraTurn false; EndTurn(); } // 判断是否获得额外回合规则掷出6点且不是起飞时 if (roll diceNumberForTakeOff !hasPieceInBase) { hasExtraTurn true; UIManager.Instance.ShowMessage($掷出6点再掷一次); } } // 如果获得额外回合不切换玩家并允许再次掷骰 if (hasExtraTurn) { StartCoroutine(PrepareForExtraTurn()); } } private bool CheckIfPlayerHasPieceInBase(int playerIndex) { // 遍历该玩家的所有棋子检查是否在基地位置索引为-1 foreach (var piece in playerPieces[playerIndex]) { if (piece.currentBoardIndex 0) // 假设-1表示在基地 return true; } return false; } private void TryTakeOffFirstPiece(int playerIndex) { foreach (var piece in playerPieces[playerIndex]) { if (piece.currentBoardIndex 0) { // 找到第一枚在基地的棋子将其移动到该玩家的起点例如索引0 piece.MoveToPosition(0); // 假设0是起点 break; } } } private bool CanAnyPieceMove(int playerIndex, int steps) { // 简化逻辑检查是否有棋子不在终点且移动后索引不超过棋盘最大索引 // 实际规则更复杂可能涉及终点区、迭子等 foreach (var piece in playerPieces[playerIndex]) { if (piece.currentBoardIndex 0 !piece.IsInFinalTrack()) { // 这里需要根据棋盘逻辑判断移动后是否合法 return true; } } return false; } private IEnumerator PrepareForExtraTurn() { yield return new WaitForSeconds(1.0f); // 等待一下让玩家看清信息 // 重置掷骰按钮状态允许再次掷骰 isPlayerTurn true; UIManager.Instance.ShowMessage($玩家{currentPlayerIndex 1}的额外回合。); } // ... 其他方法 ... }1.2 实现“撞击”与“返回基地”规则飞行棋最刺激的规则之一就是“撞击”当一枚棋子移动到已有敌方棋子的格子时会将敌方棋子“撞回”其基地。我们需要修改PieceController的移动逻辑并在移动结束时检查目标格状态。// 文件路径Assets/Scripts/PieceController.cs using UnityEngine; public class PieceController : MonoBehaviour { // ... 其他变量 ... public void MoveToPosition(int targetIndex) { StartCoroutine(MoveStepByStep(targetIndex)); } private IEnumerator MoveStepByStep(int targetIndex) { GameManager.Instance.isMoving true; int currentIndex currentBoardIndex; int stepDirection targetIndex currentIndex ? 1 : -1; while (currentIndex ! targetIndex) { currentIndex stepDirection; // 确保索引在棋盘路径范围内需要访问BoardManager currentIndex Mathf.Clamp(currentIndex, 0, BoardManager.Instance.pathPositions.Count - 1); Vector3 nextPos BoardManager.Instance.pathPositions[currentIndex].position; // 添加移动动画见下一节 yield return StartCoroutine(SmoothMove(transform.position, nextPos, 0.2f)); transform.position nextPos; // 每移动一步都检查是否撞击可选根据规则通常只在最终落点撞击 // CheckCollisionAt(currentIndex); } currentBoardIndex targetIndex; // 移动结束后检查最终落点的撞击 CheckCollisionAt(targetIndex); GameManager.Instance.isMoving false; // 通知GameManager移动完成处理回合结束或额外逻辑 GameManager.Instance.OnPieceMoveFinished(this); } private void CheckCollisionAt(int boardIndex) { // 获取当前格子上所有棋子 GameObject[] allPieces GameObject.FindGameObjectsWithTag(Piece); foreach (GameObject otherPieceObj in allPieces) { if (otherPieceObj this.gameObject) continue; PieceController otherPiece otherPieceObj.GetComponentPieceController(); if (otherPiece ! null otherPiece.currentBoardIndex boardIndex) { // 检查是否为敌方棋子 if (otherPiece.playerIndex ! this.playerIndex) { Debug.Log($撞击玩家{this.playerIndex}的棋子将玩家{otherPiece.playerIndex}的棋子撞回基地); // 将被撞击的棋子送回基地 otherPiece.SendBackToBase(); } } } } public void SendBackToBase() { // 重置棋子位置到其所属玩家的基地 currentBoardIndex -1; // -1 表示在基地 Vector3 basePosition GetBasePosition(playerIndex); transform.position basePosition; // 可以添加被撞回基地的动画或音效 } private Vector3 GetBasePosition(int pIndex) { // 假设基地位置存储在BoardManager或GameManager中 // 这里返回一个预设的坐标 switch (pIndex) { case 0: return new Vector3(-6, -3, 0); case 1: return new Vector3(6, -3, 0); case 2: return new Vector3(-6, 3, 0); case 3: return new Vector3(6, 3, 0); default: return Vector3.zero; } } public bool IsInFinalTrack() { // 判断棋子是否已进入终点前的专属轨道假设索引大于某个值为终点区 // 需要根据棋盘设计实现 return currentBoardIndex BoardManager.Instance.pathPositions.Count - 4; } // ... SmoothMove协程 ... }同时需要在GameManager中添加OnPieceMoveFinished方法用于处理移动结束后的逻辑例如判断是否获得额外回合如刚好到达特定格子、检查游戏是否结束等。2. 添加动画与视觉效果流畅的动画能极大提升游戏体验。我们将为棋子的移动、骰子滚动和UI反馈添加动画。2.1 棋子平滑移动动画我们已经在上面的MoveStepByStep中调用了SmoothMove协程现在来实现它。// 在 PieceController.cs 中添加 private IEnumerator SmoothMove(Vector3 start, Vector3 end, float duration) { float elapsed 0f; while (elapsed duration) { transform.position Vector3.Lerp(start, end, elapsed / duration); elapsed Time.deltaTime; yield return null; // 等待下一帧 } transform.position end; // 确保最终位置准确 }2.2 骰子滚动动画创建一个独立的骰子控制器让它旋转并最终显示一个随机点数。// 文件路径Assets/Scripts/DiceController.cs using UnityEngine; using UnityEngine.UI; using System.Collections; public class DiceController : MonoBehaviour { public GameObject diceModel; // 一个3D骰子模型或一个包含6个面的Sprite组 public Text diceResultUI; public float rollDuration 1.5f; public float rollSpeed 10f; private bool isRolling false; private int finalResult; // 供UI按钮调用 public void StartRollDice() { if (!isRolling) { StartCoroutine(RollDiceAnimation()); } } private IEnumerator RollDiceAnimation() { isRolling true; float elapsed 0f; finalResult Random.Range(1, 7); // 预先决定结果 // 动画阶段快速旋转 while (elapsed rollDuration) { // 如果是3D模型 if (diceModel ! null) diceModel.transform.Rotate(Random.onUnitSphere * rollSpeed * Time.deltaTime); // 如果是2D Sprite可以快速切换图片 // ChangeDiceFace(Random.Range(0,6)); elapsed Time.deltaTime; yield return null; } // 显示最终结果 ShowDiceResult(finalResult); isRolling false; // 通知GameManager掷骰结果 GameManager.Instance.ReceiveDiceResult(finalResult); } private void ShowDiceResult(int result) { // 停止旋转显示对应的面 if (diceModel ! null) { // 这里需要根据骰子模型和结果设置正确的朝向这是一个简化示例 diceModel.transform.rotation Quaternion.identity; } diceResultUI.text result.ToString(); Debug.Log($骰子动画结束结果: {result}); } // 供GameManager调用的简化方法如果不用动画 public int GetRandomDiceNumber() { return Random.Range(1, 7); } }然后修改GameManager使其与DiceController协作。// 在 GameManager.cs 中添加 public DiceController diceController; public void RollDice() { if (!isPlayerTurn || isMoving) return; // 改为触发骰子动画 diceController.StartRollDice(); } // 新增方法用于接收骰子动画结束后的结果 public void ReceiveDiceResult(int result) { // 处理掷骰结果逻辑 ProcessDiceRoll(result); }2.3 UI反馈动画使用 Unity 的Animator或简单的脚本来为UI元素添加动画例如显示“起飞”、“撞击”等文字提示的淡入淡出。// 文件路径Assets/Scripts/UI/MessagePopup.cs using UnityEngine; using UnityEngine.UI; using System.Collections; public class MessagePopup : MonoBehaviour { public Text messageText; public float displayTime 2f; public float fadeDuration 0.5f; private CanvasGroup canvasGroup; void Awake() { canvasGroup GetComponentCanvasGroup(); if (canvasGroup null) canvasGroup gameObject.AddComponentCanvasGroup(); HideMessage(); } public void ShowMessage(string msg) { messageText.text msg; StartCoroutine(ShowAndHide()); } private IEnumerator ShowAndHide() { // 淡入 yield return StartCoroutine(FadeCanvasGroup(0f, 1f, fadeDuration)); // 保持显示 yield return new WaitForSeconds(displayTime); // 淡出 yield return StartCoroutine(FadeCanvasGroup(1f, 0f, fadeDuration)); } private IEnumerator FadeCanvasGroup(float from, float to, float duration) { float elapsed 0f; while (elapsed duration) { canvasGroup.alpha Mathf.Lerp(from, to, elapsed / duration); elapsed Time.deltaTime; yield return null; } canvasGroup.alpha to; } private void HideMessage() { canvasGroup.alpha 0f; } }在UIManager中调用MessagePopup来显示信息。3. 集成音效与背景音乐声音是游戏氛围的重要组成部分。我们将管理背景音乐和游戏音效如掷骰声、移动声、撞击声。3.1 创建音频管理器// 文件路径Assets/Scripts/Managers/AudioManager.cs using UnityEngine; using System.Collections.Generic; public class AudioManager : MonoBehaviour { public static AudioManager Instance; [Header(音频源)] public AudioSource musicSource; public AudioSource sfxSource; [Header(音频剪辑)] public AudioClip backgroundMusic; public AudioClip diceRollSFX; public AudioClip pieceMoveSFX; public AudioClip collisionSFX; public AudioClip victorySFX; private Dictionarystring, AudioClip sfxClips; void Awake() { if (Instance null) { Instance this; DontDestroyOnLoad(gameObject); // 跨场景播放音乐 } else { Destroy(gameObject); return; } // 初始化字典 sfxClips new Dictionarystring, AudioClip { {DiceRoll, diceRollSFX}, {PieceMove, pieceMoveSFX}, {Collision, collisionSFX}, {Victory, victorySFX} }; PlayBackgroundMusic(); } public void PlayBackgroundMusic() { if (musicSource ! null backgroundMusic ! null) { musicSource.clip backgroundMusic; musicSource.loop true; musicSource.Play(); } } public void PlaySFX(string clipName) { if (sfxSource ! null sfxClips.ContainsKey(clipName) sfxClips[clipName] ! null) { sfxSource.PlayOneShot(sfxClips[clipName]); } else { Debug.LogWarning($音效 {clipName} 未找到或未分配。); } } public void SetMusicVolume(float volume) { if (musicSource ! null) musicSource.volume volume; } public void SetSFXVolume(float volume) { if (sfxSource ! null) sfxSource.volume volume; } }3.2 在游戏逻辑中触发音效在相应的地方调用AudioManager.Instance.PlaySFX()。掷骰时在DiceController.StartRollDice()中调用。棋子移动每一步在PieceController.SmoothMove开始时或结束时调用。发生撞击时在PieceController.CheckCollisionAt中调用。游戏胜利时在GameManager.CheckWinCondition中调用。例如修改DiceController// 在 DiceController.StartRollDice() 协程开始处添加 AudioManager.Instance.PlaySFX(DiceRoll);4. 游戏状态管理与胜利条件我们需要一个清晰的状态机来管理游戏的各个阶段并精确判断胜利者。4.1 定义游戏状态// 可以在 GameManager.cs 开头定义枚举 public enum GameState { MainMenu, // 主菜单 PlayerSetup, // 玩家设置几人游戏 Playing, // 游戏中 Paused, // 暂停 GameOver // 游戏结束 }在GameManager中添加当前状态变量和切换状态的方法。4.2 实现胜利条件检测胜利条件是一名玩家的所有四枚棋子都准确到达终点。我们需要在每枚棋子移动结束后进行检查。首先为PieceController添加一个状态标记其是否已到达终点。// 在 PieceController.cs 中添加 public bool isInGoal { get; private set; } false; public void ReachGoal() { isInGoal true; // 可以播放庆祝动画禁用交互等 GetComponentCollider2D().enabled false; // 如果是2D碰撞体 // 改变颜色或透明度 SpriteRenderer sr GetComponentSpriteRenderer(); if (sr ! null) sr.color new Color(sr.color.r, sr.color.g, sr.color.b, 0.5f); }然后修改移动逻辑当棋子移动到终点格时调用ReachGoal()。这需要BoardManager能判断某个格子索引是否为对应玩家的终点。最后在GameManager中添加检查方法// 在 GameManager.cs 中添加 private void CheckWinCondition() { foreach (var playerPiecesList in playerPieces) { bool allPiecesInGoal true; foreach (var piece in playerPiecesList) { if (piece ! null !piece.isInGoal) { allPiecesInGoal false; break; } } if (allPiecesInGoal) { int winningPlayerIndex System.Array.IndexOf(playerPieces, playerPiecesList); GameOver(winningPlayerIndex); return; } } } private void GameOver(int winnerIndex) { currentState GameState.GameOver; Debug.Log($游戏结束玩家 {winnerIndex 1} 获胜); AudioManager.Instance.PlaySFX(Victory); // 显示胜利UI UIManager.Instance.ShowGameOverPanel(winnerIndex 1); // 禁用游戏输入 isPlayerTurn false; }在每次棋子移动完成OnPieceMoveFinished后调用CheckWinCondition()。5. 构建可发布的游戏完成所有功能后我们需要将游戏打包成可执行文件以便分享或发布。5.1 项目设置优化场景管理确保Build Settings中包含了所有需要的场景如主菜单、游戏场景。通常顺序是0: MainMenu, 1: GameScene。分辨率与UI适配在Canvas Scaler组件上设置合适的 UI 缩放模式例如Scale With Screen Size确保UI在不同分辨率下显示正常。图标与名称在Player Settings中设置公司名、产品名和游戏图标。关闭开发日志在File - Build Settings - Player Settings - Resolution and Presentation中取消勾选Run In Background旁边的Display Resolution Dialog。对于发布版本可以考虑在脚本中使用条件编译来禁用Debug.Log。5.2 构建与打包打开File - Build Settings。将你的主场景如MainMenu拖入Scenes In Build列表并确保其索引为0。选择目标平台如PC, Mac Linux Standalone。点击Player Settings...进行详细设置如窗口大小、默认分辨率等。点击Build选择输出文件夹Unity 将开始编译并生成可执行文件.exe和数据文件夹。5.3 发布后测试务必在打包后的独立程序中测试所有功能因为某些行为如文件读取路径、初始分辨率在编辑器模式和独立运行时可能不同。6. 常见问题与优化建议6.1 常见问题排查问题现象可能原因解决思路棋子移动后位置不对棋盘路径点Waypoints顺序或坐标错误。在场景中可视化BoardManager的路径点检查顺序和位置。确保PieceController.MoveToPosition使用的索引与路径点列表匹配。撞击规则不生效CheckCollisionAt方法未被调用或棋子标签Tag未设置。确保移动结束时调用了碰撞检查。为所有棋子 GameObject 设置统一的 Tag如“Piece”。音效不播放AudioManager未初始化或音频剪辑未赋值或音量为零。检查场景中是否存在AudioManager实例。在 Inspector 中为AudioManager的各个AudioClip字段分配对应的音频文件。检查AudioSource组件的Volume和Mute状态。构建后UI错位Canvas Scaler 设置不当或UI锚点未正确设置。使用Canvas Scaler组件模式选Scale With Screen Size参考分辨率设为常用分辨率如1920x1080。检查所有UI元素的锚点Anchors是否相对于父物体或屏幕正确对齐。游戏卡顿每帧操作过多如FindGameObjectsWithTag在 Update 中调用或存在内存泄漏。避免在Update中频繁使用Find、GetComponent等耗时操作。将结果缓存起来。使用性能分析器Profiler定位瓶颈。6.2 性能与代码优化建议对象池管理棋子频繁实例化Instantiate和销毁Destroy棋子会产生GC垃圾回收开销。可以预先创建好所有棋子不用时隐藏或禁用使用时再激活。缓存组件引用在Awake或Start中使用GetComponent并将结果存储在私有变量中避免在Update中反复获取。使用事件系统解耦GameManager、UIManager、AudioManager之间通过直接引用调用耦合度较高。可以考虑使用 C# 事件event和delegate或观察者模式来降低耦合例如当掷骰完成时发布一个事件由GameManager订阅并处理。棋盘数据配置化将棋盘格子的属性如是否为安全格、跳跃格、终点格等存储在 ScriptableObject 或配置文件中便于设计和修改而不是硬编码在脚本里。添加本地化支持如果考虑多语言可以将所有UI文本提取到本地化文件中。7. 项目扩展思路至此一个基础但完整的飞行棋游戏已经完成。你可以在此基础上进行扩展增加更多趣味性道具系统在特定格子添加“道具”如“再掷一次”、“暂停一回合”、“与前方棋子交换位置”等。AI对手实现简单的电脑AI使其能够根据当前局面如棋子安全度、距离终点距离决定移动哪枚棋子。网络对战使用 Unity 的 Netcode 或第三方库如 Mirror实现局域网或在线多人对战。更复杂的棋盘设计非线性的棋盘路径分支路线传送门等。数据持久化保存玩家胜率、游戏设置如音效大小、规则偏好到本地。粒子特效为棋子移动、撞击、胜利添加粒子系统增强视觉冲击力。通过这个从零到一的实战项目你不仅掌握了 Unity 2D 游戏开发的基本流程场景搭建、脚本编写、物理/UI交互、动画音效集成、打包发布更重要的是学会了如何将复杂的游戏规则分解为可管理的代码模块并逐步迭代完善。