
简介这是基于Cocos2D-X开发的手游麻将完整源码面向想深入理解麻将游戏逻辑或学习跨平台2D游戏开发的开发者。包内共584个文件包含cpp/h源码、Java与Android工程文件、png/plist美术资源、ogg/mp3音频、db数据文件等压缩包约16.22MB可覆盖客户端逻辑、界面资源、网络通信与数据存储等多个层次。已有2521人学习下载。源码中重点展示了洗牌算法、出牌规则、胡牌条件判断等核心玩法实现同时涉及Cocos2D-X的Sprite/Scene/Action用法、UI触摸交互、Socket网络模块以及多平台适配与性能优化思路。通过阅读工程代码可以掌握从C游戏逻辑到Java原生调用的常见协作方式也能了解XML配置解析、SQLite数据管理和资源打包结构。适合具备一定编程基础、想从零梳理麻将服务端与客户端交互流程的开发者作为实践参考但需注意资源仅限学习研究使用不宜用于商业项目。1. 项目整体设计与思路拆解1.1 这个源码项目到底是什么接到“手游欢乐麻将源码”这个题目时我第一反应不是去搜某个现成仓库而是先问自己一个问题大家嘴里说的“源码”到底想要的是什么我做了几年棋牌类手游也接过不少类似的定制需求。绝大多数人找“欢乐麻将源码”并不是真的想研究某一家公司的某个具体资源而是想要一套能跑的、能改的、能上线的完整玩法闭环——大厅、房间、匹配、对战、结算一个不少最好客户端、服务端、数据库脚本都齐。换句话说他们想要的不是“代码”而是一个可以当成产品看待的工程样板。这篇文章我按自己实际做过的一个欢乐麻将风格手游源码项目来复盘。项目采用 Unity C# 做客户端服务端用 .NET Core数据库用 MySQL核心玩法覆盖了经典大众麻将的完整流程包括洗牌发牌、碰杠胡、听牌提示、AI托管、房间管理、结算回放。项目规模不算大单客户端加服务端源码合计也就不到五万行但它把棋牌游戏里最容易踩坑的那部分都踩了一遍。如果你是想入门棋牌开发的客户端或服务端开发或者正准备接一个外包项目做技术预研这篇文章会比网上一堆零散的“源码下载”有用得多。我会先把整体架构讲清楚再把胡牌算法、牌局状态机、同步方案、性能优化这些核心硬骨头逐个拆开最后列一下我实际开发中遇到的高频问题。1.2 技术选型与架构取舍先聊技术栈。客户端选 Unity原因很直接棋牌游戏 2D UI 交互重、动画特效多、需要跑 Android 和 iOS 双端Unity 的 UGUI 生态和跨平台能力最省事。Cocos Creator 也能做而且包体更小但如果你后续想加 3D 牌桌、捏脸、房间装扮这类偏展示的功能Unity 的扩展空间明显更大。服务端选 .NET Core 的原因稍微特殊一点。棋牌对战是典型的“连接密集、逻辑集中”场景——一局四个人多数时间在等人出牌真正的热点是房间管理和状态广播对单机并发的要求远没有 IM 或 MMORPG 高。.NET Core 的异步 Socket 和内存管理足够扛住几千个并发房间而开发效率、代码可读性比 C 高出一大截。平时自己改需求、加玩法编译快调试也省心。架构上我坚持一个原则客户端只做表现和操作所有规则判定都放在服务端。出牌、吃碰杠、胡牌、番型计算这些逻辑如果在客户端做就会出现经典的“神仙打架”问题——有人改本地代码让自己把把天胡或者把自己手里的牌改成清一色。我项目里服务端每收到一个操作请求都会把牌墙、手牌、弃牌堆重新校验一遍判定结果由服务端权威广播。这个设计让客户端源码可以放心开源因为就算有人拿走了完整客户端代码也改不出任何作弊能力。还有一个取舍是关于数据结构的。刚开始做原型时我用 JSON 字符串直接当协议传输开发确实快但联调时字段名拼错、类型对不上这类低级错误层出不穷。后来统一改成 protobuf 定义消息体前后端共用一套 .proto 文件字段变更自动生成代码这个问题基本消失。这也是我建议所有做源码项目的朋友一开始就做好的事先定义协议再写业务逻辑。1.3 源码工程怎么读拿到一套完整源码很多新手会一头扎进文件堆里然后迷失在几千个文件里。我的建议是先看目录建立全局地图。我习惯把服务端源码按模块拆成这样几块Gateway连接管理、心跳检测、消息路由Room房间生命周期、座位管理、断线重连Game牌局状态机、规则判定、AI托管Data配置表加载、玩家数据、战绩存储客户端源码则按功能域划分大厅登录、创建房间、设置、牌桌手牌区、出牌区、操作按钮、动画出牌动画、碰杠胡特效、网络连接、消息分发。这样切分之后读源码时你就能做到“按图索骥”想改胡牌规则去 Game 模块找想调整出牌动画时长去客户端动画目录找。另外我强烈建议把配置和数据分离。麻将的地方规则差异极大——四川麻将的血流成河、湖南麻将的红中赖子、广东麻将的鸡胡平胡规则细节完全不一样。如果把这些写死在代码里每接一个地区就要改一次逻辑。我的做法是定义一套规则配置表是否允许吃牌、是否有赖子、胡牌最小番型、封顶倍数全部做成服务端配置项。这样源码本身只维护一套核心逻辑玩法差异用配置驱动这也是“欢乐麻将”能出那么多地区版本的根本原因。2. 核心玩法模块拆解牌型、AI与胡牌算法2.1 牌的编码、洗牌与发牌麻将牌的本质是一组有限集合。万、条、筒各 1 到 9每款 4 张加上字牌东南西北中发白各 4 张一共 136 张不含花牌。我用的编码方式很朴素用一个整数表示一张牌0 到 8 表示万子 1 到 99 到 17 表示条子18 到 26 表示筒子27 到 33 表示字牌。这样编码的好处是大小比较、花色判断都变成了整数运算不用解析字符串判断“是否同一花色”只需要算一次除法card / 9。而且牌的排序天然等价于整数排序后续判断胡牌时非常方便。洗牌算法这里要特别说一句不要用系统自带的Random.Shuffle直接洗牌很多语言的默认随机数生成器周期不够长或者分布不够均匀实战中会出现“某种牌型重复出现”的诡异现象。我用的 Fisher-Yates 算法从牌墙末尾往前遍历每次随机交换一张牌。配合一个周期足够长的随机数种子实测一万局下来牌型分布是平稳的。发牌逻辑就相对简单了。庄家起手 14 张闲家 13 张剩下的牌作为牌墙放在中间。需要留多少张“杠尾”作为补杠牌按规则约定来一般是牌墙末尾留 8 到 14 张不参与摸打。这个细节容易被忽略但非常影响牌局体验——留少了玩家没牌可摸留多了牌墙太厚一盘打太久。2.2 手牌组织与出牌合法性手牌在内存里用什么结构存直接决定了后面所有算法的复杂程度。我用的是“计数数组”——长度为 34 的 int 数组下标就是牌编码值表示手里有几张这种牌。这样碰碰胡判断、听牌枚举这类操作都能通过数组下标直接访问复杂度是 O(1)性能极好。出牌合法性的检查相对简单玩家手里必须有这张牌。但真正的坑在碰杠的并发处理。三个玩家同时想碰同一张牌服务端必须按逆时针顺序确定优先级而不是谁先点谁就响应。客户端下发操作指令时服务端要先判断当前是否处于“出牌等待”状态收到出牌后进入“副露等待”状态收集一圈玩家的碰杠胡意愿再统一裁决。这个流程如果状态机写得不够严谨就会出现两个人都碰成功、或者已经胡了还能被杠的乱象。另外手牌的排序展示也是一个容易被忽略的体验点。客户端拿到手牌后不能直接把数组原样渲染要按“花色分组 同花色点数升序”排好。这个排序逻辑放在客户端做即可服务端只需要保证牌的集合是对的毕竟展示顺序只是视觉层面的东西。2.3 胡牌与听牌判定胡牌判定是整个源码项目里最核心、也最容易写错的算法。标准胡牌模型是将牌一对相同的牌 若干组顺子或刻子。判断一副手牌能不能胡最直白的做法是回溯拆解先枚举将牌再从剩余的牌里递归找顺子和刻子。我把核心逻辑写成了这样一段 C# 代码逻辑比较清晰也方便照着改// 手牌计数数组下标为牌编码值为数量 private bool CanHu(int[] hand) { for (int i 0; i 34; i) { if (hand[i] 2) continue; // 选当前牌作为将牌 hand[i] - 2; if (IsAllMelds(hand)) { hand[i] 2; return true; } hand[i] 2; } return false; } private bool IsAllMelds(int[] hand) { // 找到第一张数量 0 的牌 int start 0; while (start 34 hand[start] 0) start; if (start 34) return true; // 所有牌已经拆完 // 尝试拆刻子 if (hand[start] 3) { hand[start] - 3; if (IsAllMelds(hand)) { hand[start] 3; return true; } hand[start] 3; } // 尝试拆顺子字牌不参与顺子 if (start 27 start % 9 6 hand[start 1] 0 hand[start 2] 0) { hand[start]--; hand[start 1]--; hand[start 2]--; if (IsAllMelds(hand)) { hand[start]; hand[start 1]; hand[start 2]; return true; } hand[start]; hand[start 1]; hand[start 2]; } return false; }这段代码的要点在于“先拆刻子再拆顺子”的尝试顺序以及失败后的回溯恢复。复杂度上手牌最多 14 张枚举量极小性能完全不是问题。实战中更大的坑是边界条件七对、十三幺这类特殊牌型需要单独判断而且不同地区规则对“能不能胡”的标准不一样——比如有的地方要求必须“缺一门”有的要求有“258 做将”。这些都要在胡牌判定之前先做规则过滤我项目里是写了一个CheckWinCondition(hand, ruleConfig)的规则链依次判断特殊牌型、门风限制、番型底限。听牌判断则是在胡牌判定基础上做一圈枚举把 1 到 34 的每一张牌依次加入手牌调用 CanHu 看能否胡。能胡则记录这张牌为“听牌”同时生成提示文案“听三六万”。这样玩家点“听牌提示”按钮时服务端可以一次性返回所有可胡牌和对应的牌型描述。2.4 AI托管出牌策略真人中途退出、或者单人练习模式都需要 AI 接管出牌。AI 做得太蠢会被玩家骂做得太聪明又不像真人。我的经验是先保证 AI 不犯低级错误再追求“像人”。基础版 AI 的决策逻辑是一个优先级链表。第一优先级如果能胡牌直接胡第二优先级如果能杠且杠后不影响牌型结构选择杠第三优先级评估当前手牌打出“最没用”的牌。“最没用”的评估方法是给每张牌打分。单张幺九、字牌这类很难成搭的牌打低分相邻牌比如手上有 4 万和 5 万组成的搭子打高分然后选择分数最低的那张打出。这个逻辑在实战中已经能达到不错的水平至少不会出现“手里有对子还拆了打”这种让玩家血压升高的操作。进阶一点的 AI 会加入“牌河分析”——观察其他玩家打过的牌判断哪些牌是安全的别人大概率不要的哪些牌是危险的可能点炮的。这个功能在源码里我做成一个独立的SafetyAnalyzer模块它读取所有玩家的弃牌记录给每张牌计算一个危险度分数。当 AI 面临“拆搭子”这种关键决策时优先出安全牌。这个设计很值得抄因为它把防守逻辑从出牌逻辑里解耦了出来后续想加强 AI 水平只需要替换安全分析器。3. 对战流程与服务端同步的实现细节3.1 房间生命周期管理房间是一个源码项目里最容易被低估的模块。很多人以为房间就是“建一个对象存四个人”真正实现起来才发现房间生命周期涉及大量边界情况房主中途退出怎么办、有人长时间不准备怎么处理、房间内配置修改谁有权操作、对局中途有人掉线房间要不要解散。我实现房间状态时用了这样几个状态等待中、游戏中、已解散。等待中状态下房主可以调整玩法配置、踢人、解散房间玩家点击准备后进入“已准备”状态全员准备后自动开局。游戏中的房间不允许新玩家加入掉线玩家保留座位等待重连超过设定时间未重连才判定为托管。这里有一个很实用的设计房间配置的版本号。每次修改配置自动递增版本号玩家进入房间时会拉取一次对局开始时会再校验一次。这样能避免“A 玩家改了一条规则B 玩家界面上还是旧规则”的显示不一致问题。虽然技术含量不高但确实是个能省掉大量客服投诉的小技巧。3.2 牌局状态机的流转牌局本身就是一个状态机我把它切分成洗牌中、发牌中、摸牌、出牌、副露判定、补杠、流局、结算。每个状态都有明确的进入条件和退出条件状态之间的跳转只由服务端裁决后广播。举个例子出牌状态的流转是这样的服务端广播“轮到玩家 A 出牌”同时启动一个超时计时器默认 20 秒可配置。玩家 A 出牌后服务端进入“副露判定”状态分别收集其他三位玩家的碰、杠、胡意愿按逆时针顺序仲裁。如果有人碰则进入“碰牌处理”由碰牌者出牌如果有人胡则直接进入结算如果都没有则轮到下家摸牌状态回到“摸牌”。这个状态机是整个服务端最容易出 bug 的地方。我踩过最惨的一个坑是玩家 A 出牌后玩家 B 和玩家 C 同时点了“碰”服务端因为消息处理顺序问题先给 B 广播了碰成功又给 C 广播了碰成功导致牌局出现了两张牌被两个人同时碰走的严重不一致。最后修复方案是服务端在副露判定时设置一个meldLock标记第一个碰请求成功处理后其余玩家的操作请求直接返回“操作已失效”。这个标记的加锁逻辑一定要放在内存里做不能依赖数据库否则并发一高必出问题。3.3 同步方案与断线重连棋牌游戏选状态同步还是帧同步是我被问到最多的问题之一。帧同步适合格斗、RTS 这类对操作精度要求极高的游戏需要所有客户端在同一帧执行同样的逻辑。麻将的节奏是回合制的玩家的操作频率极低状态同步完全够用而且实现难度低一个量级服务端是唯一权威客户端只是“遥控器 屏幕”。状态同步下断线重连的恢复方案比较直接服务端保存每一局完整的状态快照包括四家手牌、牌墙剩余数量、弃牌记录、当前轮到谁、副露记录。玩家重连时服务端把整个快照一次性下发客户端据此重建牌桌现场。这里有一个体验细节值得讲快照下发时其他玩家的手牌数量要显示但牌面不能暴露。我在快照协议里用了一个cardBack字段对端手牌位置只发数量不发具体牌。等轮到自己操作时再单独下发一张“自己的手牌”消息。这样既保证了玩家能看到牌局进度又不会提前泄露别人的牌。4. 手游客户端体验与性能优化4.1 牌桌场景的性能瓶颈在哪很多开发者以为麻将客户端很轻量性能优化可有可无。实际上一旦进入真实对战问题会集中爆发出牌和碰杠的动画频繁触发粒子特效叠加再加上聊天表情、语音消息中低端手机上帧率经常会掉到 20 帧以下。我定位性能瓶颈时最先剪辑的是 CPU 开销占比最大的三个模块牌桌节点的创建与销毁、UI 重建、动画播放。麻将一局会有超过 100 次摸打如果每次摸牌都新建一个牌的 GameObject局末垃圾回收的压力会非常大。实测数据是一局经典麻将打完因为频繁 Instantiate 和 DestroyPSS 内存峰值比开局时高了将近 80MB。4.2 落地优化清单我最终的优化方案按优先级排序是这样的第一所有牌对象用对象池管理。开局时预创建一个包含 144 张牌对象的池子摸牌、出牌、弃牌都只是切换对象的显隐和位置不再反复创建销毁。这一步做完垃圾回收频率直接下降了约 60%。第二打牌区域的 UI 尽量合批。手牌区的每张牌如果都用单独的 Image 组件DrawCall 会随牌数线性增长。我改成把同花色手牌放到同一个 Canvas 下使用 Unity 的 Sprite Atlas 打包牌面贴图再把相邻的牌尽量合并到同一个图集实测 DrawCall 能从 40 多降到 15 左右。第三低端机动效降级。我写了一个画质分级逻辑根据设备内存和 GPU 跑分自动决定是否播放出牌拖尾、是否开启碰杠的全屏闪光。中端机只保留基础位移动画高端机才有完整特效。这个逻辑一定要做成自动判断不要依赖玩家手动设置因为大部分玩家根本不知道去哪关特效。4.3 网络层的轻量化处理网络层的优化更多是“减少没必要发的数据”。我早期实现是每次摸牌、出牌都单独发一条消息广播给房间内所有玩家。后来发现玩家从摸牌到出牌的间隔往往不到一秒两条消息完全可以合并成一条“摸牌并出牌”的复合消息这样不仅省了网络包还减少了客户端状态更新的次数。心跳策略我也调整过。最初固定每 5 秒发一次心跳后来改成自适应根据最近一次消息的时间间隔动态调整心跳频率活跃状态心跳加密静止状态心跳放慢到 10 秒一次。这样可以显著降低服务端的无效消息压力尤其是在房间列表页挂机的大量空闲连接。另外有一个很关键的优化消息只广播给房间内的 4 个玩家不要通过全局推送通道转发。有些新手做棋牌服务端时为了省事把房间消息直接塞进全局消息队列结果规模一大无关连接全被波及。棋牌游戏的房间是天然的隔离单元消息路由先按房间维度过滤一次再按玩家维度精确投递这个设计从一开始就要坚持。5. 常见问题与避坑实录5.1 高频问题速查表我在开发和联调过程中把遇到的高频问题整理成了一张表。这张表在接外包时也经常直接发给客户看能过滤掉大概一半的无效问题反馈。问题现象根因排查思路解决方案胡牌误判某些牌型能胡但提示不能胡特殊牌型七对、十三幺未单独处理在胡牌判定前打印规则链的过滤结果增加特殊牌型独立判断分支两个玩家同时碰同一张牌服务端副露判定缺少互斥锁检查 meldLock 逻辑是否覆盖所有入口内存中加状态互斥失败请求返回错误码断线重连后手牌和桌面不一致快照协议缺少弃牌记录对比服务端日志和客户端重建现场快照中补充完整弃牌时间线和副露记录低端机打到最后严重卡顿牌对象反复创建销毁导致 GC 压力大用 profiler 查看 GC Alloc 和堆内存全部改为对象池管理玩家出牌后下家摸牌有延迟出牌消息和摸牌消息分开发送抓包看两条消息的到达时间合并成一条复合消息广播AI 托管乱打拆对子、打将牌出牌评分逻辑缺少牌型结构权重打印 AI 的评分明细提高对子和搭子的权重分5.2 几个值得多说两句的坑第一个坑是“胡牌算法的回溯深度”。很多网上的源码实现为了追求性能会把递归写成迭代用栈模拟回溯但状态恢复的时候少恢复了一个分支导致某些牌型判断错误。我的建议是先用最直白的递归把正确性跑通再用真实对局数据做大规模验证最后才去优化性能。棋牌服务端一局牌的计算量极小递归回溯的性能损耗完全可以忽略不要为了“优雅”牺牲正确性。第二个坑是“牌局录像与回放”。很多需求方会要求“打完可以看回放”但回放不是简单地录屏而是要把每一局的完整操作序列保存下来。我实现的方案是服务端在每局开始时记录一个初始快照之后每收到一条操作指令就追加一条带顺序号的操作记录。回放其实就是重放这些指令客户端按顺序无条件执行不再经过服务端校验。这里要注意操作记录必须包含完整的操作者、操作类型、目标牌、时间戳缺一个字段都可能在重放时出现牌桌错乱。第三个坑是关于“快速点击导致的操作重复提交”。玩家手速快时双击出牌、连续点碰按钮会让服务端收到重复操作请求。如果不加幂等处理就会出现“同一张牌被碰两次”的严重错误。我在服务端统一加了一个操作序号机制客户端每次操作带一个自增序列号服务端记录每个玩家最近处理过的序号只处理比当前序号大的请求。这个机制花不了多少代码但能避免一大类问题。第四个坑出在“配置表热更新”上。如果服务端要调整玩法参数比如封顶倍数从 8 倍改成 16 倍不能让玩家重新下载整个客户端。我把所有玩法配置都放到服务端的配置表里客户端只负责展示配置结果。这样线上调参数只需要更新数据库或配置文件的版本号客户端拉取新配置即可。这个设计在做地方棋牌时尤其重要因为不同地区的规则差异太大了频繁发版根本跟不上需求。最后再说一个容易被忽视的点日志系统一定要从第一天就做好。棋牌游戏出问题时最痛苦的不是 bug 本身而是没有日志能还原当时发生了什么。我的服务端会为每一局牌记录完整的牌谱日志——从洗牌时的牌墙顺序、每个玩家的手牌、每次摸打到每一步的判定结果全部落到文件。这样一旦出现对局争议直接翻牌谱日志就能定位问题。推荐日志格式用 JSON 分行动态写入方便用脚本做离线统计分析。这套源码项目从原型到能跑真实对战前后花了大概三个月时间。我个人的核心体会是棋牌游戏的技术难度不在某个单独的算法而在于把客户端表现、服务端规则、网络同步、异常恢复这些环节串成一个一致的整体。胡牌算法有十分钟能跑通的版本但让它能和碰杠副露、超时托管、断线重连这些状态完美协作才是真正考验源码功底的地方。如果你也在做类似的项目建议先从胡牌算法和状态机入手把这两个硬骨头啃下来后面的一切都会顺利很多。本文还有配套的精品资源点击获取