简介:这是基于Unity 2021.1及以上版本的超级冰火人风格双人合作益智迷宫游戏完整项目源码,面向Unity游戏开发者、独立制作人和解谜游戏爱好者。项目内置三十张地图,设有红男孩与水女孩双角色控制,包含火钻石与冰钻石收集、多种关卡机关(杠杆、镜子、光束、电梯、风力机、滑轮系统)和完整配乐,可直接运行体验,也适合作为学习Unity游戏逻辑、关卡设计及换皮开发的实战范本。压缩包共两千个文件,大小约五十二兆字节,涵盖C#脚本、Unity场景、预制体、材质、动画、着色器等核心资源,同时集成AdMob、Facebook Audience Network、AppLovin等多平台广告SDK,便于快速接入商业化功能。项目结构清晰,支持轻松添加更多关卡与换皮,并附带相关文档与持续更新,能够帮助研究者理解真实项目中的目录组织与模块划分。目前已有两百零七人学习下载,是理解商业级Unity项目架构、双人合作解谜设计与广告变现流程的优质示例。
1. 先拆开“超级冰火人”这包 Unity 源码再看代码
冰火人这个品类,核心从来不是建模或手感,而是“两个角色共享一个场景、互相补位才能过关”。标题里的 Red boy And Blue girl 就是这套机制的缩写:红色角色走火区,蓝色角色走水区,岩浆、水潭、酸液、压力板和门按迷宫式路线排布。拿到源码时先别急着点 Play,先问三个问题:角色由哪个脚本控制、机关由什么触发、死亡之后回到哪里。这三个问题对应 PlayerController、Trigger 系统和 GameManager 三条主线,后续就按这个顺序解开一包 C# 源码的实际组成。适合理性思考的 Unity 入门者,也适合找现成方案改造的原型开发者。
2. 打开 Unity 工程之前:版本、目录与入口脚本
拿到压缩包第一件事是解压。路径不要带中文,Assets 文件夹下每个资源都对应一个 .meta 文件,里面的 GUID 是场景引用资源的凭证。随意删除 meta 文件或让多个工程副本混用,轻则丢失预制体引用,重则场景里的脚本全部脱离,Unity 会显示成 “Missing (Mono Script)”。稳妥做法是把解压后的文件夹直接放到短路径下,再用 Unity Hub 打开。
2.1 用 Unity Hub 选择对应版本打开工程
源码一般不带 Libraries 目录,Unity Hub 打开工程时会自动生成。常见做法是尽量用与原工程一致的编辑器版本,版本跨度太大时 Unity 会弹升级对话框。如果 Unity 提示工程由更高版本编辑器创建,优先选择已有版本打开,而不是立刻下载新版,这样可以先分析代码和场景,减少版本升级带来的干扰。
打开步骤:
- 打开 Unity Hub,点击“打开”按钮,选择解压后的工程根目录,即包含 Assets 文件夹的那一层
- 如果提示版本不一致,选择已有的相近版本,先不勾选升级到最新渲染管线
- 首次打开后等待 Library 构建完成,在 Console 面板查看红色报错,有报错先记录再点 Play
Console 里常见的 Missing Script 提示,大多数是脚本文件被移动或重命名导致引用断掉。层级面板中出现 “Missing (Mono Script)” 的节点不要马上删除,先摘录它挂载的缺失脚本名,再在 Scripts 目录里搜索找回。
2.2 源码目录的三层阅读法
这类项目在 Assets 下一般按场景、预制体、脚本、数据分目录,命名方式可能不同,但职责基本一致:
| 子目录 | 作用 | 建议关注点 |
|---|---|---|
| Scenes | 存放游戏关卡 | 入口场景是哪个,有几个关卡场景 |
| Prefabs | 存放角色、机关、粒子等可复用单位 | 预制体之间是否存在 Missing 引用 |
| Scripts | 存放 C# 逻辑 | 脚本是否按功能分目录,入口脚本是什么 |
| Data 或 Config | 存放关卡配置和存档模板 | 数据字段是否与场景对象一一对应 |
我一般会先扫一遍 Scripts 文件夹的文件数量,超过二十个还没有子目录的,就要花更多时间在全局搜索上。入口脚本通常叫 GameManager,负责全局状态、暂停和重置。先看它怎么引用红蓝两人,能快速判断整个架构是“脚本直接拖引用”还是“运行时动态查找”。
using UnityEngine; public class GameManager : MonoBehaviour { // 单例引用,其他脚本通过 GameManager.Instance 访问 public static GameManager Instance { get; private set; } [Header("角色引用")] public Transform redBoy; // 红人 public Transform blueGirl; // 蓝人 [Header("当前状态")] public bool gameOver = false; private void Awake() { // 场景里只保留一个 GameManager,重复时销毁后创建的 if (Instance != null && Instance != this) { Destroy(gameObject); return; } Instance = this; } public void ResetLevel() { // 角色死亡或卡关后重载当前场景 UnityEngine.SceneManagement.SceneManager.LoadScene( UnityEngine.SceneManagement.SceneManager.GetActiveScene().name); } }这段代码是教科书式的单例:Awake 里赋值并去重,比 Start 更早执行,保证其他脚本在 Start 阶段调用 GameManager.Instance 时不会拿到空引用。ResetLevel 直接重载场景,简单但足够可靠。如果场景里被误放了两个 GameManager,去重分支会保留先创建的,避免重复初始化。
2.3 从场景组织反推玩法流程
打开 Scenes 里第一个关卡场景后,检查 Hierarchy 面板的顶层节点。结构清晰的工程一般会有几个固定层:环境地形、机关层、角色层、UI 层。地形整块挂在同一个父节点下,机关层里每个压力板对应一个门对象,角色层只放红人和蓝人两个玩家。
如果层级面板里看不到隔离,说明作者把对象平铺了。这时可以直接在 Hierarchy 搜索框按组件名过滤,输入 Collider2D 或 SpriteRenderer 快速圈定对象范围。也可以用 Project 面板右键查找引用来定位某个脚本挂在哪些预制体上,这对后续改机关逻辑很有帮助。
3. 双角色协作的核心机制:移动、机关触发与死亡重置
冰火人的游玩体验完全建立在红蓝分工上,源码里最值得抄的部分是双角色控制与机关联动。这一章拆开讲移动、平台、踩踏板、死亡重置四块,每一块都有对应的 C# 组件。
3.1 红蓝两个角色的 PlayerController 该怎么写
双角色益智游戏有两种控制方案:红人 WASD、蓝人方向键的并行控制,以及单键切换当前操作对象。迷宫关强调同时配合,源码里常见做法是并行控制,两个玩家在同一场景中各自行动。
using UnityEngine; public class PlayerController : MonoBehaviour { [Header("移动参数")] public float moveSpeed = 5f; // 水平移动速度 public float jumpForce = 8f; // 跳跃初速度 public KeyCode leftKey = KeyCode.A; public KeyCode rightKey = KeyCode.D; public KeyCode jumpKey = KeyCode.W; private Rigidbody2D rb; private bool onGround; // 是否踩在地面上 private void Start() { rb = GetComponent<Rigidbody2D>(); } private void Update() { // 每帧读取按键并合成水平输入 float move = 0f; if (Input.GetKey(leftKey)) move -= 1f; if (Input.GetKey(rightKey)) move += 1f; // 起跳时给刚体一个竖直方向的速度 if (Input.GetKeyDown(jumpKey) && onGround) { rb.velocity = new Vector2(rb.velocity.x, jumpForce); onGround = false; } // 水平位移交给刚体,避免直接改 transform 造成穿透 Vector2 target = rb.position + new Vector2(move * moveSpeed * Time.deltaTime, 0f); rb.MovePosition(target); } private void OnCollisionEnter2D(Collision2D collision) { // 碰到地面或移动平台都视为落地 if (collision.gameObject.CompareTag("Ground")) { onGround = true; } } }关键参数表:
| 参数 | 默认值 | 说明 |
|---|---|---|
| moveSpeed | 5 | 水平速度,数值大时注意贴墙抖动 |
| jumpForce | 8 | 跳跃初速度,二段跳需求下可叠加 |
| GravityScale | 3 | 在 Rigidbody2D 上调,下落要快但起跳不能太飘 |
| rb.mass | 1 | 双角色相撞时质量差不能过大 |
Update 读输入、MovePosition 写位移是 2D 平台游戏的常见写法。和直接改 transform.position 相比,MovePosition 走刚体物理,角色能推开其他物体,也不会穿进静态碰撞体。代码里用 GetKey 而不是 GetAxis,是避免 Input Manager 里轴输入自带的灵敏度曲线带来延迟感。迷宫关卡多窄道跳跃,瞬间响应比平滑更重要。
3.2 移动平台、单向碰撞和角色粘连问题
迷宫关卡高频出现的机关是移动平台。平台由脚本或 Animator 驱动,角色站上去时如果不让角色跟随平台,平台移动时角色就会原地不动甚至掉落。
using UnityEngine; public class MovingPlatform : MonoBehaviour { public Transform pointA; // 移动起点 public Transform pointB; // 移动终点 public float speed = 2f; private void Update() { // 用 PingPong 在两段路径间往返,t 一直在 0 到 1 之间 float t = Mathf.PingPong(Time.time * speed, 1f); transform.position = Vector3.Lerp(pointA.position, pointB.position, t); } private void OnTriggerEnter2D(Collider2D other) { // 角色进入平台触发区后跟随移动 if (other.CompareTag("Player")) { other.transform.SetParent(transform); } } private void OnTriggerExit2D(Collider2D other) { // 离开平台后解除父子关系 if (other.CompareTag("Player")) { other.transform.SetParent(null); } } }3.2.1 SetParent 的坑与替代方案
SetParent 临时改变父子关系会让角色在平台移动中累积浮点数误差,离开平台时容易抖动一下。平台在高速运动时,更稳的做法是用平台速度给角色叠加,或在 OnTriggerStay2D 里用平台的速度平移角色。源码里如果看到 OnCollisionStay2D 里反复 SetParent,可以留意是否有误差补偿。
另一常见问题是角色和平台边缘粘连。静态平台 Collider 默认带摩擦,角色跳上边缘会粘住。常见做法是在平台物体上挂 PlatformEffector2D,打开 Use One Way,并将 Surface Arc 设为 180 度附近,这样角色从下方可以通过、从上方站住,同时减少边缘摩擦力带来的顿手感。
3.3 压力板触发门与死亡重置逻辑
压力板属于“脚踏触发”式机关,踩下后开门,离开后门关闭是基础版。需要注意 Trigger 体积足够大,过小会导致角色快速走过时没有触发判定。
using UnityEngine; public class PressurePad : MonoBehaviour { public GameObject targetDoor; // 要移动的门体 public float openHeight = 2f; // 门打开的抬高距离 public bool requireSpecificRole; // 是否区分红蓝角色 private Vector3 originalPos; private int pressCount = 0; // 板上的角色数量,双人同踩也能正确处理 private void Start() { originalPos = targetDoor.transform.position; } private void OnTriggerEnter2D(Collider2D other) { // 进入压力板的角色都计入引用计数 if (!requireSpecificRole || other.CompareTag("Red") || other.CompareTag("Blue")) { pressCount++; } } private void OnTriggerExit2D(Collider2D other) { // 离开一个减一,保证至少有一个角色踩着时门不关 if (!requireSpecificRole || other.CompareTag("Red") || other.CompareTag("Blue")) { pressCount = Mathf.Max(0, pressCount - 1); } } private void Update() { // 只要计数大于 0 门就保持打开,用 Lerp 平滑过渡 bool isPressed = pressCount > 0; Vector3 target = isPressed ? originalPos + Vector3.up * openHeight : originalPos; targetDoor.transform.position = Vector3.Lerp( targetDoor.transform.position, target, Time.deltaTime * 3f); } }pressCount 引用计数是双人机关的关键点。如果用简单的 enter 设为 true、exit 设为 false,红人踩住门开着,蓝人再走时门会先关一下再开,视觉上就是抖动。计数版本只在第一个人进入时加一、最后一个人离开时减到零,门的状态稳定。
死亡重置在冰火人里通常借助熔岩或酸液上的不可见 BoxCollider2D 实现,挂一个 KillZone 脚本:
using UnityEngine; public class KillZone : MonoBehaviour { public Transform respawnPoint; // 存档点,放场景空物体 private void OnTriggerEnter2D(Collider2D other) { // 熔岩、酸液、虚空统一用这个组件处理 if (other.CompareTag("Player")) { other.transform.position = respawnPoint.position; // 重置速度,避免落到存档点后继续滑行 Rigidbody2D rb = other.GetComponent<Rigidbody2D>(); if (rb != null) { rb.velocity = Vector2.zero; } } } }两个角色可以共用一套 KillZone,但 respawnPoint 要分开设置,红人回红人存档点、蓝人回蓝人存档点。迷宫关卡一般靠这种设计控制玩家的错误路线,掉进酸液后被迫返回上一个分支路口。
4. 迷宫关卡的数据驱动与进度存档:从 Scene 到 PlayerPrefs
双角色迷宫的关卡设计多由开发者直接在场景里手工摆放,代码层面真正做数据驱动的部分是关卡解锁表和进度存档。迷宫多关卡时,靠硬编码写死每一关的按钮和状态会很难维护,常见做法是配置在 ScriptableObject 或轻量 JSON 里。
4.1 用 ScriptableObject 或 JSON 做关卡配置
小关卡的属性包括关卡 ID、场景名、是否锁定、解锁条件。用 ScriptableObject 能直接在编辑器里创建资源,字段改变时立即保存,不需要改代码。
using UnityEngine; using System.Collections.Generic; // 在 Project 面板右键 -> Create -> Game -> LevelConfig 创建关卡配置 [CreateAssetMenu(fileName = "LevelConfig", menuName = "Game/LevelConfig")] public class LevelConfig : ScriptableObject { public string levelId; // 例如 "level_1_3" public string sceneName; // 对应的 Unity 场景名 public bool isLocked; // 初始锁定状态 public int requiredStars; // 解锁所需星星数 public List<string> tags; // 关卡标签,例如 "冰" "火" }将配置做成资源后,选择关卡界面遍历所有 LevelConfig 实例即可。新增一个关卡时,新建一份配置资源和场景,UI 脚本不需要改动。迷宫类项目新增关卡频率高,数据驱动比硬编码按钮列表省事得多。
4.2 用 PlayerPrefs 保存解锁进度与 C# 字符串解析
存档用 PlayerPrefs 是成本最低的方案,适合小游戏。需要注意 PlayerPrefs 只适合存短字符串和数字,把整张地图序列化成大 JSON 塞进去会有写入风险。
using UnityEngine; public class SaveManager : MonoBehaviour { // 解锁前缀 + 关卡 ID 作为键 private const string LevelPrefix = "level_unlock_"; public void UnlockLevel(string levelId) { // 单关单独存一个 int,避免整表覆盖 PlayerPrefs.SetInt(LevelPrefix + levelId, 1); PlayerPrefs.Save(); } public bool IsLevelUnlocked(string levelId) { // 默认值 0 表示未解锁 return PlayerPrefs.GetInt(LevelPrefix + levelId, 0) == 1; } public int ParseLevelNumber(string levelId) { // "level_1_3" 按 '_' 拆成三段,取最后一段解析成数字 string[] parts = levelId.Split('_'); int number = 0; if (parts.Length > 0) { int.TryParse(parts[parts.Length - 1], out number); } return number; } }Split 和 int.TryParse 是 C# 字符串处理里最常用的组合,存档、配置读取、数据解析都绕不开它们。建议用 TryParse 而不是直接 int.Parse,因为配置手写出错时,TryParse 会返回 false,不影响主流程继续跑。注意 PlayerPrefs.Save() 在桌面平台会立即写盘,移动端可能延迟到应用切后台才落盘,排查进度丢失问题时优先确认它是否被调用过。
4.3 扩大机关按钮点击范围与温度条刷新的性能问题
迷宫里有按钮机关或 UI 操作按钮时,会遇到“看着很大,点了没反应”的问题。先说结论:UGUI 默认的 Image 只要勾选了 Raycast Target,透明区域也会拦截点击。点了没反应通常不是透明问题,而是事件被其他全屏 Image 挡住,或按钮本身没勾 Raycast Target。
| 做法 | 适用场景 | 注意点 |
|---|---|---|
| 放大 Image 的 RectTransform | 方形按钮 | 按钮文字居中不跟随,需要单独调整 |
| 按钮下垫大底图 | 任意按钮 | 底图不能盖住按钮文字和图标 |
| alphaHitTestMinimumThreshold | 不规则按钮 | 只过滤透明像素,不扩大热区 |
放大热区的正解是垫底图:在按钮节点下新建一个 Image 作为子物体,勾选 Raycast Target,RectTransform 拉得比按钮大一圈,用 SetAsFirstSibling 放到最底层。给不规则按钮做精细点击判定,才需要设置 alpha 阈值:
using UnityEngine; using UnityEngine.UI; public class ButtonHitArea : MonoBehaviour { public Image targetImage; // 按钮上的 Image private void Start() { // 只让不透明像素参与点击判定,透明区域点击穿透 targetImage.alphaHitTestMinimumThreshold = 0.1f; } }温度条这类 UI 刷新也要注意。冰火人里角色进入火焰区温度上升、进水池下降,如果每帧在 Update 里读数值再赋给 Slider,场景物体多时会有无谓开销。常见做法是让状态系统在数值变化时抛事件,UI 只负责订阅:
using UnityEngine; using UnityEngine.UI; public class TemperatureBar : MonoBehaviour { public Slider slider; // 温度条滑动条 private void OnEnable() { // 订阅事件,数值变化时才刷新 UI PlayerEnergy.OnEnergyChanged += UpdateBar; } private void OnDisable() { // 退订事件,防止失效对象继续接收消息 PlayerEnergy.OnEnergyChanged -= UpdateBar; } private void UpdateBar(float currentEnergy) { slider.value = currentEnergy; } }事件订阅相比 Update 轮询,省掉了每帧读取和比较的开销,对频繁切换状态的机关界面更友好。
5. 验收与发布:读源码时的 3 个检查项与 WebGL 存档兜底
标题里写了“最新”两个字,拿到源码第一批要做的是验收流程,确认它确实能独立构建。以下是我处理这类源码项目常用的 3 个检查项。
5.1 检查场景引用、Tag 和构建列表
第一个检查项是场景中缺失脚本。打开场景后,在 Hierarchy 中点击任意 GameObject,检查 Inspector 是否出现 “Missing (Mono Script)”。出现时需要找到对应脚本重新拖挂,或者删掉节点重建。
第二个检查项是 Tag 和 Layer。控制脚本里用了 CompareTag("Ground"),如果 Ground 这个 Tag 不存在,运行时会抛异常。对照 Tag Manager 确认 Ground、Red、Blue 三个 Tag 都被定义。
第三个检查项是 Build Settings 里的场景列表。很多人只点 Play 忽略构建,发布时才发现 Scenes In Build 是空的。把入口场景拖进列表,其余关卡场景按解锁顺序排列即可。
5.2 WebGL 发布时的 PlayerPrefs 与 IDBFS 写入失败
迷宫游戏常用于网页端分发,Unity 发布 WebGL 时存档机制会变成浏览器 IndexedDB 上的 IDBFS 写入。隐私模式、存储配额超限、同一域名下有旧工程的数据残留,都可能导致写入失败,Unity 发布 WebGL 使用 idbfs 写入失败是这类项目的典型报错。
处理顺序一般这样走:先用 Chrome 打开站点,按 F12 进入开发者工具,切到 Application 面板,在 Storage 分区可以看到配额占用;展开 IndexedDB 并找到对应域名,清理残留库。然后退出隐私模式,用普通窗口重试。最后检查 Player Settings 里的压缩格式和数据缓存设置。
如果进度数据不关键,可以做内存降级,捕获存档异常后本次会话继续玩,不中断游戏。PlayerPrefs 在桌面和移动端表现稳定,WebGL 下则受浏览器配额限制,频繁写入大字段时考虑换成 localStorage 或跳过硬编码的 IDBFS 封装,避免目录残留问题。
5.3 用带对象引用的 Debug.Log 验证双角色切换
最后一个实用技巧,排查双角色切换逻辑时,在切换方法里输出带场景对象引用的日志。
private void SwitchActivePlayer(PlayerController nextPlayer) { // 日志带上对象引用,点击 Console 消息可直接定位场景对象 Debug.Log($"[PlayerSwitcher] 切换前角色: {currentPlayer.name}", currentPlayer); currentPlayer.enabled = false; currentPlayer = nextPlayer; currentPlayer.enabled = true; Debug.Log($"[PlayerSwitcher] 切换后角色: {currentPlayer.name}", currentPlayer); }Debug.Log 的第二个参数是 UnityEngine.Object 类型,传场景对象进去后,Console 会保存上下文。点击日志消息,Scene 视图会自动高亮并定位到该对象。排查陌生源码时,这样比在代码里盲猜快得多,还能同时看到两个角色是否都进入了正确的 switch 状态。
本文还有配套的精品资源,点击获取