QT斗地主游戏(游戏大厅及房间系统)

基于QT的斗地主游戏大厅与房间系统设计与实现

一、项目概述

本系统是基于QT框架开发的网络斗地主游戏核心模块,采用C/S架构实现多玩家在线游戏大厅功能。主要技术栈包括:

  • QT 5.15 GUI框架
  • QJson数据序列化
  • 自定义TCP协议通信
  • MVC分层架构设计
  • 观察者模式(信号槽机制)

二、核心模块分析

1. 游戏大厅管理模块(GameHub)

cpp

Copy

class GameHub : public QObject {
    Q_OBJECT
public:
    QVector<LoungeData> getLounges();
    int enterLounge(int id, int loungeID);
    int quickStart(int id);
    // ...其他方法
signals:
    void playerJoin(int id, const PublicData &publicData);
    void playerReady(int id, int playerID, bool isReady);
    // ...其他信号
};

功能特性:

  • 房间信息聚合管理(m_lounges容器)
  • 支持三种加入方式:指定ID/快速匹配/新建房间
  • 玩家状态变更通知机制
  • 房间容量限制(最大3人)

2. 房间管理模块(Lounge)

cpp

Copy

class Lounge {
public:
    bool enterLounge(const PublicData &publicData);
    void readyGame(int id, bool isReady);
    bool isReadyGame();
    // ...其他方法
private:
    QVector<PlayerData> m_players;
    int m_loungeID;
};

关键功能:

  • 玩家准入控制(containsID检查)
  • 准备状态同步(isReadyGame判断)
  • 玩家数据管理(QVector存储)
  • 游戏启动条件检测(3人全部准备)

3. 玩家数据模型

https://via.placeholder.com/600x400?text=PlayerData+Class+Diagram

cpp

Copy

class PlayerData {
public:
    PlayerData(const PublicData &publicData);
    bool operator==(const PlayerData &playerData) const;
    // ...属性访问方法
private:
    int m_id;
    QString m_nickname;
    QPixmap m_pfp;
    bool m_isReady;
};

数据特征:

  • 复合数据结构(基础信息+准备状态)
  • 深拷贝相等判断(QPixmap像素级比较)
  • 头像数据序列化支持(通过PixtoStr工具类)

三、关键功能实现

1. 快速匹配算法

cpp

Copy

int GameHub::quickStart(int id) {
    // 优先查找2人房间
    for(int num = 2; num >= 1; num--){
        for(auto it = m_lounges.begin(); it != m_lounges.end(); it++){
            if(it.value().playerNum() == num){
                return enterLounge(id, it.key());
            }
        }
    }
    // 递归创建新房间
    return newLounge(id);
}

算法特点:

  • 降序搜索策略(2人->1人->新建)
  • 递归容错机制
  • 时间复杂度O(n)

2. 玩家准备状态同步

sequence

Copy

客户端A 服务器 客户端B 客户端C UI readyGame(true) playerReady(A,true) playerReady(A,true) 更新准备状态 更新准备状态 客户端A 服务器 客户端B 客户端C UI

实现要点:

  • 双向状态同步(客户端→服务器→其他客户端)
  • QHash快速定位玩家(O(1)时间复杂度)
  • 状态变更时触发游戏启动检测

3. 断线处理机制

cpp

Copy

void GameHub_Server::rcv_disConnected(int id) {
    m_gameHub->leaveLounge(id); // 自动清理玩家数据
}

容错策略:

  • TCP连接状态监听
  • 玩家数据自动回收
  • 房间人数动态更新

四、UI设计与交互

1. 游戏大厅界面

https://via.placeholder.com/600x300?text=GameHub+UI
组件构成:

  • 房间列表展示区(QScrollArea)
  • 快速开始按钮
  • 房间号输入框
  • 玩家信息面板

2. 房间界面

xml

Copy


<widget class="QWidget" name="Widget_PlayerList">
    <layout class="QVBoxLayout">
        <item><widget class="Widget_PlayerInfo"/>item>
        
    layout>
widget>

交互特性:

  • 玩家列表动态渲染
  • 准备/取消准备状态切换
  • 离开房间的二次确认

五、通信协议设计

1. JSON消息格式

json

Copy

{
    "title": "gameHub",
    "name": "enterLounge",
    "body": [{
        "loungeID": 1024,
        "isReady": true
    }]
}

