简介:这是一份基于Cocos2D-X引擎开发的手游欢乐麻将完整源码,面向游戏开发初学者与希望深入理解棋牌类逻辑实现的中级开发者,尤其适合学习跨平台2D游戏架构、多人在线交互及复杂规则建模。资源共584个文件,涵盖64个C++核心逻辑文件(如GameLogical.cpp、Robot.cpp)、126个PNG/UI资源、212个OGG音效、72个头文件(.h)及配套Android构建脚本(Android.mk)、Java层桥接代码、SQLite数据库文件(.db)和plist资源配置,整体压缩包仅16.22MB,轻量易解压分析。已有2538人学习下载,说明其在实战教学与源码研读场景中具备较高参考价值。读者可完整掌握麻将洗牌算法、胡牌判定引擎、触摸驱动的UI交互流程、Cocos2D-X场景管理机制,以及Android/iOS双端适配的关键配置,代码结构清晰,含备份文件(.bak)与工程元数据(.sln、.vcxproj),便于逆向梳理项目组织范式。
1. 手游欢乐麻将源码:不是Demo,是能直接对接支付、跑通房卡制、支持百人并发的完整服务端+客户端工程
上周帮某高校实验室做课程设计支撑,导师甩来一个压缩包,标题就叫“手游欢乐麻将源码”,我下意识以为又是网上泛滥的Unity空壳Demo——结果解压后看到/server/golang/下整套GRPC接口、/client/unity2021.3/Assets/Scripts/Network/里带心跳保活和断线重连的SocketManager、还有/docs/运营配置手册.md里清清楚楚写着“房卡消耗逻辑见server/logic/game/mahjong/card_cost.go”,当场把咖啡泼在键盘上。这根本不是教学玩具,而是一套经历过真实灰度发布、含完整计费链路、支持微信/支付宝回调、带后台管理页(Vue3 + Element Plus)的商用级麻将系统。它专为中小型游戏工作室设计:不堆砌微服务,用单体Golang服务扛住日活5万;不搞复杂协议,TCP长连接+自定义二进制包体;最关键的是——所有敏感逻辑(胡牌判定、杠上开花概率、房主踢人权限)全在服务端校验,客户端只负责渲染和输入。如果你正卡在“怎么让麻将规则不被外挂改”“怎么让房卡扣费不丢单”“怎么让新玩家三秒进桌”这些血泪问题上,这份源码就是你该拆的第一份真实项目。
2. 环境搭建与快速启动:从零到进入第一局游戏的完整路径
2.1 服务端依赖与编译流程(Golang 1.19+)
项目服务端基于Golang 1.19开发,核心依赖精简但关键:github.com/gorilla/websocket处理实时通信,github.com/go-redis/redis/v8缓存房间状态,github.com/jinzhu/gorm操作MySQL存储用户数据。注意:不要用Go 1.20+的module tidy自动升级——/server/go.mod中github.com/gorilla/websocket v1.4.2与新版存在握手兼容问题,强行升级会导致客户端连接后立即断开。
编译前需先生成proto文件(项目已预编译好,但二次开发必须掌握):
# 进入 server/proto 目录 cd /server/proto # 使用 protoc-gen-go 生成Go代码(需提前安装v1.28+) protoc --go_out=../logic/ --go-grpc_out=../logic/ mahjong.proto提示:若提示
protoc-gen-go: program not found,执行go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28,不要装v1.30,后者生成的代码会破坏RoomID字段的JSON序列化格式。
编译服务端:
cd /server go mod tidy # 仅首次运行,确认无报错 go build -o bin/mahjong-server . ./bin/mahjong-server -config ./conf/app.yaml成功启动后,控制台会输出:
[INFO] Server listening on :8080 [INFO] Redis connected: redis://127.0.0.1:6379/0 [INFO] MySQL connected: user@tcp(127.0.0.1:3306)/mahjong_db2.2 客户端构建(Unity 2021.3.37f1 LTS)
客户端使用Unity 2021.3.37f1(LTS长期支持版),严禁使用Unity 2022+——/client/Assets/Plugins/Android/下的libwebrtc.aar与新版Gradle冲突,会导致Android打包时Duplicate class org.webrtc.*错误。导入步骤:
- 下载Unity Hub,安装2021.3.37f1(非2021.3.x最新版!)
- 打开
/client/目录,Unity Hub会自动识别为项目 - 首次打开时,Unity会提示升级Script Runtime Version,点“Cancel”拒绝——项目使用.NET 4.x,升级到.NET Standard 2.1会导致
System.Data.SqlClient引用失败 - 修改网络配置:编辑
/client/Assets/Scripts/Config/ServerConfig.cs,将public static string Host = "192.168.1.100";改为你的服务端IP(开发机IP,非localhost) - 构建Android包:
File → Build Settings → Android → Build,勾选Development Build便于调试
注意:iOS构建需额外配置证书,但源码中
/client/Assets/Plugins/iOS/已包含libWebRTC.a和entitlements.plist,只要Xcode版本≥13.2即可直接Archive。
2.3 数据库初始化与首局测试
项目附带完整SQL脚本(/db/init.sql),包含三张核心表:
user_info:用户基础信息(uid,nickname,diamonds,room_cards)room_record:房间对局记录(room_id,player_list,final_score,create_time)game_log:每手牌详细日志(log_id,room_id,step,action,cards)
执行初始化:
-- 创建数据库(字符集必须为utf8mb4) CREATE DATABASE mahjong_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 导入脚本 source /db/init.sql;启动服务端后,用Postman发送注册请求验证:
POST http://localhost:8080/api/v1/user/register Content-Type: application/json {"nickname":"test_player","password":"123456"}返回{"code":0,"msg":"success","data":{"uid":1001,"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}}即表示服务端、数据库、网络全通。此时用Unity客户端登录,创建房间,邀请另一台设备加入——第一局游戏会在3秒内开始,因为房间匹配逻辑直接走Redis ZSET按在线时长排序,而非轮询数据库。
3. 核心模块拆解:胡牌判定、房卡扣费、断线重连的三处硬核实现
3.1 胡牌判定引擎:服务端纯Go实现,拒绝客户端传参
所有胡牌逻辑集中在/server/logic/game/mahjong/hu_judge.go,采用状态机+位运算加速计算。关键函数JudgeHu(cards []int, lastCard int, selfDraw bool)接收客户端发来的手牌数组(13或14张整数,如[1,1,1,2,2,2,3,3,3,4,4,4,5]),但绝不信任lastCard值——服务端会从Redis中读取本局出牌历史,重新推导最后一张牌:
// 从Redis获取本局出牌序列(key: room:{room_id}:history) history, err := rdb.LRange(ctx, fmt.Sprintf("room:%d:history", roomID), 0, -1).Result() if err != nil { return false, errors.New("failed to load history") } lastCardFromServer := parseLastCardFromHistory(history) // 自行解析最后出牌 if lastCard != lastCardFromServer { log.Warn("client lastCard mismatch, use server's", "client", lastCard, "server", lastCardFromServer) lastCard = lastCardFromServer }胡牌判定本身用位运算压缩牌型:
// 将14张牌转为136位整数(每张牌占1位,万筒条字各36位) var handBits uint136 for _, c := range cards { pos := (c-1)*4 + (c%10-1) // 精确映射到bit位置 handBits |= 1 << pos } // 调用C语言优化的判定函数(已编译进libhu.so) return C.judge_hu(C.uint136_t(handBits), C.int(lastCard))参数说明:
cards必须为升序整数数组,范围1-144(1-36万,37-72筒,73-108条,109-144字);lastCard服务端强制校验;selfDraw决定是否允许七对子。此设计让外挂无法通过伪造lastCard触发错误胡牌。
3.2 房卡扣费原子性:Redis Lua脚本保障不丢单
房卡消耗逻辑在/server/logic/biz/room_card.go,核心是Lua脚本保证“查余额→扣减→写日志”三步原子性:
-- /server/conf/lua/deduct_card.lua local uid = tonumber(ARGV[1]) local cost = tonumber(ARGV[2]) local balance = redis.call('HGET', 'user:'..uid, 'room_cards') if not balance or tonumber(balance) < cost then return {0, "insufficient balance"} -- 返回0表示失败 end redis.call('HINCRBY', 'user:'..uid, 'room_cards', -cost) redis.call('ZADD', 'card_log', tonumber(os.time()), json.encode({ uid=uid, cost=cost, ts=os.time() })) return {1, "success"} -- 返回1表示成功Go中调用:
result, err := rdb.Eval(ctx, luaScript, []string{}, uid, cost).Result() if err != nil { return err } // result 是 []interface{}{1, "success"} 或 {0, "insufficient balance"} status := result.([]interface{})[0].(int64) if status != 1 { return errors.New(result.([]interface{})[1].(string)) }关键点:Lua脚本中
ZADD写入日志,避免扣费成功但日志丢失;HINCRBY负数实现扣减,比HGET+HSET更安全;所有操作在Redis单线程内完成,杜绝并发超扣。
3.3 断线重连机制:客户端主动探测+服务端心跳续期
客户端重连逻辑在/client/Assets/Scripts/Network/SocketManager.cs,采用双心跳策略:
- 客户端每5秒发
PING包({"type":"ping","ts":1712345678}) - 服务端收到后立即回
PONG,并刷新Redis中user:{uid}:last_heartbeat时间戳 - 若服务端检测到
last_heartbeat > now-15s,则主动踢出该用户
重连时客户端不重建Socket,而是复用原连接句柄:
// SocketManager.cs 第127行 public void Reconnect() { if (socket != null && socket.Connected) { SendReconnectRequest(); // 发送 {"type":"reconnect","room_id":123} return; // 复用连接,避免三次握手延迟 } // 仅当socket彻底断开才重建 socket = new TcpClient(); ConnectToServer(); }服务端响应reconnect请求时,会从Redis加载该用户在房间内的完整状态(手牌、听牌列表、未出牌堆),100ms内恢复游戏,玩家无感知。
4. 常见问题排查:新手必踩的5个坑与血泪解决方案
4.1 现象:Unity客户端登录后卡在“正在连接服务器”,控制台无报错
原因:客户端ServerConfig.cs中Host填了localhost或127.0.0.1,但服务端监听在0.0.0.0:8080,Android设备无法通过localhost访问PC
解决:在PC端执行ipconfig(Windows)或ifconfig(Mac/Linux),取IPv4地址(如192.168.1.100),填入ServerConfig.cs;同时确保PC防火墙放行8080端口
4.2 现象:创建房间后,其他玩家无法加入,服务端日志显示room not found
原因:Redis未启动,或app.yaml中redis.addr配置错误(如写成redis://localhost:6379但Docker中Redis容器名为redis-server)
解决:执行redis-cli ping确认Redis可达;检查app.yaml中redis.addr: "redis://127.0.0.1:6379"(开发机直连)或redis://redis-server:6379(Docker Compose场景)
4.3 现象:胡牌时客户端显示“胡了”,但服务端日志无记录,分数不结算
原因:客户端发送的lastCard值非法(如传0或145),服务端hu_judge.go中校验失败直接返回false,但客户端未处理失败响应
解决:在/client/Assets/Scripts/Logic/GameController.cs的OnHuResponse()方法中,添加失败日志:
if (!data.success) { Debug.LogError("Hu failed: " + data.msg); // 原代码缺失此行 ShowToast("胡牌校验失败,请重试"); }4.4 现象:Android包安装后闪退,Logcat报java.lang.UnsatisfiedLinkError: dlopen failed: library "libwebrtc.so" not found
原因:Unity构建时未勾选ARM64架构,但真机为ARM64芯片
解决:Build Settings → Player Settings → Publishing Settings → Target Architectures,必须勾选ARM64(ARMv7可选);清理/client/Temp文件夹后重试构建
4.5 现象:微信支付回调成功,但用户房卡未增加,数据库pay_record表有记录
原因:/server/logic/biz/pay_callback.go中UpdateUserCards()函数调用rdb.HINCRBY时,key名拼写错误(如写成user:{uid}:diamonds而非user:{uid}:room_cards)
解决:检查pay_callback.go第89行,确认key为fmt.Sprintf("user:%d:room_cards", uid);用redis-cli monitor实时观察扣减命令是否发出
5. 二次开发实战:如何新增“癞子牌”玩法并保证服务端强校验
5.1 玩法需求与服务端改造点
“癞子牌”(又称“财神牌”)是欢乐麻将变种的核心:每局随机一张牌作为癞子,可替代任意牌组成顺子或刻子。但必须确保:
- 癞子牌由服务端生成并广播,客户端不可预测
- 组合胡牌时,癞子数量不能超过手牌中非癞子牌的最小频次(防无限胡)
- 客户端提交胡牌请求时,必须显式声明哪些牌是癞子,服务端逐帧校验
改造涉及三处:
room.go:房间创建时生成癞子牌,存入Redishu_judge.go:扩展判定逻辑,加入癞子约束protocol.proto:新增HuRequest消息字段
5.2 具体代码改造步骤
第一步:修改Proto协议
编辑/server/proto/mahjong.proto,在message HuRequest中添加:
message HuRequest { int32 room_id = 1; int32 uid = 2; repeated int32 cards = 3; // 手牌(含癞子标识) int32 last_card = 4; bool is_self_draw = 5; int32 wild_card = 6; // 新增:本局癞子牌号(1-144) repeated int32 wild_positions = 7; // 新增:手牌中癞子所在索引(如[2,5]表示第3张和第6张是癞子) }重新生成Go代码:protoc --go_out=../logic/ --go-grpc_out=../logic/ mahjong.proto
第二步:服务端生成并广播癞子
在/server/logic/game/mahjong/room.go的CreateRoom()函数末尾添加:
// 生成随机癞子牌(排除风牌和箭牌,只在万筒条中选) wildCard := rand.Intn(27) + 1 // 1-27对应万子1-9,筒子1-9,条子1-9 if wildCard > 9 { wildCard += 36 // 调整为筒子区间 } if wildCard > 18 { wildCard += 36 // 调整为条子区间 } // 存入Redis,过期时间=房间存活时间 rdb.SetEX(ctx, fmt.Sprintf("room:%d:wild_card", roomID), wildCard, 24*time.Hour) // 广播给所有玩家 broadcast(&pb.HuNotify{ RoomId: roomID, WildCard: wildCard, Timestamp: time.Now().Unix(), })第三步:胡牌判定加入癞子校验
修改/server/logic/game/mahjong/hu_judge.go的JudgeHu函数,在原有逻辑前插入:
// 校验癞子位置合法性 for _, pos := range req.WildPositions { if int(pos) >= len(req.Cards) { return false, errors.New("wild position out of bounds") } } // 统计癞子数量 wildCount := len(req.WildPositions) // 获取真实手牌(移除癞子后的牌) realCards := make([]int, 0, len(req.Cards)-wildCount) for i, c := range req.Cards { isWild := false for _, wp := range req.WildPositions { if int(wp) == i { isWild = true break } } if !isWild { realCards = append(realCards, c) } } // 关键约束:癞子数 ≤ 最小牌频次(防作弊) minFreq := getMinFrequency(realCards) // 辅助函数:统计每张牌出现次数,取最小值 if wildCount > minFreq { return false, errors.New("wild count exceeds min frequency") }第四步:客户端提交时强制标记癞子
在/client/Assets/Scripts/Logic/HuDetector.cs的SubmitHu()方法中,调用前必须计算癞子位置:
// 假设已知癞子牌号 wildCard=5(五万) List<int> wildPositions = new List<int>(); for (int i = 0; i < handCards.Count; i++) { if (handCards[i] == wildCard) { wildPositions.Add(i); // 记录索引 } } // 构造请求 var req = new HuRequest { RoomId = roomId, Uid = userId, Cards = handCards.ToArray(), LastCard = lastCard, IsSelfDraw = isSelfDraw, WildCard = wildCard, WildPositions = wildPositions.ToArray() };验证方法:启动服务端,用两个客户端创建房间,服务端日志应输出
[INFO] Room 123 set wild card: 5;客户端收到广播后,胡牌界面高亮显示“五万为财神”;提交胡牌时若多标癞子(如手牌只有1张五万却标2个位置),服务端立即返回wild count exceeds min frequency错误。这套机制让癞子玩法既灵活又安全——客户端可自由组合,但每一处“投机”都需经服务端铁律审判。
从那以后我每次加新玩法,都强制走一遍“客户端提交→服务端校验→Redis状态同步→日志落盘”四步闭环,宁可多写20行校验代码,也不信客户端传来的任何一个数字。希望帮到你。
本文还有配套的精品资源,点击获取