ARTICLE DETAIL

资讯详情

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

Unity 2D游戏开发实战:黄金矿工钩爪物理与UGUI界面实现

Unity 2D游戏开发实战:黄金矿工钩爪物理与UGUI界面实现 简介《Unity黄金矿工游戏开发详解》完整项目源码包面向想通过2D游戏实例入门Unity开发的初学者也适合需要参考完整游戏项目的中级开发者。压缩包约68.55MB内含Unity项目源码与导出文件项目中的C#脚本、纹理、精灵、音频等资源组织清晰可在Unity编辑器中逐项查看。已有2292人学习下载。通过该项目可掌握Unity 2D游戏的核心制作流程角色动作与游戏规则的C#脚本实现、基于物理引擎的钩爪抓取与碰撞重力表现、计分与计时等UI界面的Canvas组件搭建以及资源加载与内存管理方法。将项目导入Unity后可逐物体、逐组件地理解游戏对象与脚本的协作也可修改参数观察行为变化适合用于课程设计、毕业设计或自学练手既能积累真实项目经验又为进一步开发完整小游戏提供可复用的框架与思路。1. 从零复刻黄金矿工这个Unity游戏包里到底有什么把 unity黄金矿工游戏.zip 解压后你得到的是一个可以直接运行的完整 Unity 小游戏工程场景里有一根按固定角度来回摆动的钩爪、一层埋在泥土里的矿石、右上角一条逐渐缩水的计时条以及一套完整的弹出商店。这个包最适合三类人去读要交课程设计的学生、想通过一个小项目把 Unity UGUI 与 2D 物理串起来的入门开发者以及准备把它当面试作品、需要讲清楚每个系统设计理由的求职者。先给一个反直觉的结论这个游戏看上去是在“挖矿”实际上最核心的难点是钩爪的摆锤规律与回收加速度两个手感参数——它们决定玩家觉得钩子“听话”还是“滑溜”数值差 0.2体验就隔了一个档位。2. 搭建黄金矿工的场景骨架矿石、钩爪与场景资源怎么组织2.1 场景对象结构固定视角下的层级划分黄金矿工的玩法和摄像机是强绑定的。整个矿井就是一张固定画面玩家全程不需要移动视角所以摄像机直接用 Orthographic 模式钉在原地即可。做过地图漫游类项目的人会习惯性考虑 Unity 摄像机跟随但这里反而要避免挂任何跟随脚本——一旦钩爪回拉时摄像机被拖着动玩家立刻会晕2D 小游戏里这是灾难。场景结构我会按下面这样组织把“摆动”“伸长”“抓取”三个行为拆到不同节点上GoldMiner/ ├── GameRoot // 空物体挂 GameController 总控 │ ├── Main Camera // Orthographic, Size5, 固定不动 │ ├── Background // 泥土背景 Sprite铺满屏幕底部区域 │ ├── MineLayer // 所有矿石的生成挂点 │ ├── HookSystem // 钩爪总节点 │ │ ├── Anchor // 轴心点位于画面顶部中间 │ │ ├── HookArm // 从 Anchor 往下延伸的钩爪臂 │ │ └── HookHead // 钩子头带碰撞体和刚体 │ └── UIRoot // Canvas, Screen Space - OverlayAnchor、HookArm、HookHead 三个节点是父子链关系Anchor 管摆动角度HookArm 管钢丝绳长度HookHead 管抓取交互。这样调试时互不干扰改了 Anchor 的旋转不会把 HookHead 的坐标带乱。MineLayer 只是一个空挂点方便 GameController 在关卡开始时统一往它下面生成矿石结算时也能一次性遍历。摄像机参数方面Projection 选 OrthographicSize 按背景图设计分辨率换算。如果你的背景图是 1920x1080Size 一般取 5.4 左右调准的标准是背景底部边缘刚好露在画面底部。UIRoot 不要放在 Camera 子节点下保持 Overlay 模式避免 Canvas 跟着摄像机抖动。2.2 用 BaseMineral 统一矿石行为为什么不需要写四个类矿石只有金块、钻石、石头、钱袋四种它们之间没有“跳跃、攻击”这类行为差异只有数值差异。所以不要写四个 MonoBehaviour一个 BaseMineral 脚本加枚举就够了——这也是很多 Unity 初学者最容易犯的过度设计为每种矿石各建一套脚本结果百分之八十的代码是重复的。using UnityEngine; public enum MineralType { Gold, Diamond, Stone, Bag } [DisallowMultipleComponent] public class BaseMineral : MonoBehaviour { public MineralType mineralType; public int value 100; // 卖出的金币 public float weight 1f; // 抓取后对回拉速度的减益越大越慢 public int hp 1; // 石头设 3表示要抓三次才能抓破 public Sprite brokenSprite; // 被抓一次后的裂纹贴图 public int HitOnce() { hp--; if (hp 1 brokenSprite ! null) { GetComponentSpriteRenderer().sprite brokenSprite; } if (hp 0) return Mine(); return 0; } int Mine() { int reward value; Destroy(gameObject); return reward; } }逻辑说明HitOnce 处理的是“石头要被敲三次才碎”的规则返回值 0 表示本次没挖到返回正整数则是实际获得的分数。这样把减血、换贴图、销毁、结算集中在一条调用链里GameController 不需要再去查表判断矿石类型。之所以不写四个子类是因为 Unity 的组件模型下Inspector 配参比继承更好用策划调数值时只需要在场景里选中矿石改 weight 和 value不用改代码重新编译。参数建议第一关只放 Gold 和 BagGold 的 weight 控制在 1.01.3Bag 可以给 0.5让玩家第一钩就能感受到“轻的飞回来、重的拖回来”的对比。Diamond 的 weight 给 2.2 以上、value 给 500它必须是后期高风险高回报的代表而不是默认最优解。2.3 钩爪组件配置为什么不用 HingeJoint2D做钩爪摆动Unity 自带 HingeJoint2D 加 Motor 也能转但实际用起来很痛苦关节限位、马达速度、Break Force 三个参数互相耦合调了半天钩子可能自己抽筋。我一般不用关节而是让 Anchor 是普通 TransformHookHead 挂 Rigidbody2D摆动由脚本旋转 Anchor发射与回拉由脚本直接操作 HookHead 的刚体速度。这样行为完全可预期出问题也好查。组件/参数推荐值说明HookHead Rigidbody2DGravity Scale0, BodyTypeDynamic钩子不参与重力用脚本模拟发射Collision DetectionContinuous高速发射时防止穿透矿石HookHead CircleCollider2DRadius 0.25太小会从矿石边缘滑过去HookArm LineRenderer顶点数随长度更新比拉伸贴图更适合做钢丝绳发射速度1216低于 10 手感肉高于 20 容易瞬移穿层按表格把组件挂好再写逻辑脚本里的 GetComponent 才不会返回空引用。我习惯先把 HookHead 的碰撞体半径调到比矿石的最小边小一点再用发射测试观察如果钩子从金块边缘擦过但没触发抓取说明半径太小如果稍微偏离一点就粘上说明半径太大。这个值没有固定最优解它和你的矿石碰撞体大小强相关属于典型的“手感参数要按你自己的美术资源重调”的坑。注意MineLayer 下的矿石碰撞体建议统一用 BoxCollider2D别用 CircleCollider2D。矿石是方块圆形碰撞会在边角处出现抓不中的玄学情况你只能把半径改到很大结果整个手感都废了。3. 摆锤与抓取黄金矿工最核心的力学手感从哪里来3.1 摆锤运动不依赖物理引擎的定点旋转黄金矿工的摆锤是“绕一个固定轴心来回旋转”不是自然物理摆动。做这个玩法时不要用 AddTorque否则你还要反推阻尼系数纯属给自己找麻烦。正确做法是直接写角度驱动让 Anchor 节点匀速往返旋转。using UnityEngine; public class SwingArm : MonoBehaviour { public float swingSpeed 45f; // 每秒摆动的角度 public float maxAngle 70f; // 左右最大摆角 public bool isThrown false; // 发射后停止摆动 private float currentAngle 0f; private int direction 1; void Update() { if (isThrown) return; currentAngle swingSpeed * Time.deltaTime * direction; if (Mathf.Abs(currentAngle) maxAngle) { direction * -1; currentAngle Mathf.Sign(currentAngle) * maxAngle; } transform.rotation Quaternion.Euler(0f, 0f, currentAngle); } public void Launch() { isThrown true; } }逻辑说明currentAngle 是当前帧的累计角度direction 控制旋转方向超过 maxAngle 时反转并钳制到边界防止 overshoot 造成的左右晃动。用 transform.rotation 而不是 Rigidbody2D.MoveRotation是为了让摆锤完全不受物理引擎干扰这样发射前的瞄准是稳定可预测的。isThrown 置为 true 后摆锤停住进入抓取流程。参数说明swingSpeed 取 4055 比较合适低于 35 玩家得等很久才等到够角度的勾高于 60 瞄准基本靠运气。maxAngle 取 6075 度角度太小矿石集中在中央没什么问题角度太大钩子会打到画布外面。这里用了 Time.deltaTime意味着当 Time.timeScale 0 时——比如弹出商店暂停——摆锤自动停下比你自己用一个 bool 去控制省事。3.2 抓取判定碰撞体加 LayerMask还是 Raycast抓取判定有两种常见做法。第一种是给 HookHead 挂 CircleCollider2D用 OnTriggerEnter2D 检测矿石层第二种是每帧从钩头发射一根射线用 RaycastHit2D 探测。我选第一种。原因是黄金矿工里的矿石是静止的不存在高速逃逸目标碰撞回调比每帧轮询更省性能而且碰撞体的直径本身就是“抓取范围”的一部分调半径等于调手感不需要改一行逻辑。碰撞方案必须做两件事矿石层命名为 Mineral钩头只检测该层在 Physics2D 设置里把 Mineral 和 Default 的碰撞矩阵勾掉避免钩子撞到背景和 UI 触发器。抓取回调代码void OnTriggerEnter2D(Collider2D other) { if (other.CompareTag(Mineral)) { var mineral other.GetComponentBaseMineral(); if (mineral null) return; // 石头第一次被抓只扣血钩子原路收回 if (mineral.HitOnce() 0) { currentMineral mineral; } else { currentMineral null; } StartCoroutine(ReturnHook()); } }逻辑说明石头没有被完整挖掉时钩子也要收回来这是原版游戏的关键设计——不能让钩子抓住一块大石头停在原地。这里把引用存到 currentMineral是为了在回拉结束后判断“这块石头是已经碎了还是只是掉了一点血”而不是在触发瞬间直接销毁。先用 CompareTag 过滤再用 GetComponent 抓组件比直接 GetComponent 后再判空少一次组件查询矿石多的时候能省一点开销。3.3 回收速度与重量物体速度怎么获取怎样避免瞬移回收是手感的第二个黑匣子。直接给钩头速度赋一个固定负数会显得非常呆正确做法是按矿石重量折算目标速度并在 FixedUpdate 里做速度逼近。玩家判断“这次挖到好东西了”的第一信号就是钩子回拉明显变慢。using UnityEngine; public class HookRetract : MonoBehaviour { public Transform anchor; public float baseSpeed 10f; public float pullAccel 12f; public float weightFactor 0.12f; private Rigidbody2D hookRb; private BaseMineral carried; private bool retracting; void Awake() hookRb GetComponentRigidbody2D(); public void StartRetract(BaseMineral mineral) { carried mineral; retracting true; } void FixedUpdate() { if (!retracting) return; float speed baseSpeed; if (carried ! null) speed * Mathf.Max(0.25f, 1f - carried.weight * weightFactor); Vector2 dir ((Vector2)anchor.position - hookRb.position).normalized; Vector2 targetVel dir * speed; hookRb.velocity Vector2.Lerp(hookRb.velocity, targetVel, pullAccel * Time.fixedDeltaTime); } }逻辑说明速度折算是乘法而不是减法这样 bag 那种 0.5 重量的物体丢进去只是轻微减速stone 的 3.0 重量则直接掉到 0.25 倍速下限差异足够明显。Vector2.Lerp 的第三个参数用 pullAccel 乘以 fixedDeltaTime等于每物理帧把当前速度往目标速度逼近一截不会出现上一帧 10、下一帧 -10 的瞬间跳变。物体速度怎么获取标准答案是只要挂在 Rigidbody2D 上就用 rb.velocity不要自己用 transform.position 差值去算。自算值在物理步长下是锯齿线打印出来根本没法用来调试。如果你发现速度一直是 0优先检查钩头是不是被别的脚本用 Transform 组件移动了——Rigidbody2D.velocity 只在物理引擎驱动时才会更新。3.4 发射手感按一下甩出去不要蓄力原版黄金矿工是点击即发射发射方向就是当前钩头方向。不要做任何蓄力机制蓄力会把原先 1 秒一次的发射节奏拖到 3 秒整个游戏的节奏就垮了。发射时给 HookHead 一个沿当前旋转方向的初速度而不是把 position 直接瞬移过去发射速度 1216配合 Continuous 碰撞检测玩家会看到钩子从锚点快速滑出而不是闪到半空。另一个槽点常出现在按钮上发射按钮用 OnPointerDown 而不是 OnClick。OnClick 在鼠标抬起时才触发玩家手指离开屏幕后钩子才甩出去体感上就像卡了一下。这属于 UGUI 输入事件里最容易忽略的延迟哪怕闭卷做这个小游戏我也会先把输入方式改成按下即发再调其他东西。4. UGUI界面与关卡循环得分、时间条与商店的实现4.1 Canvas 设置与时间条UGUI 系统基础里的第一件事UGUI 系统基础里最先要定的不是字体颜色而是 CanvasScaler 的缩放模式。黄金矿工是横屏固定分辨率游戏所以把 Scale Mode 设为 Scale With Screen Size参考分辨率 1920x1080Match 设为 0.5。这样不管在桌面窗口还是手机横屏上UI 都不会因为屏幕比例变化而错位。时间条用 Image 的 fillAmount 实现比 Slider 少一层组件也不需要处理拖拽事件。代码非常简单using System.Collections; using UnityEngine; using UnityEngine.UI; public class TimerController : MonoBehaviour { public Image timerFill; public float timeLimit 60f; public GameController controller; float remain; void Start() { remain timeLimit; StartCoroutine(RunTimer()); } IEnumerator RunTimer() { while (remain 0f) { remain - Time.deltaTime; timerFill.fillAmount remain / timeLimit; yield return null; } controller.OnTimeUp(); } }逻辑说明fillAmount 本身是 0 到 1所以直接除 timeLimit 就能映射进度。Time.deltaTime 在 Time.timeScale 0 时不会累加也就是弹出商店暂停的那段时间计时条也同步冻结——这是刻意设计原版黄金矿工的商店暂停的是整个世界不是只停一个计时器。注意不要用 Time.unscaledDeltaTime。用了就会出现商店界面还停在屏幕上、但时间条继续掉水的怪象玩家会觉得被暗算了。暂停时 UI 按钮的过渡动画如果也想动把那部分单独交给不受 timeScale 控制的驱动器不要全局放开倒计时。4.2 得分飘字让自己的数值反馈逐渐消失金矿被钩回后需要在得分栏附近弹出一行“100”再慢慢淡出。渐隐不要用 Text.color 逐帧改 alpha那会经过一层很难看的灰用 CanvasGroup 控制整体透明度颜色更干净也方便后续加缩放动画。using System.Collections; using UnityEngine; using UnityEngine.UI; public class FloatingScore : MonoBehaviour { public CanvasGroup group; Text scoreText; void Awake() scoreText GetComponentText(); public void Show(int value, Vector2 worldPos) { scoreText.text ${value}; transform.position Camera.main.WorldToScreenPoint(worldPos); StartCoroutine(FadeAndRise()); } IEnumerator FadeAndRise() { float t 0f; float dur 0.8f; Vector3 start transform.position; while (t dur) { t Time.deltaTime; float p t / dur; transform.position start Vector3.up * (p * 60f); group.alpha 1f - p; yield return null; } Destroy(gameObject); } }逻辑说明Unity 脚本控制逐渐消失最直接的方式就是把 alpha 从 1 线性降到 0。飘字挂到 Canvas 下生成时从世界坐标转屏幕坐标这样钩子抓回金块的一瞬间数字从矿石头顶升起。如果你在别处用了摄像机跟随这里 WorldToScreenPoint 的结果也会跟着镜头变会出现飘字不在矿石上方的错位固定视角下这个方案是稳的。4.3 关卡数值表与商店道具逻辑关卡节奏让很多人头疼。这组数值可以直接抄前两关不出现石头让玩家专注学摆锤时机第三关石头登场第一次感受“沉”这个反馈钻石从第二关开始给钩子压力高价值伴随高回收风险。关卡时间目标分矿石组合160s8004 金块 2 袋金币255s16008 金块 3 钻石350s260012 金块 5 钻石 3 石头450s400010 金块 6 钻石 6 石头545s6000全类型混排钻石加重如果你把钻石放第一关玩家会在 60 秒里只抓钻石游戏立刻变成机械劳动。这类节奏翻车的本质是缺少“风险和收益的权衡”第一关用 Bag 教玩家抓轻的第二关用 Diamond 让玩家冒险第三关用 Stone 惩罚贪心层次就出来了。商店道具给两个就够炸弹清除屏幕上所有石头价格随关卡递增力量药水20 秒内免疫重量减速。炸弹实现是点击时遍历 MineLayer 下所有 BaseMineral 且 hp 大于 1 的物体销毁后播一个粒子特效力量药水只改 HookRetract.weightFactor 为 0。不要加第三个道具多加一个道具意味着多一套 UI 图标、多一个状态机分支在小项目里性价比很低。4.4 存档与关卡解锁PlayerPrefs 的边界关卡解锁、最高分、金币余额这类数据用 PlayerPrefs 就够了不要引 SQLite也不要上 JSON 序列化库。三个 keyhighestScore、coin、unlockLevel。写存取时记住两条规则一是在加分结算、购买道具的瞬间同步写不要等 OnApplicationQuit——移动端退后台时系统不保证回调执行二是写完后立刻 PlayerPrefs.Save() 落盘一次防止进程被系统杀掉时丢档。如果后续要上微信小游戏PlayerPrefs 这套 API 在 WebGL 和微信环境里同样可用不需要换存储方案。5. 常见问题排查穿模、粉红材质与回拉抖动5.1 钩子高速发射后穿过矿石现象发射速度调到 18 以上后钩头经常出现在矿石背后明明碰到了却没有触发抓取。原因碰撞检测默认是 Discrete 离散检测。一帧内物体位移超过碰撞体厚度时两帧之间都检测不到相交钩子就像瞬移穿墙了一样。这是 2D 物理的高速物体经典问题。解决把 HookHead 的 Rigidbody2D 的 Collision Detection 改成 Continuous同时把钩头 CircleCollider2D 半径从 0.2 提到 0.250.3。如果还是偶尔穿透用射线方案做兜底每帧从钩头位置朝速度方向发一条短射线命中矿石立即转抓取。我实际项目里碰撞加射线混用的情况不少碰撞负责正常抓取射线只用来弥补高速时的漏判。5.2 材质变成亮紫色现象某个矿石或者背景 Sprite 显示成刺眼的紫粉色像贴图丢失。原因项目切到了 URP 渲染管线而美术资源用的是 Built-in 管线的 Shader或者 Shader 在打包时被裁剪了。这在导入第三方美术资源时最常见不是贴图坏了是渲染管线不匹配。解决在 Project Settings 里把 Active Render Pipeline 清空回 Built-in或者把材质重挂为 URP 对应的 2D/Sprite 着色器。注意从 URP 切回内置后需要重新导入一次所有 Sprite 资源否则图片会发灰。这个紫粉其实是最显眼的反馈看到它不用怀疑自己写的着色器代码先查管线。5.3 读取钩子速度一直是 0现象脚本里 Debug.Log(hookRb.velocity) 打出 (0,0)但画面里钩子明明在飞。原因钩子是用 transform.position 或 Translate 移动的。Rigidbody2D.velocity 只在物理引擎驱动时更新transform 移动完全不写入刚体速度。这个混用还会造成碰撞表现和位移不一致钩子经常滑过矿石。解决统一改用 Rigidbody2D.velocity 赋值回收逻辑放 FixedUpdate。黄金矿工这种 2D 物理游戏只要一混用 transform 和刚体就会出现“钩头滑过矿石”和“回拉卡帧”最后只能把组件推倒重挂。调试物体速度怎么获取记住一个原则有刚体就从刚体读永远不要自己用坐标差值替代。5.4 时间条在商店弹出时还在扣现象商店界面打开Time.timeScale 设为 0玩家买完炸弹回到场景发现时间已经快没了。原因计时逻辑用了 Time.unscaledDeltaTime或者计时器挂在一个不受 timeScale 影响的驱动器上。常见情况是为了让商店按钮的过渡动画在暂停时继续播放把整个 UI 系统的 deltaTime 换成了 unscaled结果把计时器也带跑了。解决时间条统一用 Time.deltaTime商店只改 Time.timeScale 0。如果确实有个别 UI 动效需要在暂停时播放为它单独写一个受 unscaledDeltaTime 驱动的脚本别让全局时间源混用。检查顺序是先确认 timeScale 确实为 0再看计时脚本读的是哪个 deltaTime最后检查有没有多个脚本同时在跑倒计时。5.5 打包后 UI 错位或者发虚现象编辑器里 UI 完美打出 APK 后按钮位移文字边缘发糊。原因CanvasScaler 没有正确设置缩放或者字体和图标在打包时被图集压缩成了不合适的格式。移动端上 ETC 压缩对文字很不友好边缘会裂开。解决CanvasScaler 的 Scale Mode 必须设 Scale With Screen Size参考分辨率 1920x1080Match 0.5字体和图标放进图集压缩格式选 ASTC。UI 错位和包体大是两件事不要因为做包体优化把 UI 精度拖垮——我曾经为了压包体把所有贴图压到最低档结果下一版就开始收到 UI 看不清的反馈属于典型的修一个又引出另一个的连锁翻车。6. 发布前的手感验证与包体优化收尾阶段的两个具体动作6.1 手感验证清单用数据而不是感觉判断手感不是玄学是可测量的。第一关 Gold weight 1.0 时从任意极限角度发射回收时长应该稳定在 1.21.5 秒低于 0.9 秒钩子像拉弹弓高于 2 秒玩家会觉得卡网。Diamond weight 2.5 时回收时长相比金块要增加约 35%这个差值让玩家明确感知“重”。验证方法是找三个人各玩前五关记录“看到目标但钩子滑过去”的次数超过三次说明碰撞体半径太小或者发射速度过快同步记录停手不玩的时间点如果第一关就反复放弃多半不是难度问题是摆动速度或发射手感让你觉得不可控。6.2 包体优化先看 Build Report再动手压资源2D 休闲游戏打出包超过 80MB 就该自查了。常见问题是把 Resources 里所有美术资源无脑拖进去其中一半是没用上的高清贴图。做法是构建完成后打开 Build Report按资源大小排序优先处理单个超过 5MB 的贴图所有 UI 和 Sprite 打进一个图集Texture Compression 选 ASTC从打包列表剔除没用到的基础包。我第一次做这类项目时没做这一步最终包体 250MB后来发现一半来自一张从未上场的 4K 背景图。这两个动作做完项目就已经不是“能跑”而是“能拿出去”了。我吃过最大的教训是重量设置不能只按直觉来初版把钻石 weight 设成 0.8结果 60% 的玩家只抓钻石第三关就开始无聊最后只能重调配置再开测试很耗人。现在我做这个方向前会先列一张矿石手感表把 weight、回收时长、价值三列的先填估算值再动手后面能省掉六成弯路。希望帮到你。本文还有配套的精品资源点击获取
返回列表