简介:一份基于Unity引擎开发的黄金矿工游戏完整项目,面向Unity初学者及二维游戏开发者,提供可直接打开运行的源码工程,帮助快速上手Unity二维游戏开发。项目中演示了如何利用Unity二维工具创建角色、场景与交互逻辑,涵盖使用C#脚本控制角色动作、判定游戏规则与计算分数,并通过内置物理引擎模拟抛钩抓取金矿时的碰撞和重力效果。在资源管理上,项目展示了纹理、精灵、音频等素材的加载、播放与释放策略,同时借助画布和界面组件搭建计分板、时间显示等界面,便于理解游戏优化与反馈机制。压缩包约六十八点五五兆,已有二千二百九十二人浏览学习,适合作为从零到一掌握Unity工作流、脚本编写与二维游戏设计的实践范本。
1. 黄金矿工不是随便抄抄的小游戏:一个钩爪为什么值得你花两周
用 Unity 从零复刻黄金矿工游戏,最难的不是画出矿石贴图,也不是把场景铺满,而是那个看起来平平无奇的钩爪——它要同时做到摆动、发射、抓取、拉升四个动作,且每个动作的物理手感都要对。网上很多开源版黄金矿工能跑,可只要你玩到第三关就会发现问题:钩子撞到石头会穿模,抓住金块后一路抖着飞回地面,拉炸药桶时旁边的东西也被带上来。核心原因只有一个,黄金矿工的玩法建立在一套「变长绳索 + 钟摆刚体 + 抓取判定」的复合物理之上,直接改坐标的简化做法注定撑不过五关。本文面向学 Unity 两三月、想要一个完整作品的开发者,也会让想用这个项目做课程设计或面试展示的读者拿到直接可复用的方案。我会把钩爪的物理模型、物品数值表、场景搭建、关键参数和踩坑记录全部拆开讲,跟着做能跑通一个核心循环完整、手感可调的版本。
2. 拆解黄金矿工的核心机制:钩爪状态机、物品表与物理选型
2.1 钩爪的三段状态循环:待机、延伸、回收
黄金矿工的玩法循环看起来就三句话:等玩家点击、钩爪伸出去、抓住东西拉回来。但任何一个状态切换处理不干净,就会出现「钩子已经抓住矿石却还继续往下穿」或者「空钩返回时速度比抓了矿石还慢」这种逻辑错乱。我的习惯是把这个循环显式建模成一个状态机,而不是在 Update 里堆一堆 if。
状态划分上我会拆成四个:Ready 待机、Extending 延伸、Reeling 回收、Stuck 卡住。最后一个 Stuck 是很少被讲到的状态,用来处理钩爪撞到不可抓取的障碍物——比如矿洞底部边界或石头墙——这时候钩爪不能一直停在原地,而要自动切换回回收状态。卡住不处理,玩家就会看着钩子悬在矿洞里发呆,整个游戏进程直接卡死。
以下是最小可用的状态机骨架,不掺物理逻辑,只负责状态流转和事件广播:
// HookStateMachine.cs public enum HookState { Ready, Extending, Reeling, Stuck } public class HookStateMachine : MonoBehaviour { public HookState Current { get; private set; } = HookState.Ready; public event System.Action<HookState> OnStateChanged; public void SwitchTo(HookState next) { if (Current == next) return; Current = next; OnStateChanged?.Invoke(next); } public bool IsBusy => Current != HookState.Ready; }这个组件单独挂在一个空物体上,钩爪控制脚本订阅 OnStateChanged 事件来响应状态变化。这么做的原因是为了让状态管理、输入响应和物理操作解耦:输入在 Update 里处理,物理在 FixedUpdate 里处理,状态切换则由事件在两者之间安全传递。实际调试时你会发现,黄金矿工大部分诡异行为都是状态和物理不同步造成的,把状态机独立出来后,问题定位会快非常多。
状态切换的具体触发规则是:Ready 下收到鼠标左键按下就进 Extending;Extending 下检测到可抓取物体就进 Reeling,检测到不可抓取边界就进 Stuck;Stuck 短暂等待后自动进 Reeling;Reeling 下绳长回到最短,钩爪回到顶部,重新进 Ready。一轮循环结束时还要重置钩爪的位置、角度和关节状态,这个重置逻辑放到下一章实现部分一起讲。
2.2 物品数据表:价值、重量和深度怎么设计才合理
黄金矿工的爽感来源是「用有限时间做出选择」:是钩那个价值高但位置深的钻石,还是稳妥地抓手边的金块?这个选择之所以存在,是因为每种矿物在价值、重量、生成深度上有明显的权衡关系。钻石价值最高,但往往埋得最深、拉回来最慢;金块均衡;银器价值低但重量轻、回拉快;石头没有任何经济价值,纯粹是干扰项。
这些属性必须数据化,不能在每个预制体上单独填一份。我的做法是用 ScriptableObject 定义物品模板,同一类矿石的所有实例共享一份数据,预制体只负责挂脚本引用。
// ItemData.cs [CreateAssetMenu(fileName = "ItemData", menuName = "GoldMiner/ItemData")] public class ItemData : ScriptableObject { public string itemName; public int worth; // 卖出价格,范围 1~1000 public float weight; // 相对重量系数,0.5~3.0 public float spawnDepthMin; // 生成深度下限(相对矿洞顶部) public float spawnDepthMax; // 生成深度上限 public bool isTNT; // 炸药桶标记 public float explosionRadius; // TNT 爆炸半径 }数值设计的推荐区间我列一下:worth 控制在 1~1000,金块常见 100~300,钻石 600~800,银器 30~80,石头 1~5。weight 控制在 0.5~3.0,重量越大,拉升时钩爪回收速度衰减越明显。spawnDepthMin/Max 用相对矿洞顶部的距离表示,矿石生成时在这个区间内随机取深度。还要给每种物品配一个生成权重,这个权重单独放配置文件,不会写在代码里。
物品预制体的碰撞体布置是另一个容易出问题的地方。每个矿石预制体上放两个碰撞子物体:一个 Bodies 用的 Collider2D,负责物理阻挡,属性 IsTrigger 不勾选;另一个 GrabTrigger 用的 Collider2D,负责被钩爪抓取判定,属性 IsTrigger 勾选。抓取触发器明显小于矿石身形,大约只有矿石视觉大小的一半。
这样拆两个碰撞体的原因:如果只用物理碰撞体做抓取,钩爪的刚体会先撞到矿石弹开,触发不了抓取;如果整体都做成触发器,矿石之间就不存在物理阻碍,矿层结构会完全失效。二合一的方案最稳,代价是预制体层级多了一层,但排查抓取问题时你会非常感谢这个设计。
2.3 为什么直接改坐标是翻车的根源
新人在 Unity 里做钩爪,最容易想到的方案是直接控制 Transform.position,朝着一个方向移动,碰到目标就带着目标往回走。这个做法在最初十几分钟里看起来一切正常,但你会发现两个无解的问题:一是矿石之间的碰撞完全失效,钩爪会穿过石头缝隙里的金块直接抓最深处的钻石,矿层布局变得毫无意义;二是带回矿石时,矿石的 Rigidbody2D 会和场景里的其他刚体产生纠缠,出现石块连根拔起、矿石互相弹飞的物理污染。
标准做法是用三个 2D 物理组件配合:Rigidbody2D 负责钩爪自身的动力学,HingeJoint2D 把钩爪挂到顶部锚点形成钟摆,DistanceJoint2D 控制绳索长度。HingeJoint2D 让钩爪在没人操作时像钟摆一样来回摆动,视觉上告诉你「可以按了」;玩家点击后断开铰链,钩爪沿摆动切线方向飞出,同时 DistanceJoint2D 一头连锚点、一头连钩爪,限制最大绳长。
选型原则我用一句话概括:凡是和绳索、摆动、拉升相关的行为,交给物理关节;凡是和生成、计数、状态切换相关的逻辑,留在脚本里。这样物理引擎替你处理碰撞、反弹和摩擦,脚本只需要关心业务状态。代价是钩爪的行为不再是 100% 确定性的路径,它可能因为碰撞阻挡、摩擦衰减和重力影响偏离你的预期,但这也正是黄金矿工这个项目最有学习价值的地方。
3. 用 Unity 跑通可玩原型:场景结构、钩爪关节和 UI 落地
3.1 场景层级与预制体布局:把物理和逻辑彻底分层
我一般按四个顶层节点组织场景:GameManager、UI Canvas、MineRoot、HookRoot。GameManager 挂计时和关卡结算逻辑,UI Canvas 放金币和计时文字,MineRoot 下面是矿洞背景和所有矿石实例,HookRoot 下面是钩爪、锚点、绳索渲染器。这四个节点互不嵌套,职责边界清楚。
钩爪预制体的结构比想象中复杂一点,它的层级如下:
HookRoot ├── AnchorPoint(空物体,位于地面顶部 y 边界位置) ├── Hook(Rigidbody2D + HingeJoint2D + DistanceJoint2D + Collider2D) │ └── HookVisual(贴图或 SpriteRender,纯粹的视觉子物体) ├── RopeRenderer(LineRenderer,挂在 AnchorPoint 下) └── HookStateMachine(空物体,挂状态机脚本)AnchorPoint 是钟摆的悬挂点,它的坐标就是 HingeJoint2D 的 connectedAnchor。Hook 物体挂 Rigidbody2D、HingeJoint2D 和 DistanceJoint2D:HingeJoint2D 的 connectedBody 指向 AnchorPoint 所在物体上的刚体;DistanceJoint2D 的 connectedBody 同样指向锚点。RopeRenderer 用 LineRenderer 从 AnchorPoint 画到 Hook,两点连线,不做复杂绳索物理模拟,因为 Performance 上完全没必要。
所有矿石做成独立 Prefab。矿石层级结构是 Body(碰撞体)和 GrabTrigger(触发器)两个子物体加 MineableItem 脚本。MineableItem 脚本暴露两个属性:所属 ItemData 实例和当前实例化后的 Rigidbody2D 引用。生成矿石时 SpawnManager 读配置文件,按深度区间算出实际坐标,在 MineRoot 下实例化。
3.2 钩爪发射逻辑:断铰链、给冲量、放绳索
这是整个黄金矿工物理的核心步骤。钩爪处于 Ready 状态时,HingeJoint2D 是启用的,鉤爪像钟摆一样围绕 AnchorPoint 摆动。玩家点击鼠标左键后,第一步关闭 HingeJoint2D,让钩爪从约束中释放;第二步给 Rigidbody2D 一个沿摆动切线方向的冲量,确保钩爪不会原地垂直下落;第三步启用 DistanceJoint2D 并设置初始绳长,钩爪一边受重力下坠、一边被绳索拉住,形成一条向前下方延伸的弧线。
// HookLauncher.cs - 挂载在钩爪物体上 void Launch() { sm.SwitchTo(HookState.Extending); // 断开铰链,钩爪恢复自由刚体 anchorHinge.enabled = false; // 启用绳索约束 ropeJoint.enabled = true; ropeJoint.distance = 0.5f; // 初始绳长,避免点击瞬间钩爪位移 // 冲量方向:原摆动速度方向 + 向右/向左随机偏移 Vector2 baseDir = hookBody.velocity.normalized; if (baseDir.magnitude < 0.01f) { baseDir = Vector2.down; } Vector2 launchDir = (baseDir + Vector2.down * 0.5f).normalized; hookBody.AddForce(launchDir * launchImpulse, ForceMode2D.Impulse); }关键参数是 launchImpulse 和随机偏移量。launchImpulse 推荐设置为 4~7,过小钩爪飞不远,过大绳索还没起作用就直接怼到矿洞边界。随机偏移量控制在 0.3~0.6 之间,让每次发射方向都有细微差异,避免玩家背下固定路径;但偏移也别超过 0.6,否则钩爪弹道会变得难以预测,玩家会觉得手感飘。
发射后钩爪不会直接朝一个方向直线飞,它会在重力作用下下坠,同时 DistanceJoint2D 逐步放大绳长,让钩爪能一直向深处延伸。绳索延伸速率由 extendSpeed 控制,推荐 6~10 每秒。这里有一个非常重要的实现细节:绳长的改变必须在 FixedUpdate 中完成,不要在 Update 里直接改 DistanceJoint2D.distance。因为 distance 属性和物理引擎的求解器同步,帧率波动大的设备上在 Update 里改会出现肉眼可见的抖动。
3.3 抓取与回收:触发器判定、刚体挂载和拉升回程
钩爪延伸过程中,它的抓取判定是用 Collider2D 的 IsTrigger 做的。钩爪自身挂一个 Collider2D,同样设置 IsTrigger,和矿石预制体上的 GrabTrigger 碰撞时触发 OnTriggerEnter2D。触发后要立刻把钩爪从 Extending 切到 Reeling,并处理矿石的物理挂载。
挂载方案我推荐用 FixedJoint2D 而非直接把矿石设为钩爪子物体。把矿石变成子物体会导致它的局部坐标和父物体耦合,后续做物品旋转、动画和回收判定都麻烦;FixedJoint2D 则保持两者为独立刚体,物理引擎自动处理连接关系,回收时解除连接只需要移除组件。
// HookCatch.cs - 挂在钩爪物体上 void OnTriggerEnter2D(Collider2D other) { if (sm.Current != HookState.Extending) return; MineableItem item = other.GetComponentInParent<MineableItem>(); if (item == null) return; // 状态切换 sm.SwitchTo(HookState.Reeling); // 用固定关节连接矿石和钩爪 FixedJoint2D joint = item.gameObject.AddComponent<FixedJoint2D>(); joint.connectedBody = hookBody; joint.autoConfigureConnectedAnchor = true; // 把矿石的刚体重力关了,避免拉扯过程中矿石自行下坠 item.Rigidbody.gravityScale = 0f; }注意判断里要排除 Stuck 和 Ready 状态,不能让一个已经卡住或待机的钩爪还能继续抓东西。MineableItem 脚本暴露 Rigidbody 属性,脚本内部缓存 GetComponent 结果,避免每次抓取时重复查找。
回收阶段的绳长收缩同样在 FixedUpdate 里做。这里要把拉升速度 reelSpeed 和物品重量关联起来:真实感要求重的矿石拉得慢,轻的银器拉得快。实现方案是给 reelSpeed 加一个 weight 系数,物品越重,拉升速度越低。
// HookRope.cs - 绳长控制,FixedUpdate 中执行 void FixedUpdate() { if (!ropeJoint.enabled) return; if (sm.Current == HookState.Extending) { float target = ropeJoint.distance + extendSpeed * Time.fixedDeltaTime; ropeJoint.distance = Mathf.Min(target, maxRopeLength); } else if (sm.Current == HookState.Reeling) { float weightFactor = 1f / Mathf.Max(1f, currentItemWeight); float target = ropeJoint.distance - reelSpeed * weightFactor * Time.fixedDeltaTime; ropeJoint.distance = Mathf.Max(target, minRopeLength); if (ropeJoint.distance <= minRopeLength) { FinishReeling(); } } DrawRope(); }currentItemWeight 来自当前抓取矿石的 ItemData.weight,空钩返回时取 1.0。权重因子用除法做的好处是:重量为 2.0 的钻石回收速度减半,重量为 0.5 的银器回收速度翻倍,曲线比较自然。绳长到达 minRopeLength 时调用 FinishReeling,把矿石转成金币、销毁物体、重置钩爪到 Ready 状态。
3.4 UGUI 计时器与金币显示:状态驱动的界面刷新
UI 部分我用 UGUI 的 Text 组件,不做过度设计。Canvas 下两个 Text:GoldText 和 TimerText,位置分别在左下角和右上角。金币文本用整数显示,计时文本保留一位小数。
// GameUIManager.cs void LateUpdate() { goldText.text = GameManager.Instance.Gold.ToString(); float remain = GameManager.Instance.TimeRemaining; timerText.text = remain.ToString("0.0"); }选择 LateUpdate 刷新文本是因为它在一帧内所有逻辑执行完毕后运行,不会出现界面数值和游戏状态差一帧的割裂感。每帧刷新量的开销很小,不值得为这种程度的数据做事件驱动刷新;但如果你后期加商店、加多关卡,建议改成只在数值变化时刷新,避免不必要的字符串分配产生 GC。
GameManager 持有回合时长和当前金币数,用单例暴露给其他脚本:
// GameManager.cs public class GameManager : MonoBehaviour { public static GameManager Instance { get; private set; } public int Gold { get; private set; } public float TimeRemaining { get; private set; } public float roundDuration = 90f; void Awake() { Instance = this; TimeRemaining = roundDuration; } void Update() { if (TimeRemaining > 0f) { TimeRemaining -= Time.deltaTime; } else { EndRound(); } } public void AddGold(int amount) { Gold += amount; } void EndRound() { // 播放结算动画 / 显示商店面板,这里先留空 } }注意 EndRound 不能直接 Time.timeScale = 0 来暂停游戏。timeScale 会影响所有物理和计时,如果之后用同一套时间系统恢复,会出现物理状态和 UI 时间错位。正确的做法是引入一个单独的游戏状态枚举(Playing、RoundEnd、Shop),在 Update 开头检查状态,只有 Playing 时才扣计时。
4. 手感调优:钩爪速度、拉升阻尼与难度曲线的参数设置
4.1 钩爪转速、发射冲量、绳长上限:手感从这三个量开始
黄金矿工的手感其实就是「钩爪响应够不够快、路径够不够可预测」。三个参数决定 80% 的手感:钟摆周期、发射冲量、绳长上限。
钟摆周期由钩爪 Rigidbody2D 的 gravityScale 和 HingeJoint2D 的锚点位置决定。Unity 的 2D 物理默认重力加速度是 -9.81,如果钩爪的 gravityScale 保持 1.0,钟摆会摆得又快又急,玩家根本来不及瞄准。我的推荐值是 gravityScale 0.3~0.5,钟摆摆动的节奏会明显放慢,玩家有足够时间判断发射时机。这个值低于 0.2 时钩爪会显得死气沉沉,高于 0.7 时又变得过度灵敏、难以瞄准。
发射冲量决定钩爪离开锚点后的初速度。它应该和钟摆在发射瞬间的线速度方向一致,然后叠加一个向下的分量,让钩爪的弹道呈浅抛物线。冲量 4~6 的区间比较合适。小于 3,钩爪飞不出去,经常会觉得明明瞄准了却够不到;大于 8,钩爪直接冲破绳长上限怼在矿洞边界上,看着很假。
绳长上限 maxRopeLength 直接控制矿洞的纵深。推荐设为 8~12,对应矿洞深度约 4~6 块矿石的堆叠高度。绳长过长会让玩家满地图乱抓,策略感下降;绳长过短则钻石等深层矿石永远够不到,失去目标感。调这个参数时要和矿石生成深度区间一起改,保证最深处的矿石在绳长上限的 85% 左右处可以被碰到。
4.2 拉升阻尼与物品重量:让拉金子像拉金子,拉银器像扯棉花
拉升阶段最影响手感的是物品重量对回收速度的影响。第三章已经实现了一个 weight 因子,这里讲怎么调出层次感。
金块的 weight 设为 1.5 左右,回收时钩爪明显变慢但还有推进感;银器 weight 0.6,回收像扯棉花一样几乎不减速;钻石 weight 2.5,回收非常沉重,玩家拉钻石时要承担被卡住的风险——这个风险就是难度的来源。石头 weight 3.0,回收极慢,玩家勾到石头基本就是浪费时间,这种情况下手感要刻意做得「亏」,让玩家做出正确的取舍判断。
拉升阻尼还涉及 Rigidbody2D 的 linearDrag。这个值不要全局调,因为钩爪在空钩返回和带物品返回时,物理表现完全不同。空钩返回时 linearDrag 0.5 左右就够,带物品时由于物品自身 Rigidbody2D 也会产生阻力,钩爪的 linearDrag 建议 1.0~1.5,不至于在回收终点产生明显的弹簧回弹。
如果遇到钩爪回收时在接近顶部的位置反复弹跳,基本可以判断是 linearDrag 太小或者 minRopeLength 太大。绳索收缩到最短距离时,DistanceJoint2D 会产生一个虚拟的刚性约束,把钩爪猛地扯住,LinearDrag 不够就会来回震荡。解决办法是提高钩爪的 linearDrag 到 1.5 以上,并且把 minRopeLength 控制在 0.3~0.5。
4.3 难度曲线:生成概率、价格波动与关卡时长
难度曲线是黄金矿工数值系统里最容易偷懒也最不能偷懒的部分。很多复刻版从第一关到第五关只有矿石出现概率变了,深度和价格完全没动,玩两关就腻。
我的调法是把三个维度分开:矿石生成权重、深度区间、时限。第一关生成权重金块 60%、银器 25%、石头 10%、钻石 5%,深度区间集中在浅层 0~4;第二关开始出现钻石,权重升到 10%,深度区间下探到 3~8;第三关金块权重降低到 40%,石头升到 25%,矿洞整体更深。时限从 90 秒起步,每关减少 5 秒。
价格波动也是重要的难度维数。黄金矿工的经典策略是「等价格涨了再卖」,钻石价格会随关卡在 600~800 之间波动。实现方案是每关开始时给每种物品生成一个价格乘子 0.8~1.2,加在基础 worth 上。这个乘子显示在物品的 Tooltip 或 UI 上,玩家需要判断当前价格是否划算。
价格波动的随机性要控制在正负 20% 以内,超过这个范围会出现钻石卖 400 的情况,玩家会觉得自己被系统惩罚。控制方法是用随机种子生成一个平滑变化的乘子,而不是每关独立随机:上一关乘子 1.1 的钻石,下一关乘子在 0.95~1.15 之间波动,形成「这个钻石越来越贵了」的预期。
5. 黄金矿工常见问题排查:物理碰撞、UI 穿模和资源管理的坑
5.1 钩爪穿过矿石但没触发抓取
这是最高频的问题:钩爪明明从金块的正上方经过,看着穿过去了,却没有抓取反应。
现象:钩爪的 Collider2D 和金块的碰撞体发生了物理穿透,但 OnTriggerEnter2D 没触发。
原因:两种最常见。一是钩爪的 Collider2D 没勾 IsTrigger,玩家用的又是 OnTriggerEnter2D,自然只有物理碰撞没有触发回调。二是金块的两个碰撞体设置反了:物理碰撞体也勾了 IsTrigger,或者抓取触发器的层级找错了,GetComponentInParent 没匹配到 MineableItem。
解决:检查钩爪 Collider2D 的 IsTrigger 是否勾选;检查金块预制体里 Body 碰撞体 IsTrigger 不勾选、GrabTrigger 勾选;在 OnTriggerEnter2D 里加一行 Debug.Log($"{other.name}"),确认实际触发的是哪个碰撞体。确认逻辑没问题但依然穿模,检查钩爪 Rigidbody2D 的 collision detection 模式,工业级做法是把钩爪的 Collision Detection 设为 Continuous,防止高速运动下穿透薄片碰撞体。
5.2 物品在拉升过程中疯狂抖动,甚至从钩爪上掉下来
现象:抓住金块后,金块在回收途中不停抖动,有时直接脱离 FixedJoint2D飞出去。
原因:FixedJoint2D 的 connectedBody 指向钩爪,但金块自身的 Rigidbody2D 仍在参与物理模拟。两个刚体之间如果涉及多方接触(比如金块刮蹭到矿洞墙体),Unity 物理求解器会反复调整位置约束,产生高频抖动。
解决:抓取时把金块的 Rigidbody2D 的 simulated 属性设为 false,让它脱离物理模拟,只作为视觉和逻辑对象存在。但注意 simulated = false 会同时禁用碰撞,金块就能穿过其他矿石——这其实符合黄金矿工的原始规则,拉起来的金块不再和其他物体交互。另外把 FixedJoint2D 的 enableCollision 设为 false,不让两刚体间产生对撞。
5.3 Time.timeScale = 0 导致物理全部冻结
现象:实现暂停功能时写了 Time.timeScale = 0,倒计时确实停了,但恢复时发现钩爪卡住了,或者恢复后钩爪位置突然改变。
原因:timeScale = 0 会把所有基于 Time.deltaTime 的物理计算暂停,包括 DistanceJoint2D 和 HingeJoint2D。恢复时物理引擎会重新计算挂起期间的积分,导致位置跳跃。
解决:不要用 Time.timeScale 做暂停。用 GameManager 里的状态枚举,在 Update 开头检查是否为 Playing,非 Playing 就直接 return,不执行任何计时和输入逻辑。游戏内计时用 Time.deltaTime 手动累减,不依赖 timeScale。物理系统始终以真实时间运行,暂停只是不再接收输入和更新逻辑。
5.4 打包后材质变成紫粉色,或者字体丢失
现象:编辑器里一切正常,打包到 Windows 或 Android 后矿石呈现鲜艳的洋红色,UI 字体变成方格。
原因:紫红色是 Unity 找不到 Shader 的默认显示。常见原因是工程用了 URP/HDRP 渲染管线,而矿石材质引用的是 Built-in 管线的 Standard Shader,打包时该 Shader 没有被包含。字体丢失则通常是因为动态字体在打包时没有正确打包字体资源,或字体文件还在 StreamingAssets 里没被识别。
解决:统一使用当前工程渲染管线对应的 Shader 重新创建材质,比如 URP 项目创建材质时选择 URP/Lit。不要保留任何 Built-in Shader 的材质。字体方面,检查 UI Text 的 Font 是否引用了 Resources 或 Assets 内实际的 .ttf 文件,打完包后在编辑器里用 Addressable Group 检查字体是否被正确标记。打包后在目标平台跑一遍 UI 场景是最基础的验证。
5.5 钩到一块石头,旁边的金块也被一起拉上来
现象:钩爪抓住石头后,石头附近的多个金块也跟着飞上来,最后一起被回收。
原因:石头和金块之间相互碰撞,通过多个 FixedJoint2D 的级联效应绑到了一起。常见触发条件是在场景中用大量矿石紧密堆叠,矿石之间的接触力把一个矿石拉走时,它拖动了相邻矿石。
解决:抓取时把矿石的 simulated 设为 false 后,其他矿石仍然处在物理模拟中,所以这个级联效应的根源是石头在拉升过程中刮蹭到了旁边的矿石。解决办法是在矿石预制体上给物理碰撞体设置合理的碰撞 Layer,例如 ImmovableRock 和 OreItem 分到不同 Layer,在 Layer Collision Matrix 里关闭 OreItem 之间的碰撞,让矿石之间不会相互传导力。这个调整会显著改变矿洞里的物理表现,需要重新测试手感。
6. 从「能玩」到「有手感」:黄金矿工的验证清单与玩法延伸
判断你的黄金矿工是否做到位,我会跑五组验证。第一组是空钩测试:不抓任何东西,连续发射十次,钩爪每次都应该有明显的抛物线弹道且不穿边界。第二组是单物抓取:浅层金块从发射到回收到手不超过 4 秒,且整个过程没有抖动。第三组是重量区分:分别抓银器和钻石,回收速度应该有肉眼可见的差距。第四组是暂停测试:暂停 30 秒再恢复,钩爪状态和倒计时都应该无缝继续。第五组是打包验证:在目标平台(我一般先打 Windows 再打 Android)上完整跑一关,确认没有紫粉色材质、UI 字体正常、物理帧率稳定。这五组都通过,你的核心循环就是稳的。
在此基础上加玩法,我的习惯是从最小改动开始:先加一个炸药桶道具,触碰后爆炸把周围矿石弹开,这能直接测试你对物理级联的处理是否到位;再加商店系统,用每关结算的宝石买加速器、磁铁和备用钩爪,这类道具本质是改变手感参数,不需要新增复杂系统;最后考虑双人模式,一人操作钩爪、一人操作商店购买,切换判定的核心逻辑是共享同一个 GameManager 实例。
我个人最大的教训是:不要在美术和 UI 上过早花时间,先把物理手感做到自己玩起来上瘾,再开始加商店、加动画。手感不对的游戏加了再多系统玩家也不会留下来。希望这篇拆解能帮你少走一个月的弯路,动手把钩爪的参数调到让人放下鼠标前先犹豫一下再收手——那就是手感对了。
本文还有配套的精品资源,点击获取