Hi,请  登录  或  注册

棋牌游戏开发软件教程:2026年手把手带你用Cocos Creator + Node.js 搭建一套房卡斗地主

最近不少开发者朋友在后台问:有没有一份真正能跟着敲的房卡棋牌开发教程?别光讲理论,代码多贴一点,最好能把整个流程从头到尾跑通。

橙色平台这几年一直在做棋牌游戏开发相关的技术输出和源码支持,房卡类产品是我们接触最多的需求之一。今天这篇,我就把团队内部沉淀下来的一套斗地主开发流程完整放出来,从环境搭建到联调上线,每一步都配上核心代码。文章比较长,建议先收藏,跟着节奏慢慢来。

棋牌游戏开发软件教程:2026年手把手带你用Cocos Creator + Node.js 搭建一套房卡斗地主

一、开发环境与项目初始化

这套教程基于 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 包含牌型枚举和比大小算法,代码量较大,橙色平台已将其打包进配套源码包中,文末可下载。

棋牌游戏开发软件教程:2026年手把手带你用Cocos Creator + Node.js 搭建一套房卡斗地主

四、客户端实现: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展示
  }
}

棋牌游戏开发软件教程:2026年手把手带你用Cocos Creator + Node.js 搭建一套房卡斗地主

五、联调测试与房卡扣费完善

启动服务端 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 可替代内存做房间状态缓存。橙色平台的技术支持团队也可以为开发者提供部署架构咨询服务。

棋牌游戏开发软件教程:2026年手把手带你用Cocos Creator + Node.js 搭建一套房卡斗地主

橙色平台房卡源码获取与技术支持

如果你正在做房卡棋牌类项目,无论是想二次开发还是解决技术卡点,都可以直接联系客服获取源码与一对一技术支持。

扫描下方二维码,添加 微信,备注“橙色平台房卡源码”,我们会第一时间把完整代码包和部署文档发给你,并邀请你一起探讨棋牌开发的那些事儿。

客服微信二维码
文章名称:棋牌游戏开发软件教程:2026年手把手带你用Cocos Creator + Node.js 搭建一套房卡斗地主
除非特别注明,本站所有文章均为原创,转载请注明出处:264玫瑰资源库
部分教程资源来源于互联网,请谨慎辨别广告内容,避免上当受骗!

评论 抢沙发

登录

找回密码

注册