由于存在玩家合伙作弊,严重影响比赛的公平性,因此对于棋牌游戏来说提供游戏录像回放功能是很重要的。
这里以斗地主为例分享下如果给斗地主添加游戏回放功能。
一、录像回放的本质
首先要理解的是回放功能并不是当时的游戏录屏,而是将当时的玩家信息、扑克信息与游戏过程中的出牌等数据保存到数据库中,需要回放的时候客户端程序在将这些数据模拟成正常游戏数据跑一遍,从而实现了游戏的回放功能。
二、数据结构
知道游戏录像的原理后一切都变得明朗,第一步将游戏过程抽象成数据对象
斗地主游戏场景比较简单,先给玩家发牌,之后一直是玩家出牌,直至出牌结束;相对与麻将来说少了“抓牌”、“吃”、“碰”、“杠”等操作。
基于这些逻辑数据结构可以这么设计
struct GameRecordPlayer { DWORD dwUserID; WORD wGander; std::string kAvatar; std::string kNickName; std::string kAddress; std::string kIp; std::vector<BYTE> cbCardData; void StreamValue(datastream& kData, bool bSend) { Stream_VALUE(dwUserID); Stream_VALUE(wGander); Stream_VALUE(kAvatar); Stream_VALUE(kNickName); Stream_VALUE(kAddress); Stream_VALUE(kIp); Stream_VALUE(cbCardData); } }; struct GameRecordOperateResult { enum Type { TYPE_NULL, //过 TYPE_OutCard //出牌 }; GameRecordOperateResult() { cbActionType = 0; wOperateUser = 0; cbCardCount = 0; ZeroMemory(cbCardData, sizeof(cbCardData)); } BYTE cbActionType; //操作类型 WORD wOperateUser; //操作用户 BYTE cbCardCount; //扑克数量 BYTE cbCardData[MAX_COUNT]; //扑克列表 void StreamValue(datastream& kData, bool bSend) { Stream_VALUE(cbActionType); Stream_VALUE(wOperateUser); Stream_VALUE(cbCardCount); Stream_VALUE_SIZE(cbCardData); } }; struct GameRecord { std::vector<GameRecordPlayer> kPlayers; std::vector<GameRecordOperateResult> kAction; void StreamValue(datastream& kData, bool bSend) { StructVecotrMember(GameRecordPlayer, kPlayers); StructVecotrMember(GameRecordOperateResult, kAction); } void CleanUp() { kPlayers.clear(); kAction.clear(); } };
分为三个结构体;
GameRecord为游戏录像完整记录,GameRecordPlayer为玩家信息,GameRecordOperateResult为出牌过程的记录。
GameRecordPlayer包括了用户的一些基本信息(昵称、头像、IP、地址),同时还包括游戏开始后的 扑克数据。
GameRecordOperateResult包括操作类型(要不起,出牌)、操作玩家、出牌的扑克数量与扑克数据。
三、记录玩家信息
简化回放流程,我们从确定地主后开始记录数据,回放叫地主并不是很关键部分;
在确定地主并分配底牌后执行下面代码记录玩家数据:
for (BYTE i = 0; i < GAME_PLAYER; i++){ GameRecordPlayer player; player.dwUserID = m_pITableFrame->GetTableUserItem(i)->GetUserID(); player.wGander = m_pITableFrame->GetTableUserItem(i)->GetGender(); player.kAvatar = m_pITableFrame->GetTableUserItem(i)->GetAvatar(); player.kNickName = m_pITableFrame->GetTableUserItem(i)->GetNickName(); player.kAddress = m_pITableFrame->GetTableUserItem(i)->GetAddress(); player.kIp = m_pITableFrame->GetTableUserItem(i)->GetClientIp(); for (BYTE k = 0; k < m_cbStartHandCardCount[i]; k++){ player.cbCardData.push_back(m_cbHandCardData[i][k]); //记录扑克 } m_kGameRecord.kPlayers.push_back(player); }
四、记录游戏流信息
1、在服务器往客户端发送出牌数据前加入记录代码
GameRecordOperateResult r; r.cbActionType = GameRecordOperateResult::TYPE_OutCard; r.wOperateUser = wChairID; r.cbCardCount = cbCardCount; CopyMemory(r.cbCardData, m_cbTurnCardData, m_cbTurnCardCount*sizeof(BYTE)); m_kGameRecord.kAction.push_back(r);
2、在服务器往客户端发送要不起命令前加入记录代码
GameRecordOperateResult r; r.cbActionType = GameRecordOperateResult::TYPE_NULL; r.wOperateUser = wChairID; r.cbCardCount = 0; ZeroMemory(r.cbCardData, sizeof(r.cbCardData)); m_kGameRecord.kAction.push_back(r);
五、序列化后保存到数据库
通过第三、第四步操作后,游戏流程已经被记录到GameRecord对象中,最后只要在游戏结束时候将数据保存到数据库中就行了。
datastream kDataStream; m_kGameRecord.StreamValue(kDataStream, true); m_pITableFrame->WriteTableScore(ScoreInfoArray, CountArray(ScoreInfoArray), kDataStream);
服务端基本流程就是这些!
这篇文章存在大量的datastream操作,后续文章有空会对序列化与反序列化类datastream进行深度分析。
网狐棋牌游戏服务器实现回放功能