尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Unity 2D弹射游戏实战:愤怒的小鸟物理碰撞与弹道优化

发布时间:2026/9/29 18:49:48

资讯中心
01
ARTICLE

Unity 2D弹射游戏实战:愤怒的小鸟物理碰撞与弹道优化

Unity 2D弹射游戏实战:愤怒的小鸟物理碰撞与弹道优化
简介这是一份基于Unity引擎的“愤怒的小鸟”跨平台移植项目资源目标读者是Unity初中级开发者重点解决Android触屏操作向桌面鼠标交互的适配问题。资源共3601个文件压缩包41.87MB包含C#脚本、场景与Prefab、材质、动画控制器、音频贴图及DLL库等完整工程内容能直观查看将Android触摸事件Input.touch替换为桌面鼠标事件OnMouseDown的代码改动方式。项目还涉及物理引擎中刚体与碰撞器的发射模拟、UI按钮与屏幕分辨率适配、Build多平台打包以及桌面端性能优化等关键环节为理解同类型游戏的跨平台迁移提供完整参考。已有1190人学习下载适合学习Unity事件系统转换、弹射玩法实现及工程发布流程的开发者对照实践。1. unity愤怒的小鸟一个能跑起来的 2D 弹射物理项目到底装着什么“unity愤怒的小鸟”这套资源说白了就是一个把《愤怒的小鸟》核心玩法搬到 Unity 里的完整工程压缩包里一般会有场景、预制体、C# 脚本、贴图和音效。你打开后就能看到弹弓、小鸟、砖块、绿皮猪以及最关键的物理碰撞链路。它解决的不是“做个 CG 宣传片”而是把弹射出手感、碰撞计分、关卡循环这些可复现逻辑讲明白。适合正在学 Unity 物理、想拿现成项目做毕业设计、或者第一次接触 Rigidbody2D 与碰撞矩阵的人。我自己拿到后第一件事不是改代码而是先看 Unity 版本号通常这类项目基于 2020.3 或 2021.3 LTS需要电脑上已经装好对应版本。要是你刚从网上下下来打开全报错八成是版本不匹配别急着删工程先检查 Manifest.json。2. 先拆场景与脚本结构从预制体到回调搞清这套代码的骨架2.1 文件夹和场景清单解压后先看哪几个文件解压后先看 Assets 目录不要直接双击场景。通常结构长这样Assets/ Scenes/ GamePlay.unity Scripts/ Bird.cs Block.cs Pig.cs SlingShot.cs GameManager.cs CameraFollow.cs Prefabs/ Bird.prefab Block.prefab Pig.prefab Sprites/ Audio/这份资源没上 Addressables也没挂 AssetBundle所有资源都在 Assets 下平铺你要做的第一件事是在 Unity Hub 里选择“打开项目”找到工程根目录然后让它自动导入。导入后如果 Skybox 发紫、贴图全粉先到 Player Settings 里确认公司名和项目名是全英文。项目名带中文在打包 Android 时经常报错这是 Unity 的老毛病连 2021 LTS 都没彻底修干净。打开 GamePlay.unity 后你会看到场景里摆着一个弹弓节点、一只等待状态的小鸟、右侧一群砖块和几只 Pig。这个场景的主相机设成了正交模式Size 一般在 6 到 8 之间。我一般习惯先把相机 Size 记下来后面调 UI 缩放时要用。2.2 预制体分家Bird/Block/Pig 的组件与脚本挂载预制体划分很清晰Bird 挂 Rigidbody2D CircleCollider2D Bird.csBlock 挂 Rigidbody2D BoxCollider2D Block.csPig 挂 Rigidbody2D CircleCollider2D Pig.cs。最容易踩坑的地方是有人会把 3D 的 Rigidbody 和 Collider 也当成可用组件结果 2D 物体不掉落碰撞也触发不了。记住这里全是 2D 物理不是 3D 那一套。Bird.cs 的核心字段你打开源码就会看到大概长这样using UnityEngine; public class Bird : MonoBehaviour { public bool isReady false; // 当前是否可以拖拽 public bool isFly false; // 已经发射出去 public float maxDragRadius 2f; public Rigidbody2D rb2d; public void ResetToSling(Vector3 slingPivot) { rb2d.simulated true; rb2d.isKinematic true; // 拖拽期间不受重力 transform.position slingPivot; } }逻辑说明这一层只是在准备阶段把小鸟放到弹弓上真正的拖拽计算在 SlingShot.cs 里。isReady 和 isFly 是后面做单次发射防重复的关键你可以把 isFly 当成“这颗子弹是否已经打出去”的锁。参数说明rb2d.isKinematic 在拖拽阶段设为 true这样小鸟不会自己滑下去等到松手再恢复成 dynamic。另外注意Pig 如果挂了 IsTrigger碰撞事件就收不到 OnCollisionEnter2D只有 OnTriggerEnter2D。你要是发现 Pig 一直不销毁先去检查 Inspector 里那个勾是不是误开了。2.3 GameManager 与关卡状态事件回调比到处查找 Tag 更容易维护早期很多克隆项目在 Block.cs 里直接写GameObject.FindWithTag(GameManager).GetComponentGameManager().AddScore(100);这种写法能跑但只要你给 GameManager 改名或换了场景Tag 没同步就翻车。更稳的做法是把 GameManager 做成单例并且用 C# 事件对外广播分数变化UI 自己订阅更新。示例using System; using UnityEngine; public class GameManager : MonoBehaviour { public static GameManager Instance; public event Actionint OnScoreChanged; public int Score { get; private set; } private void Awake() { if (Instance null) Instance this; else Destroy(gameObject); } public void AddScore(int amount) { Score amount; OnScoreChanged?.Invoke(Score); } }逻辑说明单例在 Awake 里赋值OnScoreChanged 是事件委托。任何 UI 脚本只要在 OnEnable 里订阅就能在分数变化时自动刷新不用每帧检查。参数说明AddScore 只负责累加和广播不用管是谁加的也不用检查关卡状态。用事件之后以后再加音效、加震动都方便谁要听谁订阅。这套骨架比一堆 FindWithTag 规范很多。新手阶段别嫌它绕等你把同一个工程维护到第十天就知道事件回调省掉多少头发。3. 拖拽瞄准与发射手感物理力怎么给才像真的弹弓3.1 弹弓拖拽与拖拽半径限制先做减法再放手弹弓发射的起点不在鼠标也不在小鸟身上而在弹弓支点。常见做法是让小鸟跟随鼠标位置移动但限制在小鸟初始点和支点之间不超过 maxDragRadius。如果你的资源里没自带拖拽限制就会出现把小鸟拖进屏幕外、松手后力大无比直接飞丢的惨剧。SlingShot.cs 里的拖拽部分逻辑一般是void OnMouseDrag() { Vector3 mousePos Camera.main.ScreenToWorldPoint(Input.mousePosition); mousePos.z 0f; Vector2 offset mousePos - slingPivot.position; offset Vector2.ClampMagnitude(offset, bird.maxDragRadius); bird.transform.position slingPivot.position new Vector3(offset.x, offset.y, 0f); }逻辑说明ClampMagnitude 把拖拽向量限制在 maxDragRadius 内超出部分只保留方向、截断长度这样即使鼠标甩到屏幕边缘小鸟也不会脱离弹弓。参数说明slidingPivot.position 是弹弓的中心支点maxDragRadius 一般设 1.5 到 2.5数值太小会感觉瞄准生硬太大又容易误触。真实项目里这个拖拽点还需要跟随手指在弹弓皮筋上滑动但核心还是同一个 Clamp 思路。你先保方向再调手感。3.2 发射方向换算将拖拽位移反过来作用到刚体松开鼠标后要做的是把小鸟拉回支点然后给一个向左上方的力。很多人搞反方向是因为思维惯性觉得“拖到哪儿就往哪儿飞”实际上弹弓是反着弹的。正确写法是把拖拽位移取反。public void Launch() { Vector2 dragVec bird.transform.position - slingPivot.position; Vector2 dir -dragVec.normalized; float power dragVec.magnitude * forceCoefficient; bird.rb2d.isKinematic false; bird.rb2d.velocity dir * power; bird.isFly true; }逻辑说明dragVec 从支点指向小鸟取反方向就是从弹弓指向空中速度大小由拖拽距离乘系数决定。velocity 直接赋值比 AddForce 更可控不会因为受力时间产生额外漂移。参数说明forceCoefficient 一般取 8 到 12取决于你砖块质量。太大力会穿墙太小又砸不碎砖。这里我会多给一个建议发射瞬间把 rb2d.gravityScale 设成 1.0如果之前调过 0.5小鸟飞行高度完全不同。物理参数改动后要跑一局看手感别光在数值面板上猜。3.3 弹道预测点用运动学方程画轨迹而不是贝塞尔曲线硬拟合很多初学者想画弹道先想到贝塞尔曲线但那只是视觉拟合跟物理引擎的抛物线相差很大。正确做法是用 Unity 的刚体初始速度和重力加速度直接算离散点。public void DrawTrajectory(Vector3 start, Vector2 initVelocity, int count, float interval) { ListVector3 points new ListVector3(); Vector3 pos start; Vector2 v initVelocity; float g Physics2D.gravity.y * bird.rb2d.gravityScale; for (int i 0; i count; i) { points.Add(pos); v new Vector2(0f, g * interval); pos new Vector3(v.x * interval, v.y * interval, 0f); } lineRenderer.positionCount points.Count; lineRenderer.SetPositions(points.ToArray()); }逻辑说明这里每间隔 interval 秒用初速度推算下一帧位置并累加重力影响。因为引擎里的物理也是同样的积分方式画出来的轨迹和实际飞行几乎重合。count 是点数点数越多越精确但别超过 30不然 LineRenderer 在处理多小鸟时会变卡。实际项目里建议只在拖拽时显示轨迹松开后立刻清空。这样玩家看到的是“这条线就是我松手后的路线”非常直观。如果你在原资源里没找到这段代码自己加上去即可不影响原逻辑。3.4 单次发射状态机防止玩家连续按住拖动没有状态管理的小鸟弹弓会出现“一只鸟飞出去还能拉回来”的鬼畜现象。解决方式很简单加一个状态锁。public enum BirdState { Ready, Dragging, Flying, Waiting }Ready 表示小鸟在弹弓上且未被拖拽Dragging 表示正在瞄准Flying 表示已经发射。只有 Ready 状态能响应 OnMouseDown只有 Dragging 状态能响应 OnMouseDrag 和 OnMouseUp。发射后立刻切到 Flying并且不再接受鼠标事件。这个状态机写进 SlingShot.cs 的鼠标回调入口即可。我建议你别偷懒省略这一步当初我偷懒直接用 bool结果多只鸟连续发射一直掉一点找半天才想起来是状态位没清。4. 碰撞检测、计分与关卡循环让小鸟砸下去而不是穿过去4.1 物理材质与碰撞矩阵被砸的砖块要先晃再碎砖块被小鸟砸到时的表现取决于 PhysicsMaterial2D 的弹性和摩擦力。没有物理材质时砖块会像一块水泥砸上去没有滑落和滚动感玩起来非常僵硬。常见做法是给砖块和 Pig 都建一个“Bouncy”物理材质摩擦系数 0.4弹性系数 0.2小鸟用摩擦 0.1、弹性 0.4 的材质。在 Assets 里右键 - Create - Physics Material 2D然后设置参数砖块小鸟PigFriction0.40.10.3Bounciness0.20.40.1另外要在 Project Settings - Physics 2D 里把 Bird、Block、Pig 放到不同 Layer并且只勾选需要碰撞的矩阵。举个例子小鸟层碰砖块层和 Pig 层砖块层互碰但 UI 层不参与物理。这个就是 LayerMask 在物理层面的含义。后面 UI 遮挡问题是另一个概念叫 RenderingLayerMask别混在一起物理层只影响碰撞渲染层只影响相机和 UI 的显示顺序。4.2 小鸟销毁的延迟机制计算得分后再延迟两帧很多克隆资源里的问题是小鸟碰到 Pig 后立刻 Destroy(Pig.gameObject)在同一帧里分数还没加上去Pig 就没了玩家看到分数突兀地 0。正确做法是先把得分算进 GameManager再让 Pig 延迟消失给玩家视觉反馈。private void OnCollisionEnter2D(Collision2D collision) { if (collision.gameObject.CompareTag(Bird)) { GameManager.Instance.AddScore(100); StartCoroutine(DieLater()); } } IEnumerator DieLater() { yield return new WaitForSeconds(0.2f); Destroy(gameObject); }逻辑说明先加 100 分再等 0.2 秒让 Pig 播放一个闪白或晃动动画然后销毁。WaitForSeconds 在这里是给视觉留出缓冲等几帧再执行销毁。参数说明scoreValue 100 是资源里的常见设定你可以把 Pig 和砖块的分数拆开放在 Inspector 里方便不同关卡调权重。注意 Destroy 放在协程里以后要避免对象已经被销毁协程还继续访问空引用所以销毁前如有动画要在同一个协程里控制。4.3 计分与星星评价剩余小鸟直接加进奖励分这儿很多资源只实现了总分没有三颗星的评级。要做成“愤怒的小鸟”那样需要把剩余小鸟数量折算成分数或直接作为星数条件。常见做法是关卡结束时统计剩余小鸟数大于等于 3 给三星等于 2 给两星剩下给一星。public void LevelFinished(bool isWin, int remainingBirds) { int starCount 0; if (isWin) { if (remainingBirds 3) starCount 3; else if (remainingBirds 2) starCount 2; else starCount 1; GameManager.Instance.AddScore(starCount * 50); // 附加奖励分 } // 把 starCount 传给 UI 播放星星动画 }逻辑说明先用剩余小鸟数量判断星级再给总分加奖励分。这里加奖励分的目的是让玩家觉得“省下小鸟是划算的”但又不至于让总分对后续解锁造成压倒性影响。参数说明starCount 乘 50 只是参考值你可以改成 0如果不想让奖励分破坏关卡平衡。4.4 回合结束判定等所有刚体静止再切场景游戏里不能在小鸟刚飞出就判定结果必须等所有砖块、Pig、小鸟停稳后再统计。常见做法是在 GameManager 里写一个协程每 0.5 秒检查场景里所有 Rigidbody2D 的速度之和。IEnumerator CheckObjectsStill() { while (true) { float totalVelocity 0f; Rigidbody2D[] bodies FindObjectsOfTypeRigidbody2D(); foreach (Rigidbody2D body in bodies) { totalVelocity Mathf.Abs(body.velocity.x body.velocity.y); } if (totalVelocity 0.1f) break; yield return new WaitForSeconds(0.5f); } // 判定胜利/失败 }逻辑说明velocity 之和接近 0 代表场景基本静置才进入关卡结束逻辑。FindObjectsOfType 在关卡末尾调用一次没问题但不要每帧调用。参数说明0.1f 是阈值太小会导致永远等不到静止太大又会在砖块还没停止时提前判定。建议先用默认值试跑几关再按关卡规模微调。这里我还想说一个很容易忽略的坑Time.timeScale 如果被设成了 0上面的协程和 WaitForSeconds 都不会走。所有基于场景静止的判定都要先检查 Time.timeScale 是否是 1。5. 避坑Unity 物理、相机和图层导致的 6 个常见炸点5.1 相机跟随的抖动与偏移现象小鸟飞远后相机要么猛地跳一截要么画面上下颠簸。原因把 CameraFollow 脚本写在了 Update 里并且直接修改相机世界坐标的 x忽略了 Z 轴和相机的平滑插值。Update 的调用频率和物理不同物理每帧插值会有延迟导致相机追不上或超调。解决把脚本改成 LateUpdate并限制 Z 轴永远锁定void LateUpdate() { Vector3 target new Vector3(bird.position.x, 0f, -10f); transform.position Vector3.Lerp(transform.position, target, Time.deltaTime * 5f); }逻辑说明LateUpdate 在物理和 Update 之后调用能拿到最终位置再用 Lerp 做平滑防止抖。参数说明Z 固定 -10保持正交相机和 Sprite 层距离Lerp 乘 Time.deltaTime 所以不要逐帧依赖数值。5.2 拖拽时小鸟自己飞走现象鼠标按住小鸟时它还好好在弹弓上但每帧都在微微位移或直接落下去。原因拖拽过程中 Rigidbody2D 的 isKinematic 没打开重力持续生效。Unity 物理会在拖拽的空隙做刚体计算把 Bird 从鼠标位置推走。解决在 OnMouseDown 里立刻设置 rb2d.isKinematic true同时 rb2d.velocity Vector2.zeroOnMouseUp 发射前恢复 isKinematic false。这个顺序不能反过来先恢复再设置 velocity 会导致一帧误差。5.3 发射方向反了现象小鸟往弹弓后方飞出或者直接砸向地面。原因射向偏移计算时把从支点到鼠标的向量当成初速度方向没有取反。弹弓是皮筋反向弹射所以力方向一定是拖拽向量的反向。解决发射逻辑里写Vector2 dir (-dragVec).normalized;并且将 power 乘以一个正数系数。如果你发现方向还是歪的去检查 slingPivot.position 是不是拿到了弹弓皮筋后端而不是弹弓中心。5.4 碰撞没反应或直接穿透现象小鸟穿过砖块或者砖块纹丝不动。原因很可能在预制体上同时挂了一个 3D Collider导致 2D 碰撞检测拿不到。另一个常见原因是 Collider2D 的 Collision Detection 设成了 Discrete小鸟高速飞过时直接穿透你需要改成 Continuous。还有就是 Layer 碰撞矩阵里没勾选 Bird 和 Block 层那么物理引擎直接忽略这对碰撞。解决打开预制体查看 Inspector确保只有 2D 物理组件把 Bird 和速度快的物体的 Collision Detection 设为 Continuous再到 Project Settings - Physics 2D 里确认 Layer 碰撞矩阵。5.5 切场景切不过去一直黑屏等待现象关卡打完后不进入结果界面日志卡在等待刚体静止的循环里。原因CheckObjectsStill 的阈值设成 0导致它永远等不到速度为 0 的那一刻。刚体在休眠前会有极小的抖动绝对静止不存在。解决把阈值从 0 改成 0.1f并且加一个超时计数器。超过 3 秒还没静止强制进入下一阶段避免死循环。5.6 UI 挡不住子弹或者反过来挡住点击现象暂停面板打开后小鸟还能被拖动或者手指点在按钮上却拖动了小鸟。原因UI EventSystem 和世界空间物体的鼠标事件没有隔离。Unity 的 OnMouseDown 也会穿过 UI 触发而 UI 的 RaycastTarget 又会吃掉一部分点击。解决在弹弓的 SlingShot 主物体上加一个 CircleCollider2D 作为点击响应并在 OnMouseDown 开始时检查EventSystem.current.IsPointerOverGameObject(Input.touches[0].fingerId)如果是 UI 层就返回不执行拖拽逻辑。物理层和渲染层分别处理物理层管碰撞矩阵渲染层管 UI 显示顺序两者别混在一起用。6. 进阶把克隆项目做出原版手感弹道描点、移动端优化和关卡配置6.1 弹道描点优化用对象池而不是每帧创建第三个章节里我们已经用 List 存轨迹点但如果你的 LineRenderer 需要渲染大量点每帧创建数组会导致 GC 压力。更稳定做法是预先分配最大点数lineRenderer.positionCount maxPoints; Vector3[] positions new Vector3[maxPoints];循环里只填充数组前 count 个元素并用 SetPositions 整块写入。这样不产生临时 List移动端也能跑得稳。6.2 移动端优化关闭阴影、降低物理频率这个资源如果直接在手机上跑通常会在高分辨率下发热严重。我一般会在启动脚本里强制关闭阴影QualitySettings.shadows ShadowQuality.Disable; QualitySettings.vSyncCount 0; Time.fixedDeltaTime 0.02f;逻辑说明手机 2D 游戏阴影本来就可有可无PhysicalParticle 也只用默认采样频率务稳定 50 帧物理计算足够。参数说明fixedDeltaTime 默认 0.02 代表每秒 50 次物理迭代太高会延长每一次的 CPU 开销。发布 Android 时在 Player Settings 里勾选 Build App Bundle可选 IL2CPP能明显压缩包体。6.3 ScriptableObject 配置关卡数据最后我还习惯把每个关卡的砖块位置、Pig 数量、剩余小鸟数提取成 ScriptableObject。这样新增关卡只需要在 Assets 目录右键创建配置不用改任何代码[CreateAssetMenu(fileName LevelData, menuName AngryBirds/LevelData)] public class LevelData : ScriptableObject { public string levelName; public int startBirdCount; public Vector2[] brickPositions; public Vector2[] pigPositions; }逻辑说明数据从场景里剥离放进可复用资源。GameManager 根据 levelName 加载对应 Scene 或用数据动态生成关卡。参数说明menuName 里的路径会在 Unity 菜单栏的 Assets/Create 里显示直接选择就能生成新配置。从那以后我每次拿到一个新克隆项目都会强制把所有关卡参数先搬进 ScriptableObject再碰玩法代码。这套习惯帮我省下的调参时间比脚本本身值钱得多。希望帮到你。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。