最近不少开发者朋友在后台问:有没有一份真正能跟着敲的房卡棋牌开发教程?别光讲理论,代码多贴一点,最好能把整个流程从头到尾跑通。
橙色平台这几年一直在做棋牌游戏开发相关的技术输出和源码支持,房卡类产品是我们接触最多的需求之一。今天这篇,我就把团队内部沉淀下来的一套斗地主开发流程完整放出来,从环境搭建到联调上线,每一步都配上核心代码。文章比较长,建议先收藏,跟着节奏慢慢来。

一、开发环境与项目初始化
这套教程基于 Cocos Creator 3.8 和 Node.js 18 + TypeScript,数据库选用 MySQL 8.0。橙色平台为方便开发者快速上手,已经把项目的骨架仓库放到了我们的Git仓库里,文末会说明获取方式。如果你想从零开始搭建,跟着下面的步骤走就行。
先在终端里建好项目目录:
mkdir orange-fangka-ddz cd orange-fangka-ddz mkdir server client
服务端初始化:
cd server npm init -y npm install express socket.io mysql2 typescript ts-node @types/node @types/express npx tsc --init
tsconfig.json 核心配置:
{ "compilerOptions": { "target": "ES2020", "module": "commonjs", "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true } }
客户端直接打开 Cocos Creator,新建一个 TypeScript 项目,命名为 ddz-client。橙色平台建议开发者统一使用 3.8.x 版本,避免因版本差异导致API不兼容。
二、数据库表结构设计
房卡模式的核心是房间与玩家资产,我们建两张基础表即可覆盖Demo需求。后续橙色平台会提供包含战绩、代理分销等模块的完整版SQL,现在先把地基打好。
CREATE DATABASE orange_fangka; USE orange_fangka; CREATE TABLE rooms ( id INT AUTO_INCREMENT PRIMARY KEY, room_code VARCHAR(6) NOT NULL UNIQUE COMMENT '6位房号', creator_id INT NOT NULL, base_score INT DEFAULT 1, player_count TINYINT DEFAULT 3, card_cost INT DEFAULT 1 COMMENT '消耗房卡数', status TINYINT DEFAULT 0 COMMENT '0等待 1游戏中 2已结束', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE players ( id INT AUTO_INCREMENT PRIMARY KEY, openid VARCHAR(64) NOT NULL, nickname VARCHAR(32), avatar_url VARCHAR(255), card_balance INT DEFAULT 0 COMMENT '房卡余额', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );
三、服务端核心实现:房间调度与实时通信
服务端采用 Socket.IO 来维持客户端长连接,房间管理和游戏流程全部封装在 RoomManager 类中。这是整个房卡棋牌的“大脑”。
server/src/index.ts 入口骨架:
import express from 'express'; import { createServer } from 'http'; import { Server } from 'socket.io'; import { RoomManager } from './RoomManager'; const app = express(); const httpServer = createServer(app); const io = new Server(httpServer, { cors: { origin: '*' } }); const roomManager = new RoomManager(); io.on('connection', (socket) => { console.log(`[橙色平台] 新客户端连接: ${socket.id}`); socket.on('create_room', (data, callback) => { const room = roomManager.createRoom(data.playerId, data.baseScore); callback({ roomCode: room.roomCode }); }); socket.on('join_room', (data, callback) => { const result = roomManager.joinRoom(data.roomCode, data.playerId, socket); if (result.success) socket.join(data.roomCode); callback(result); }); // 出牌、叫地主等事件稍后注册 }); httpServer.listen(3000, () => { console.log('[橙色平台] 房卡服务端已启动,端口 3000'); });
RoomManager 负责房间状态维护、发牌、轮转等逻辑:
// server/src/RoomManager.ts import { Socket } from 'socket.io'; interface Player { id: number; socketId: string; handCards: number[]; isLandlord: boolean; } interface Room { roomCode: string; players: Map<string, Player>; status: 'waiting' | 'playing'; baseScore: number; bottomCards: number[]; currentTurn: string; lastPlay: { cards: number[]; playerId: string } | null; } export class RoomManager { private rooms: Map<string, Room> = new Map(); createRoom(playerId: number, baseScore = 1) { const roomCode = Math.random().toString(36).substring(2, 8).toUpperCase(); this.rooms.set(roomCode, { roomCode, players: new Map(), status: 'waiting', baseScore, bottomCards: [], currentTurn: '', lastPlay: null }); // 实际项目需校验并扣除房卡,Demo阶段跳过 return { roomCode }; } joinRoom(roomCode: string, playerId: number, socket: Socket) { const room = this.rooms.get(roomCode); if (!room) return { success: false, msg: '房间不存在' }; if (room.players.size >= 3) return { success: false, msg: '房间已满' }; room.players.set(socket.id, { id: playerId, socketId: socket.id, handCards: [], isLandlord: false }); if (room.players.size === 3) this.startGame(room); return { success: true }; } startGame(room: Room) { room.status = 'playing'; const cards = this.shuffleCards(); const players = Array.from(room.players.values()); players.forEach((p, i) => { p.handCards = cards.splice(0, 17); }); room.bottomCards = cards; // 底牌 // 随机指定地主(实际需叫分流程,此处简化) const landlordIdx = Math.floor(Math.random() * 3); players[landlordIdx].isLandlord = true; players[landlordIdx].handCards.push(...room.bottomCards); room.currentTurn = players[landlordIdx].socketId; players.forEach(p => { this.getSocket(p.socketId)?.emit('game_start', { handCards: p.handCards, bottomCards: room.bottomCards, landlordId: players[landlordIdx].socketId }); }); } private shuffleCards(): number[] { const cards = Array.from({length: 54}, (_, i) => i + 1); for (let i = cards.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [cards[i], cards[j]] = [cards[j], cards[i]]; } return cards; } // 注意:此处需实际注入io对象来获取socket,Demo代码略 private getSocket(socketId: string): Socket | undefined { return undefined; } }
出牌事件的处理逻辑单独拆在 index.ts 中:
socket.on('play_cards', (data, callback) => { const { roomCode, cards } = data; const room = roomManager.getRoom(roomCode); if (!room || room.currentTurn !== socket.id) return callback({ success: false, msg: '非法操作' }); const player = room.players.get(socket.id)!; const valid = cards.every((c: number) => player.handCards.includes(c)); if (!valid) return callback({ success: false, msg: '手牌中无此牌' }); // 牌型比较(简化版,完整版在CardLogic.ts中) if (room.lastPlay && room.lastPlay.playerId !== socket.id) { const bigger = CardLogic.compare(cards, room.lastPlay.cards); if (!bigger) return callback({ success: false, msg: '打不过上家' }); } player.handCards = player.handCards.filter(c => !cards.includes(c)); room.lastPlay = { cards, playerId: socket.id }; io.to(roomCode).emit('cards_played', { playerId: socket.id, cards, remainCount: player.handCards.length }); if (player.handCards.length === 0) { io.to(roomCode).emit('game_over', { winnerId: socket.id }); room.status = 'finished'; return; } const playerIds = Array.from(room.players.keys()); room.currentTurn = playerIds[(playerIds.indexOf(socket.id) + 1) % 3]; callback({ success: true }); });
完整的 CardLogic.ts 包含牌型枚举和比大小算法,代码量较大,橙色平台已将其打包进配套源码包中,文末可下载。

四、客户端实现:Cocos Creator场景搭建与交互
客户端场景结构建议如下:
-
Canvas
-
BG (背景)
-
PlayerSeats (3个座位节点,显示头像昵称)
-
MyHandCards (手牌容器,通过代码动态生成)
-
PlayArea (桌面出牌展示区)
-
ActionButtons (出牌、不出、叫地主等按钮)
-
InfoBar (房号、底分、剩余牌数)
-
核心控制脚本 GameManager.ts:
// assets/scripts/GameManager.ts import { _decorator, Component, Node, Label, Button } from 'cc'; import { io, Socket } from 'socket.io-client'; const { ccclass, property } = _decorator; @ccclass('GameManager') export class GameManager extends Component { private socket!: Socket; private myHandCards: number[] = []; private selectedCards: Set<number> = new Set(); private roomCode = ''; @property(Node) handCardContainer: Node = null!; @property(Node) playArea: Node = null!; @property(Button) playBtn: Button = null!; @property(Button) passBtn: Button = null!; @property(Label) roomLabel: Label = null!; start() { this.socket = io('ws://localhost:3000'); this.initEvents(); this.playBtn.node.on('click', this.onPlay, this); this.passBtn.node.on('click', this.onPass, this); } initEvents() { this.socket.on('game_start', (data) => { this.myHandCards = data.handCards; this.renderMyCards(); }); this.socket.on('cards_played', (data) => { this.updatePlayArea(data.cards); if (data.playerId === this.socket.id) { this.selectedCards.clear(); this.renderMyCards(); } }); this.socket.on('game_over', (data) => { console.log(`[橙色平台] 本局赢家: ${data.winnerId}`); }); } renderMyCards() { this.handCardContainer.removeAllChildren(); this.myHandCards.forEach(cardId => { const cardNode = new Node(`card_${cardId}`); // 加载对应牌面图片,代码略 cardNode.on(Node.EventType.TOUCH_START, () => this.toggleSelect(cardId, cardNode)); this.handCardContainer.addChild(cardNode); }); } toggleSelect(cardId: number, node: Node) { if (this.selectedCards.has(cardId)) { this.selectedCards.delete(cardId); node.setPosition(node.position.x, node.position.y + 20); } else { this.selectedCards.add(cardId); node.setPosition(node.position.x, node.position.y - 20); } } onPlay() { const cards = Array.from(this.selectedCards); if (cards.length === 0) return; this.socket.emit('play_cards', { roomCode: this.roomCode, cards }, (res: any) => { if (!res.success) console.warn(res.msg); }); } onPass() { this.socket.emit('pass', { roomCode: this.roomCode }, (res: any) => { if (!res.success) console.warn(res.msg); }); } updatePlayArea(cards: number[]) { // 清空桌面,按牌值创建sprite展示 } }

五、联调测试与房卡扣费完善
启动服务端 npx ts-node src/index.ts,客户端在Cocos Creator中点击预览。你可以开两个浏览器窗口加一个模拟器,凑齐三人测试房间创建、加入、发牌和出牌完整流程。
正式上线前,橙色平台建议补全房卡扣费模块。在 createRoom 中添加:
// 伪代码示例 const player = await db.query('SELECT card_balance FROM players WHERE id = ?', [playerId]); if (player.card_balance < roomConfig.cost) return callback({ success: false, msg: '房卡不足' }); await db.query('UPDATE players SET card_balance = card_balance - ? WHERE id = ?', [roomConfig.cost, playerId]); await db.query('INSERT INTO card_consumption ...');
部署层面,推荐使用 PM2 管理 Node 进程,Nginx 代理 WebSocket,数据库做读写分离,高并发下 Redis 可替代内存做房间状态缓存。橙色平台的技术支持团队也可以为开发者提供部署架构咨询服务。

橙色平台房卡源码获取与技术支持
如果你正在做房卡棋牌类项目,无论是想二次开发还是解决技术卡点,都可以直接联系客服获取源码与一对一技术支持。
扫描下方二维码,添加 微信,备注“橙色平台房卡源码”,我们会第一时间把完整代码包和部署文档发给你,并邀请你一起探讨棋牌开发的那些事儿。
玫瑰资源库













![[源码分享] 创胜系列定制版本嘉年华房卡源代码【开发引擎Cocos Creator2.4.3】-玫瑰资源库](https://www.264rose.com/wp-content/uploads/2024/10/c4ca4238a0b9238-10.jpg)




