文章目錄:
- 1.開發背景
- 2.專案功能&&效果展示
- 3.開發環境&&主要技術
- 4.專案模塊&&設計思路
- 5.專案主要功能實作
- 5.1 登陸注冊
- 5.2 匹配功能
- 5.3 判斷是否是自己的回合&&落子判斷
- 5.4 將對手下的棋同步到自己的棋盤上
- 6.專案原始碼
1.開發背景
??五子棋是一種傳統的棋種,它不僅容易上手,老少皆宜,而且趣味橫生,引人入勝:它不僅能增強思維能力,提高智力,而且富含哲理,有助于修身養性,為了將自己所學到的知識更系統地用起來,也為了檢測自己對Linux相關知識的掌握情況,所以這款網路對戰五子棋專案就誕生了~~
2.專案功能&&效果展示
① 支持用戶登陸、注冊功能
② 支持用戶在線匹配功能
③ 游戲結束后,支持重新匹配功能
④將對局資訊持久化存盤在資料庫中
用戶登錄界面:

用戶注冊界面:

游戲界面:

對局情況:

3.開發環境&&主要技術
開發環境
① Linux平臺
② gcc/g++:7.3.1版本
③ C++語言
④ MySQL資料庫
主要技術
① 多執行緒
② 互斥鎖
③ 條件變數
④ JSON
⑤ AJAX異步請求
⑥ MySQL資料庫
4.專案模塊&&設計思路
- HTTP模塊:用來接收來自網路的請求,使用到開源庫 httplib
- 用戶管理模塊:① 管理用戶資訊② 用戶資訊持久化(room_player.hpp)
- 房間管理模塊:針對兩個匹配用戶創建房間,保存游戲屬性(room_player.hpp)
- 會話模塊:針對登錄用戶創建唯一的會話ID,通過Cookie回傳給瀏覽器(session.hpp )
- 資料庫管理模塊:database.hpp
- 工具模塊:tools.hpp
- 專案驅動模塊:webgobang.hpp、webgobang.cpp
設計思路:
此專案大體的資料流向是前端通過AJAX請求將JSON格式的資料傳送到后端,后端通過httplib庫中的方法獲取到前端當中的JSON資料,然后對該資料進行相應操作后,將結果回傳給前端,前端根據回傳的結果,走相應的邏輯,并向用戶反饋一個結果
專案架構圖:

5.專案主要功能實作
5.1 登陸注冊
??用戶點擊登錄按鈕后,前端會對用戶輸入的郵箱和密碼進行獲取,將資料轉為JSON格式(序列化),并通過AJAX,采用POST的http請求方法將該資料發送到后端去,后端校驗該資料之后,將其持久化到資料庫中,并且根據輸入的郵箱和密碼生成相應的MD5碼作為當前用戶的cookie資訊回傳回去,當前端接收到http服務端的回應后,會回呼success:function(data)函式,做出相應的操作;


后端代碼如下:
http_svr_.Post("/login", [this](const Request& res, Response& resp){
//1.校驗用戶提交的賬號和密碼(同資料庫當中的資料進行校驗)
Json::Reader r;
Json::Value v;
r.parse(res.body, v);
//將請求正文中的內容反序列化
cout << res.body << endl;
//在資料庫中校驗郵箱、密碼,若正確回傳用戶id
int user_id = this->db_svr_->QueryUserExist(v);
//2.組織http回應進行回復(json)
string tmp = "";
if(user_id > 0)
{
Session sess(v, user_id);
string session_id = sess.GetSessionId();
//回復的會話格式 JSESSIONID=xxxxx
tmp = "JSESSIONID=" + session_id;
all_sess_->SetSessionInfo(session_id, sess);
//放到用戶管理類當中, 進行管理
//當用戶登錄之后, 將登錄的用戶保存在map當中
this->pm_->InsertPlayer2Map(user_id);
}
Json::Value resp_json;
resp_json["status"] = user_id <= 0 ? false:true;
//將該Json-Value物件序列化 并進行回應
resp.body = this->Serializa(resp_json);
resp.set_header("Set-Cookie", tmp.c_str());
resp.set_header("Content-Type", "application/json");
});
注冊邏輯與登錄邏輯類似,此處不再詳解
5.2 匹配功能
此游戲玩家一共有三個狀態在線狀態(ONLINE)、匹配狀態(MATCH)、游戲狀態(PLAYING),當玩家登錄上來后會將用戶的狀態改為在線狀態,當玩家點擊開始匹配按鈕時,玩家狀態會變為MATCHING,并且將其加入匹配池(vector) 當中,喚醒匹配執行緒進行匹配,如果匹配池中匹配玩家數量 < 2,則匹配執行緒(從匹配池中獲取玩家進行匹配的執行緒) 不進行匹配,如果匹配池中匹配玩家數量為奇數,則匹配池中最后一個玩家不進行匹配,等待下一次匹配,其余玩家兩兩配對,

