https://github.com/yunwei37/UNO-game-oop
目錄
1. 需求分析
UNO紙牌已經風靡全球數十年,被譽為是世界上最好玩的紙牌遊戲,據說由意大利一個理髮師發明,簡單易學,版本眾多,被加入許多新的功能,玩法更加刺激,而在此遊戲中最考的是集中和反應,還有相互間的思維較量。
基於此,我們開發了一款可聯機對戰的UNO紙牌遊戲:
1.1. UNO卡牌遊戲的基本功能
- 友好的圖形用戶界面
- 支持2種uno遊戲模式
- 支持 2 - 8人蔘與遊戲
- 支持單人遊戲,其他參與者為AI‘
- 支持不同玩家局域網內聯機參與遊戲
1.2. UNO卡牌遊戲的規則
每副uno牌包括:108張牌和一張說明書(108 張紙牌中包括76張數字牌,32張特殊牌)。Uno由紅黃藍綠4種顏色,每種色牌各有0號牌1張、1~9號牌各兩張,各種顏色還各有6張普通功能牌(“draw 2(加兩張)”、“skip(跳過下家)”、“reverse(逆轉方向)”各兩張。
首先,每人發8張牌,勝利條件是誰的牌首先出完;可以出與上家顏色相同或數字相同的牌,或者wild牌。然後,可以出draw 2(+2) 或draw 4(+4)來陷害下家,讓下家摸牌,下家可以出相應的牌來轉移或累加要摸的牌,直到最後被陷害的玩家沒有更大的牌時,就要摸相應的數量的牌,這樣總有人要摸很多牌。然後,玩家在打完倒數第二張牌時要喊UNO(剩一張),捉住其他玩家忘了喊剩一張而罰他摸兩張也是遊戲的樂趣之一。
具體規則可參考:
2. 總體設計
本課程設計基於Qt與C++實現一個具有友好的圖形用戶界面的在線多人UNO牌遊戲,參照一般意義上的UNO牌的規則,遊戲支持兩種UNO遊戲模式:一種模式為通用UNO牌玩法,第二種模式可搶出牌,同時。遊戲程序支持1 - 8 人使用,可選玩家人數,如果實際玩家不足設定的玩家人數,遊戲將採用AI模擬其他玩家。也可僅有一人蔘與,其他全部使用AI模擬(單機遊戲)。同時遊戲可以在一臺計算機上運行,也可在由多臺計算機在局域網內聯機運行。玩家可以創建房間,並將自己的計算機作為server端,其他玩家作為client端可加入房間進行聯機。遊戲程序採用C/S架構,集成server與client,不區分server端程序與client端程序版本。
我們採用了git進行協作開發,倉庫地址:
https://github.com/yunwei37/UNO-game-oop
參考的編碼規範:
分工:
- :組長,負責網絡通信、多線程部分和總體架構設計、前後端整合;
- :負責後端邏輯設計和實現
- :負責後端邏輯實現
- :負責前端界面設計和實現
3. 系統設計難點
遊戲採用前後端分離的模式,將前後端分為兩個小組進行開發,最後進行合併:前端負責界面設計與實現;後端負責具體邏輯與網絡部分;
3.1. 前端
前端主要是界面顯示的部分,初界面、設置界面、準備界面、遊戲界面及勝利界面五個界面,採用qt的 Widget 類進行派生實現。
3.2. 後端
後端架構框圖:
遊戲的後端邏輯由幾個主要的類以及它們的聯繫來完成。這些類可以記錄玩家的信息、某一步的操作信息、當前遊戲的狀態等。它們之間的關係如上圖所示。
-
Qobject
是 Qt 類的基類,因為遊戲的圖形界面通過 Qt 實現,同時需要採用信號 / 槽機制通信,所以其他類都是 Qobject 的派生類。 -
Backend
是後端類,可以由前端從中獲取狀態信息和通過它進行操作。 -
NetServer、netThread
類用於傳遞聯網玩家的數據報,以便實現信息的通訊。 -
PlayerThread
的成員是一個玩家的身份信息和操作信息。而 AIthread 和netThread
是PlayerThread
的子類,分別是AI玩家類和聯網玩家類。 - 還有一個很重要的類是
card
,每個類是一張遊戲牌,成員包括卡牌本身的信息以及提供私有成員信息的接口。
3.3. 前後端接口
前後端接口是主要的由於全局中只有一個玩家,即操作者;其他玩家的牌不需要在前端顯示,可以把uno抽象成一個狀態機來看:前端輸入行為,獲取當前狀態;後端通過行為計算當前狀態。
3.3.1. 交互定義
前後端的交互發生在:
- 玩家做出選擇對應動作,即按下按鈕之後;
- 其他玩家完成交互動作時’
前端用戶作出操作後將數據通過函數調用返回給後端,在後端完成計算之後,前端等待一定時間(可加載動畫)之後刷新界面;
backend.cpp:
後端類:可以由前端進行獲取狀態信息和進行操作;
每個其他玩家都是一個類,在backend中聲明;前端和後端唯一的接口就是這個類。
在前端每次完成相關顯示動作之後調用函數
getCurrentStatue();
獲取當前狀態;注意,如果沒有任何操作就能改變狀態的話,該函數也會改變狀態,如連續發牌,或跳過當前玩家;
需要每次調用過getCurrentStatue()函數後依次查看其他信息是否有更改
狀態 flag:
-2 Error
-1 創建類時初始化,遊戲準備狀態,等待網絡連接;
0 進入遊戲,開始發牌;
1 當前玩家可進行操作,選擇摸牌或出牌;
2 當前玩家已選擇摸牌,更新手牌;
3 當前玩家在摸牌後可以選擇出牌;(可能存在)
4 當前玩家已選擇出牌,更新手牌和牌堆;(如果出的是顏色牌,需要在前端選擇顏色,這一部分交給前端判斷)
(2 - 4代表回合結束)
對於下一個玩家,在其回合開始前可顯示:
5 上一個玩家忘了叫UNO,可以質疑;
6 跳過回合
7 需要摸牌n張並跳過回合
8 遊戲勝利
9 遊戲失敗
部分規則:
以自己為第一個出牌玩家;
目前不支持連出多張+N牌;
可以選擇摸牌,不需要沒有牌的時候再進行,選擇摸完牌之後如果手牌上的牌能夠出牌,可以立即選擇出牌;
前端向後端發送狀態則採用信號與槽的方式進行:
public slots:
// 前端調用
void startGame();
void sayUNO();
void playCard(int cardID,Card::COLOR color);
void drawCard();
4. 模塊設計
4.1. 前端模塊設計
前端設計的關注點在於不同場景的切換,隨著遊戲的深入,一共有初界面、設置界面、準備界面、遊戲界面及勝利界面五個界面,將其設計為了五個類,分別為mainwindow
、mysetwindow
、readywindow
、mygamewindow
及victory
。其中 mainwindow
界面中選擇遊戲模式後進入 mysetwindow
,在其中設置名字後進入 readywindow
,開始匹配;在匹配結束後進入到 mygamewindow
,倘若最終獲勝則順利進入到 victory
界面中。
在五個界面中的設計過程中,運用最多的組件為按鈕 mypushbutton
類,調用已有類之外,加入圖片顯示及附加效果,使按鈕符合本遊戲的需要,同時在Qt設計中通過定時器可以設計出按鈕彈起落下的動畫效果,進而通過按鈕的選擇進行切換界面和參數確定。繪圖方法採用 Qpainter
,一般情況下確定位置定點畫圖,在mygamewindow
中還通過確定中心而實現了組件的旋轉。
在前端顯示中,遊戲參與者以及卡牌都有自己的類 PlayerWidget
以及 CardWidget
, PlayerWidget
類中實現遊戲玩家頭像名字的顯示,通過動態數組實現牌組。CardWidget
則為單個卡牌界面的定義類,其中包含單個卡牌的詳細信息,以及其在遊戲過程中可能會出現的移動效果。
4.2. 遊戲邏輯
卡牌的定義在cards.h文件中:
每副遊戲牌共有108張卡牌,遊戲牌分四種顏色:紅色、綠色、藍色及黃色,每種顏色各有25張牌(合共100張),其中19張為數字牌(0牌有一張,1-9有兩張),其餘6張(24張)為功能牌:"skip"(跳牌)、"draw two"(罰牌2張)及"reverse"(反轉出牌方向),每種各2張。另有黑色特別牌8張:"wild"(轉色)及"wild draw four"(轉色及罰牌4張),每種各4張。
出牌時客戶端保存著自己的手牌handcard數組,handcard數組的元素Card來源必須是調用Card::getCardById()得到的卡牌,或是從Card::getAllCards()數組中取得的卡牌。
4.3. AI模塊
本設計使用了簡單AI設計,Aithread類繼承playerThread類,Aithread為計算機提供簡單的與玩家對戰的策略。實際上,AI會利用 getPlayerValidCards() 來計算可以用來出牌的手牌的ID,當然在大部分情況下,可以用來出牌的手牌不止一張,這時,選擇隨機的選擇出牌ID。如果抽中的牌是功能牌,比如萬能牌(Wild),則需要AI隨機的為功能牌分配顏色。最後,如果沒有能夠打出去的手牌,則Ai選擇抽取一張牌。
4.4. 網絡邏輯:
通過UDP廣播發現局域網中可能存在的其他玩家,由對方建立TCP連接加入遊戲房間;通過遠程玩家類實現遠程操作。以下是通信設計:
4.4.1. 遊戲階段
房間創建
角色:遊戲房間創建者(同時作為服務端和客戶端)、其他的遊戲參與者(客戶端)
遊戲房間創建時不用設定人數,但系統限制最多8人,是由遊戲房間創建者保存目前加入的人數。
其他的遊戲參與者需要進入房間,進入房間時向遊戲房間的創建者告知自己已經加入,並攜帶自己的player_name
,由服務端保存
加入房間
客戶端加入房間時:JOIN_ROOM <player_name>
若房間未滿,服務端向客戶端迴應確認加入的數據包,分配player_id
,並在迴應數據包中攜帶當前的玩家數量player_count
(包含當前玩家)、當前玩家的列表(包含當前玩家)。
客戶端保存此player_id
作為與服務器通信的憑據標識,保存player_count
、以及玩家列表顯示在UI界面當中
JOIN_ACK <player_count> <player_id>\n
<player_id> <playername>\n
<player_id> <playername>\n
<player_id> <playername>\n
EOF
然後服務端再向所有已經在房間內的其他玩家宣告此玩家加入了遊戲,其他玩家需要更新自己存儲的玩家列表,並顯示在UI當中。
交互規範:NEWPLAYER <player_id> <playername>
-
player_id
表示新來的玩家的id,playername
就是新來的玩家的名字
雙向心跳檢測
在服務端與客戶端啟動之後,就應該啟動雙向心跳檢測,有新的客戶端進入房間,則新客戶端與服務端直接也要啟動雙向心跳檢測,遊戲過程中亦應該保持心跳檢測。
交互規範:
- 服務端廣播:
SERVERKEEPALIVE
- 客戶端廣播:
CLIENTKEEPALIVE <player_id>
若發生客戶端檢測到服務端掉線,則直接返回開始界面
服務端檢測到某客戶端掉線,則向所有其他玩家廣播此客戶端掉線,參數為掉線的客戶端的id
。
交互規範:PLAYERLEAVE <player_id>
若在遊戲準備階段掉線,則服務端、每個客戶端需從玩家列表從移除此玩家。
若在遊戲中掉線,則直接結束遊戲。
遊戲開始
遊戲房間創建者啟動遊戲時,向所有玩家進行廣播GAMESTART
,沒有參數
遊戲房間創建者即服務端,就默認作為第一個出牌
每次出牌時,玩家向服務器提交出牌信息,服務器直接向場上所有玩家轉發此玩家的出牌信息。
關於卡牌的定義在cards.h
文件中
每副遊戲牌共有108張卡牌,遊戲牌分四種顏色:紅色、綠色、藍色及黃色,每種顏色各有25張牌(合共100張),其中19張為數字牌(0牌有一張,1-9有兩張),其餘6張(24張)為功能牌:"skip"(跳牌)、"draw two"(罰牌2張)及"reverse"(反轉出牌方向),每種各2張。另有黑色特別牌8張:"wild"(轉色)及"wild draw four"(轉色及罰牌4張),每種各4張。
出牌時客戶端保存著自己的手牌handcard
數組,handcard
數組的元素Card
來源必須是調用Card::getCardById()
得到的卡牌,或是從Card::getAllCards()
數組中取得的卡牌。
網絡交互時,從牌堆數組取出卡牌card
,調用card.getCardId()
取出card_id
後才可以交給Messagefactory
進行處理。同樣地,從MessageExtractor
獲取的card_id
,需要通過Card::getCardById(card_id)
才能使用。
交互規範:
PLAYERACTION <player_id>\n
DRAWCARD <card_id>\n
PUTCARD <card_id>\n
EOF
-
PLAYERACTION
作為標識符,後面跟著player_id
代表這是對應哪個玩家的行動 - 第二行的
DRAWCARD
、第三行的PUTCARD
的後面都跟著卡牌的card_id
,表示抽了或者出了某張牌。 - 沒有抽牌或者沒有出牌,對應的
card_id
應該填-1
- 若是把抽了的牌直接出牌使用,則第二行的
DRAWCARD
、第三行的PUTCARD
的後面跟著卡牌的card_id
相等。
通過卡牌的card_id
獲取相應的卡牌,可以通過Card::getCardById()
,然後再調用getCardType
。
調用示例:
Card handcard[3]={
Card::getCardById(0),
Card::getCardById(5),
Card::getCardById(26)
};
assert(Card::getCardById(1) == Card(Card::COLOR::RED, Card::CARD_TYPE::NUMBERIC, 1));
assert(handcard[0] == Card::getCardById(0));
4.4.2. 交互規範
定義的詳細交互規範見下表:
Identifier | Factory | Extractor | ExtractResult Class |
---|---|---|---|
JOIN_ROOM |
join_room_factory(std::string) |
join_room_extractor(const char * message) |
ResultJoinROOM |
JOIN_ACK |
join_ack_factory(int player_count, int player_id, std::map<int, std::string> & player_map) |
join_ack_extractor(const char * message) |
ResultJoinACK |
NEWPLAYER |
newplayer_factory(int new_player_id, std::string player_name) |
join_ack_extractor(const char * message) |
ResultNewPlayer |
PLAYERLEAVE |
playerleave_factory(int player_id) |
join_ack_extractor(const char * message) |
ResultPlayerLeave |
GAMESTART |
gamestart_factory() |
gamestart_extractor() |
ResultGameStart |
CLIENTKEEPALIVE |
client_keepalive_factory(int player_id) |
client_keepalive_extractor(const char * message) |
ResultClientKeepAlive |
SERVERKEEPALIVE |
server_keepalive_factory() |
server_keepalive_extractor() |
ResultServerKeepAlive |
PLAYERACTION |
player_action_factory(int player_id, int draw_card_id, int put_card_id) |
player_action_extractor(const char * message) |
ResultPlayerAction |
5. 程序運行界面
5.1. 開始界面
5.2. 設置界面
5.3. 等待界面
5.4. 遊戲主界面
5.5. 選擇卡牌
6. 總結
本次課程設計是一次綜合性的大型程序設計,UNO遊戲的開發涉及了計算機網絡、面向對象設計思維、Qt圖形框架等多個方面。本次的程序設計過程中遇到了很多問題,,這些問題不僅僅有知識儲備的不足與對於已掌握的知識的應用不熟練,也存在於多人協作的溝通交流問題。問題的解決過程中我們收穫了許多,歸納的來說,可以歸結為以下三點:
- 加深對面向對象思想的理解
與傳統面向過程開發過程有所不同的是,面向對象思維要求我們抽象化對象,將程序的操作對象與所要執行的過程轉化為類與類屬性。本次程序設計中我們使用了大量的類、繼承、虛函數、重載、模板大大提高了程序的複用性,也對於程序開發過程提供了很大的方便。
- 現代開發理念
在一個涉及多種模塊的程序中,更為現代的開發方式是封裝。確定程序的架構後,將不同的模塊封裝起來,一方面易於組合使用模塊,另一方面明確清晰的接口也是高效合作的基石。
- 開發工具與多人協作
現代程序開發不是單打獨鬥,多人遠程協作與項目管理是程序規範開發中非常重要的一環。本次程序設計中使用Git進行版本控制和項目管理為程序的開發帶來了極大的方便,也讓我們加深對實際軟件的工程過程的理解。
7. 程序使用說明
- 進入遊戲時,可選擇遊戲模式一、模式二、退出;模式二有搶出牌的設計,同時發牌更少,遊戲速度更快;
- 點擊模式一或模式二進入設置界面,可以選擇當前玩家姓名、遊戲人數;
- 點擊確定後進入等待連接界面,如果是單機遊戲可直接點擊開始;網絡多人聯機需要在此界面等待他人加入;另外的玩家可在開始界面選擇網絡遊戲按鈕,嘗試連接,準備好之後點擊開始;
- 進入遊戲後,可點擊卡牌,並按按鈕選擇出牌;或在只有一張牌的時候喊uno;如果沒牌可出,則自動摸牌;(具體可參考遊戲規則)
8. 系統開發日誌
系統開發日誌---後端
2020/05/19-05/23
1.蒐集整理資料,瞭解大致的框架和所需的類。
2.為了完善功能,發現需要增加喊uno和判斷某玩家是否勝利的函數。
2020/0524-05/31
1.設計所需的類,以及類之間的繼承關係。
2.設計各個類包含的函數,是實現功能的基礎。
2020/06/01-06/05
1.完成部分類中邏輯較為獨立的函數。
2.完成各個類的頭文件部分。
2020/06/06-06/10
1.編寫測試程序與命令行主函數
2.調試運行代碼,改正語法bug。
2020/06/10-06/15
1.測試改正邏輯bug與完善優化運行情況。
2.優化程序接口
系統開發日誌---前端
2020.5.19 —— 2020.6.5
實現:
- 確定前端界面的UI風格
- 製作前端界面需要的素材
問題:難以找到統一風格的素材元件
解決:通過部分素材,自己製作相關風格的素材
2020.6.6——2020.6.12
實現:
- 設計界面顯示需要的類,同時選取合適的素材作為背景
- 實現初始界面以及設置界面,可以通過按鈕實現界面的切換,可以通過輸入字符串得到用戶名。
問題:初始對於QT的熟悉度較低,實現圖片描繪和界面切換時時常出現bug
解決:通過QT教程及相關學習教程熟悉了基本函數,對於相關函數的功能有了更多瞭解,可以靈活地消除bug
2020.6.13——2020.6.25
實現:
- 實現準備載入界面的設計
- 設計遊戲界面需要的玩家和卡牌類
問題:同一界面按鈕的切換實現起來出現問題較多,玩家及卡牌類設計相關信息較多,經常忽略信息
解決:多運用動態數組實現對對象的獲取
2020.6.26——2020.7.2
實現:
- 實現遊戲主界面的設計
- 實現遊戲界面內部不同操作的效果
- 實現勝利界面的設計
問題:遊戲界面相較於之前界面變化較多,設計信息較多,較多的變化導致圖片描繪經常出現bug,特定卡牌出現的效果較為複雜
解決:將繼承性貫徹到底,將主類和子類的函數實現分配合理