协议规范:

  • 消息类型三级结构(title/name/body)
  • 二进制数据Base64编码(头像传输)
  • 状态码标准化(1=成功,2=满员等)

2. 网络层封装

cpp

Copy

class GameHub_Socket : public

关键代码

gamehub_socket.cpp

#include "gamehub_socket.h"

#include "src/common/pixtostr.h"

#include <QJsonArray>

GameHub_Socket::GameHub_Socket(Socket *socket)
{
    m_socket = socket;
    this->setParent(m_socket->parent());
    connect(m_socket, &Socket::rcv_gameHubJSON, this, &GameHub_Socket::getGameHubJSON);
}

void GameHub_Socket::send_getLounges() const
{
    // Message body
    QJsonArray bodyArr;
    QJsonObject bodyObj;
    bodyArr.append(bodyObj);

    // Message title
    QJsonObject object;
    object["title"] = "gameHub";
    object["name"] = "getLounges";
    object["body"] = bodyArr;

    // Send message
    m_socket->sendJSON(object);
}

void GameHub_Socket::send_getPublicData() const
{
    // Message body
    QJsonArray bodyArr;
    QJsonObject bodyObj;
    bodyArr.append(bodyObj);

    // Message title
    QJsonObject object;
    object["title"] = "gameHub";
    object["name"] = "getPublicData";
    object["body"] = bodyArr;

    // Send message
    m_socket->sendJSON(object);
}

void GameHub_Socket::send_enterLounge(int loungeID) const
{
    // Message body
    QJsonArray bodyArr;
    QJsonObject loungeObj;
    loungeObj["loungeID"] = loungeID;
    bodyArr.append(loungeObj);

    // Message title
    QJsonObject object;
    object["title"] = "gameHub";
    object["name"] = "enterLounge";
    object["body"] = bodyArr;

    // Send message
    m_socket->sendJSON(object);
}

void GameHub_Socket::send_quickStart() const
{
    // Message body
    QJsonArray bodyArr;
    QJsonObject bodyObj;
    bodyArr.append(bodyObj);

    // Message title
    QJsonObject object;
    object["title"] = "gameHub";
    object["name"] = "quickStart";
    object["body"] = bodyArr;

    // Send message
    m_socket->sendJSON(object);
}

void GameHub_Socket::send_newLounge() const
{
    // Message body
    QJsonArray bodyArr;
    QJsonObject bodyObj;
    bodyArr.append(bodyObj);

    // Message title
    QJsonObject object;
    object["title"] = "gameHub";
    object["name"] = "newLounge";
    object["body"] = bodyArr;

    // Send message
    m_socket->sendJSON(object);
}

void GameHub_Socket::send_getPlayers() const
{
    // Message body
    QJsonArray bodyArr;
    QJsonObject bodyObj;
    bodyArr.append(bodyObj);

    // Message title
    QJsonObject object;
    object["title"] = "gameHub";
    object["name"] = "getPlayers";
    object["body"] = bodyArr;

    // Send message
    m_socket->sendJSON(object);
}

void GameHub_Socket::send_readyGame(int loungeID, bool isReady) const
{
    // Message body
    QJsonArray bodyArr;
    QJsonObject loungeObj;
    loungeObj["loungeID"] = loungeID;
    loungeObj["isReady"] = isReady;
    bodyArr.append(loungeObj);

    // Message title
    QJsonObject object;
    object["title"] = "gameHub";
    object["name"] = "readyGame";
    object["body"] = bodyArr;

    // Send message
    m_socket->sendJSON(object);
}

void GameHub_Socket::send_leaveLounge(int loungeID) const
{
    // Message body
    QJsonArray bodyArr;
    QJsonObject loungeObj;
    loungeObj["loungeID"] = loungeID;
    bodyArr.append(loungeObj);

    // Message title
    QJsonObject object;
    object["title"] = "gameHub";
    object["name"] = "leaveLounge";
    object["body"] = bodyArr;

    // Send message
    m_socket->sendJSON(object);
}