匹配池:使用vector用來存放MATCH狀態的玩家
匹配執行緒:從匹配池中獲取到兩個玩家資訊后,為這兩名玩家創建一個房間,并將這兩名玩家的狀態變為PALYING狀態,為其分配創建出的房間號和對應所執的黑棋/白棋,
將玩家放入匹配池代碼如下:
int PushPlayer2MatchPool(int user_id)
{
//1.設定用戶的狀態為MATCHING狀態
pm_->SetUserStatus(user_id, MATCHING);
//2.放到匹配池當中
pthread_mutex_lock(&vec_lock_);
match_pool_.push_back(user_id);
match_pool_num_++;
printf("match_pool_num_: %d\n", match_pool_num_);
pthread_mutex_unlock(&vec_lock_);
//通知匹配執行緒作業
pthread_cond_broadcast(&vec_cond_);
return 0;
}
匹配執行緒代碼如下:
static void* MatchServer(void* arg)
{
pthread_detach(pthread_self());
WebGobang* wg = (WebGobang*)arg;
while(1)
{
pthread_mutex_lock(&wg->vec_lock_);
while(wg->match_pool_num_ < 2)
{
pthread_cond_wait(&wg->vec_cond_, &wg->vec_lock_);
printf("i am MatchServer, i working\n");
}
//匹配池當中的人數一定為大于等于2
// 奇數: 一定由一個玩家要輪空
// 偶數:兩兩進行匹配
int last_id = -1;
auto& vec = wg->match_pool_;
size_t vec_size = vec.size();
if(vec_size % 2)
{
//當前人數是奇數
last_id = vec[vec_size - 1];
vec_size -= 1;
}
for(int i = vec_size - 1; i >= 0; i -= 2)
{
int player_one = vec[i];
int player_two = vec[i - 1];
//假設匹配成功
int room_id = wg->rm_->CreateRoom(player_one, player_two);
//當前匹配上的這兩個用戶的狀態設定成為PLAYING
wg->pm_->SetUserStatus(player_one, PLAYING);
wg->pm_->SetUserStatus(player_two, PLAYING);
wg->pm_->SetRoomId(player_one, room_id);
wg->pm_->SetRoomId(player_two, room_id);
// 給用戶分配黑棋還是白棋
wg->pm_->SetUserChessName(player_one, "黑棋");
wg->pm_->SetUserChessName(player_two, "白棋");
cout << "player_one: " << player_one << ", player_two:" << player_two << ", room_id: "<< room_id << endl;
}
wg->MatchPoolClear();
pthread_mutex_unlock(&wg->vec_lock_);
if(last_id > 0)
{
wg->PushPlayer2MatchPool(last_id);
}
}
return NULL;
}
void MatchPoolClear()
{
match_pool_.clear();
match_pool_num_ = 0;
}
當用戶點擊開始匹配按鈕之后,前端就會向后端發送一個AJAX請求,用來進行通知后端進行匹配,當后端將當前用戶放入匹配池中之后,就會將相應的結果回傳給前端,前端在獲得這個結果之后,若前端收到的回傳值為0,則證明當前用戶正在處于匹配狀態(MATCH),而此時前端就要通過setInterval()函式不停的向后端發送AJAX請求,獲知當前匹配的結果,直到當前用戶匹配成功為止,setInterval函式,在該函式內部不停呼叫AJAX請求,并且每隔1秒發送一次,當回圈30次、匹配成功、匹配失敗則停止像后端獲取結果
前端獲取匹配結果代碼如下:
SetMatch: function () {
if(game.match_flag === false){
return;
}
console.log("SetMatch Func , 開始匹配...");
$.ajax({
url: "/SetMatch",
type: "Get",
dataType: "JSON",
//async : false ==> 禁止異步請求 chrome 有可能會禁止 ajax 的同步請求
async: false,
success: function (data) {
if (data.status === 0) {
console.log("SetMatch Func , 開始匹配狀態設定成功...");
game.GetMatchResult()
} else {
alert("匹配失敗, 重新匹配...");
}
},
});
},
GetMatchResult: function (){
//回圈的獲取匹配結果
//向后臺回圈發送獲取匹配結果, 直到匹配成功
let s = 0;
let _time = setInterval(() => {
console.log("GetMatchResult, " + s++);
$.ajax({
url: "/Match",
type: "Get",
dataType: "JSON",
//async : false ==> 禁止異步請求 chrome 有可能會禁止 ajax 的同步請求
async: false,
success: function (data) {
game.match_status = data.status;
console.log(data.status);
if (data.status === 1) {
/*
* data = {
* status : 匹配狀態
* room_id : xxx,
* chess_name: xxxx
* }
* */
console.log("匹配成功, room_id is: " + data.room_id);
alert("匹配成功, 您執 " + data.chess_name + ", 黑棋先走");
game.room_id = data.room_id;
game.my_chess_name = data.chess_name;
/*
* 當匹配成功之后, 持有白棋的人, 需要回圈去獲取持黑棋棋子的落子位置
* */
if(data.chess_name === "白棋"){
game.GetPeerStep();
}
game.match_flag = false;
game.start_match.style.background = '#d0cdcd';
game.start_match.style.color = '#505050';
}else if(data.status === 0 && s >= 30){
//當沒有匹配上, 一直到回圈結束,才彈框提示"匹配人數太少"
alert("當前匹配人數太少, 請重新匹配...");
}else if(data.status === -1){
alert("當前會話失效了, 請重新登錄!");
window.location.href = "index.html";
}
},
});
if(s >= 30 || game.match_status === 1 || game.match_status === -1){
clearInterval(_time);
}
}, 1000);
},
5.3 判斷是否是自己的回合&&落子判斷
我們在創建房間的時候規定玩家1執黑棋,玩家2執白棋,每局游戲都讓黑棋先走,我們在Room類中定義一個who_turn_記錄當前是誰的輪回,我們初始who_turn_=p1,只需要判斷who_turn_是否等于前端傳遞的user_id_,如果相同則向前端回傳1,代表是自己的回合可以落子,若不同則向前端回傳0,代表著是對方的回合,不能落子,當一方落子后,會將who_turn_置為對方;
Room類
int IsMyTurn(int user_id)
{
return user_id == who_turn_ ? 1 : 0;
}
int Step(int user_id, const string& body)
{
if(user_id != who_turn_)
{
return 0;
}
step_vec_.push_back(body);
//將who_turn_置為對方
who_turn_ = (user_id == p1_ ? p2_ : p1_);
step_num_++;
return 1;
}
RoomManager類
int IsMyTurn(const int room_id, const int user_id)
{
return map_room_[room_id].IsMyTurn(user_id);
}
當用戶點擊棋盤上的某一點的時候,就向后端發送一個AJAX請求,判斷當前是否是自己的回合,如果是則允許落子,否則,不允許落子, 如果當前是允許落子的,那么在棋盤上繪制對應棋子之前,會再發送一個AJAX請求到后端,后端會對當前房間的 whose_turn_ 進行修改,并且保存當前所走棋子的位置資訊在vector中(為了同步到對方的棋盤上)
前端獲結果判斷是否是自己的輪回,若是則讓flag = true,否則讓flag=flase
IsMyTurn:function (){
let flag = false;
let user_info = {
room_id : game.room_id,
user_id : game.user_id
};
$.ajax({
url: "/IsMyTurn",
type: "Post",
data: JSON.stringify(user_info),
dataType: "JSON",
//async : false ==> 禁止異步請求 chrome 有可能會禁止 ajax 的同步請求
async: false,
success: function (data) {
if (data.status === 1) {
flag = true;
} else {
flag = false;
}
},
});
而當flag=flase,直接return,不會落子,當flag = true,則點擊會落子
canvasClick: function (e) {
let dx = parseInt(Math.floor(e.offsetX + 15) / 30);
let dy = parseInt(Math.floor(e.offsetY + 15) / 30);
if (dx < 1 || dx > 14 | dy < 1 || dy > 14)
return;
if(game.IsMyTurn() === false) {
return;
}
let WBobj = {
x : dx,
y : dy,
ox: (dx * 30) - 25,
oy: (dy * 30) - 25,
mz: game.chess_Name[game.e % 2],
dm: document.createElement("div"),
class: game.e % 2 == 1 ? "Wchess" : "Bchess",
list: game.um++,
user_id : game.user_id,
room_id : game.room_id
}
if (game.chess_Board[dx - 1][dy - 1] == 0) {
$.ajax({
url: "/Step",
type: "Post",
data: JSON.stringify(WBobj),
dataType: "JSON",
//async : false ==> 禁止異步請求 chrome 有可能會禁止 ajax 的同步請求
async: false,
success: function (data) {
/*
* data.status:
* -1 : 會話失效
* 0 : 下棋失敗(原因是不是自己的輪回)
* 1 : 下棋成功
* */
if (data.status === 1) {
console.log("落子成功!該對方下棋了");
/*
* 當自己落子成功之后, 就呼叫函式請求對方的落子位置
* */
game.GetPeerStep();
} else if(data.status === -1){
alert("會話失效, 請重新登錄...");
window.location.href = "index.html";
}else if(data.status === 0){
alert("別著急, 請等待對方落子");
}
},
});
game.h.push(WBobj)
WBobj.dm.classList.add(WBobj.class);
WBobj.dm.style.left = WBobj.ox + "px";
WBobj.dm.style.top = WBobj.oy + "px";
game.chess_Board[dx - 1][dy - 1] = game.chess_Name[game.e % 2];
game.lianz[dx - 1][dy - 1] = WBobj.dm;
game.win(dx - 1, dy - 1, game.chess_Name[game.e % 2], game.winXY[0], game.e % 2)
game.win(dx - 1, dy - 1, game.chess_Name[game.e % 2], game.winXY[1], game.e % 2)
game.win(dx - 1, dy - 1, game.chess_Name[game.e % 2], game.winXY[2], game.e % 2)
game.win(dx - 1, dy - 1, game.chess_Name[game.e % 2], game.winXY[3], game.e % 2)
game.cName.innerText = game.e % 2 == 0 ? game.chess_Name[1] + "走" : game.chess_Name[0] + "走";
game.sChesee.className = game.e % 2 == 1 ? "state-chess Bchess" : "state-chess Wchess";
game.e++;
game.centent.appendChild(WBobj.dm)
}
},
5.4 將對手下的棋同步到自己的棋盤上
玩家1點擊棋盤上的某個位置的時候落子時,前端會向后端發送一個AJAX請求,后端在接收到該請求后,會對當前房間的 whose_turn_ 進行修改,并且保存當前所走棋子的位置資訊在一個vector中,然后回傳相應的結果,當落子成功后,前端就會呼叫setInterval函式回圈獲取對方落子位置,同理,呼叫setInterval函式進行回圈獲取,即向后端發送AJAX請求,對端落子后獲取成功,則將該棋子繪制到當前用戶的棋盤上,這樣就實作了兩個用戶對戰棋盤之間的同步,
Room類:
int GetPeerStep(int user_id, string& s)
{
if(step_num_ <= 0)
{
return -1;
}
if(user_id != who_turn_)
{
return -2;
}
s = step_vec_[step_num_ - 1];
return 0;
}
RoomManager類:
int GetRoomStep(const string& body, string& s)
{
Json::Reader r;
Json::Value v;
r.parse(body, v);
int room_id = v["room_id"].asInt();
int user_id = v["user_id"].asInt();
return map_room_[room_id].GetPeerStep(user_id, s);
}
前端js代碼如下:
GetPeerStep: function (){
/*
* 回圈獲取對方落子的位置, 直到拿到對方的落子位置, 結束回圈
* 1.沒有拿到, 就回圈獲取
* 2.拿到了, 則需要將對方的落子位置, 展現在自己的棋盤上
* */
let _time;
let user_info = {
room_id : game.room_id,
user_id : game.user_id
}
let s = 0;
let status = -100;
_time = setInterval(() => {
console.log("輪訓獲取對方落子位置: " + s++);
$.ajax({
url: "/GetPeerStep",
type: "Post",
data: JSON.stringify(user_info),
dataType: "JSON",
//async : false ==> 禁止異步請求 chrome 有可能會禁止 ajax 的同步請求
async: false,
success: function (data) {
/*
* data.status : 當前獲取有沒有成功
* 1:表示成功
* data.peer_step : 對端的落子位置
*
* */
status = data.status;
if (data.status === 1) {
console.log("peer_step:" + data.peer_step)
game.peer_step = data.peer_step;
/*
* 將對端落子位置展示在自己的棋盤上
* */
game.Step();
}else if(data.status === -1){
window.location.href = "index.html"
}else if(data.status === 0){
console.log("沒有獲取到, 繼續去獲取");
}
},
});
if(status === 1 || s >= 60 || status === -1){
clearInterval(_time);
}
}, 1000);
},
6.專案原始碼
查看原始碼請點------->【專案原始碼】
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/295276.html
標籤:其他
上一篇:Java——五子棋(1)

