十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Unity 2D飞行棋实战:从零构建完整游戏项目与核心逻辑实现

Unity 2D飞行棋实战:从零构建完整游戏项目与核心逻辑实现 1. 先搞清楚这个“完整飞行棋项目”到底要做什么如果你看到“从零构建完整飞行棋项目”这个标题最该关心的不是Unity或C#的语法细节而是这个项目做完之后你手里会有一个什么样的东西。一个“完整”的飞行棋游戏意味着它至少包含几个核心模块一个可交互的棋盘、能按规则移动的棋子、一个或多个玩家轮流掷骰子的逻辑、以及判定胜负的条件。对于想通过实战学习Unity 2D开发的人来说这个项目最大的价值在于它能让你把UI系统、物理或伪物理移动、状态管理、事件驱动这些抽象概念串成一个看得见、能运行、可扩展的具体案例。很多人学Unity容易陷入两个极端要么只看官方文档和零散教程知识点是散的要么一上来就想做复杂的大项目结果卡在架构上。飞行棋这个选题刚好卡在中间——它规则明确逻辑闭环但实现过程中会逼着你解决2D游戏开发里最常见的问题比如精灵Sprite的导入与布局、UI与游戏世界的交互、基于状态机的回合控制、以及如何用C#脚本把这一切粘合起来。所以无论你是刚学完C#基础想找项目练手还是有一定Unity经验但没独立完成过一个完整游戏流程这个实战都能帮你把知识“缝”起来。在开始动手之前我建议先抛开代码在纸上或思维导图里拆解一下飞行棋的需求。你需要一个NxN的棋盘格子每个格子是一个位置状态四种颜色的棋子每种颜色N个一个随机生成1-6点数的骰子以及控制游戏流程的“裁判”逻辑。想清楚这些再打开Unity你的每一步操作目标都会明确很多。2. 搭建项目骨架从导入素材到创建基础场景动手的第一步不是写代码而是把“舞台”搭好。对于2D游戏Unity的初始设置很关键。2.1 项目初始化与素材准备打开Unity Hub创建一个新的2D项目。项目名称和位置按自己习惯来但渲染管线Render Pipeline这里建议选择URPUniversal Render Pipeline。虽然飞行棋对图形要求不高但URP是Unity现在主推的管线学习它对于未来做其他2D项目尤其是需要灯光、后处理效果的更有好处。创建后在Project窗口建立清晰的文件夹结构例如Art/Sprites存放图片、Scripts存放C#脚本、Prefabs存放预制体、Scenes存放场景、Audio存放音效。飞行棋的素材可以自己用绘图工具简单绘制也可以找一些免费的2D素材。你需要准备棋盘背景图一张完整的棋盘图片或者用多个格子图块Tile拼成。棋子精灵四种颜色如红、黄、蓝、绿的棋子图片每种颜色准备多个通常为4个。骰子精灵6张图片分别对应骰子的1点到6点。或者用3D骰子模型但2D项目用图片更简单。UI素材按钮如“掷骰子”、“下一个玩家”、面板、数字字体等。将准备好的图片拖入Art/Sprites文件夹。选中这些图片在Inspector面板中确保Texture Type设置为Sprite (2D and UI)并根据图片内容设置Pixels Per Unit例如如果棋子图片是64x64像素希望它在游戏中显示为1个单位大小就设为64。点击Apply。2.2 构建游戏场景在Hierarchy窗口右键创建2D Object - Sprite将其重命名为Board然后把棋盘背景图拖拽到它的Sprite Renderer组件的Sprite属性上。调整Board的Transform位置和缩放让它居中并填满相机视野的大部分区域。接下来创建棋子。在Art/Sprites文件夹中选中一个棋子图片直接拖拽到Scene视图的棋盘起点位置比如棋盘四个角的某个格子。Unity会自动为其创建一个GameObject。在Inspector面板中将其重命名为RedPiece1然后点击Add Component添加一个Box Collider 2D用于后续点击检测。不要急着创建所有棋子我们先做好一个然后把它做成预制体Prefab。在Project窗口中创建一个Prefabs文件夹。然后将Hierarchy中的RedPiece1拖入这个文件夹蓝色图标表示预制体创建成功。之后你可以删除Hierarchy中的这个实例或者保留它作为参考。用同样的方法创建其他颜色棋子的预制体。2.3 设置2D相机与UI画布Unity新建的2D项目自带一个Main Camera其Projection默认为Orthographic正交投影这很适合2D游戏。检查一下相机的Size属性调整它直到能完整看到棋盘。然后创建UI。右键Hierarchy -UI - Canvas这会自动创建一个Canvas画布和一个EventSystem。Canvas是所有UI元素的容器。选中Canvas在Inspector中将UI Scale Mode设置为Scale With Screen Size参考分辨率可以设为1920 x 1080这样UI在不同屏幕分辨率下能自适应缩放。在Canvas下右键创建UI - Button重命名为RollDiceButton调整其位置和大小将按钮文本改为“掷骰子”。用同样的方法可以创建一个Text元素来显示当前玩家信息和骰子点数重命名为InfoText。至此一个最基础的视觉框架就搭好了。虽然还不能动但你已经拥有了棋盘、棋子预制体、操作按钮和显示区域。接下来就是用C#脚本让它们“活”起来。3. 编写核心游戏逻辑状态、骰子与棋子移动游戏逻辑是项目的灵魂。我们分步实现先从游戏状态管理和骰子开始。3.1 创建游戏管理器GameManager游戏管理器是一个单例脚本负责管理游戏全局状态如当前玩家、游戏阶段、胜负判定等。在Scripts文件夹右键创建C#脚本命名为GameManager。using UnityEngine; using UnityEngine.UI; public class GameManager : MonoBehaviour { public static GameManager Instance; // 单例实例 public enum GameState { WaitingForRoll, MovingPiece, SwitchingPlayer, GameOver } public GameState currentState; public int currentPlayerIndex 0; // 0:红, 1:黄, 2:蓝, 3:绿 public Color[] playerColors; // 在Inspector中分配颜色用于UI提示 public Text infoText; private void Awake() { if (Instance null) { Instance this; } else { Destroy(gameObject); } currentState GameState.WaitingForRoll; UpdateUI(); } void UpdateUI() { if (infoText ! null) { infoText.text $玩家 {currentPlayerIndex 1} 的回合 ({playerColors[currentPlayerIndex].ToString()}) - 请掷骰子; } } // 这个方法将由骰子脚本在掷骰结束后调用 public void OnDiceRollFinished(int diceValue) { if (currentState ! GameState.WaitingForRoll) return; infoText.text $玩家 {currentPlayerIndex 1} 掷出: {diceValue}; currentState GameState.MovingPiece; // 这里触发棋子移动逻辑我们稍后实现 Debug.Log($玩家{currentPlayerIndex}掷出{diceValue}点应移动棋子); } public void SwitchToNextPlayer() { currentPlayerIndex (currentPlayerIndex 1) % 4; // 假设4个玩家 currentState GameState.WaitingForRoll; UpdateUI(); } }将这个脚本挂载到场景中一个空的GameObject上命名为_GameManager。在Inspector中将Info Text拖拽赋值并为Player Colors数组分配四种颜色。3.2 实现骰子系统骰子可以用随机数模拟但为了视觉效果我们做一个简单的动画。创建一个DiceController脚本。using UnityEngine; using UnityEngine.UI; public class DiceController : MonoBehaviour { public Sprite[] diceFaces; // 在Inspector中按顺序分配1-6点的骰子面图片 public Image diceImage; // 用于显示骰子图片的UI Image组件 public Button rollButton; // “掷骰子”按钮 private bool isRolling false; private float rollDuration 1.5f; // 掷骰动画持续时间 private float rollTimer 0f; private int finalDiceValue 1; void Start() { if (rollButton ! null) { rollButton.onClick.AddListener(StartRoll); } diceImage.sprite diceFaces[0]; // 初始显示1点 } void Update() { if (isRolling) { rollTimer Time.deltaTime; // 在动画期间快速切换骰子面模拟滚动 int randomFace Random.Range(0, 6); diceImage.sprite diceFaces[randomFace]; if (rollTimer rollDuration) { FinishRoll(); } } } void StartRoll() { if (isRolling || GameManager.Instance.currentState ! GameManager.GameState.WaitingForRoll) return; isRolling true; rollTimer 0f; rollButton.interactable false; // 掷骰过程中禁用按钮 finalDiceValue Random.Range(1, 7); // 提前决定最终点数 Debug.Log($骰子最终点数已决定: {finalDiceValue}); } void FinishRoll() { isRolling false; diceImage.sprite diceFaces[finalDiceValue - 1]; // 显示最终点数对应的图片 rollButton.interactable true; // 通知游戏管理器掷骰完成 GameManager.Instance.OnDiceRollFinished(finalDiceValue); } }创建一个UIImage作为骰子显示器将DiceController脚本挂载上去并在Inspector中把diceFaces数组按顺序赋好值把rollButton和diceImage拖拽赋值。3.3 实现棋盘格子与棋子移动这是最复杂的部分。我们需要一个数据结构来表示棋盘。创建棋盘数据创建一个BoardManager脚本或者将这部分逻辑整合到GameManager中。这里为了清晰我们创建一个Board脚本管理格子。using UnityEngine; using System.Collections.Generic; public class Board : MonoBehaviour { public static Board Instance; public Transform[] waypoints; // 在Inspector中按顺序拖入所有路径点的Transform // 可以用空物体作为路径点在Scene视图中摆放到棋盘格子上 private void Awake() { Instance this; } // 根据棋子当前位置和步数计算目标位置索引 public int GetTargetWaypointIndex(int currentIndex, int steps) { int targetIndex currentIndex steps; // 简单处理如果超出最大索引则不再前进实际飞行棋有跳跃、终点等规则此处简化 if (targetIndex waypoints.Length) { targetIndex waypoints.Length - 1; } return targetIndex; } public Vector3 GetWaypointPosition(int index) { if (index 0 index waypoints.Length) { return waypoints[index].position; } return Vector3.zero; } }在场景中创建空物体重命名为Waypoint_0,Waypoint_1... 按照飞行棋棋盘路径顺序摆放好然后全部拖拽到Board脚本的Waypoints数组里。创建棋子逻辑为棋子预制体创建脚本Piece。using UnityEngine; public class Piece : MonoBehaviour { public int playerID; // 0红, 1黄... public int pieceID; // 第几个棋子 public int currentWaypointIndex 0; // 当前所在路径点索引 private bool isMoving false; private Vector3 targetPosition; public float moveSpeed 5f; void Update() { if (isMoving) { transform.position Vector3.MoveTowards(transform.position, targetPosition, moveSpeed * Time.deltaTime); if (Vector3.Distance(transform.position, targetPosition) 0.01f) { isMoving false; // 移动结束通知游戏管理器 GameManager.Instance.OnPieceMoveFinished(this); } } } public void MoveSteps(int steps) { int targetIndex Board.Instance.GetTargetWaypointIndex(currentWaypointIndex, steps); targetPosition Board.Instance.GetWaypointPosition(targetIndex); currentWaypointIndex targetIndex; isMoving true; } }连接掷骰与移动现在回到GameManager补充OnDiceRollFinished方法让它触发当前玩家的棋子移动。// 在GameManager中添加 public Piece[] allPieces; // 在Inspector中拖入所有棋子实例 public void OnDiceRollFinished(int diceValue) { if (currentState ! GameState.WaitingForRoll) return; infoText.text $玩家 {currentPlayerIndex 1} 掷出: {diceValue}; currentState GameState.MovingPiece; // 简化逻辑只移动当前玩家的第一个棋子 Piece pieceToMove GetCurrentPlayerPiece(0); // 需要实现这个方法 if (pieceToMove ! null) { pieceToMove.MoveSteps(diceValue); } else { // 如果没有可移动的棋子直接切换玩家 SwitchToNextPlayer(); } } // 添加棋子移动结束的回调 public void OnPieceMoveFinished(Piece piece) { // 这里可以加入判定是否到达终点、是否撞子等 Debug.Log($棋子移动结束); // 简单处理直接切换玩家 SwitchToNextPlayer(); }你需要实现GetCurrentPlayerPiece方法从allPieces数组中找出属于当前玩家的棋子。同时在场景中实例化所有棋子的预制体比如每种颜色4个共16个并拖拽到GameManager的allPieces数组中并正确设置每个Piece脚本的playerID和pieceID。至此一个最核心的“掷骰-移动-切换回合”的循环就完成了。点击运行你应该可以点击按钮掷出随机点数然后看到对应玩家的棋子沿着路径点移动。4. 完善游戏规则与功能扩展基础循环跑通后我们需要把飞行棋的规则补全并优化体验。4.1 实现飞行棋特殊规则真实的飞行棋规则更复杂包括起飞掷得6点方可让一枚棋子从基地起飞至起点。跳跃棋子走到与自己颜色相同的格子可以跳跃到下一个同色格。撞子棋子走到已有敌方棋子的格子可将敌方棋子撞回基地。终点棋子必须恰好走到终点才能胜利否则多出的步数要往回走。我们需要扩展Board和Piece的逻辑。以“撞子”为例在Board中维护一个字典记录每个路径点上有哪方的棋子。// Board.cs 新增 public Dictionaryint, Piece pieceAtWaypoint new Dictionaryint, Piece(); public void RegisterPieceAtWaypoint(int waypointIndex, Piece piece) { if (pieceAtWaypoint.ContainsKey(waypointIndex)) { // 如果这个点已经有棋子触发撞子逻辑 Piece otherPiece pieceAtWaypoint[waypointIndex]; if (otherPiece.playerID ! piece.playerID) { SendPieceBackToBase(otherPiece); } } pieceAtWaypoint[waypointIndex] piece; // 新棋子占据该点 } public void UnregisterPieceFromWaypoint(int waypointIndex, Piece piece) { if (pieceAtWaypoint.ContainsKey(waypointIndex) pieceAtWaypoint[waypointIndex] piece) { pieceAtWaypoint.Remove(waypointIndex); } } void SendPieceBackToBase(Piece piece) { piece.currentWaypointIndex -1; // -1 表示在基地 piece.transform.position GetBasePosition(piece.playerID); // 实现一个返回基地位置的方法 UnregisterPieceFromWaypoint(piece.currentWaypointIndex, piece); }在Piece的MoveSteps方法中在移动开始前先从原位置注销移动到新位置后向棋盘注册并触发规则检查。// Piece.cs 修改MoveSteps public void MoveSteps(int steps) { // 1. 从旧位置注销 Board.Instance.UnregisterPieceFromWaypoint(currentWaypointIndex, this); // 2. 计算并移动 int targetIndex Board.Instance.GetTargetWaypointIndex(currentWaypointIndex, steps); targetPosition Board.Instance.GetWaypointPosition(targetIndex); currentWaypointIndex targetIndex; isMoving true; // 3. 移动结束后在新位置注册并检查规则在Update中移动结束后调用 } // 在Update移动结束后加入 if (!isMoving) { Board.Instance.RegisterPieceAtWaypoint(currentWaypointIndex, this); // 还可以在这里检查是否跳跃、是否到达终点等 GameManager.Instance.OnPieceMoveFinished(this); }4.2 优化UI与交互体验目前的UI还很简陋。我们可以增加玩家信息面板显示每位玩家的棋子状态哪些在基地、哪些在途中、哪些已到达终点。棋子选择当玩家掷骰后如果有多个棋子可移动应弹出选择界面。动画与音效为骰子滚动、棋子移动、撞子、胜利等事件添加简单的动画DoTween插件很好用和音效AudioSource。游戏状态保存简单的PlayerPrefs可以保存当前回合和棋子位置实现中途退出再进入。例如实现棋子选择// 在GameManager中 public void OnDiceRollFinished(int diceValue) { ... // 找出当前玩家所有可移动的棋子 ListPiece movablePieces GetAllMovablePiecesForCurrentPlayer(diceValue); if (movablePieces.Count 0) { // 无棋可走 SwitchToNextPlayer(); } else if (movablePieces.Count 1) { // 只有一个可走直接移动 movablePieces[0].MoveSteps(diceValue); } else { // 多个可走显示选择UI UIManager.Instance.ShowPieceSelection(movablePieces, diceValue); } }你需要创建一个UIManager脚本来管理这些复杂的UI显示和交互。4.3 加入AI对手可选为了让单人可玩可以加入简单的AI。在GameManager的SwitchToNextPlayer中判断下一个玩家是否是AI。public bool[] isAIPlayer; // Inspector中设置哪个玩家是AI void SwitchToNextPlayer() { currentPlayerIndex (currentPlayerIndex 1) % 4; currentState GameState.WaitingForRoll; UpdateUI(); if (isAIPlayer[currentPlayerIndex]) { // 如果是AI延迟一段时间后自动掷骰 StartCoroutine(AITakeTurn()); } } System.Collections.IEnumerator AITakeTurn() { yield return new WaitForSeconds(1f); // 等待1秒模拟思考 // 找到AI的骰子按钮并“点击” DiceController.Instance.rollButton.onClick.Invoke(); }AI的移动决策可以做得更复杂比如评估哪个棋子移动后收益最大离终点更近、能撞子等但这需要更完善的规则判断函数。5. 项目调试、打包与常见问题排查功能完成后在Unity编辑器中反复测试是必须的。不要等到所有代码写完才第一次运行。5.1 调试技巧善用Debug.Log在状态切换、方法调用、计算关键值如目标路径点索引的地方打印日志。使用断点如果你使用Visual Studio或Rider可以在代码行左侧点击设置断点运行后触发时代码会暂停方便查看变量值。确保Unity Editor中Edit - Preferences - External Tools下的外部脚本编辑器设置正确。Inspector调试将重要的公共变量如currentState,currentPlayerIndex在Inspector中显示运行时可以实时观察其变化。场景视图调试在Board脚本中可以用Gizmos绘制路径点连线方便在Scene视图中检查路径顺序。// Board.cs 末尾添加 void OnDrawGizmos() { if (waypoints null) return; Gizmos.color Color.white; for (int i 0; i waypoints.Length - 1; i) { if (waypoints[i] ! null waypoints[i 1] ! null) { Gizmos.DrawLine(waypoints[i].position, waypoints[i 1].position); } } }5.2 常见问题与解决棋子移动不流畅或卡顿检查Piece的Update方法中移动逻辑的moveSpeed和Time.deltaTime使用是否正确。确保没有在每帧创建大量临时对象如new Vector3。点击按钮没反应检查EventSystem是否存在且唯一。检查按钮的onClick事件是否绑定了正确的方法。检查脚本中rollButton.onClick.AddListener(StartRoll)是否在Start或Awake中被调用。预制体实例化后脚本变量丢失确保预制体上脚本的公共变量如playerID在预制体模式下设置好或者在实例化后通过代码动态赋值。2D Sprite渲染顺序错乱调整Sprite Renderer的Sorting Layer和Order in Layer。通常UI在一个层背景在一个层棋子在中间层。游戏逻辑状态混乱仔细检查GameManager中GameState的转换条件确保每个状态转换如WaitingForRoll-MovingPiece-SwitchingPlayer都是清晰且唯一的。多用Debug.Log打印当前状态。构建Build后运行出错检查所有资源如图片、预制体的引用是否有效。检查是否有使用Application.dataPath等编辑器路径在打包后需要替换为Application.persistentDataPath。在File - Build Settings中确保添加了当前场景。5.3 项目优化与打包资源优化对于2D游戏确保图片的压缩格式如PNG和Max Size设置合理避免包体过大。可以使用Sprite Atlas将多个小图打包成图集减少Draw Call。代码优化避免在Update中做复杂计算或频繁查找对象如GameObject.Find。使用缓存将找到的对象存起来或消息系统如UnityEvent或观察者模式来解耦。打包设置打开File - Build Settings。选择目标平台如PC, Mac Linux Standalone。点击Player Settings设置公司名、产品名、图标、分辨率等。对于2D游戏通常选择Windowed模式并设置一个默认分辨率。点击Build选择输出文件夹等待编译完成。完成打包后在你指定的文件夹中会生成一个可执行文件.exe及其数据文件。运行它进行最终测试。这个从零开始的飞行棋项目核心价值在于让你体验一个完整2D游戏从设计、搭建、编码、调试到打包的全流程。它涉及的UI、物理碰撞检测、状态机、数据管理、AI逻辑等都是游戏开发的基础。当你把这个项目跑通并理解每一部分代码的作用后再去看更复杂的游戏机制就会发现它们都是由这些基础模块组合、演化而来的。动手实现一遍比看十篇教程都管用。
返回列表