void GameHub_Socket::getGameHubJSON(const QJsonObject &object)
{
    // Main
    QString name = object["name"].toString();
    if(name == "getLounges"){
        this->proc_getLounges(object);
    }else if(name == "getPublicData"){
        this->proc_getPublicData(object);
    }else if(name == "enterLoungeOK"){
        this->proc_enterLoungeOK(object);
    }else if(name == "enterLoungeFailed"){
        this->proc_enterLoungeFailed(object);
    }else if(name == "getPlayers"){
        this->proc_getPlayers(object);
    }else if(name == "playerJoin"){
        this->proc_playerJoin(object);
    }else if(name == "playerLeave"){
        this->proc_playerLeave(object);
    }else if(name == "playerReady"){
        this->proc_playerReady(object);
    }else if(name == "gameStart"){
        this->proc_gameStart(object);
    }
}

void GameHub_Socket::proc_getLounges(const QJsonObject &object)
{
    // Get bodyArr
    QJsonArray bodyArr = object["body"].toArray();

    // Get data
    QVector<LoungeData> lounges;
    for(int i=0; i<bodyArr.size(); i++){
        QJsonObject loungeObj = bodyArr.at(i).toObject();
        LoungeData loungeData;
        loungeData.setLoungeID(loungeObj["loungeID"].toInt());
        loungeData.setPlayerNum(loungeObj["playerNum"].toInt());
        lounges.push_back(loungeData);
    }

    // Send signal with data
    emit rcv_getLounges(lounges);
}

void GameHub_Socket::proc_getPublicData(const QJsonObject &object)
{
    // Get bodyArr
    QJsonArray bodyArr = object["body"].toArray();

    // Get data
    QJsonObject userObj = bodyArr.first().toObject();
    PublicData publicData;
    publicData.setId(userObj["id"].toInt());
    publicData.setNickname(userObj["nickname"].toString());
    publicData.setPfp(PixToStr::strToPix(userObj["pfp"].toString()));

    // Send signal with data
    emit rcv_getPublicData(publicData);
}

void GameHub_Socket::proc_enterLoungeOK(const QJsonObject &object)
{
    // Get bodyArr
    QJsonArray bodyArr = object["body"].toArray();

    // Get data
    QJsonObject bodyObj = bodyArr.first().toObject();
    int loungeID = bodyObj["loungeID"].toInt();

    // Send signal with data
    emit rcv_enterLoungeOK(loungeID);
}

void GameHub_Socket::proc_enterLoungeFailed(const QJsonObject &object)
{
    // Get bodyArr
    QJsonArray bodyArr = object["body"].toArray();
    
    // Get data
    QJsonObject bodyObj = bodyArr.first().toObject();
    int status = bodyObj["status"].toInt();

    // Send signal with data
    emit rcv_enterLoungeFailed(status);
}

void GameHub_Socket::proc_getPlayers(const QJsonObject &object)
{
    // Get bodyArr
    QJsonArray bodyArr = object["body"].toArray();

    // Get data
    QVector<PlayerData> players;
    for(int i=0; i<bodyArr.size(); i++){
        QJsonObject loungeObj = bodyArr.at(i).toObject();
        PlayerData playerData;
        playerData.setId(loungeObj["id"].toInt());
        playerData.setNickname(loungeObj["nickname"].toString());
        playerData.setPfp(PixToStr::strToPix(loungeObj["pfp"].toString()));
        playerData.setIsReady(loungeObj["isReady"].toBool());
        players.push_back(playerData);
    }

    // Send signal with data
    emit rcv_getPlayers(players);
}

void GameHub_Socket::proc_playerJoin(const QJsonObject &object)
{
    // Get bodyArr
    QJsonArray bodyArr = object["body"].toArray();

    // Get data
    QJsonObject playerObj = bodyArr.first().toObject();
    PublicData publicData;
    publicData.setId(playerObj["id"].toInt());
    publicData.setNickname(playerObj["nickname"].toString());
    publicData.setPfp(PixToStr::strToPix(playerObj["pfp"].toString()));

    // Send signal with data
    emit rcv_playerJoin(publicData);
}

void GameHub_Socket::proc_playerLeave(const QJsonObject &object)
{
    // Get bodyArr
    QJsonArray bodyArr = object["body"].toArray();

    // Get data
    QJsonObject playerObj = bodyArr.first().toObject();
    int id = playerObj["id"].toInt();

    // Send signal with data
    emit rcv_playerLeave(id);
}

void GameHub_Socket::proc_playerReady(const QJsonObject &object)
{
    // Get bodyArr
    QJsonArray bodyArr = object["body"].toArray();

    // Get data
    QJsonObject playerObj = bodyArr.first().toObject();
    int id = playerObj["id"].toInt();
    bool isReady = playerObj["isReady"].toBool();

    // Send signal with data
    emit rcv_playerReady(id,isReady);
}

