房间管理模块
房间管理模块,就是对每一间游戏房间进行管理,因此我首先需要先创建出一个房间类,实现了房间类的功能,再去创建房间管理的类,从而实现房间管理的类的功能。
房间类主要是对匹配成对的玩家建立一个小范围的关联关系,⼀个房间中任意⼀个用户发生的任何动作,都会被广播给房间中的其他用户。
房间中的两个主要动作:下棋和聊天。
游戏房间包含了房间id,玩家数量,房间状态、黑棋白棋玩家的id,以及棋盘,在线用户管理和数据模块管理的指针等字段。在游戏房间中,需要实现的是处理下棋动作、处理聊天动作和处理玩家退出房间的动作,以及将动作处理广播给房间的所有玩家的方法,已经判断下棋后是否五星连珠,胜利的方法。
将处理下棋的动作、处理聊天的动作和处理玩家退出房间的动作交给了总处理方法,通过请求的类型来分别处理不同的业务请求。
房间类
成员变量
成员变量均为私有。对于在线用户管理类的对象和数据模块管理类的对象,我们使用指针定义出来。因为在后续的房间管理类中,依然需要用到相同的在线管理类的对象和数据模块管理类的对象,使用指针可以避免拷贝,提供程序的效率。
/*棋盘的大小为15*15*/
#define BOARD_ROW 15
#define BOARD_COL 15
/*1代表白棋玩家,2代表黑棋玩家*/
#define CHESS_WHITE 1
#define CHESS_BLACK 2
/*定义房间状态:游戏开始,游戏结束*/
typedef enum{GAME_START, GAME_OVER}room_status;
/*房间类中,一个房间,需要有的信息是:房间的id,房间的状态,房间玩家数量,玩家的id(黑白棋),在线用户管理,数据模块管理*/
class room
{
private:
uint64_t _room_id;/*房间id*/
room_status _statu;/*房间状态*/
uint64_t player_count;/*玩家数量*/
uint64_t _white_id;/*白棋玩家的id*/
uint64_t _black_id;/*黑棋玩家的id*/
user_table* _tb_user;/*数据模块管理*/
online_manager* _online_user;/*在线用户管理*/
std::vector<vector<int>> _board;/*棋盘*/
public:
};
成员方法
私有成员方法
私有成员方法中,有两个成员方法,一个是判断是否五星连珠的方法,一个是判断输赢的方法。其实这两个方法是结合起来使用的,也就是说,在判断输赢的方法中,是需要调用判断是否五星连珠的方法的。
首先,判断是否五星连珠的方法:从当前位置开始,分正方向和反方向去寻找连续相同颜色棋子的数量,一开始数量是1个,也就是当前位置的棋子。然后先从正方向去寻找,找到一个,计数+1,直到走出棋盘或者颜色不一样,该方向结束。然后反方向寻找,同样的方法,将所有连续的相同颜色棋子个数加起来,最后返回即可。
/*判断是否五星连珠的方法,参数为:当前棋子的坐标,棋子后续的偏移量,也就是方向,然后是棋子的颜色*/
/**/
bool five(int row,int col,int row_off,int col_off,int color)
{
int cnt = 1;//一开始,连续相同颜色棋子的个数只有一个,也就是当前的棋子
int search_row = row+row_off;/*当前坐标加上坐标的偏移量,也就是向左或向右或正斜或反斜*/
int search_col = col+col_off;/*当前坐标加上坐标的偏移量,也就是向左或向右或正斜或反斜*/
/*首先,判断一个方向的*/
while(search_row>=0 && search_row<BOARD_ROW && search_col>=0 && search_col<BOARD_COL
&& _board[search_row][search_col]==color)
{
/*同色棋子加1*/
cnt++;
search_row +=row_off;
search_col +=col_off
}
/*判断反方向*/
search_row = row - row_off;
search_col = col - col_off;
while(search_row >= 0 && search_row < BOARD_ROW &&
search_col >= 0 && search_col < BOARD_COL &&
_board[search_row][search_col] == color)
{
//同色棋子数量++
cnt++;
//检索位置继续向后偏移
search_row -= row_off;
search_col -= col_off;
}
return (cnt>=5);
}
判断输赢的方法:
其实判断输赢,无非就是每下一步棋子,然后就进行判断,去四个方向上寻找是否出现五星连珠,如果是,则赢,返回赢家的id,如果没有,游戏继续。
uint64_t check_win(nt row, int col, int color)
{
// 从下棋位置的四个不同方向上检测是否出现了5个及以上相同颜色的棋子(横行,纵列,正斜,反斜)
if (five(row, col, 0, 1, color) ||
five(row, col, 1, 0, color) ||
five(row, col, -1, 1, color)||
five(row, col, -1, -1, color))
{
//任意一个方向上出现了true也就是五星连珠,则设置返回值
return color == CHESS_WHITE ? _white_id : _black_id;
}
return 0;
}
公有成员方法
1.构造函数和析构函数
对于房间中的信息,需要进行初始化的有房间的id,房间的玩家数量,房间状态,在线用户管理对象和数据模块管理对象,以及棋盘。
room(uint64_t room_id, user_table *tb_user, online_manager *online_user)
:_room_id(room_id),_tb_user(tb_user),_online_user(online_user)
,_statu(GAME_START),_player_count(0),_board(BOARD_ROW, std::vector<int>(BOARD_COL, 0))
{
DLOG("%lu 房间创建成功!!", _room_id);
}
~room()
{
DLOG("%lu 房间销毁成功!!", _room_id);
}
2.获取房间各种信息的方法
获取房间id、获取房间状态、获取房间玩家数量,添加玩家成为白棋或黑棋玩家,获取白棋、黑棋玩家id。
uint64_t get_rid(){ return _room_id; }/*获取房间id*/
uint64_t get_white_user(){ return _white_id; }/*获取白棋玩家id*/
uint64_t get_black_user(){ return _black_id; }/*获取黑棋玩家id*/
room_status statu(){ return _statu; }/*获取房间状态*/
int player_count(){ return _player_count; }/*获取玩家数量*/
void add_white_user(uint64_t uid){ _white_id = uidl; _player_count++; }/*添加白棋玩家到房间中*/
void add_black_user(uint64_t uid){ _black_id = uid; _player_count++; }/*添加黑棋玩家到房间中*/
3.处理下棋动作
流程:
下棋动作分有3种情况:
①在下棋过程中,对手掉线,那么不战而胜
②在下棋的过程中,位置已经被占用
③下完棋子后,判断是否五星连珠,如果是,则胜利,胜利或失败后的操作交由总处理方法去处理。
/*处理下棋动作*/
Json::Value handle_chess(Json::Value& req)
{
/*在下棋动作,分几种情况进行处理
1.一方掉线,那么另一方不战而胜
2.在下棋的时候,棋子的位置已经被占用,提示玩家重新选择下棋位置
3.一方下完棋子后,判断是否五星连珠,如果是,则胜利*/
/*首先,创建一个用于响应的Json*/
Json::Value resp_json = req;/*直接拷贝req的数据,因为req的数据中已经有了下棋玩家的一些数据*/
/*进行判断,双方是否都在线,如果一方掉线,那么另一方不战而胜*/
if(_online_user.is_in_game_room(_white_id)==false)/*白棋玩家掉线*/
{
resp_json["result"] = true;
resp_json["reason"] = "对方掉线,不战而胜!";
resp_json["winner"] = (Json::UInt64)_black_id;
return resp_json;
}
if(_online_user.is_in_game_room(_black_id)==false)/*黑棋玩家掉线*/
{
resp_json["result"] = true;
resp_json["reason"] = "对方掉线,不战而胜!";
resp_json["winner"] = (Json::UInt64)_white_id;
return resp_json;
}
/*双方在线*/
/*获取走棋的位置,判断当前走棋是否合理(位置是否已经被占用)*/
int chess_row = req["row"].asInt();//当前下棋的位置
int chess_col = req["col"].asInt();//当前下棋的位置
uint64_t cur_uid = req["uid"].asUInt64();//当前下棋的玩家
if(_board[chess_row][chess_col]!=0)
{
json_resp["result"] = false;
json_resp["reason"] = "当前位置已经有了其他棋子!";
return json_resp;
}
/*位置合理,下棋,*/
int cur_color = cur_uid == _white_id ? CHESS_WHITE : CHESS_BLACK;//看看当前下棋的玩家是黑棋还是白棋,需要对应起来
_board[chess_row][chess_col] = cur_color;//下棋
/*下完棋后需要判断是否有玩家胜利(从当前走棋位置开始判断是否存在五星连珠)*/
uint64_t winner_id = check_win(chess_row,chess_col,cur_color);
if (winner_id != 0)/*如果返回来不为0,说明游戏结束*/
{
json_resp["reason"] = "五星连珠,战无敌!";
}
json_resp["result"] = true;
json_resp["winner"] = (Json::UInt64)winner_id;
return json_resp;
}
4.处理聊天动作
流程:
①接收到聊天信息,获取聊天信息
②然后检测聊天信息中是否有敏感词,如果有,则表示不能发送
③如果没有,则发送
/*处理聊天动作*/
Json::Value handle_chat(Json::Value& req)
{
/*处理聊天动作很简单,就是查看聊天消息中有没有敏感词*/
Json::Value resp_json = req;
/*获取其中的信息*/
std::string msg = req["message"].asString();
size_t pos = msg.find("垃圾");
if(pos != std::string::npos)
{
resp_json["result"] = false;
resp_json["reason"] = "消息中包含了敏感词,不能发送";
return resp_json;
}
resp_json["result"] = true;
return resp_json;
}
5.处理玩家退出房间动作
流程:
玩家退出房间,有两种情况,第一种是在游戏对战中退出,第二种是游戏结束后正常退出
①游戏对战中退出,即房间状态为GAME_START,那么先获取胜利者的id和失败者的id,即黑棋还是白棋的id,然后交由数据管理模块去进行数据的更新,然后将房间信息返回。
②如果是正常退出,那么房间玩家数量减一下就🆗了。
/*处理玩家退出房间动作*/
void handle_exit(uint64_t uid)
{
Json::Value resp_json;
/*对于玩家退出房间,有两种情况*/
/*游戏进行中时退出房间,这种情况下,对手不战而胜*/
if(_statu==GAME_START)
{
uint64_t winner_id = (Json::UInt64)(uid == _white_id?_black_id:_white_id);//找到胜利玩家的id
resp_json["optype"] = "put_chess";
json_resp["result"] = true;
json_resp["reason"] = "对方掉线,不战而胜!";
json_resp["room_id"] = (Json::UInt64)_room_id;
json_resp["uid"] = (Json::UInt64)uid;
json_resp["row"] = -1;
json_resp["col"] = -1;
json_resp["winner"] = (Json::UInt64)winner_id;
uint64_t loser_id = winner_id == _white_id ? _black_id : _white_id;//找到失败玩家的id
_tb_user->win(winner_id);/*从数据管理模块中处理胜利玩家的数据*/
_tb_user->lose(loser_id);/*从数据管理模块中处理失败玩家的数据*/
_statu = GAME_OVER;
broadcast(json_resp);//广播
}
/*游戏结束后正常退出房间*/
_player_count--;
return;
}
6.总的请求处理函数,在函数内部,区分请求类型,根据不同的请求调用不同的处理函数,得到响应进行广播
流程:首先判断一下房间是否匹配
①根据Json传来的请求类型,判断是需要处理什么样的动作
②如果是下棋动作,那么将Json对象交给下棋处理的方法,然后根据返回来的结果,判断游戏是否结束,如果结束,找出胜利和失败者,并且交给数据管理模块去进行数据的更新。
③如果是聊天动作,那么直接交给处理聊天动作的方法即可。
④最后将Json对象进行广播。
void handle_request(Json::Value& req)
{
Json::Value resp_json;
/*1.校验房间号是否匹配*/
uint64_t room_id = req["room_id"].asUInt64();
if(_room_id!=room_id)
{
resp_json["optype"] = req["optype"].asString();
resp_json["result"] = false;
resp_json["reason"] = "房间号不匹配!";
return broadcast(resp_json);
}
//2. 根据不同的请求类型调用不同的处理函数
if(req["optype"].asCString()=="put_chess")
{
resp_json = handle_chess(req);
if (json_resp["winner"].asUInt64() != 0)
{
uint64_t winner_id = json_resp["winner"].asUInt64();
uint64_t loser_id = winner_id == _white_id ? _black_id : _white_id;
_tb_user->win(winner_id);
_tb_user->lose(loser_id);
_statu = GAME_OVER;
}
}
else if(req[optype].asCString()=="chat")
{
resp_json=handle_chat(req);
}
else
{
json_resp["optype"] = req["optype"].asString();
json_resp["result"] = false;
json_resp["reason"] = "未知请求类型";
}
std::string body;
json_util::serialize(json_resp, body);
DLOG("房间-广播动作: %s", body.c_str());
return broadcast(resp_json);
}
7.广播
流程:
①先将Json对象进行序列化
②获取房间中用户的通信连接,然后将消息发送过去。
/*将指定的信息广播给房间中所有玩家*/
void broadcast(Json::Value& rsp)
{
/*进行序列化,然后获取房间中所有用户的通信连接,然后将消息发送出去*/
std::string body;
json_util::serialize(rsp,body);
wsserver_t::connection_ptr wconn = _online_user->get_conn_from_room(_white_id);
if(wconn.get()!=nullptr)
{
wconn->send(body);
}
else
{
DLOG("房间-白棋玩家连接获取失败");
}
wsserver_t::connection_ptr bconn = _online_user->get_conn_from_room(_black_id);
if(bconn.get()!=nullptr)
{
bconn->send(body);
}
else
{
DLOG("房间-黑棋玩家连接获取失败");
}
return;
}
房间管理类
将房间类实现好之后,接下来就是需要将房间统一管理起来了。
在房间管理类中,需要对每一间房间进行编号,但是不能乱编,为了避免房间编号出现错误,因此采用计数器的方式,并且将房间交由智能指针shared_ptr去管理,这样就方便进行操作了。
因此,如何通过房间号,获取对应的房间的智能指针,以及通过用户id,获取到对应的房间信息是需要实现的功能之一。以及,房间管理类需要有创建房间的方法,销毁房间的方法、删除房间中指定用户的方法。接下来,将一一实现:
成员变量
使用unordered_map将用户id与房间id映射起来,将房间id与管理房间的智能指针映射起来,方便通过房间id获取对应的房间的智能指针,以及通过用户id,找到房间id,从而获取房间的智能指针。
因此,成员变量如下:
using room_ptr = std::shared_ptr<room>;
class room_manager
{
private:
uint64_t _next_rid;/*房间编号计数器*/
std::unordered_map<uint64_t,room_ptr> _rooms;/*将房间编号与智能指针建立映射关系*/
std::unordered_map<uint64_t,_next_rid> _user;/*将房间编号与用户id建立映射关系*/
std::mutex _mutex;
online_manager* _online_user;
user_table* _tb_user;
public:
};
成员方法
1.构造方法和析构方法
房间管理类中,需要将房间计数器,在线用户管理,数据模块管理对象进行初始化。
/*初始化房间ID计数器*/
room_manager(user_table *ut, online_manager *om):
_next_rid(1), _tb_user(ut), _online_user(om)
{
DLOG("房间管理模块初始化完毕!");
}
~room_manager() { DLOG("房间管理模块即将销毁!"); }
2.创建房间
房间房间的前提是,在匹配对战中的两个玩家依然在线,因此,在创建前,需要判断一下,双方是否都在线。在线的话,那么就创建房间,将玩家id加入房间中,接着将房间管理起来,最后返回这个房间的智能指针。
*创建房间,并将两个玩家添加到房间中去*/
room_ptr create_room(uint64_t uid1,uint64_t uid2)
{
//两个用户在游戏大厅中进行对战匹配,匹配成功后创建房间
//1. 校验两个用户是否都还在游戏大厅中,只有都在才需要创建房间。
if(_online_user->is_in_game_hall(uid1)==false)
{
DLOG("用户:%lu 不在大厅中,创建房间失败!", uid1);
return room_ptr();
}
if(_online_user->is_in_game_hall(uid2)==false)
{
DLOG("用户:%lu 不在大厅中,创建房间失败!", uid2);
return room_ptr();
}
//2. 创建房间,将用户信息添加到房间中
std::unique_lock<std::mutex> lock(_mutex);
//创建一个执行房间的智能指针
room_ptr rp(new room(_next_rid, _tb_user, _online_user));
rp->add_white_user(uid1);
rp->add_black_user(uid2);
//3.将房间管理起来
_rooms.insert(std::make_pair(_next_rid,rp));
_users.insert(std::make_pair(uid1,_next_rid));
_users.insert(std::make_pair(uid2,_next_rid));
_next_rid++;
//4.返回管理房间的智能指针
return rp;
}
3.通过房间id获取房间信息
通过映射关系,找到该房间的智能指针,返回。
/*通过房间id获取房间信息*/
room_ptr get_room_by_rid(uint64_t rid)
{
std::unique_lock<std::mutex> lock(_mutex);
auto it = _rooms.find(rid);
if(it==_rooms.end())
{
return room_ptr();
}
return it->second;
}
4.通过用户id获取房间信息
首先他哦难过用户id与房间id的映射关系,找到房间id,然后 通过房间id与房间智能指针的映射关系,找到房间的智能指针,返回。注意,不能直接调用通过房间id获取房间信息的方法,因为在两个方法中,都上了一把互斥锁,如果直接调用,会造成死锁的问题。
*通过用户id获取房间信息*/
room_ptr get_room_by_uid(uint64_t uid)
{
std::unique_lock<std::mutex> lock(_mutex);
auto uit = _users.find(uid);
if(uit==_users.end())
{
return room_ptr();
}
uint64_t rid = uit->second;
auto rit = _rooms.find(rid);
if(rit==_rooms.end())
{
return room_ptr();
}
return rit->second;
}
5.通过房间ID销毁房间
销毁房间的意思是,移除对房间智能指针的管理。并且在销毁房间之前,需要将房间中的玩家数量进行移除,保证房间已经空出去了,然后再进行销毁。
因此,首先先通过房间id获取房间的智能指针,然后通过房间的智能指针,获取玩家的id,然后将玩家从管理中移除,最后将房间移除。
/*通过房间ID销毁房间*/
void remove_room(uint64_t rid)
{
/*销毁一个房间,需要先将房间中的玩家全部移除,最后再将房间的管理移除出去,即销毁成功*/
/*通过房间id,获取房间的指针*/
room_ptr rp = get_room_by_rid(rid);
if (rp.get() == nullptr)
{
return;
}
/*通过房间指针,获取到房间内玩家的id*/
uint64_t uid1 = rp->get_white_user();
uint64_t uid2 = rp->get_black_user();
/*通过玩家的id,移除房间管理中的用户信息*/
std::unique_lock<std::mutex> lock(_mutex);
_users.erase(uid1);
_users.erase(uid2);
/*最后通过房间id,移除出去*/
_rooms.erase(rid);
}
6.删除房间中指定用户,如果房间中没有用户了,则销毁房间,用户连接断开时被调用
删除房间中指定的用户,即玩家在断开连接后,会去调用房间类中的处理玩家退出房间的方法,接着,判断一下房间里面还有没有人,如果没有人了,那就调用通过房间ID销毁房间的方法。
void remove_room_user(uint64_t uid)
{
/*通过用户id,找到对应的房间*/
room_ptr rp = get_room_by_uid(uid);
if (rp.get() == nullptr)
{
return;
}
/*通过处理玩家退出房间的动作,来移除该名玩家*/
rp->handle_exit(uid);
//房间中没有玩家了,则销毁房间
if (rp->player_count() == 0)
{
remove_room(rp->get_rid());
}
return ;
}