1. 项目概述与核心思路最近在社区里看到不少朋友对Unity 2D游戏开发感兴趣特别是那种上手快、机制简单但又有一定挑战性的小游戏。我自己也带过不少新人项目发现“小鸟飞行躲避障碍”这个经典玩法几乎是每个Unity初学者从“Hello World”迈向“我的第一个游戏”的绝佳跳板。它麻雀虽小五脏俱全物理运动、碰撞检测、游戏状态管理、UI交互、得分逻辑一个不落。今天我就以一个从业者的角度从头到尾拆解一遍这个项目的开发实战不仅告诉你每一步怎么做更会分享我踩过的坑和那些教程里不会写的“骚操作”。这个项目的核心就是控制一只小鸟通常是一个Sprite在场景中持续向右或向上飞行同时玩家需要通过点击或按键控制小鸟跳跃或上升来避开随机生成、迎面而来的管道、柱子等障碍物。每成功通过一组障碍得分加一一旦碰撞发生游戏结束。听起来简单但要做好让手感流畅、难度曲线合理里面的门道可不少。无论是想入门Unity的新手还是想巩固2D游戏开发基础的朋友跟着走一遍你收获的将不仅仅是一个能跑起来的Demo更是一套解决同类问题的通用思路。2. 整体架构与核心模块设计在动手写代码之前花点时间规划一下整体架构能让你在开发后期省去大量重构的麻烦。对于这个小游戏我们可以将其核心模块分解为以下几个部分2.1 游戏对象与组件规划首先我们需要明确场景中会有哪些“演员”玩家角色 (Player/Bird)这是游戏的核心。它需要一个SpriteRenderer来显示外观一个Rigidbody2D组件来接受物理引擎管理实现重力与碰撞以及一个Circle Collider 2D或Box Collider 2D来定义其碰撞边界。控制脚本如BirdController将挂载在它上面。障碍物 (Obstacle)通常是上下成对出现的管道。它们需要SpriteRenderer和Collider2D如Box Collider 2D。为了高效管理和复用我们必须将其制作成Prefab预制体。一个优秀的实践是创建一个空的GameObject作为“障碍物对”的根节点下面挂载上下两个管道子物体这样我们可以整体移动和生成这个“对”。游戏管理器 (GameManager)这是一个“幕后总指挥”通常以单例模式实现。它不直接显示在场景中但负责全局状态游戏开始、进行中、结束的逻辑切换分数计算与更新背景滚动控制障碍物生成器的调用等。障碍物生成器 (Spawner)负责在特定位置按一定时间间隔实例化障碍物Prefab。它需要控制生成频率和障碍物的垂直随机位置以创造不同的通过间隙。用户界面 (UI)包括开始按钮、分数显示TextMeshPro - Text组件、游戏结束面板包含最终分数和重玩按钮等。2.2 核心逻辑流程图整个游戏的运行逻辑可以用一个简单的循环来描述游戏启动 - 显示开始UI - 玩家点击开始 - 小鸟受物理控制障碍物开始生成 - 循环{ 玩家输入控制小鸟跳跃 - 检测小鸟与障碍物/边界碰撞 - 若碰撞则游戏结束否则更新分数 } - 显示结束UI - 点击重玩回到初始状态这个流程中碰撞检测和游戏状态管理是两个最容易出问题的关键点我们后面会重点讲。2.3 为什么选择这样的架构很多新手喜欢把所有的代码都塞到玩家角色身上比如让小鸟自己计分、自己判断游戏结束。这在小项目中看似可行但随着功能增加比如添加音效、粒子特效、难度递增代码会变得高度耦合难以维护。采用GameManager集中管理状态各司其职是更工程化的做法。GameManager作为单例可以被场景中任何其他脚本轻松访问例如障碍物脚本在通过时通知GameManager加分碰撞检测脚本在发生碰撞时通知GameManager结束游戏。3. 核心细节解析与实操要点接下来我们深入到每个核心模块看看具体怎么做以及有哪些需要注意的“坑”。3.1 玩家控制让小鸟“跳”起来小鸟的跳跃是游戏手感的灵魂。通常我们模拟的是类似《Flappy Bird》的瞬时上升力。实现方案对比直接修改Transform (不推荐)transform.position Vector3.up * speed * Time.deltaTime。这种方式完全 bypass 物理引擎虽然简单但很难与基于物理的碰撞检测完美配合手感生硬。使用Rigidbody2D.AddForce (常用)在玩家点击时给小鸟的Rigidbody2D施加一个向上的瞬时力ForceMode2D.Impulse。例如rb.AddForce(Vector2.up * jumpForce, ForceMode2D.Impulse);。这种方式能产生更自然的抛物线运动与物理碰撞整合性好。直接设置Velocity (更直接)rb.velocity new Vector2(rb.velocity.x, jumpVelocity);。这种方式能精确控制每次跳跃的垂直速度手感稳定且易于调整是我个人更推荐的方式。实操步骤与脚本示例using UnityEngine; public class BirdController : MonoBehaviour { public float jumpVelocity 5f; // 跳跃的垂直速度 private Rigidbody2D rb; private bool isGameRunning false; void Start() { rb GetComponentRigidbody2D(); // 游戏开始前先让小鸟不受重力影响静止在空中 rb.simulated false; } void Update() { // 只有在游戏运行状态才接受输入 if (!isGameRunning) return; // 检测鼠标点击或空格键 if (Input.GetMouseButtonDown(0) || Input.GetKeyDown(KeyCode.Space)) { Jump(); } } void Jump() { // 直接将垂直速度设置为向上的固定值水平速度保持不变 rb.velocity new Vector2(rb.velocity.x, jumpVelocity); // 这里可以触发跳跃音效或动画 // AudioManager.Instance.PlayJumpSound(); } // 由GameManager调用开始游戏 public void StartGame() { isGameRunning true; rb.simulated true; // 启用物理模拟 // 给予一个初始的轻微向右速度如果游戏是横向卷轴的话 // rb.velocity new Vector2(1f, 0); } // 由GameManager调用游戏结束 public void GameOver() { isGameRunning false; // 可以停止所有物理运动或播放死亡动画 // rb.velocity Vector2.zero; } }注意Rigidbody2D的Body Type建议设置为Dynamic。Gravity Scale重力缩放系数需要仔细调试它直接决定了小鸟下落的速度和手感。通常从5-10开始尝试。3.2 障碍物的生成与移动障碍物需要源源不断地从屏幕一侧生成并向另一侧移动。生成器(Spawner)脚本要点using UnityEngine; public class ObstacleSpawner : MonoBehaviour { public GameObject obstaclePairPrefab; // 上下管道对的预制体 public float spawnRate 2f; // 生成间隔秒 public float yPosRange 2f; // 垂直方向随机范围 private float timer 0f; private bool isSpawning false; void Update() { if (!isSpawning) return; timer Time.deltaTime; if (timer spawnRate) { SpawnObstacle(); timer 0f; } } void SpawnObstacle() { // 计算一个随机的Y轴位置 float randomY Random.Range(-yPosRange, yPosRange); Vector3 spawnPos new Vector3(transform.position.x, randomY, transform.position.z); // 实例化预制体 GameObject newObstacle Instantiate(obstaclePairPrefab, spawnPos, Quaternion.identity); // 可以将新生成的障碍物设为生成器的子物体便于场景管理可选 // newObstacle.transform.parent transform; } public void StartSpawning() isSpawning true; public void StopSpawning() isSpawning false; }将Spawner对象放在屏幕右侧可见区域之外。障碍物预制体自身需要挂载一个脚本负责向左移动。障碍物移动脚本public class ObstacleMovement : MonoBehaviour { public float moveSpeed 3f; // 向左移动的速度 public float destroyXPosition -10f; // 当X坐标小于此值时销毁以节省性能 void Update() { // 每一帧向左移动 transform.Translate(Vector3.left * moveSpeed * Time.deltaTime); // 如果移出屏幕左侧边界则销毁自身 if (transform.position.x destroyXPosition) { Destroy(gameObject); } } }实操心得障碍物的移动不要放在FixedUpdate里除非你的游戏逻辑严重依赖物理帧。使用Update中的Translate并乘以Time.deltaTime可以确保在不同帧率下移动速度一致。destroyXPosition的值需要根据你的屏幕宽度和障碍物宽度来设定确保它完全离开屏幕后再销毁。3.3 碰撞检测精准判定游戏结束碰撞检测是游戏逻辑的核心。我们需要区分小鸟撞到障碍物游戏结束 vs 小鸟通过障碍物之间的空隙得分。1. 为碰撞体设置Tag标签这是最清晰、最推荐的做法。在Unity编辑器中选中障碍物预制体或上下管道。在Inspector窗口顶部点击Tag下拉框选择Add Tag...。创建一个新Tag命名为Obstacle。再次选中障碍物将其Tag设置为Obstacle。2. 在玩家脚本中编写碰撞逻辑在BirdController脚本中添加以下方法void OnCollisionEnter2D(Collision2D collision) { // 如果碰撞到的物体Tag是“Obstacle”则游戏结束 if (collision.gameObject.CompareTag(Obstacle)) { Debug.Log(Hit Obstacle! Game Over.); // 通知GameManager GameManager.Instance.GameOver(); } }同时我们需要检测“通过”得分。一个常见的做法是在障碍物对的中间空隙处放置一个透明的、带有Trigger的碰撞体比如一个矩形并为其设置一个Tag如ScoreZone。3. 创建得分区域在障碍物对的预制体结构中在上下管道中间创建一个空的子GameObject命名为ScoreZone。为其添加一个Box Collider 2D调整大小覆盖整个空隙并勾选Is Trigger属性。为其创建一个新Tag如ScoreZone并赋予它。4. 在玩家脚本中编写触发逻辑void OnTriggerEnter2D(Collider2D other) { if (other.CompareTag(ScoreZone)) { Debug.Log(Scored!); // 通知GameManager加分 GameManager.Instance.AddScore(1); // 为了防止重复计分可以立即禁用或销毁这个得分区域 other.gameObject.SetActive(false); // 或 Destroy(other.gameObject); } }重要注意事项确保小鸟的碰撞体Collider 2D不是Trigger而得分区域的碰撞体是Trigger。OnCollisionEnter2D用于处理物理碰撞OnTriggerEnter2D用于处理穿透触发。混淆两者会导致检测失败。3.4 游戏状态管理GameManager单例模式GameManager是整个游戏的大脑。使用单例模式可以让我们在任何脚本中都能方便地访问它。using UnityEngine; using UnityEngine.UI; // 如果使用旧版UI using TMPro; // 如果使用TextMeshPro public class GameManager : MonoBehaviour { public static GameManager Instance; // 单例实例 public enum GameState { Menu, Playing, GameOver } public GameState currentState GameState.Menu; public int score 0; public TextMeshProUGUI scoreText; // 分数显示UI public GameObject gameOverPanel; // 游戏结束UI面板 [SerializeField] private BirdController bird; [SerializeField] private ObstacleSpawner spawner; void Awake() { // 简单的单例实现确保只有一个实例 if (Instance null) { Instance this; } else { Destroy(gameObject); } } void Start() { // 初始化游戏状态 EnterMenuState(); } public void StartGame() { currentState GameState.Playing; score 0; UpdateScoreUI(); // 激活小鸟控制 if (bird ! null) bird.StartGame(); // 开始生成障碍物 if (spawner ! null) spawner.StartSpawning(); // 隐藏游戏结束面板如果可见 if (gameOverPanel ! null) gameOverPanel.SetActive(false); } public void AddScore(int points) { if (currentState ! GameState.Playing) return; score points; UpdateScoreUI(); // 可以在这里添加得分音效 } void UpdateScoreUI() { if (scoreText ! null) scoreText.text Score: score.ToString(); } public void GameOver() { if (currentState ! GameState.Playing) return; currentState GameState.GameOver; Debug.Log(Game Over! Final Score: score); // 停止小鸟控制 if (bird ! null) bird.GameOver(); // 停止生成障碍物 if (spawner ! null) spawner.StopSpawning(); // 显示游戏结束面板 if (gameOverPanel ! null) { gameOverPanel.SetActive(true); // 可以在面板上更新最终分数 TextMeshProUGUI finalScoreText gameOverPanel.GetComponentInChildrenTextMeshProUGUI(); // 需要根据实际UI结构查找 if (finalScoreText ! null) finalScoreText.text Final Score: score; } } public void RestartGame() { // 这是一个简单的重启直接重新加载当前场景 // UnityEngine.SceneManagement.SceneManager.LoadScene(UnityEngine.SceneManagement.SceneManager.GetActiveScene().name); // 更优雅的方式重置状态而不重载场景 currentState GameState.Menu; // 销毁所有已生成的障碍物 GameObject[] obstacles GameObject.FindGameObjectsWithTag(Obstacle); foreach (GameObject obs in obstacles) Destroy(obs); // 重置小鸟位置和速度 if (bird ! null) { bird.transform.position Vector3.zero; // 初始位置 Rigidbody2D rb bird.GetComponentRigidbody2D(); if (rb ! null) rb.velocity Vector2.zero; } // 隐藏游戏结束面板显示开始菜单 if (gameOverPanel ! null) gameOverPanel.SetActive(false); // 注意这里需要重新进入菜单状态等待玩家点击开始 EnterMenuState(); } void EnterMenuState() { currentState GameState.Menu; // 这里可以显示开始按钮等UI元素 } }在Unity编辑器中你需要将BirdController和ObstacleSpawner的实例拖拽到GameManager脚本的对应字段上。scoreText和gameOverPanel也需要从UI层级中拖拽赋值。4. 实操过程与核心环节实现现在让我们把以上模块组装起来完成一个可玩的游戏流程。4.1 场景搭建与对象配置创建2D项目在Unity Hub中新建一个2D核心模板项目。导入素材准备或从Asset Store下载小鸟、管道障碍物、背景的Sprite图片。将它们导入项目并确保Texture Type设置为Sprite (2D and UI)。设置场景创建Bird创建一个Sprite将小鸟图片赋给它。添加Rigidbody 2DBody Type: Dynamic,Gravity Scale: 5添加Circle Collider 2D并调整合适大小。将BirdController脚本挂载上去。创建Obstacle Pair Prefab创建一个空GameObject命名为ObstaclePair。创建两个子Sprite分别代表上管道和下管道摆好位置中间留出空隙。为上下管道分别添加Box Collider 2D并设置Tag为Obstacle。在ObstaclePair根节点下创建一个空子对象ScoreZone添加Box Collider 2D勾选Is Trigger调整大小覆盖空隙设置Tag为ScoreZone。将ObstacleMovement脚本挂载到ObstaclePair根节点上。将整个ObstaclePair从层级视图拖入项目视图的Assets文件夹创建为Prefab。完成后可以删除层级视图中的实例。创建Spawner创建一个空GameObject命名为Spawner放在屏幕右侧外部。挂载ObstacleSpawner脚本并将刚创建的ObstaclePair预制体拖拽到其obstaclePairPrefab字段。创建GameManager创建一个空GameObject命名为GameManager挂载GameManager脚本。搭建UI创建Canvas。创建TextMeshPro - Text显示分数ScoreText锚定在屏幕左上角。创建Button作为开始按钮StartButton。创建Panel作为游戏结束面板GameOverPanel初始设置为不可见。在面板内添加Text显示“Game Over”和最终分数以及一个“Restart”按钮。配置GameManager引用在编辑器中将Bird、Spawner、ScoreText、GameOverPanel分别拖拽到GameManager脚本的对应公开字段。配置按钮事件选中StartButton在Inspector的Button组件On Click()事件中点击号将GameManager对象拖入选择函数GameManager.StartGame。同理为Restart按钮配置GameManager.RestartGame事件。4.2 参数调试与手感打磨这是将游戏从“能跑”变成“好玩”的关键步骤。小鸟手感反复调整BirdController中的jumpVelocity和Rigidbody2D的Gravity Scale。jumpVelocity决定了一次跳跃能飞多高Gravity Scale决定了下降有多快。目标是让跳跃感觉灵敏、可控下落又不至于太慢。可以尝试jumpVelocity在5-8之间Gravity Scale在3-7之间。障碍物参数ObstacleMovement的moveSpeed决定了游戏的基础难度。速度越快反应时间越短。可以从3开始。ObstacleSpawner的spawnRate决定了障碍物的密度。间隔越短难度越大。可以从2秒开始。yPosRange决定了空隙的垂直随机范围。范围越大每次通过的挑战变化越大但也要保证不会出现完全无法通过的极端位置。碰撞体大小微调小鸟和管道的碰撞体大小。有时候视觉边缘和碰撞体边缘完全一致会显得过于苛刻可以适当将碰撞体缩小一点即让视觉模型比碰撞体大一圈这样玩家在视觉上感觉“擦边而过”时实际上并未触发碰撞能提升游戏体验的宽容度。4.3 添加“波兰”Polish基础功能完成后添加一些效果能让游戏质感大幅提升。粒子特效在小鸟跳跃时播放一个小型的上升粒子碰撞时播放一个爆炸粒子。Unity的Particle System组件很容易实现。音效创建AudioManager单例或使用更简单的AudioSource。为跳跃、得分、碰撞、背景音乐分别添加音效。注意使用PlayOneShot来播放短促音效避免被截断。动画为小鸟添加一个扇动翅膀的简单帧动画Animation窗口为游戏结束添加一个屏幕震动或变灰的效果。背景循环创建一个背景Sprite并为其挂载一个脚本让其以一定速度向左缓慢平移transform.Translate(Vector3.left * bgSpeed * Time.deltaTime)当它完全移出屏幕时将其位置重置到另一侧实现无限循环滚动。5. 常见问题与排查技巧实录开发过程中你几乎一定会遇到下面这些问题。我把它们和解决方法整理出来希望能帮你节省时间。5.1 物理与碰撞相关问题问题1小鸟穿过障碍物没有触发碰撞。检查清单碰撞体是否存在确保小鸟和障碍物都有Collider 2D组件。Rigidbody 2D至少其中一方通常是小鸟必须有Rigidbody 2D组件。物理碰撞需要Rigidbody驱动。Layer Collision Matrix检查Edit - Project Settings - Physics 2D确保小鸟和障碍物所在的Layer是相互碰撞的对应复选框被勾选。默认情况下所有Layer都相互碰撞。碰撞体大小和位置在Scene视图中勾选Gizmos中的Collider显示确保碰撞体框线位置和大小符合预期没有因为缩放或子物体关系而错位。代码方法名确认碰撞检测函数名拼写完全正确是OnCollisionEnter2D而不是OnCollisionEnter那是3D的。问题2得分区域被触发多次一次通过加了无数分。原因小鸟的碰撞体可能在ScoreZone的Trigger区域内停留了多帧每帧都触发了一次OnTriggerEnter2D。解决方案在触发一次得分后立即禁用或销毁该得分区域物体如上文代码所示other.gameObject.SetActive(false);。更优雅的做法是在ScoreZone上挂一个脚本内部设置一个bool hasScored标志位。5.2 性能与对象管理问题问题3游戏运行一段时间后变卡。可能原因障碍物在移出屏幕后没有被销毁成千上万的GameObject堆积在场景中。解决方案确保ObstacleMovement脚本中有检查并销毁移出屏幕对象的逻辑上文已实现。也可以在Spawner中维护一个对象池Object Pool复用障碍物对象而不是频繁地Instantiate和Destroy这对移动设备性能提升显著。问题4重新开始游戏后旧的障碍物还在。原因RestartGame逻辑没有清理场景中已存在的障碍物。解决方案在GameManager的RestartGame方法中通过Tag找到所有Obstacle并销毁它们。代码示例已在上一节提供。5.3 输入与UI问题问题5在游戏结束面板点击“重玩”按钮后小鸟无法再次控制。排查检查RestartGame方法是否正确地重置了GameManager的currentState为Menu。检查BirdController的StartGame()方法是否被成功调用在点击开始按钮时。确保GameManager对bird的引用没有丢失。检查小鸟的Rigidbody2D的simulated属性是否在StartGame()中被设为true在GameOver()中被设为false。问题6UI文本不更新分数。排查确认GameManager中的scoreText字段在Inspector中正确绑定了UI Text对象。确认更新分数的代码UpdateScoreUI()在AddScore()方法中被调用。如果使用TextMeshPro确保引用了正确的命名空间using TMPro;并且字段类型是TextMeshProUGUI而不是旧的Text。5.4 打包与平台相关问题问题7在编辑器里运行正常打包成PC或Android后点击没反应或位置错乱。UI适配检查Canvas的Canvas Scaler组件。对于PC独立游戏UI Scale Mode设为Scale With Screen SizeReference Resolution设为你的目标分辨率如1920x1080。对于移动端可能需要设为Constant Physical Size或Constant Pixel Size并结合锚点。输入差异在移动设备上Input.GetMouseButtonDown(0)同样对应触摸屏幕。但如果你的游戏只需要点击这没问题。如果需要更复杂的手势可能需要使用Input.touches。路径与资源确保所有用代码Resources.Load动态加载的资源在打包设置(File - Build Settings - Player Settings)中已被包含。对于直接拖拽到Inspector的公开引用Unity会自动处理。开发这样一个小游戏最大的收获不是代码本身而是理解了一个完整游戏循环是如何构建的以及状态管理、物理交互、事件通信这些核心概念如何协同工作。当你成功运行起自己的第一个版本看着小鸟随着你的点击起伏成功躲过一个又一个障碍时那种成就感是无可替代的。接下来你可以尝试给它加上更多的“波兰”比如更丰富的障碍物类型、道具系统、本地高分记录甚至尝试用不同的美术风格彻底改造它。这个小小的项目是你游戏开发之路一块坚实的基石。