void GameHub_Socket::proc_gameStart(const QJsonObject &object)
{
    // Get bodyArr
    QJsonArray bodyArr = object["body"].toArray();

    // Get data
    QJsonObject bodyObj = bodyArr.first().toObject();
    int gameID = bodyObj["gameID"].toInt();

    // Send signal with data
    emit rcv_gameStart(gameID);
}

gamehub.cpp

// 包含 GameHub 类的头文件,用于引入该类的声明
#include "gamehub.h"

// 包含公共文件头文件,用于获取公共数据
#include "src/common/public_file.h"

// GameHub 类的构造函数,接收一个 QObject 指针作为父对象
GameHub::GameHub(QObject *parent)
    : QObject{parent}
{
    // 构造函数体为空,可能用于初始化一些成员变量,这里没有具体操作
}

// 获取所有房间的信息
// 返回值:包含所有房间信息的向量,每个元素是一个 LoungeData 对象
QVector<LoungeData> GameHub::getLounges()
{
    // 用于存储所有房间信息的向量
    QVector<LoungeData> lounges;
    // 遍历 m_lounges 中的每个房间
    for(auto it = m_lounges.begin(); it != m_lounges.end(); it++){
        // 创建一个新的 LoungeData 对象
        LoungeData loungeData;
        // 设置房间的 ID
        loungeData.setLoungeID(it.value().loungeID());
        // 设置房间的玩家数量
        loungeData.setPlayerNum(it.value().playerNum());
        // 将该房间信息添加到向量中
        lounges.push_back(loungeData);
    }
    // 返回包含所有房间信息的向量
    return lounges;
}

// 根据玩家 ID 获取公共数据
// 参数 id:玩家的 ID
// 返回值:该玩家的公共数据
PublicData GameHub::getPublicData(int id)
{
    // 调用 Public_File 类的静态方法获取玩家的公共数据
    return Public_File::publicData(id);
}

// 玩家尝试加入指定房间
// 参数 id:玩家的 ID
// 参数 loungeID:要加入的房间 ID
// 返回值:加入结果的状态码,1 表示成功,2 表示房间满员,3 表示房间不存在
int GameHub::enterLounge(int id, int loungeID)
{
    // 检查房间是否存在,如果不存在则返回 3
    if(m_lounges.contains(loungeID) == false)
        return 3;

    // 获取指定房间的引用
    Lounge &lounge = m_lounges[loungeID];
    // 检查房间是否已满,如果已满则返回 2
    if(lounge.playerNum() >= 3)
        return 2;

    // 获取玩家的公共数据
    PublicData publicData = this->getPublicData(id);
    // 玩家加入房间
    lounge.enterLounge(publicData);

    // 获取房间内所有玩家的 ID
    QVector<int> IDs = lounge.playerIDs();
    // 向房间内所有玩家发送有新玩家加入的信息
    for(int &playerID: IDs)
        emit playerJoin(playerID,publicData);

    // 加入成功,返回 1
    return 1;
}

// 玩家快速开始游戏,自动匹配房间
// 参数 id:玩家的 ID
// 返回值:匹配到的房间 ID 或新创建房间的 ID,-1 表示出错
int GameHub::quickStart(int id)
{
    /* 查找人数为 2 的房间,若不存在,则查找人数为 1 的房间
       若仍不存在,则创建房间 */
    int loungeID;
    bool exists = false;
    // 先查找人数为 2 的房间,再查找人数为 1 的房间
    for(int num = 2; num >= 1 and exists == false; num--){
        for(auto it = m_lounges.begin(); it != m_lounges.end(); it++){
            if(it.value().playerNum() == num){
                // 找到合适的房间,记录房间 ID
                loungeID = it.value().loungeID();
                exists = true;
                break;
            }
        }
    }

    // 若房间存在,尝试加入房间。若不存在,则创建房间
    if(exists == true){
        int status = this->enterLounge(id,loungeID);
        if(status == 1)
            // 加入成功,返回房间 ID
            return loungeID;
        else
            // 加入失败,递归调用 quickStart 继续尝试
            return this->quickStart(id);
    }else
        // 没有合适的房间,创建新房间
        return this->newLounge(id);
    // 出错,返回 -1
    return -1;
}

