
简介这份Unity黄金矿工游戏项目为2D游戏学习者提供了完整的工程源码与导出文件适合想通过实际案例掌握Unity工作流、C#脚本编写、物理交互与UI搭建的开发者无论是高校学生、独立开发者还是培训机构都可将其作为教学或自学素材。压缩包共68.55MB内含项目场景、C#脚本、纹理精灵与音频等资源结构清晰可直接在Unity编辑器中打开并逐对象检查组件与脚本逻辑项目按照场景、脚本与资源分类组织便于按模块学习。已有2292人学习下载是入门Unity 2D开发的热门参考。通过阅读角色控制、抓取判定、计分规则等核心脚本可以理解物理引擎在钩爪抓金矿玩法中的运用并学习Canvas计分板、时间显示等UI系统的构建方式修改参数或替换素材还能快速验证不同手感与关卡平衡帮助读者从模仿走向创新逐步提升游戏开发能力。1. 一份能直接跑起来的 Unity 黄金矿工真正的难点是物理参数不是代码逻辑很多第一次拿到这个 Unity 黄金矿工工程包的人第一反应是去读钩子发射的代码以为难点在怎么把钩子甩出去。跑起来之后才发现一半时间都耗在调物理参数上钩子要么高速穿模要么把整座矿山拖飞绳索要么画在半空不跟钩子走。这份压缩包解出来就是一个能直接运行的 2D 完整工程核心玩法链条齐全——抓钩发射与回收、矿块生成、计分系统、倒计时、TNT 道具和关卡循环脚本只集中在几个几百行的 cs 文件里没有用复杂的框架和资源商店插件。适合两类人刚学完 Unity 基础、想做一个有完整闭环的小游戏练手准备面试、想用一个小项目把 2D 物理、UGUI、事件流讲清楚。下面按我拆包时的顺序把每个模块的用法、参数意义和踩过的坑都过一遍照着复现成本很低。2. 抓钩发射与回收LineRenderer、DistanceJoint2D 与两个坐标坑2.1 目录里有什么先从 Scene 和五个脚本的关系入手解开压缩包后建议不要直接双击场景先在 Project 窗口里过一遍 Assets 结构。常见排布大致是Scenes/Main.unity主场景Scripts下放着 HookController、MineController、MineSpawner、GameManager、UIController 这几个核心脚本Prefabs里是不同尺寸的金块、钻石、石头和 TNT 的预制体Sprites是游戏素材。第一次打开时如果 Inspector 里有脚本显示 Missing先别慌多半是解压路径里中文目录导致 Unity 引用失效整个工程挪到纯英文路径再打开。脚本职责先说清楚HookController 是钩子的发射、回收和绳索绘制MineController 挂在每个矿块上负责被钩住后的响应MineSpawner 按权重表在场景里铺矿GameManager 管倒计时、分数和游戏状态UIController 只做 UGUI 的刷新。这个分层方式不算复杂但足够演示一个单场景游戏怎么拆分逻辑后续想加音效、加动画都不会把代码全堆进一个文件。2.2 发射钩子方向向量、发射速度与 LineRenderer 画线发射逻辑是所有玩家的第一个动手点。鼠标点击后以玩家小车的位置为起点鼠标位置减去起点得到一个方向向量归一化后给钩子的刚体赋值速度。这里我一般直接用Rigidbody2D.velocity而不是AddForce因为发射是瞬时的用力的方式会让速度从 0 开始加速手感会飘。速度给一个固定值场景里 15 到 20 之间比较稳。public class HookController : MonoBehaviour { public Transform fireOrigin; // 玩家小车上的挂点也是绳索起点 public Rigidbody2D hookRb; // 钩子刚体 public LineRenderer rope; // 绳索 public float fireSpeed 18f; // 发射速度太大容易穿模 public float retrieveSpeed 12f; // 回收速度 private DistanceJoint2D _joint; private bool _retrieving; void Start() { _joint GetComponentDistanceJoint2D(); _joint.enabled false; rope.useWorldSpace true; // 关键用世界坐标画线 rope.positionCount 2; } public void FireHook(Vector2 direction) { transform.position fireOrigin.position; gameObject.SetActive(true); _retrieving false; _joint.enabled false; // 每次发射前重置关节 hookRb.isKinematic false; hookRb.velocity direction.normalized * fireSpeed; } }Direction 是发射方向normalized 保证斜向发射时速度不会被拆成两个分量后变小。rope.useWorldSpace true是第一个坑很多新手用默认的 localSpace钩子一离开原点线就画在场景别的位置。代码里每次发射前都重置 joint 和刚体状态是为了防止上一局残留的等待回收状态把这次发射搞乱。2.3 抓取与回收为什么用 DistanceJoint2D 而不是把矿块塞进钩子子物体钩子碰到矿块后要让它停下来并把矿块拖回来。最省事的写法是把矿块的 transform 设成钩子的子物体然后移动钩子矿块自动跟着走。但这样矿块的 Rigidbody2D 会和父级刚体产生奇怪的物理插值矿块之间碰撞也会抖动。正确做法是用关节钩子上挂一个 DistanceJoint2D钩住矿块时把矿块的刚体指定为 connectedBody然后钩子进入回收状态靠代码往玩家位置移动矿块被关节拖过来。public class MineController : MonoBehaviour { public int score 10; // 矿块价值 private bool _hooked; private HookController _hook; void OnTriggerEnter2D(Collider2D other) { if (_hooked) return; if (!other.CompareTag(Hook)) return; _hooked true; _hook other.GetComponentHookController(); // 停止钩子、开启关节并连接到矿块刚体 _hook.AttachAndRetrieve(GetComponentRigidbody2D()); // 关闭矿块的碰撞防止回收途中二次触发 GetComponentCollider2D().enabled false; } }矿块的 Collider 保持非 Trigger钩子的 Collider 设为 Trigger这样钩子高速飞进矿堆时不会把矿块顶开又能通过 Trigger 检测到碰撞。_hooked标志用来防重入否则矿块同一帧被多个碰撞点触发会重复调用回收逻辑。public void AttachAndRetrieve(Rigidbody2D mineRb) { hookRb.velocity Vector2.zero; hookRb.isKinematic true; // 回收阶段钩子由代码控制不再受物理影响 _joint.enabled true; _joint.connectedBody mineRb; // 把矿块刚体挂到关节上这是核心 _joint.distance 0.2f; // 矿块与钩子保持 0.2 米的距离 _retrieving true; } void Update() { if (!_retrieving) return; // 每帧朝玩家挂点移动距离足够近就完成回收 transform.position Vector3.MoveTowards( transform.position, fireOrigin.position, retrieveSpeed * Time.deltaTime ); }这里有个物理细节要提醒钩子变成 kinematic 后Rigidbody2D.velocity就不再生效这也是很多人在网上搜unity 物体速度怎么获取却拿不到数值的原因。kinematic 刚体的运动状态不能通过 velocity 读取只能用transform.position差值估算速度。关节连接后钩子每帧位移一米矿块就被 DistanceJoint2D 拖着走一米物理引擎会自动处理矿块与其他矿堆的碰撞表现比子物体方案稳得多。2.4 摄像机跟随与速度读取让视野和物理手感都正常这个项目是横版固定视角摄像机不需要全跟随但玩家小车会被大金块拉动如果不给镜头一个缓冲画面会跟着剧烈抖动。我一般给摄像机加一个很简单的跟随脚本把 x 坐标朝玩家目标的 x 坐标做 Lerp 平滑y 锁定在场景中线。如果场景比屏幕大还可以限制摄像机移动边界。public class CameraFollow : MonoBehaviour { public Transform target; public float smoothTime 0.2f; void LateUpdate() { Vector3 pos transform.position; pos.x Mathf.Lerp(pos.x, target.position.x, smoothTime); pos.y Mathf.Lerp(pos.y, target.position.y, smoothTime); pos.x Mathf.Clamp(pos.x, -6f, 6f); pos.y Mathf.Clamp(pos.y, -3f, 3f); transform.position pos; } }放在 LateUpdate 里是因为物理更新发生在 FixedUpdateUpdate 里读到的 target 位置可能还是上一帧的旧值LateUpdate 能保证拿到本帧最终位置。顺带说一句想调试钩子手感可以在 Inspector 里盯着 Rigidbody2D 的 velocity2D 刚体速度就是Vector2magnitude就是当前速度大小调 fireSpeed 时用这个值做参考比肉眼判断准。3. 矿块生成与物理权重一张权重表决定一关的手感3.1 矿块种类、分数与质量先建一张能改的权重表黄金矿工的乐趣在于赌徒心理因此矿块不能只按体积分类还要让玩家在拉大金块很爽但可能超时和拉小石头保底之间做选择。这份工程里矿块一般分五类小金块、大金块、钻石、石头、TNT。下面是我调过一轮比较顺手的基础参数不是固定标准但作为起点够用。矿块分数建议质量(mass)出现权重说明小金块201.230高频低风险回收快大金块605.514分高但拉得慢中期要博钻石1001.58轻且分高应该有但别太多石头02.535纯负面干扰权重不能超 40TNT01.06抓到触发爆炸清场景质量这一列最容易被人忽略。大金块如果也按体积比例配一个 10 的质量回收时钩子会被拉得极慢体验很差如果质量只有 2大金块又会被钩子拽着到处飞撞击其他矿块时物理表现很假。5 左右的 infill 质量配合回收速度 12手感是沉重但不至于拖不动。石头质量设置成 2.5是因为它要成为明显的负面反馈卡一下节奏但又不能完全拽不动。3.2 随机生成与实例化让不同矿块按概率铺进场景权重随机的实现方式常见做法是定义一个可序列化的条目列表每个条目存预制体和权重。生成时先累计总权重Roll 一个 0 到总权重之间的数然后逐条累加Roll 值落在哪个区间就生成哪个矿块。这段代码可以直接复制到 MineSpawner 里。[System.Serializable] public class SpawnEntry { public GameObject prefab; public int weight; } public class MineSpawner : MonoBehaviour { public ListSpawnEntry entries; // 在 Inspector 里配置 public Vector2 spawnArea; // 生成区域宽高 public int spawnCount 12; void Start() { for (int i 0; i spawnCount; i) { SpawnOne(); } } void SpawnOne() { int total 0; foreach (var e in entries) total e.weight; int roll Random.Range(0, total); // 包含 0不包含 total int cursor 0; GameObject chosen entries[0].prefab; foreach (var e in entries) { cursor e.weight; if (roll cursor) { chosen e.prefab; break; } } Vector2 pos new Vector2( Random.Range(-spawnArea.x / 2f, spawnArea.x / 2f), Random.Range(-spawnArea.y / 2f, spawnArea.y / 2f) ); Instantiate(chosen, pos, Quaternion.identity); } }Random.Range(0, total)在整数重载下是左闭右开永远不会返回 total所以权重表里第一个条目的区间包含 0最后一个条目的区间不会越界。生成区域spawnArea建议按摄像机可见范围略微缩小避免矿块刷在屏幕边缘一半在外面的尴尬位置。Instantiate 之后如果需要对矿块做一些初始状态设置应该在这里拿到返回的实例引用统一处理不要写进矿块自己的 Awake。3.3 物理参数怎么调不追求固定值按手感需求给区间调物理是这项目里最玄学的部分不同电脑上表现可能有差异因为 Unity 的 Fixed Timestep 默认 0.02 秒机器性能差时物理步长会产生偏差。我调试时固定遵守几个原则速度类参数用固定值不用力累积回收阶段钩子改用 kinematic避免物理引擎在高速移动时产生额外的抖动矿块之间的碰撞靠 Collider 的自身 Material 处理不要每个矿块挂物理材质。如果发现钩子发射后碰到矿块经常弹开检查钩子的 Rigidbody2DcollisionDetectionMode高速物体默认 Discrete 检测容易隧穿改成 Continuous 会好很多。代价是 Continuous 对性能有额外开销在这个场景里钩子只有一根完全扛得住。线性阻尼linearDrag建议 0.1 到 0.3太高会让矿块在回收途中显得像在水里游泳。重力缩放保持 1不要为了手感好把重力调成 0.5否则矿块被拉脱后会在空中飘很久整个游戏失去重量感。4. 游戏流程与 UI倒计时、计分、TNT 与关卡循环4.1 状态机与计时用 Time.deltaTime 还是 Time.timeScale一个完整的黄金矿工关卡至少要有三态Playing进行中、GameOver结算、Ready下一关准备。很多新手直接把倒计时写在 Update 里不判断状态时间到了之后 UI 还在继续减因为 GameOver 被调用了但状态没锁。GameManager 里用一个简单枚举状态机Update 开头先判断状态不是 Playing 就 return能挡住大半问题。public enum GameState { Ready, Playing, GameOver } public class GameManager : MonoBehaviour { public float levelTime 60f; public int targetScore 500; public GameObject gameOverPanel; public TextMeshProUGUI scoreText; public TextMeshProUGUI timerText; private GameState _state; private float _timeRemaining; private int _score; void Start() { _state GameState.Playing; _timeRemaining levelTime; gameOverPanel.SetActive(false); } void Update() { if (_state ! GameState.Playing) return; _timeRemaining - Time.deltaTime; timerText.text Mathf.CeilToInt(_timeRemaining).ToString(); if (_timeRemaining 0f) GameOver(); } public void AddScore(int amount) { if (_state ! GameState.Playing) return; _score amount; scoreText.text _score.ToString(); if (_score targetScore) GameOver(); } public void GameOver() { if (_state GameState.GameOver) return; _state GameState.GameOver; gameOverPanel.SetActive(true); } }倒计时用Time.deltaTime还是Time.unscaledDeltaTime取决于你是否打算用Time.timeScale做暂停。如果全局 timescale 设成 0 来做结算暂停那么 deltaTime 也会归零倒计时会停住这一点有时候是希望的效果有时候不是。如果想让倒计时在暂停时继续走就用 unscaledDeltaTime但 UI 动画和钩子移动这时候也得换 unscaled否则画面冻结而数字在跳会很突兀。这个项目里没有做暂停功能所以直接用 deltaTime 最干净。4.2 计分 UICanvas Scaler 适配与 TMP 更新UGUI 部分这个工程用的是 Canvas TextMeshProUGUI计分和倒计时各一个文本。Canvas 的 Canvas Scaler 组件建议设成 Scale With Screen Size参考分辨率 1920x1080。不设的话窗口拉伸时 UI 会挤在左下角或者超出屏幕这是新手最容易忽略的适配操作属于unity 分辨率设置的基础问题。UI 更新不要每帧都 SetText字符串拼接和重建网格在频繁调用时有开销。倒计时每秒刷一次就够了做法是记录上次更新的整数秒只有秒数变化才改文本计分则在 AddScore 里同步更新。虽然这 demo 规模下每帧刷新也看不出问题但养成这个习惯对后面做正式项目有帮助。4.3 TNT 与强力药水2D 没有 AddExplosionForce爆炸得自己算TNT 被钩住后不应该被拖回背包而是应该在抓到的位置直接爆炸清掉周围的矿块这也是黄金矿工里的爽点之一。第一次写爆炸时很容易惯性去找AddExplosionForce但那是 3D 刚体的方法2D 里没有需要自己遍历圆形范围内的碰撞体并对每个刚体施加方向力。void Explode(Vector2 center, float radius, float power) { Collider2D[] hits Physics2D.OverlapCircleAll(center, radius); foreach (var hit in hits) { Rigidbody2D rb hit.GetComponentRigidbody2D(); if (rb null) continue; Vector2 dir (hit.transform.position - center).normalized; rb.AddForce(dir * power, ForceMode2D.Impulse); // 如果需要清场在这里对矿块做 Destroy 或进入回收 MineController mine hit.GetComponentMineController(); if (mine ! null mine.score 0) { GameManager.Instance.AddScore(mine.score); Destroy(hit.gameObject); } } }方向必须用normalized否则爆炸中心附近的物体受力会小于边缘物体除以距离后反而更小实际表现是中心纹丝不动、外围乱飞。设置 ForceMode2D.Impulse 是为了让力变成一次速度突变而不是持续加速度。这里顺手做了一个加分让炸掉大金块也有分拿。4.4 关卡循环同一场景重置 vs 重新加载场景关卡结束后有两种重置方式。第一种是SceneManager.LoadScene(SceneManager.GetActiveScene().name)实现最快但场景里的静态引用、初始状态全都重新跑一遍加载耗时明显移动端更是如此。第二种是自己在 GameManager 里写 ResetLevel遍历所有生成的矿块销毁重置分数、时间和状态同一场景内完成下一关。我推荐第二种因为它能保留已经完成的初始化也为后续做渐入渐出的转场动画留了空间。关卡参数可以做一个简单的难度递增表每关时间从 60 秒递减到 45、35目标分数逐关乘以 1.2石头权重随关卡数增大。把这些数值存在一个关卡配置结构体数组里关卡结束读取下一个数组成员比在脚本里写死三个变量清晰得多。做法是把关卡配置做成可序列化的数组Inspector 里直接调不用重新编译。5. 实战排查穿透、线漂移、拉飞、重复结算、紫红材质五个翻车点5.1 现象钩子高速穿过矿块明明撞到了就是抓不住触发场景是 fireSpeed 调到 20 以上或者从斜坡高速发射钩子直接穿过矿块飞到屏幕外。原因Rigidbody2D 默认的碰撞检测模式是 Discrete物体在一个物理步长内移动距离大于碰撞体厚度时引擎认为没有碰撞这是经典的隧穿。另一个常见原因是钩子的 Collider 是 Trigger矿块 Collider 也是 Trigger两个 Trigger 相遇时没有触发 OnTriggerEnter 的层级矩阵配置。解决在 HookController 的 Start 里把hookRb.collisionDetectionMode CollisionDetectionMode2D.Continuous;并且确认 Project Settings 的 Physics2D 层碰撞矩阵里钩子所在的层和矿块所在层勾选了碰撞。速度控制在 20 以内发射速度不是越大越爽。5.2 现象LineRenderer 画的线不跟钩子走线永远从初始位置延伸具体表现是钩子飞出屏幕绳索还停在发射原点或者线只画了一半终点在世界坐标零点。原因LineRenderer 默认 useWorldSpace 为 false线的坐标是相对于挂载物体的局部坐标而钩子脚本把 line 挂在玩家小车上坐标空间对不上。另一个原因是坐标更新写在了 Update 里而物理移动发生在 FixedUpdateUpdate 里读到的是物理更新前的位置。解决把rope.useWorldSpace true并且把 SetPosition 挪到 LateUpdate。这样无论物理怎么移动LateUpdate 都取到本帧最终位置再更新渲染线就不会漂。5.3 现象大金块一拉就满天飞玩家小车被拽出画面触发场景是抓到大金块后钩子快速回拉金块带着钩子和小车一起呈现摆锤式甩动甚至把整个视角带偏。原因大金块质量配小了。小车没有 Rigidbody2D 或者质量太小钩子 kinematic 移动时把动量传导到整个链条矿块质量过大时回收速度太高导致系统震荡。如果用的是 SpringJoint2Dfrequency 和 dampingRatio 没调好也会有这种弹簧振子表现。解决按第 3 章的参数表把大金块质量提升到 5 左右DistanceJoint2D 的 distance 设置到 0.15 到 0.2让矿块贴紧钩子而不是距离半米被拖着走回收速度从 12 开始调超过 15 就要做好小车被拽走的心理准备。小车的 Rigidbody2D 要么不设动态要么固定铰链否则它也要扛力。5.4 现象倒计时到 0 后 UI 数值还在跳结算面板弹两次触发场景是时间耗尽后分数还在变化或者 GameOver 面板出现、关了又弹出来。原因GameOver 被 Update 多帧调用因为没有状态保护。前端按钮如果监听了同一个 GameOver 事件还会二次弹窗。另一个隐蔽原因是计时脚本挂在多个物体上场景里有两个 GameManager。解决先用 FindObjectsByType 检查场景里有没有重复的 GameManager。再加状态锁if (_state GameState.GameOver) return;放在 GameOver 开头。事件监听者取消订阅的代码写在 OnDestroy 里防止场景销毁后残留调用。5.5 现象打开场景后所有图片和物体材质变紫红色脚本突然丢失触发场景是从压缩包直接解压后双击工程或者换了一台电脑打开材质球全部显示 magenta部分脚本显示 Missing Script。原因紫红色材质是 Unity 找不到对应 shader 时的兜底表现。压缩包里的美术资源如果用了第三方 shader比如二次元卡通渲染 shadershader 文件引用路径断了就会整片紫红。脚本丢失则往往是预制体的 GUID 引用被破坏或者在中文路径下解压导致 meta 文件失效。解决紫红材质去 Project 窗口搜对应材质把 Shader 重新指定为内置的 Sprites/Default或者从同包里的 Shader 文件夹重新导入。脚本丢失先检查工程路径是否纯英文如果还有问题选中预制体按右键 Reimport让 Unity 重建元数据。还不行就只能在场景里重新绑定脚本组件这个没有后悔药所以下载资源后第一件事就是备份原始包。6. 进阶验证三件事确认你的黄金矿工拿得出手第一件事是调试可视化。在 HookController 的 FireHook 里加一行Debug.DrawRay(fireOrigin.position, direction.normalized * 3f, Color.red, 2f);发射时能看到实际方向是否与鼠标指向一致这一步能挡住 90% 的方向偏了但说不清问题。再用 Unity Profiler 跑一遍重点看 Physics2D 的开销占比。如果矿块数量超过 20 个物理开销会明显上升这是正常的真正需要警惕的是 Hook 的 Rigidbody2D 设置了 Continuous 后有没有带来额外性能损耗这个在 Profiler 的 Physics 模块里能看到。第二件事是输入适配。鼠标点击之外给手柄留一条路径很多玩家会用手柄玩。常见做法是把发射方向绑定到右摇杆Unity 的输入系统里默认轴 Horizontal 和 Vertical 是左摇杆右摇杆需要手动添加入口。至少把左摇杆或键盘的上下左右映射到方向发射键用 Fire1这样在 Editor 里按空格也能测试。Vector2 inputDir new Vector2( Input.GetAxisRaw(Horizontal), Input.GetAxisRaw(Vertical) ); if (inputDir.magnitude 0.1f Input.GetButtonDown(Fire1)) { FireHook(inputDir.normalized); }Input.GetAxisRaw 返回 -1、0、1 三档不做平滑适合处理方向型输入。如果使用输入系统的旧接口记得在 Project Settings 的 Input Manager 里检查右摇杆轴名称是否与代码一致否则真机上手柄右摇杆没反应网上unity 手柄右摇杆没反应的问题大半出在这。第三件事是打磨钩子动画和音效的触发时机。钩子发射时给钩子一个 scale 从 0.8 到 1.0 的快速回弹回收到玩家手上时播放一个上滑动画配合一个简单的 AudioSource 播放金石碰撞声游戏质感会上一个台阶。这些动画不需要 Animator 状态机直接在 Update 里用Mathf.PingPong或AnimationCurve.Evaluate就能做一个曲线资源搞定比挂 Animator 省事。我拆过不少 Unity 项目包拿到手先做三件事全局搜一遍Time.timeScale把 Physics2D 层碰撞矩阵过一遍再检查 OnTriggerEnter2D 里的 tag 是否与预制体一致。这三件事做完项目基本能安全跑起来。这个黄金矿工包的三分之二的价值都在物理参数和关节设置上代码反而是最不用担心的部分。希望这些坑位和参数区间能帮你省下几个晚上的调试时间把这套工程改成自己的版本后再回头看看 DistanceJoint2D 的 connectedBody你会理解为什么它比子物体方案稳。本文还有配套的精品资源点击获取