ARTICLE DETAIL

资讯详情

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

用Unity制作愤怒的小鸟:弹弓算法与2D物理破坏

用Unity制作愤怒的小鸟:弹弓算法与2D物理破坏 简介一款基于Unity引擎开发的愤怒的小鸟游戏工程面向学习Unity游戏开发以及跨平台移植的开发者重点呈现Android触屏操作向桌面鼠标交互迁移的完整实现过程。工程共含3601个文件压缩后约41.87MB核心类型包括21个C#脚本、25个预制体、38个材质、23个场景资源、61张贴图及57个动态链接库等整体目录结构完整便于对照学习。项目围绕平台差异处理、发射逻辑改造、物理引擎调参与界面适配等关键环节展开读者可掌握Input.touch向OnMouseDown的代码转换思路、刚体与碰撞器的物理配置方法以及Unity在个人电脑端发布打包的注意事项。压缩包内还附带工程构建的辅助文件与构建报告适合二次开发与学习调试。目前已有1190人学习下载较适合具备基础Unity引擎操作经验、希望理解跨平台适配实战细节的初中级开发者。1. 为什么我劝你把「愤怒的小鸟克隆」作为进入 Unity 的第一个动手项目拿 Unity 做「愤怒的小鸟」克隆是我见过最适合新手完成的第一个 2D 物理项目。玩法链路很短把鸟放到弹弓上往后拖松手鸟飞出去撞碎木板、玻璃和猪头。这条链路恰好串起 Unity 里最常被反复调用的输入、刚体、碰撞器、摄像机跟随和关卡管理每一个环节的反馈都直观到能直接看见。跑酷和打砖块也是常见入门项目但它们的逻辑更线性做完很容易变成“照着教程摆物件”调不出自己的理解。愤怒小鸟不一样你改一个力度参数整关的倒塌方式立刻改变这种即时反馈对摸清 Unity 2D 物理引擎的脾性很有用。后面的内容全部围绕这个克隆项目展开弹弓算法、碰撞破坏、关卡搭建、常见翻车点以及把这套骨架改成自己游戏的方向。跟着从头写一遍比你零散刷十个教程都管用。2. 把弹弓手感拆出来拖拽距离与发射方向的计算弹弓是这个克隆项目的灵魂。很多教学版本把发射写成一段十几行的更新逻辑看起来是“能弹出去”但你一上手就会发现手感和原版差太远。差距不在代码长短而在几个小地方拖拽的最大距离有没有被钳制松手时用的是速度赋值还是冲量加力皮筋用 LineRenderer 画的时候层级对不对。这一段我们逐项拆开讲。2.1 拖拽的核心逻辑把鼠标位置换算成弹弓上的点并限制最大拉距拖拽阶段要解决的问题是手指按住屏幕上的小鸟移动时小鸟必须跟着走但不能无限远离弹弓。无限远离会带来两个问题一是玩家把鸟拖到屏幕外就看不见了二是松手时的位移向量过大发射力度会爆炸。所以第一步一定是用 ClampMagnitude 限制拉力半径。using UnityEngine; public class SlingShot : MonoBehaviour { [SerializeField] private Transform bird; [SerializeField] private Transform leftAnchor; [SerializeField] private Transform rightAnchor; [SerializeField] private float maxPullDistance 2f; // 最大拉距越小越“紧” [SerializeField] private float launchPower 320f; // 发射冲量力度 private Vector2 slingPos; private Rigidbody2D birdRb; private bool isDragging; void Start() { slingPos transform.position; birdRb bird.GetComponentRigidbody2D(); } void Update() { if (Input.GetMouseButtonDown(0) TryGrabBird()) // 只从弹弓附近抓鸟 { isDragging true; birdRb.isKinematic true; // 拖拽阶段关掉物理模拟防止鸟在手里乱滑 } if (isDragging) { Vector2 mouseWorld Camera.main.ScreenToWorldPoint(Input.mousePosition); Vector2 offset mouseWorld - slingPos; offset Vector2.ClampMagnitude(offset, maxPullDistance); // 关键钳制 bird.position slingPos offset; DrawSling(bird.position); } if (Input.GetMouseButtonUp(0) isDragging) { isDragging false; LaunchBird(); } } }这里有一个使用上的前提birdRb.isKinematic true之后鸟不再受重力影响所以可以直接用 Transform 改位置如果鸟是动态刚体且没有切到 kinematic就别在 Update 里挪它否则会跟物理模拟打架产生抖动。maxPullDistance的值跟场景缩放强相关我通常把弹弓和猪头之间的整个关卡放在 20 个单位宽的范围里弹弓拉距设 2 到 2.5。如果你把 Sprite 做得非常大这个值就要跟着放大没有固定值。另外一个细节TryGrabBird用圆圈判定还是坐标范围都行最简单的是拿手指世界坐标到鸟的距离小于一个固定阈值防止关卡里有第二只鸟时点错。2.2 松手之后的发射矢量用冲量还是用速度赋值这里有条分岔路松手发射那几行是区分教程代码和手感代码的分水岭。错误写法是直接给birdRb.velocity赋值看起来鸟也确实飞出去了但 velocity 赋值绕开了物理引擎的碰撞细化鸟容易高速穿透目标特别是关卡里的薄木板。正确做法是用AddForce配合ForceMode2D.Impulse让引擎在下一个物理帧完整处理运动与碰撞。private void LaunchBird() { Vector2 pullOffset slingPos - (Vector2)bird.position; // 拉的方向往左弹的方向往右 birdRb.isKinematic false; // 恢复动态刚体让物理引擎接管 // 冲量加力力度与拉距线性相关但乘一个可调系数 birdRb.AddForce(pullOffset * launchPower, ForceMode2D.Impulse); rightAnchor.gameObject.SetActive(false); // 松手后收回前支点皮筋 }很多人看到AddForce就忽略了ForceMode2D.Impulse的含义冲量模式是把力度直接叠加到刚体速度上跟 FixedUpdate 是否稳定帧率无关而默认的ForceMode2D.Force是持续力要靠 mass 和时间累积参数调起来要难得多。这里我通常把 launchPower 放在 250 到 400 之间配合刚才说的 2 单位拉距。数值不是凑出来的单位拉距下要能飞越 8 个单位宽的关卡按能量关系可以反推但实践中直接二分法调最快——把 power 设成一个值发射一次看鸟飞多远再按比例修改。Time.fixedDeltaTime在 Impulse 模式下可写可不写不同项目习惯不同关键是写清楚注释避免后人误改。还有一部分手感来自“拉弓”的视觉反馈拉得越远皮筋拉得越开发射后皮筋要回弹复位。这部分和 LineRenderer 共用端点数据我们接下去说。2.3 皮筋用 LineRenderer 怎么画一根线的事情坑全在渲染层级皮筋通常做法是两条 LineRenderer一条从弹弓左上支点到鸟一条从右上支点到鸟。拉弓时两条线都指向鸟的当前位置松手后两条线的第二个点回到各自支点视觉上就是皮筋回弹。代码很简单坑在别处。private void DrawSling(Vector2 birdPos) { leftLine.positionCount 2; leftLine.SetPosition(0, leftAnchor.position); leftLine.SetPosition(1, birdPos); rightLine.positionCount 2; rightLine.SetPosition(0, rightAnchor.position); rightLine.SetPosition(1, birdPos); }LineRenderer的widthMultiplier我一般设 0.08 到 0.15太细了远处看不清太粗了又像动画片。材质用自带 Default-Line 即可。最容易翻车的是渲染顺序LineRenderer 的层级如果排在背景之后你看到的皮筋就被弹弓架挡住了看起来像穿模。解决方案有两种一是把皮筋所在 GameObject 的 Sorting Layer 设成 Foreground并让 Order in Layer 比背景大二是把左右支点分别放在两个空物体里左支点在鸟后面一层、右支点在鸟前面一层——原版愤怒小鸟就是这么做的左右皮筋分前后层级鸟夹在中间。别嫌麻烦渲染顺序问题在 2D 游戏里非常常见早一步定好规则后面做破坏特效、碎片飞溅时就不用再改。3. 用 Unity 2D 物理造“一碰就散”的木架和猪头弹弓写完游戏里另外一半核心是受击物。愤怒小鸟的爽感主要来自两处猪头被撞飞和木架像真的积木一样塌掉。这两个效果都依赖 Unity 2D 物理的正确配置。我见过不少人把碰撞器、刚体一股脑全加上结果鸟一撞整个木架纹丝不动或者像豆腐一样全散完全说不出问题出在哪。3.1 碰撞器选型和质心圆形猪头与盒形木板选择不是随意的猪头是圆的木板是方的这并不只是为了好看。Unity 的 2D 物理对圆形碰撞体的响应逻辑是沿着法线方向反弹所以圆形的猪头在被鸟撞到时更容易“滚开”“弹飞”符合卡通角色的碰撞观感。长方形木板如果用 Box Collider 2D碰撞法线比较平直堆叠时能形成可靠的平面支撑不会像圆角那样互相滑开。这里我提醒一个新手高频问题一块木板倒下用的还是 Box Collider 2D物理引擎反馈“碰撞法线垂直”的撞墙就不会产生滑动这是对的但如果你想要木头从中间断开、侧面滑落的细节用复合碰撞器会让破碎逻辑更麻烦。常规做法是给木板和猪头都单独用 Box 或 Circle不追求把木纹切片做得太细因为物理碰撞拆得越细性能开销越大。// 给木板: 盒碰撞器 动态刚体 连续碰撞检测 Rigidbody2D rb woodBlock.GetComponentRigidbody2D(); rb.gravityScale 1f; rb.mass 0.5f; rb.collisionDetectionMode CollisionDetectionMode2D.Continuous; BoxCollider2D box woodBlock.AddComponentBoxCollider2D(); box.size new Vector2(1f, 0.2f); // 薄木板连续碰撞检测是为了避免高速小鸟穿过木板不过 Continuous 只对动态刚体之间有效如果木板是静态刚体鸟用高速冲量打在静态木板上会直接穿透。所以木板、玻璃板、猪头我统一用动态刚体 Continuous这是愤怒小鸟克隆里比较省心的碰撞组合。gravityScale调到 1 或者略低调成 0.8 会有更轻的木料手感重了木架只要放上去就开始下陷。质心这块也值得单独说。Rigidbody2D 默认质心在几何中心但如果你在制作 Sprite 时把中心点偏移了视觉上板子是正的质心可能在角上一放上去就歪。出现“明明摆好了却总往一边倒”的玄学状况先看 Pro 里 Rigidbody 2D 的 Center Of Mass 是不是在中心再去检查物理材质。3.2 让木架受击碎裂替换碎片是性价比最高的做法要从鸟的一撞中做出碎片飞散的效果方案有很多SpringJoint 把碎块连起来、破片实时模拟、甚至物理网格切分。但对第一次做完整项目的人来说替换碎片是效率最高、也最不容易出错的思路。思路是木板受击且撞击力度超过阈值时先隐藏完整木板再在同一位置实例化一个“碎块预制体”。碎块预制体可以是一堆带刚体和碰撞器的小方块能各自飞散、弹跳最后落在地上。private void OnCollisionEnter2D(Collision2D collision) { float impact collision.relativeVelocity.magnitude; if (impact 5f !hasShattered) { hasShattered true; // 记录碎块不等数量随机角度给速度 GameObject shards Instantiate(shardPrefab, transform.position, transform.rotation); foreach (Rigidbody2D rb in shards.GetComponentsInChildrenRigidbody2D()) { rb.velocity collision.relativeVelocity * 0.3f; rb.AddTorque(Random.Range(-2f, 2f), ForceMode2D.Impulse); } Destroy(gameObject); } }impact 5f的阈值决定了“轻碰”和“砸碎”的分界线。以场景缩放 20 个单位宽为例小鸟速度在 12 到 18 之间能碎木板如果阈值设 2手滑一下鸟蹭到木头也碎不够爽。relativeVelocity.magnitude是碰撞双方的相对速度不是单边速度所以木架本身如果有下落速度鸟轻轻撞到也会碎——这个细节很多人没意识到。碎块预制体里的每个小碎块需要有自己的刚体和碰撞器但不用再写脚本让它自己物理模拟飞出就行。另外碎片太多会造成性能压力一次替换控制在 6 到 12 个碎块之间碎片刚体开着默认休眠阈值落地后会自动休眠省下 CPU 开销。3.3 木架为什么开局就塌Joint2D 在愤怒小鸟克隆里是雷区原版愤怒小鸟里的木屋大多只是“叠放”而不是靠关节绑死。个别新手会想当然地给木头之间加 FixedJoint2D 或 HingeJoint2D希望木屋更结实结果开局一秒钟整栋屋子开始莫名抖动甚至直接扭曲变形。原因是关节会引入新的约束解算在 2D 物理里每增加一个关节解算器就多一组迭代堆叠结构里的多个关节叠加就会产生抖动甚至爆炸。愤怒小鸟的玩法核心本来就是“撞碎”木屋就是要能被撞散所以“松散叠放 适当摩擦力”才是正路。PhysicsMaterial2D woodMaterial new PhysicsMaterial2D(); woodMaterial.friction 0.6f; // 摩擦大一点木头之间不打滑 woodMaterial.bounciness 0.1f; // 没有弹性避免跳起来 woodBlock.GetComponentCollider2D().sharedMaterial woodMaterial;代码里用 sharedMaterial 挂材质注意 PhysicsMaterial2D 会影响同一种类所有碰撞器的行为所以别把一块木板的物理材质替换掉后其他木板也跟着变。摩擦 0.6 已经能让叠放的木板不轻易打滑但小鸟撞上去时内力会克服摩擦力该散照样散这才是我们想要的效果。如果木屋横梁在开局就开始滑先查是不是用了 Joint2D排除了关节问题再去查木板的质心是不是很偏。这类晃动九成是材质或质心的问题不是引擎 bug。4. 把关卡拼起来二维数组生成、摄像机跟随与飞行路径预览一个愤怒小鸟关卡由弹弓位、鸟位、木架、猪头、地面和背景组成。真要零起步摆场景每个关卡手动摆放也没多大问题但做得有模有样需要能快速批量出关卡所以大多数克隆项目会用二维数组或 ScriptableObject 保存关卡数据。这里我讲一套实用做法。4.1 用二维数组生成关卡地图从数据到场景的标准套路把地面、木板、玻璃板、猪头映射成 0、1、2、3拼成一个二维数组关卡代码读取后按行列生成对应 Prefab。这样调整关卡布局只改数组不用回场景拖半天后面做编辑器扩展也方便。public class LevelGenerator : MonoBehaviour { public GameObject woodPrefab; public GameObject glassPrefab; public GameObject pigPrefab; public Vector2 cellSize new Vector2(1.2f, 0.6f); private int[,] levelMap new int[,] { { 0, 0, 0, 1, 0, 0, 0 }, { 1, 0, 3, 3, 3, 0, 1 }, { 0, 2, 0, 2, 0, 0, 0 }, }; void GenerateLevel() { for (int row 0; row levelMap.GetLength(0); row) { for (int col 0; col levelMap.GetLength(1); col) { Vector3 pos transform.position new Vector3(col * cellSize.x, -row * cellSize.y, 0); switch (levelMap[row, col]) { case 1: Instantiate(woodPrefab, pos, Quaternion.identity); break; case 2: Instantiate(pigPrefab, pos, Quaternion.identity); break; case 3: Instantiate(glassPrefab, pos, Quaternion.identity); break; } } } } }二维数组的行列对应世界坐标的位置关系注意 row 轴在数据里是从上往下所以在 world 坐标里是减 row 乘 cellSize.y。cellSize要跟预制体的实际尺寸匹配如果木板预制体宽 1.2 个单位而 cellSize.x 设为 2生成出来的木板之间会飘着空隙物理碰撞没事视觉上很假。正确做法是先做一个单元格放到场景里看尺寸再填 cellSize避免反复改数组。数组行列数量也别太夸张这套逻辑适合 8 行以内的中小型关卡超过之后更推荐用文本文件或 ScriptableObject 保存不然一个 20 行 20 列的二维数组在编辑器里维护起来很难受。4.2 摄像机跟随LateUpdate 加 Lerp再加边界钳制愤怒小鸟的关卡宽度比较大摄像头既要能拍整关跟随鸟飞出去时又要有推镜的流畅感。基础做法大家都会写void LateUpdate() { Vector3 target new Vector3(bird.position.x, bird.position.y, transform.position.z); transform.position Vector3.Lerp(transform.position, target, Time.deltaTime * 2f); }用 LateUpdate 而不是 Update是为了等角色的移动逻辑先执行完摄像机能拿到这帧最终的坐标避免一帧的延迟与抖动。Time.deltaTime * 2f里的 2f 是平滑系数越大跟得越紧越小越漂如果在弹弓场景里镜头要稳住不随小鸟快速甩头系数可以降到 1 到 1.5。另一个细节摄像头跟随 y 轴容易让镜头在猪头倒塌时上下晃动到玩家心烦这时候需要把目标 y 做一下钳制。float clampedY Mathf.Clamp(bird.position.y, groundY 2f, groundY 6f); Vector3 target new Vector3(bird.position.x, clampedY, transform.position.z);这里的 groundY 是地面所在高度。这套逻辑在愤怒小鸟克隆里很实用因为木架一旦倒下鸟的 y 会瞬时变化直接跟随会让镜头产生一个很难看的甩动。把 y 限制在一个范围内画面看起来就稳当得多。4.3 用贝塞尔曲线画飞行预览线玩家凭什么知道这一发会飞去哪原版愤怒小鸟的虚线预览是整个手感闭环的一部分。玩家拖拽时就能看到一条抛物线估算落点。实现上有两种思路一种是完全按物理公式模拟轨迹另一种是用贝塞尔曲线拟合。在 Unity 复刻里我更喜欢用二次贝塞尔因为控制点直观、曲线自然而且不同鸟可以微调曲线形状来表现“重鸟”与“轻鸟”的弹道差异。private Vector2 EvaluateQuadratic(Vector2 p0, Vector2 p1, Vector2 p2, float t) { float u 1 - t; return u * u * p0 2 * u * t * p1 t * t * p2; } private void DrawTrajectory() { Vector2 start bird.position; Vector2 pullOffset slingPos - (Vector2)bird.position; // 控制点沿拉的反方向决定曲线的“拱起”程度 Vector2 control start pullOffset * 1.8f; Vector2 end start pullOffset * 3.5f; trajectoryLine.positionCount 30; for (int i 0; i 30; i) { float t i / 29f; trajectoryLine.SetPosition(i, EvaluateQuadratic(start, control, end, t)); } }关于参数调整pullOffset * 1.8f是拱度数值越大弧线越高pullOffset * 3.5f是曲线总长度即预览的飞行距离这个值最好跟实际发射力度能覆盖的最远距离保持一致否则玩家看到路径到 3.5 倍处鸟却飞不到会感觉被戏弄。我一般把这两个系数跟 launchPower 联动发射力度大了预览曲线也相应伸长这样“所见即所得”。贝塞尔曲线的终点 P2 并不等于真实抛物线的落点所以这条预览曲线只是辅助提示不必和真实物理逐帧匹配。如果你想让虚线更贴近真实弹道可以直接用抛物线公式叠加重力但二次贝塞尔在调玩家手感时更舒服——它允许你人为夸大或缩小弹道的弧度。4.4 计分与 UI给猪头记分给木架记分别给砖头记分最后补一下计分规则。原版愤怒小鸟的计分由猪头分数、剩余小鸟数量和建筑残骸分数组成。在克隆项目里最直接的是给猪头一个分数字段撞到或者砸到就给玩家加分木架本身不记分但可以统计“被击碎的木架块数”作为隐藏奖励。public class Pig : MonoBehaviour { public int scoreValue 500; void OnCollisionEnter2D(Collision2D collision) { if (collision.relativeVelocity.magnitude 2f) { GameManager.Instance.AddScore(scoreValue); Destroy(gameObject); } } }注意这里不要在 OnCollisionEnter2D 里让分数重复累加因为这个回调在连续物理帧里会发生多次每次的 relativeVelocity.magnitude 都可能大于阈值。常见做法是给猪头一个 bool isDead第一次触发加完分就置位。上面这个简化版本在真实项目里需要补这个保护否则一局下来多出几倍分数。5. 排查笔记克隆愤怒小鸟物理时最常见也最迷惑的五个坑物理相关的翻车有时候没有报错窗口里看起来一切正常但手感就是不对。这一节我把调试过程中最容易撞上的五个问题列出来按“现象、原因、解决”顺序说。5.1 小鸟高速穿过木板和猪头完全没有碰撞现象明明是冲量打过去的鸟直接从木板中间穿过去猪头纹丝不动。原因这一类几乎都是碰撞检测模式的问题。Unity 2D 的默认碰撞检测是 Discrete它只在固定时间步进上采样速度足够高的鸟在一个物理帧内的位移超过薄木板厚度就会穿透。另一个次要原因是木板的 Box Collider 太薄厚度少于 0.05 个单位时即使是 Continuous 也容易漏。解决把小鸟和所有受击物的 Rigidbody2D 的 CollisionDetectionMode 全设成 Continuous同时把木板的碰撞厚度控制在 0.1 个单位以上。如果还是穿先确认是不是飞得太快再把物理的 Fixed Timestep 从默认 0.02 改成 0.01。改小会增加 CPU 开销只有确认穿透时才需要动这个全局参数。5.2 木架开局就开始塌鸟还没弹出去就已经输了一半现象把木头搭好按 Play木板慢慢下沉几秒后整栋木架自己散架。原因最常见的是木头与木头之间的碰撞体有重叠或间隙错位。场景里看起来木头是贴着的但生成时位置用了近似值碰撞体之间真实存在微小空隙物理引擎让刚体下沉填满空隙于是整个结构开始蠕动。另一个原因是刚体 mass 设置不合理或 gravityScale 超过 1.5。解决搭木架用代码生成并调整装配位置不要全手动摆放检查所有木板 Box Collider 2D 的 size 与实际 Sprite 尺寸一致gravityScale 在 1 到 1.1 之间。还有一个技巧刚搭建好时给每个木板刚体加一段 isKinematic true到小鸟发射前的 0.5 秒再统一改成 false。这样既能保证开局画面完整又不会让物理引擎提前把木架压塌。5.3 松手后小鸟原地不动或者只做匀速飘移现象拉弓一松鸟不飞或者像太空漂浮一样慢慢滑行完全不像是被弹出去的。原因拖拽期间 isKinematic true松手时已经恢复为 false但鸟还是不飞。常见原因有三个发射用的是 ForceMode2D.Force 而不是 Impulse且力度因乘了 Time.fixedDeltaTime 被缩小到接近 0或者松手瞬间鸟已经回到了弹弓原点pullOffset 约等于零向量再或者发射逻辑写在了 FixedUpdate 里和 GetMouseButtonUp 的判断时机错位。解决把发射改为AddForce(pullOffset * launchPower, ForceMode2D.Impulse)。在发射前加一行 Debug.Log 打印 pullOffset 的模长如果打印结果接近 0说明拖拽更新位置没生效去检查是不是拖拽阶段移动的 Transform 被物理重置了如果正常再检查是不是把 power 除以了某个大数。5.4 皮筋被背景盖住玩家看不到弹弓现象拖拽时皮筋消失了只有鸟在动或者皮筋跑到背景后面。原因LineRenderer 的 Sorting Layer 低于弹弓所在场景的渲染层。2D 游戏的渲染顺序由 Sorting Layer 和 Order in Layer 决定LineRenderer 默认在 Default 层弹弓架子也在 Default 层但后者的渲染顺序把皮筋盖掉了。解决专门给弹弓和皮筋建 Sorting Layer建议顺序为 Background、Sling-Back、Birds、Sling-Front、Foreground。左右皮筋分别挂到 Sling-Back 和 Sling-Front这样皮筋穿过鸟的前后视觉上既不穿帮也比较协调。设置好层级后进 Play 模式检查一次即可。5.5 力度手感忽大忽小同样的拉距有时飞远有时飞近现象用几乎相同的拖拽距离发射鸟有时飞得很远有时没劲多试几次结果都不一样。原因一是把 Time.timeScale 动态改了二是在发射时叠加了物理上不应该存在的速度比如鸟在拖拽前因为碰撞而带有速度松手后加上冲量结果自然不同三是 launchPower 被写在了 LateUpdate 里被帧率影响。解决发射前把 birdRb.velocity 和 angularVelocity 清零再执行 AddForce。发射逻辑统一放在同一个方法里不要在 Update、LateUpdate 分别加力。手感相关参数不要运行时改在编辑器里调好再打包。Impulse 模式本身与帧率无关多数问题都出在残留速度上。6. 进阶方向给小鸟加技能指示器并用手势录制验证手感克隆做到能弹、能碎、能计分骨架就完成了。接下来可以把它变成你自己的游戏而不是像素级照搬原版。至少两个方向投入产出比很高给小鸟加可触发的主动技能以及用手势录制验证手感。6.1 技能攻击指示器Skill Attack Indicators的简单实现技能指示器比做技能本身更重要因为玩家在拖拽弹弓的同时需要知道松开后如何触发技能、技能影响多大范围。常见做法是画一个圆形范围或者给目标加高亮边框用 Physics2D.OverlapCircleAll 检测范围内目标。这个思路和很多 ARPG 里的技能指示器一致在 2D 侧视图中也很直观。public class RedBirdSkill : MonoBehaviour { public GameObject skillIndicatorPrefab; private GameObject indicator; private bool hasSkill true; void Update() { // 玩家在飞行中按空格触发范围技能 if (Input.GetKeyDown(KeyCode.Space) hasSkill InFlight()) { hasSkill false; var targets Physics2D.OverlapCircleAll(transform.position, 2.5f); foreach (var hit in targets) { if (hit.CompareTag(Destructible)) { Destroy(hit.gameObject); } } } } void OnMouseDown() { // 拖拽瞄准时显示范围圈 indicator Instantiate(skillIndicatorPrefab, transform.position, Quaternion.identity); indicator.transform.localScale Vector3.one * 5f; } }指示器的关键参数是半径2.5f 表示技能半径。如果指示器是圆形它的直径必须等于 2 倍半径也就是 5f否则玩家看到圈内目标能碎、圈外目标也碎会认为判定出错。这个简单实现对圆形范围够用也可以扩展到扇形或直线技能。直线型通常用 Raycast 判定指示器则用贴图拉伸长度来显示范围。注意触发时机小鸟技能放在飞行阶段拖拽阶段先预览指示器松手飞行后才允许按键触发这样玩家的操作直觉是连贯的。6.2 手感验证轨迹回放是调参时最可靠的依据手感参数调到最后最容易出现的问题是分不清当前参数和上一次改参数之前的差别。我的办法是做一个简单的轨迹录制回放private ListVector3 trail new ListVector3(); void Update() { if (bird.GetComponentRigidbody2D().velocity.magnitude 0.1f) { trail.Add(bird.position); } } // 回放时把 trail 里的点画出来 void DrawTrail() { for (int i 0; i trail.Count - 1; i) { Debug.DrawLine(trail[i], trail[i 1], Color.cyan, 10f); } }把新旧两条轨迹用不同颜色在 Scene 视图里叠加一眼就能看出改动参数后的偏差。这也是我调参的日常习惯改一次 launchPower 或贝塞尔系数录一次轨迹再把两次轨迹叠图对比而不是凭眼睛看“好像飞得远一点了”。做完整套愤怒小鸟克隆你会发现任何别的 2D 物理游戏里的拖拽、弹射、击碎、追踪镜头全都在这个骨架里了。希望这些来自一线的调参和翻车经验对你有用。本文还有配套的精品资源点击获取
返回列表