// 创建一个新的房间
// 参数 id:创建房间的玩家 ID
// 返回值:新创建房间的 ID,-1 表示出错
int GameHub::newLounge(int id)
{
    // 获取一个不存在的房间号
    for(int loungeID=1; loungeID<INT_MAX; loungeID++){
        if(m_lounges.contains(loungeID))
            // 房间号已存在,继续查找
            continue;

        // 创建一个新的房间
        Lounge lounge(loungeID);
        // 让创建房间的玩家加入该房间
        lounge.enterLounge(this->getPublicData(id));
        // 将新房间添加到 m_lounges 中
        m_lounges.insert(loungeID,lounge);
        // 返回新房间的 ID
        return loungeID;
    }

    // 出错,返回 -1
    return -1;
}

// 获取指定房间的玩家列表
// 参数 id:玩家的 ID
// 参数 loungeID:房间的 ID,默认为 -1
// 返回值:该房间的玩家列表
QVector<PlayerData> GameHub::getPlayers(int id, int loungeID)
{
    // 检查指定房间是否存在且玩家在该房间内
    if(loungeID != -1 and m_lounges[loungeID].containsID(id) == true)
        // 存在则返回该房间的玩家列表
        return m_lounges[loungeID].players();

    // 遍历所有房间,查找包含该玩家的房间
    for(auto it = m_lounges.begin(); it != m_lounges.end(); it++)
        if(it.value().containsID(id) == true)
            // 找到后返回该房间的玩家列表
            return it.value().players();

    // 未找到,返回空的玩家列表
    return QVector<PlayerData>();
}

// 玩家准备游戏
// 参数 id:玩家的 ID
// 参数 isReady:玩家是否准备好
// 参数 loungeID:房间的 ID,默认为 -1
void GameHub::readyGame(int id, bool isReady, int loungeID)
{
    // 获取房间号
    int newLoungeID = loungeID;
    if(loungeID == -1 or m_lounges.contains(loungeID) == false){
        // 若房间号无效或房间不存在,遍历所有房间查找该玩家所在的房间
        for(auto it = m_lounges.begin(); it != m_lounges.end(); it++)
            if(it.value().containsID(id) == true){
                // 找到后记录房间号
                newLoungeID = it.key();
            }
        if(newLoungeID == -1)
            // 未找到该玩家所在的房间,直接返回
            return;
    }

    // 获取该房间的引用
    Lounge &lounge = m_lounges[newLoungeID];
    // 设置该玩家的准备状态
    lounge.readyGame(id,isReady);

    // 获取房间状态,判断是否所有玩家都已准备好
    bool start = lounge.isReadyGame();
    if(start == true)
        // 若准备就绪,则发出 signal_gameStart 信号,携带所有玩家的 ID
        emit signal_gameStart(lounge.playerIDs());
    else{
        // 若有玩家未准备好,则向房间内所有玩家发送有玩家准备状态改变的信息
        QVector<int> playerIDs = lounge.playerIDs();
        for(int &playerId: playerIDs)
            emit playerReady(playerId,id,isReady);
    }
}

// 玩家离开房间
// 参数 id:玩家的 ID
// 参数 loungeID:房间的 ID,默认为 -1
void GameHub::leaveLounge(int id, int loungeID)
{
    // 获取房间号
    int newLoungeID = loungeID;
    if(loungeID == -1 or m_lounges.contains(loungeID) == false){
        // 若房间号无效或房间不存在,遍历所有房间查找该玩家所在的房间
        for(auto it = m_lounges.begin(); it != m_lounges.end(); it++)
            if(it.value().containsID(id) == true){
                // 找到后记录房间号
                newLoungeID = it.key();
            }
        if(newLoungeID == -1)
            // 未找到该玩家所在的房间,直接返回
            return;
    }

    // 获取该房间的引用
    Lounge &lounge = m_lounges[newLoungeID];
    // 玩家离开房间
    lounge.leaveLounge(id);

    // 若房间人数不为零,向房间内所有玩家发送有玩家离开的信息
    if(lounge.playerNum() != 0){
        QVector<int> playerIDs = lounge.playerIDs();
        for(int &playerID: playerIDs)
            emit playerLeave(playerID,id);
        // 若房间为空,删除该房间
    }else
        m_lounges.remove(newLoungeID);
}

你可能感兴趣的:(QT学习笔记,qt,游戏,数据库)