
简介一份面向儿童早教与娱乐场景的Unity益智休闲游戏源码项目采用C#开发支持Unity 5.5.2f1及以上版本从场景搭建、玩法逻辑到广告接入均有完整可运行实现适合儿童游戏开发者、教育产品团队以及希望了解休闲游戏完整结构的游戏编程学习者。项目围绕恐龙挖骨主题包含挖骨头、钓鱼骨头、探索洞穴、清理骨头四种互动解谜方式挖洞与挖掘、从海洋中捞化石、使用手电筒探索黑暗洞穴、用专用工具清理并排列骨骼碎片成恐龙拼图。同时整合拼图、钓鱼、清洁等小游戏配合互动地图和两个可解锁的迷你游戏形成从收集、解谜到奖励的闭环体验并接入AdMob与Unity广告兼顾趣味性和商业化需求。压缩包共收录2005个文件大小约135.17MB文件类型以.h与.cpp为主辅以xml、json配置文件和pdf文档便于对照学习代码逻辑与资源结构降低二次开发门槛。目前已有128人学习浏览。借助该源码可以掌握儿童游戏关卡设计、触控交互与反馈、广告SDK集成、游戏进度管理以及基于Unity 5.5.2f1的项目模块划分思路是一套可直接借鉴的早教游戏工程范本。1. 恐龙挖骨游戏把“挖掘考古”做出教育反馈的 Unity 项目“恐龙挖骨游戏”这类亲子休闲题材表面上是铲子、刷子和化石骨头放进一个沙盘剩下交给碰撞体。实际立项后才发现最耗时间的不是恐龙模型也不是按钮而是三件事土层怎么做到“看起来被挖开了”但还能随时恢复骨头在手忙脚乱点击时怎么避免误触把进度弄乱以及拼骨架时让孩子有探索空间又不会卡关的引导节奏。这类项目的基础玩法人人都能列出来但真正决定留存的往往是交互反馈和移动端性能。儿童用户手指接触面积大、容错率低、专注时间短核心交互必须避开精密瞄准、隐式选中、多步操作。下文从场景组织、挖土实现、骨骼装配、优化存档与自定义关卡五个层面把一套可复现的做法讲清楚参数和对应代码一并给出。2. 场景组织与分层设计把“挖”做在渲染层把“判定”做在物理层2.1 为什么不能用多个碰撞体做土层很多第一次做挖掘玩法的人会先把泥土块按网格切成几十个 Tile每个 Tile 挂一个 BoxCollider2D点击哪个就销毁哪个。这个做法看着直接但在儿童项目里有两个问题一是碎片太多真机上 Physics2D 的碰撞体数量超过 100 个后低端 Android 的点击响应会明显变慢二是“挖了一半”的状态很难表达Tile 只能整块消失边缘锯齿感很强孩子会觉得土不是被挖开而是被切掉。这个项目里更适合的拆分方式是泥土只是渲染层骨头才是物理层。泥土由一张带透明通道的贴图来表现点击时把对应像素的 Alpha 改成 0骨头独立挂在另一个节点上带有自己的 Collider2D。这样土再多也不会产生碰撞计算骨头只要几十个碰撞体就够了。2.2 场景层级与绘制顺序推荐的结构是相机下挂三个节点DirtLayer、BoneLayer、FXLayer。它们在 SpriteRenderer 的 Sorting Order 上依次是 0、10、20。泥土盖住骨头骨头盖住背景特效粉尘粒子、闪光永远最前。这里有一个容易踩的细节如果恐龙化石裸露在地表外很多比如脊椎骨高出泥土表面不要让它们混在同一层。高出部分的骨头仍画在 BoneLayer但挖到之前给一个半透明的“未挖掘”材质或者干脆把初始骨骼贴图做成被尘土覆盖的灰度图挖到后替换成彩色版。这样既避免骨骼突兀地悬在土上也不会让 Sprite 的遮挡关系乱掉。2.3 骨骼数据的建模方式每根骨头在代码里抽象成一个BoneDef它只描述“是哪块骨头、长什么样、该插到骨架的哪个槽位”不关心任何渲染组件。public enum BoneSlot { Neck, Spine, Tail, LeftLeg, RightLeg, LeftArm, RightArm } [Serializable] public class BoneDef { public string boneName; public BoneSlot slot; public Sprite boneSprite; // 未清理时的外观 public Sprite cleanSprite; // 清理后的外观 public int score 10; public string factKey; // Json 里的知识文本 Key }BoneDef 用 [Serializable] 标记是整个项目里很关键的设计它允许你在 Inspector 里逐一配置骨头属性又允许运行时用 Json 存档记录“已经获得了哪几根”。Slot 枚举用来和骨架拼图槽位做映射。这里没有依赖任何动态类型和反射儿童教育项目在线程安全和序列化上没有负担后续做随机埋骨、关卡配置都可以复用这组数据。分层设计的收益在后期特别明显美术想换土的颜色、想改骨头的轮廓只需要动对应 Sprite程序想调整难度只需要改擦除半径和拾取规则。渲染、逻辑、数据三个层面互不污染这是把它和普通“一堆脚本堆在场景里”的 Demo 区分开的第一个标志。节点/对象层关键组件点击/碰撞策略Background背景SpriteRenderer, SortingOrder 0忽略点击DirtLayer泥土SpriteRenderer 可写 Texture2D点击时擦除像素BoneLayer骨头SpriteRenderer BoxCollider2D/CapsuleCollider2D挖到后触发拾取FXLayer特效ParticleSystem / 粒子池无碰撞3. 挖土与拾取的 C# 实现擦除像素、判定命中、防误触3.1 可写贴图Texture2D 比 RenderTexture 更适合这个场景土在屏幕上被“挖开”本质上是让 Unity 的 Sprite 显示一张透明像素越来越多的贴图。常见做法有两种一种是把泥土材质渲染到 RenderTexture再用另一个笔刷 Texture 以减色混合去画另一种是直接维护一张 Texture2D在鼠标/手指位置把圆形区域的 Alpha 写入 0。两者对比RenderTexture 优势是绘制效率高、不产生额外的 CPU 像素循环但它依赖相机或者单独一个 RenderTexture 的 Blit 流程在编辑器里没问题打到 Android 上很容易出现纹理格式不支持或者分辨率缩放不一致。儿童教育游戏往往还要在低端平板跑我一般优先选择 Texture2D 方案单张 1024×1024 的 RGBA 贴图行动画时只更新局部区域性能完全可控代码逻辑也更直观。3.2 像素擦除的核心代码下面这段把点击坐标转成贴图像素坐标再挖掉一个圆形区域。注意它处理了两个容易出错的地方贴图在 Sprite 上可能被拉伸所以用 uv 映射而不是本地坐标GetPixels 只取范围矩形不要对整张贴图做全量更新。using UnityEngine; public class DirtEraser : MonoBehaviour { [Header(泥土渲染组件)] public SpriteRenderer dirtRenderer; public int textureSize 1024; // 土贴图分辨率移动端建议 512 或 1024 public float eraseRadius 0.04f; // 擦除半径UV 空间 0~1 private Texture2D dirtTex; private Color[] allClear; // 预缓存整张透明色恢复土层用 void Awake() { // 复制一份源贴图避免修改到项目资产 Texture2D src (Texture2D)dirtRenderer.sprite.texture; dirtTex new Texture2D(textureSize, textureSize, TextureFormat.RGBA32, false); dirtTex.SetPixels(src.GetPixels()); dirtTex.Apply(); dirtRenderer.sprite Sprite.Create( dirtTex, dirtRenderer.sprite.rect, new Vector2(0.5f, 0.5f), dirtRenderer.sprite.pixelsPerUnit); // 生成一张全零透明图后面做“重新埋起来”的逻辑 allClear new Color[textureSize * textureSize]; for (int i 0; i allClear.Length; i) allClear[i] new Color(0, 0, 0, 0); } public bool EraseAt(Vector2 worldPos) { // 将世界坐标转到 sprite 的局部 uv Vector2 local transform.InverseTransformPoint(worldPos); Vector2 uv new Vector2(local.x 0.5f, local.y 0.5f); if (uv.x 0 || uv.x 1 || uv.y 0 || uv.y 1) return false; int cx (int)(uv.x * textureSize); int cy (int)(uv.y * textureSize); int r Mathf.Max(2, (int)(eraseRadius * textureSize)); // 最小擦 2px int x0 Mathf.Clamp(cx - r, 0, textureSize - 1); int y0 Mathf.Clamp(cy - r, 0, textureSize - 1); int w Mathf.Clamp(cx r, 0, textureSize - 1) - x0; int h Mathf.Clamp(cy r, 0, textureSize - 1) - y0; Color[] pixels dirtTex.GetPixels(x0, y0, w, h); for (int i 0; i pixels.Length; i) { int px i % w; int py i / w; float dx (x0 px) - cx; float dy (y0 py) - cy; if (dx * dx dy * dy r * r) pixels[i].a 0f; } dirtTex.SetPixels(x0, y0, w, h, pixels); dirtTex.Apply(); // 返回 false 表示这次点击没有挖到有效像素理论上速度太快会漏 return true; } public void ResetDirt() { dirtTex.SetPixels(allClear); dirtTex.Apply(); } }参数里最需要调的是eraseRadius。它是 UV 空间的值0.05 对应贴图 5% 宽度。儿童游戏建议初始给 0.06 左右因为幼儿手指按下去采样到的点经常偏斜太小会让他们觉得“挖不动”。等孩子熟练后可以在设置里用滑块把它降到 0.035作为三档难度之一来卖点。Unity 里手指事件的坐标转换是另一个重灾区。如果用Camera.main.ScreenToWorldPoint一定要先取Input.mousePosition的 z 值为相机距离直接传二维向量会得到一个z0的位置导致世界坐标偏到相机原点。最稳妥的做法是给 DirtLayer 挂一个单独的相机用Camera.ScreenToWorldPoint(new Vector3(pos.x, pos.y, -cam.transform.position.z))。3.3 拾取判定先撞骨头再挖土像素擦除只处理视觉那怎么判断“挖到骨头了”没有必要去读取贴图 Alpha 判断某一块是否被挖穿直接在 BoneLayer 上挂 Collider2D每一帧检测手指位置是否落在骨头碰撞体内同时要求对应骨头附近足够范围内的土已经被挖掉。public class BoneDigChecker : MonoBehaviour { public LayerMask boneMask; // 只检测 BoneLayer public virtual BoneCollected CheckBone(Vector3 screenPos) { Vector3 worldPos Camera.main.ScreenToWorldPoint( new Vector3(screenPos.x, screenPos.y, -Camera.main.transform.position.z)); // 用一个小球形覆盖手指按压范围而不是单点检测 Collider2D hit Physics2D.OverlapCircle(worldPos, 0.08f, boneMask); if (hit null) return null; BoneCollected bc hit.GetComponentInParentBoneCollected(); if (bc ! null bc.isUncovered) { bc.Collect(); return bc; } return null; } }这里的核心是isUncovered标志。这个标志不来自像素检测而是由 DirtEraser 在挖土时维护一个覆盖范围记录当骨头的包围盒中心点被挖过两次以上时设为 true。用事件绑定比每帧在 Update 里把所有骨头的 isUncovered 轮流刷新一遍要干净很多。防误触逻辑也建议放在这里骨头拾取后立刻触发一个 0.3 秒的无敌帧防止孩子在兴奋状态下快速连点把刚拿到的骨头又放回去。无敌帧在儿童教育项目里是一个经常被忽略的细节它能明显减少“我明明拿到了怎么又没了”的挫败感。4. 骨骼拼装与教育反馈引导、亮点、UI 交互4.1 拖拽与吸附挖到的骨头先进入一个“背包网格”背包是底部一排固定槽位每个槽里是一个可拖拽的 Image。孩子把骨头拖到骨架区时OnEndDrag 做一次距离判断距离小于阈值就直接吸附并播放音效。using UnityEngine; using UnityEngine.EventSystems; public class BoneDragItem : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler { [Header(目标骨架槽位)] public BoneSlot targetSlot; public float snapDistance 1.2f; private CanvasGroup canvasGroup; private Transform originalParent; private Vector3 originalPos; void Awake() { canvasGroup GetComponentCanvasGroup(); } public void OnBeginDrag(PointerEventData eventData) { originalParent transform.parent; originalPos transform.position; canvasGroup.blocksRaycasts false; // 避免拖拽时射线被自己挡住 } public void OnDrag(PointerEventData eventData) { transform.position eventData.position; } public void OnEndDrag(PointerEventData eventData) { canvasGroup.blocksRaycasts true; // 遍历所有骨架槽位 SkeletonSlot slot SkeletonSlot.FindBySlot(targetSlot); if (slot ! null Vector2.Distance(transform.position, slot.transform.position) snapDistance * 100f) { transform.SetParent(slot.transform, true); transform.localPosition Vector3.zero; slot.OnBonePlaced(this); return; } // 吸附失败回到背包 transform.SetParent(originalParent, true); transform.position originalPos; } }拖拽逻辑里比较重要的参数是snapDistance。这里写成*100f是因为 Canvas 在“屏幕空间 - 相机”模式下做了缩放视口高度不同时像素距离偏差很大。更稳的做法是直接让 SkeletonSlot 也继承 RectTransform用RectTransformUtility.WorldToScreenPoint来计算屏幕坐标差再和阈值比较。如果骨架槽位本身很小孩子很难对准这时不要直接放大槽位导致视觉比例失调而应该扩大判定区域把 SkeletonSlot 的 Image 透明度设为 0挂一个 1.6 倍大的targetGraphic做透明热区。这种“视觉小、响应大”的设计在儿童游戏里普遍有效。4.2 教育反馈识别到骨头后说什么每根骨头被正确放入弹出一个小卡片内容从BoneDef.factKey对应的一张 Json 表里读取。Json 表要包含三部分骨头名称、一句儿童能理解的百科比如“这是剑龙的尾刺主要用来防御”、一个引发互动的提问“数数这个骨架有几根腿骨”。提问是知识卡片真正产生教育价值的地方否则卡片只会是孩子快速滑过的装饰品。弹窗不打断正在进行的拖拽动作。推荐把知识卡做到角落里一个横幅位置15 秒后自动收进“已收集图鉴”而不是模态弹出——模态弹窗频繁出现对低龄用户的挫败感很强他们可能正在连续拼图突然被卡片挡住会直接失去兴趣。4.3 扩大按钮点击范围儿童手指的误触率决定了几乎所有 UI 按钮都不能只依赖原始尺寸。Unity 的Button组件的点击区域由Image.raycastTarget的 RectTransform 范围决定最简单的方案是给按钮根节点挂一个透明扩热区脚本并把可见的图标缩小放在热区中心。using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; public class ExpandButtonClickArea : MonoBehaviour, IPointerClickHandler, IPointerDownHandler { public float padding 40f; // 向外扩展的像素iOS 建议不小于 44 private Button targetButton; void Start() { targetButton GetComponentInChildrenButton(); } public void OnPointerClick(PointerEventData eventData) { if (targetButton ! null) targetButton.onClick.Invoke(); } public void OnPointerDown(PointerEventData eventData) { if (targetButton ! null) targetButton.OnPointerDown(eventData); } }把该组件挂在一个全透明的 Image 上尺寸设为 160×160再嵌套一层 80×80 的可见按钮就能既满足视觉比例又满足触控。这里OnPointerDown转发是必要的否则 Button 的按压动画不会播放孩子会感觉“按钮没反馈”。4.4 难度滑块与自适应参数游戏设置里做一个“挖掘力度调节”滑块本质上是DirtEraser.eraseRadius的映射。滑块范围 0.02 到 0.08初始值 0.055每次修改后直接赋给 DirtEraser 的公共字段并在本地 PlayerPrefs 里存一份。因为擦除逻辑只在点击时读取该字段不需要任何重建或重启运行时改难度极其流畅。对应的还有“时间挑战”模式一个计时浮层显示剩余秒数倒计时结束前收集到指定骨头数量算过关。这个模式的主要作用是延长游戏时长但要注意给每个孩子一个“无限模式”入口因为儿童教育应用里强制失败会显著拉低口碑。5. 移动端性能优化与进度存档把项目跑在幼儿园的平板上5.1 泥土像素写入要控制频率Texture2D.SetPixels 是 CPU 密集操作每帧多次擦除时容易造成掉帧。优化的核心是合并相邻擦除事件不要在每次鼠标移动都执行一次完整的 SetPixels Apply而是把 0.05 秒内收到的所有擦除点先暂存到一个 List等事件结束后一次性合并成矩形区域再写入。void Update() { // 擦除点攒够 8 个或时间到 50ms 就提交一次 eraser.SubmitBatch(); }提交时不需要重新挖整张贴图只挖一个合并后的包围盒这样在大屏上效果最明显。注意 Texture2D.Apply 也会把 GPU 数据上传一整块如果贴图是 2048×2048即使只改了一个角落传输量也是整张图。因此移动端用 1024 以下分辨率或者把贴图拆成 4 张 512是更稳的做法。5.2 对象池复用粒子与土屑挖掘特效如果每次点击 Instantiate 一个粒子系统几秒后堆几十个 GameObjects低端机上立刻明显掉帧。做法是启动时预创建 8 个粒子系统点击时轮转使用先停掉上一个再重新播放。public class DirtParticlePool : MonoBehaviour { public ParticleSystem[] pool; private int index; public void PlayAt(Vector3 pos, int amount) { if (pool.Length 0) return; ParticleSystem p pool[index]; index (index 1) % pool.Length; p.transform.position pos; p.Emit(amount); } }参数amount根据擦除半径动态调整擦除半径 0.06 时出 6 个土粒0.03 时出 3 个。土粒数量不直接影响教育价值但太少了会让人感觉挖了个寂寞太多了又糊屏建议在真机上用帧率条现场调。5.3 用 JsonUtility 做存档Unity 自带 JsonUtility 序列化 List 数组时不如第三方库方便但加一层包装即可。存档数据用bool[]记录每根骨头的收集状态用DirtEraser.ResetDirt()支持“重新开始”。[Serializable] public class GameSaveData { public bool[] boneCollected; // 已挖掘标记 public int currentDinoId; // 当前完成的恐龙编号 public float bestTime; // 最佳通关时间 public float difficulty; // 滑块存档0.02~0.08 } public static class SaveManager { public static void Save(GameSaveData data) { string json JsonUtility.ToJson(data); PlayerPrefs.SetString(dino_dig_save, json); PlayerPrefs.Save(); } public static GameSaveData Load() { string json PlayerPrefs.GetString(dino_dig_save, ); if (string.IsNullOrEmpty(json)) return new GameSaveData(); return JsonUtility.FromJsonGameSaveData(json); } }用 PlayerPrefs 的原因是不需要处理文件路径权限、也不用在切后台时做复杂的写保护。但对一份教育游戏源码来说数据量小PlayerPrefs 恰好够用。需要导出的只是“我完成了哪些恐龙”不是连续 1000 局回放没有性能压力。5.4 Profiler 里重点卡掉的三个点在 Unity Profiler 中打开 Deep Profile跑一次完整的“挖土 − 拼骨 − 过关”流程重点核对三类问题Texture2D.Apply()的 GPU 耗时如果超过 3ms 就降分辨率或缩小提交区域Physics2D.OverlapCircle的次数如果每帧超过 2 次且都命中同一些碰撞体考虑做每 0.1 秒一次的采样transform.position的频繁写入Canvas 里每帧移动多个骨骼时禁用GraphicRaycaster的射线检测结束时再恢复。6. 进阶玩法用 ScriptableObject 做随机埋骨与多恐龙关卡6.1 一次配置多套关卡数据项目做进第二阶段后通常不会只有一个恐龙骨架。把每关的地图布局抽象成 ScriptableObject 资产让策划或美术直接拖配置而不用开代码是“源码可用性”的最大加分项。[CreateAssetMenu(fileName NewDigLevel, menuName DinoDig/DigLevel)] public class DigLevelData : ScriptableObject { public string dinoName; public Sprite skeletonPreview; // 骨架阴影预览 public int gridWidth 6; public int gridHeight 4; public BonePos[] bonePositions; // 每根骨头的格子坐标 [Serializable] public class BonePos { public BoneDef bone; // 复用前面的数据 public int x; public int y; public float rotationZ; public bool isBuriedDeep; // 深层骨头需要多挖几次 } }运行时读取 gridWidth×gridHeight把泥土贴图初始化为每格深色带纹理的图案然后遍历骨头坐标绘制骨架影子。孩子拼骨头时只有骨头下方区域的土被清空影子才开始可见这比“一开始什么都看不见”更符合儿童认知也不难实现。6.2 蜘蛛网式小关卡一张图放置一个恐龙骨架孩子挖完后进入下一张整体做成线性地图解锁。每关的 bonePositions 由代码从 ScriptableObject 里取不做运行时随机化。面向 3~6 岁儿童物理随机化会导致难度不可控真正的随机只适用于“成年向休闲”玩法。如果想让大人也有乐趣可以加一个“专家模式”用 [6.1] 的同一配置数据把所有 bones 位置稍加偏移并减少土屑粒子难度即时提升。6.3 验证手感的清单每次在真机上验证不用看数据先走一遍 45 秒完整流程快速刮同一块区域三次确认没出现卡顿或掉帧故意把手指按在骨头正中央确认没有直接穿透触发“挖到”反馈尝试两块骨头叠放时拖拽确认没有被背后的槽位误吸附回到游戏后按 Home 键再回确认存档与 UI 没有回退。这套验证逻辑比看 Profiler 更贴近儿童真实使用场景教育类项目尤其值得执行。本文还有配套的精品资源点击获取