简介:这份资源是面向Unity初学者与2D小游戏开发爱好者的记忆翻牌游戏完整源代码,基于Unity 2019.4.18f1c1版本与C#语言实现,可帮助读者理解翻牌配对类休闲游戏的完整开发流程。项目围绕牌面翻转与匹配展开,涵盖OnMouseDown点击事件处理、枚举状态管理、Sprite渲染切换、Collider碰撞检测、脚本间通信、UI界面搭建以及Animator翻转动画等核心知识点,同时涉及单元测试框架与Plastic SCM版本管理等工程化内容。压缩包整体约62.47MB,文件类型以C#脚本、Unity场景与资源文件为主,配合项目配置与测试相关文件,结构完整便于直接导入学习。目前已有2022人学习下载,适合希望从零掌握Unity 2D开发、提升C#编程与引擎使用能力的入门及进阶开发者参考。
1. 翻牌记忆游戏:从一堆散乱脚本到能跑通的 Unity2D 工程
很多人第一次接触 Unity2D 小游戏开发,都是从翻牌记忆这类项目入手的。它规则简单——点开两张牌,图案一致就消除,不一致就翻回去——但真正动手写的时候,你会发现卡住新手的从来不是玩法逻辑,而是「牌怎么摆」「点击怎么响应」「翻回去的时机怎么控制」这些琐碎问题。这份记忆翻牌源代码(C#)就是冲着这些痛点来的:它把一套完整的、能直接跑起来的 Unity2D 工程拆成了若干职责清晰的 C# 脚本,涵盖卡牌生成、点击检测、配对判定、动画翻转和关卡重置。适合两类人:一是刚学完 Unity 基础、想找一个完整小项目练手的入门开发者;二是需要快速搭一个翻牌玩法原型、再往上叠美术和数值的从业者。下面我按「拿到工程怎么跑 → 核心脚本怎么读 → 参数怎么调 → 哪里容易翻车」的顺序,把这份资源拆开讲一遍。
2. 工程结构与运行链路:先让场景跑起来再谈改代码
2.1 目录里有什么,各自负责哪一段
拿到这份源代码,第一件事不是急着打开脚本,而是先认清工程目录的职责划分。一个能跑的记忆翻牌工程,通常包含这几个关键部分:Scenes文件夹放主场景,Scripts放全部 C# 逻辑,Prefabs放卡牌预制体,Sprites或Textures放卡面图案资源,Audio放翻牌和配对的音效。卡牌预制体是整个玩法的物理载体,它身上挂着碰撞体(用于点击检测)和至少一个控制脚本(用于翻转和状态记录)。
我一般会先打开主场景,看 Hierarchy 里的对象层级:通常有一个空物体作为棋盘容器(Board),下面动态生成 N 张卡牌;还有一个 GameManager 之类的常驻对象,负责全局状态。理解这条链路比逐行读代码重要得多——因为翻牌游戏的 bug 十有八九出在「谁在什么时候通知谁」上。
提示:如果打开场景后 Hierarchy 里只有零星几个对象,卡牌是运行时生成的,这是正常设计,不要以为工程缺东西。
2.2 从点击到配对:一条完整的调用链
把工程跑起来之前,先在心里过一遍调用链,后面读代码会顺很多。典型流程是这样的:玩家点击某张卡牌 → 卡牌的碰撞体触发OnMouseDown或射线检测 → 脚本把这张牌加入「已翻开」列表 → 判断列表长度是否达到 2 → 达到就比对两张牌的 ID → 相同则标记为已配对并播放消除效果,不同则延迟一小段时间后翻回去。
这条链路里有两个容易忽略的点。第一,判断「已翻开」不能只看动画状态,要用一个独立的布尔字段记录逻辑状态,否则动画播放中途连点会出问题。第二,比对完成后必须清空临时列表,否则第三次点击会拿旧数据去比。这两点在下面的代码里都会体现。
2.3 让工程跑起来的最小步骤
在 Unity Hub 里用对应版本打开工程文件夹,等待资源导入完成。如果版本差异导致报错,优先看 Console 里的第一条红字,通常是 API 命名或包缺失。导入完成后,在 Project 窗口找到主场景双击打开,点播放按钮即可试玩。
# 工程根目录的典型结构,用命令行快速确认关键文件夹是否齐全 ls -R Assets | head -40 # 预期能看到 Scenes / Scripts / Prefabs / Sprites 这几类目录这段命令只是帮你确认资源完整性,不涉及任何构建操作。如果Scripts目录为空,说明工程可能只打包了场景没带脚本,需要重新核对资源包。确认目录齐全后,检查ProjectSettings里的输入设置,确保鼠标点击能被正常捕获——有些工程默认只开了触摸输入,PC 上点不动就是这个原因。
3. 核心脚本拆解:卡牌状态机与配对判定怎么写
3.1 卡牌脚本:一个字段管状态,一个方法管翻转
卡牌脚本是整个工程的心脏。它要维护的核心状态其实就三个:这张牌的图案 ID、它当前是否正面朝上、它是否已经被配对消除。很多人图省事,用旋转角度去反推是否翻开,结果动画一卡就判断错。正确做法是用独立的布尔字段。
using UnityEngine; using System.Collections; public class Card : MonoBehaviour { public int cardId; // 图案 ID,配对时比对这个值 public Sprite frontSprite; // 正面图案 public Sprite backSprite; // 背面图案 private bool isFaceUp = false; // 逻辑状态,不依赖动画 private bool isMatched = false; // 已配对标记,防止重复点击 private SpriteRenderer sr; void Start() { sr = GetComponent<SpriteRenderer>(); sr.sprite = backSprite; } // 由 GameManager 调用,而不是自己处理点击 public void Flip() { if (isMatched || isFaceUp) return; // 已配对或已翻开直接忽略 StartCoroutine(FlipRoutine()); } private IEnumerator FlipRoutine() { isFaceUp = true; // 这里用缩放模拟翻转,实际工程可能用旋转或动画 for (float t = 0; t < 0.15f; t += Time.deltaTime) { transform.localScale = new Vector3( Mathf.Lerp(1f, 0f, t / 0.15f), 1f, 1f); yield return null; } sr.sprite = frontSprite; for (float t = 0; t < 0.15f; t += Time.deltaTime) { transform.localScale = new Vector3( Mathf.Lerp(0f, 1f, t / 0.15f), 1f, 1f); yield return null; } } public void FlipBack() { if (isMatched) return; isFaceUp = false; sr.sprite = backSprite; } public void SetMatched() { isMatched = true; // 配对成功后的表现,比如淡出或缩小消失 StartCoroutine(FadeOut()); } private IEnumerator FadeOut() { for (float a = 1f; a > 0f; a -= Time.deltaTime * 3f) { var c = sr.color; c.a = a; sr.color = c; yield return null; } gameObject.SetActive(false); } }逻辑说明:Flip方法里先做双重拦截——已配对或已翻开就 return,这是防止连点翻车的核心。翻转用协程分两段做,前半段缩到 0 换图,后半段再展开,视觉上就是翻牌。isFaceUp在协程一开始就置为 true,而不是等动画结束,这样在动画播放期间再次点击会被正确拦截。
参数说明:cardId是配对比对的唯一依据,生成卡牌时必须成对赋值;frontSprite和backSprite在 Inspector 里拖入,或者由 GameManager 运行时赋值;翻转时长 0.15 秒是手感经验值,调大到 0.3 会显得拖沓,调小到 0.05 又看不清翻转过程。
3.2 游戏管理脚本:生成棋盘与配对判定
GameManager 负责三件事:开局生成打乱的卡牌、接收卡牌点击、判定配对。它持有一个「当前已翻开卡牌」的列表,长度到 2 就触发比对。
using UnityEngine; using System.Collections; using System.Collections.Generic; public class GameManager : MonoBehaviour { public GameObject cardPrefab; public Sprite[] cardSprites; // 所有图案,长度应为偶数 public Transform boardRoot; public int rows = 4; public int cols = 4; private List<Card> flippedCards = new List<Card>(); private bool isChecking = false; // 比对期间锁住输入 void Start() { GenerateBoard(); } void GenerateBoard() { int total = rows * cols; // 每个图案出现两次,凑成 total 张 List<int> ids = new List<int>(); for (int i = 0; i < total / 2; i++) { ids.Add(i); ids.Add(i); } // 洗牌 for (int i = 0; i < ids.Count; i++) { int r = Random.Range(i, ids.Count); (ids[i], ids[r]) = (ids[r], ids[i]); } for (int i = 0; i < total; i++) { var go = Instantiate(cardPrefab, boardRoot); var card = go.GetComponent<Card>(); card.cardId = ids[i]; card.frontSprite = cardSprites[ids[i]]; // 按行列摆放,间距根据卡牌尺寸调整 int row = i / cols; int col = i % cols; go.transform.localPosition = new Vector3(col * 1.2f, -row * 1.2f, 0); } } // 卡牌点击后调用此方法 public void OnCardClicked(Card card) { if (isChecking) return; if (flippedCards.Contains(card)) return; card.Flip(); flippedCards.Add(card); if (flippedCards.Count == 2) { StartCoroutine(CheckMatch()); } } private IEnumerator CheckMatch() { isChecking = true; yield return new WaitForSeconds(0.6f); // 留时间给玩家看第二张牌 if (flippedCards[0].cardId == flippedCards[1].cardId) { flippedCards[0].SetMatched(); flippedCards[1].SetMatched(); } else { flippedCards[0].FlipBack(); flippedCards[1].FlipBack(); } flippedCards.Clear(); isChecking = false; } }逻辑说明:GenerateBoard先把图案 ID 成对塞进列表再洗牌,保证每种图案恰好两张。OnCardClicked里用flippedCards.Contains防止同一张牌被点两次。CheckMatch用isChecking锁住输入,避免比对动画期间玩家又点第三张牌导致列表错乱。
参数说明:rows和cols决定棋盘规模,4×4 是入门默认值,改成 4×6 需要保证cardSprites长度至少为 12;WaitForSeconds(0.6f)是翻回去前的停顿,太短玩家看不清第二张牌,太长会显得卡顿;卡牌间距 1.2 是相对卡牌尺寸的经验值,卡牌变大要同步调大。
3.3 点击检测:射线还是碰撞体,选哪个
Unity2D 里检测点击有两种常见做法。一是给卡牌挂BoxCollider2D,用OnMouseDown回调;二是从摄像机发射射线,用Physics2D.Raycast命中判断。前者写起来快,适合原型;后者更可控,适合需要区分 UI 和场景点击的场合。
这份工程用的是碰撞体方案,因为卡牌本身就是独立对象,OnMouseDown直接挂在卡牌脚本上最省事。但要注意:OnMouseDown依赖主摄像机,如果场景里有多个摄像机或者摄像机没开Physics2D相关设置,点击会失效。我一般会在卡牌脚本里加一句转发,把点击事件交给 GameManager 统一处理,而不是让卡牌自己判断配对——职责分离后,改玩法时不用动卡牌脚本。
// 挂在卡牌预制体上,转发点击给管理器 void OnMouseDown() { if (GameManager.Instance != null) GameManager.Instance.OnCardClicked(this); }这里用单例Instance是为了让卡牌能拿到管理器引用。如果你的工程不想用单例,也可以在生成卡牌时把管理器引用注入进去,效果一样。
4. 参数调优与手感打磨:让翻牌不像在点按钮
4.1 翻转动画的时长与曲线
翻牌手感好不好,八成看动画。上面代码用的是线性缩放,实际调的时候建议换成缓动曲线。Unity 的AnimationCurve可以在 Inspector 里可视化编辑,把翻转前半段调快、后半段调慢,会有「啪」一下翻过去的感觉。翻转总时长控制在 0.2 到 0.3 秒之间比较舒服,低于 0.15 秒玩家来不及反应,高于 0.4 秒会显得拖沓。
配对成功的消除动画同理。直接SetActive(false)太生硬,加一个 0.2 秒的缩小加淡出,玩家会有明确的「消掉了」的反馈。失败翻回去的动画可以比翻开稍快一点,暗示「这张不对,赶紧翻回去」。
4.2 棋盘布局与卡牌尺寸的匹配
棋盘布局不是随便摆的。卡牌间距太小会误触,太大又显得松散。经验公式是:间距 = 卡牌宽度 × 1.2 到 1.4。如果卡牌是 1×1 单位,间距取 1.2 到 1.4;卡牌是 2×2,间距就取 2.4 到 2.8。行列数也要考虑屏幕比例,4×4 在竖屏上偏挤,横屏上刚好;如果目标平台是手机竖屏,3×4 或 4×5 更合适。
另外,棋盘整体要居中。生成时算一下总宽高,把boardRoot的位置偏移到屏幕中心,否则卡牌会从左上角开始铺,右边和下面留一大片空白。
4.3 音效与反馈的时机
音效不是锦上添花,是手感的一部分。翻牌音效要在翻转动画开始的瞬间播放,配对成功音效在判定成立的瞬间播放,失败音效在翻回去之前播放。三个音效的时机差个零点几秒,玩家的感受完全不同。常见做法是把音效播放也放进协程,和动画节点对齐,而不是在逻辑判断里随手播。
注意:音效文件不要直接拖到场景里播,用
AudioSource.PlayOneShot在脚本里控制时机,否则容易出现音效和动画对不上的情况。
5. 避坑与排查:翻牌游戏最容易翻车的五个地方
5.1 连点导致翻开三张牌
现象:玩家手快,在两张牌还没比对完的时候点了第三张,结果三张牌都翻开了,配对逻辑彻底乱掉。
原因:OnCardClicked里没有输入锁,或者锁的时机不对。isChecking只在CheckMatch协程里置位,但协程启动前有个空档。
解决:在flippedCards.Count == 2成立的瞬间就置isChecking = true,而不是等协程里再置。同时OnCardClicked开头就判断isChecking,双保险。
5.2 同一张牌被点两次算作一对
现象:玩家双击同一张牌,系统判定为「翻开两张」,然后拿这张牌和自己比对,居然配对了。
原因:flippedCards里没有去重,或者去重判断写在Flip之后。
解决:在OnCardClicked里先判断flippedCards.Contains(card),包含就直接 return,不执行任何翻转和添加操作。
5.3 翻回去的动画和逻辑状态不同步
现象:牌翻回去了,但isFaceUp还是 true,导致这张牌再也点不开。
原因:FlipBack只改了图片没改状态字段,或者状态字段在协程里改但协程被打断了。
解决:FlipBack里第一件事就是把isFaceUp = false,再改图片。所有状态变更都放在逻辑层,动画层只负责表现,不要用动画状态反推逻辑状态。
5.4 卡牌生成后位置重叠
现象:所有卡牌都堆在同一个位置,只能看到最上面一张。
原因:生成时没有按行列计算偏移,localPosition全是默认值。
解决:在生成循环里用i / cols和i % cols算出行列,再乘以间距。注意Instantiate时父物体设为boardRoot,这样localPosition才是相对棋盘的坐标。
5.5 图案数量不够导致索引越界
现象:Console 报IndexOutOfRangeException,卡牌生成到一半就停了。
原因:cardSprites数组长度小于rows * cols / 2,cardSprites[ids[i]]取不到值。
解决:生成前先校验cardSprites.Length >= rows * cols / 2,不满足就在 Console 里打一条明确的错误提示,而不是让它默默崩掉。这个校验我每次都会加,血泪经验。
6. 进阶技巧:把翻牌工程改成可配置的关卡系统
基础版跑通之后,下一步通常是让它支持多关卡。与其复制多份场景,不如把棋盘参数抽成配置。常见做法是定义一个LevelConfig类,用ScriptableObject存每关的行列数、图案集合、限时和步数上限,GameManager 开局读配置生成棋盘。
using UnityEngine; [CreateAssetMenu(fileName = "LevelConfig", menuName = "Memory/LevelConfig")] public class LevelConfig : ScriptableObject { public int rows = 4; public int cols = 4; public Sprite[] sprites; // 本关可用图案 public float timeLimit = 60f; // 限时,0 表示不限 public int maxMoves = 0; // 步数上限,0 表示不限 }逻辑说明:CreateAssetMenu让你能在 Project 窗口右键直接创建配置文件,每关一个 asset。GameManager 里把rows、cols、cardSprites替换成从LevelConfig读取,就能做到不改代码换关卡。
参数说明:timeLimit和maxMoves是两种不同的压力设计,限时考验反应,限步考验记忆策略,一般二选一即可,同时开会让难度陡增。sprites数组长度同样要满足rows * cols / 2的约束,这个校验建议写进OnValidate里,在 Inspector 改参数时就报错,而不是运行时才崩。
验证关卡配置是否生效,最简单的办法是在 GameManager 的Start里打一行日志,输出当前关卡的行列和图案数量,跑起来看 Console 对不对。我习惯每加一个配置项就加一条这样的日志,确认无误后再删掉。
从那以后我每次拿到一个能跑的工程,都强制先跑通再改代码,绝不在一堆报错的情况下动逻辑。希望这份拆解能帮你少走点弯路,把翻牌这个小玩法真正吃透。
本文还有配套的精品资源,点击获取