本文公眾號來源:后端技術漫談?
作者:蠻三刀把刀
前言
前兩章教程,我們使用WebSocket的基礎特性打造了一個小小聊天室,并在第二章對其進行了集群化改造。
系列教程回顧:
手把手搭建WebSocket多人在線聊天室
【多人聊天室】WebSocket集群/分布式改造
在本文中,我將介紹如何使用WebSocket向實時多人答題對戰游戲提供服務端,并詳細介紹通接口的設計。
這是我在最近作業競賽中設計的小項目,和小伙伴們一起設計了整個游戲流程和后端代碼,前端頁面暫時就不放開給大家了,大家可以參考前兩章教程自己動手寫一下前端頁面。
本文內容摘要:
在線游戲常用的通訊方案
如何使用WebSocket實現游戲對戰實時通信
游戲步驟的畫面演示和對應的WebSocket接口設計
本文源碼:(媽媽再也不用擔心我無法復現文章代碼啦)
https://github.com/qqxx6661/websocket-game-demo
正文
WebSocket實現在線多人游戲——對戰答題
在線游戲常用的通訊方案
參考:
https://blog.csdn.net/honey199396/article/details/54603860
HTTP
優點:協議較成熟,應用廣泛、基于TCP/IP,擁有TCP優點、研發成本很低,開發快速、開源軟件較多,nginx,apache,tomact等
缺點:無狀態無連接、只有PULL模式,不支持PUSH、數據報文較大
特性:基于TCP/IP應用層協議、無狀態,無連接、支持C/S模式、適用于文本傳輸
TCP
優點:可靠性 、全雙工協議、開源支持多、應用較廣泛、面向連接、研發成本低、報文內容不限制(IP層自動分包,重傳,不大于1452bytes)
缺點:操作系統:較耗內存,支持連接數有限、設計:協議較復雜,自定義應用層協議、網絡:網絡差情況下延遲較高、傳輸:效率低于UDP協議
特性:面向連接、可靠性、全雙工協議、基于IP層、OSI參考模型位于傳輸層、適用于二進制傳輸
WebScoket
優點:協議較成熟、基于TCP/IP,擁有TCP優點、數據報文較小,包頭非常小、面向連接,有狀態協議、開源較多,開發較快
缺點:
特性:有狀態,面向連接、數據報頭較小、適用于WEB3.0,以及其他即時聯網通訊
UDP
優點:操作系統:并發高,內存消耗較低、傳輸:效率高,網絡延遲低、傳輸模型簡單,研發成本低
缺點:協議不可靠、單向協議、開源支持少、報文內容有限,不能大于1464bytes、設計:協議設計較復雜、網絡:網絡差,而且丟數據報文
特性:無連接,不可靠,基于IP協議層,OSI參考模型位于傳輸層,最大努力交付,適用于二進制傳輸
總結
對于弱聯網類游戲,必須消除類的,卡牌類的,可以直接HTTP協議,考慮安全的話直接HTTPS,或者對內容體做對稱加密;
對于實時性,交互性要求較高,可以優先選擇Websocket,其次TCP協議;
對于實時性要求極高,且可達性要求一般可以選擇UDP協議;
局域網對戰類,賽車類,直接來UDP協議吧;
WebSocket實現雙人在線游戲實時通信
我們采用websocket作為我們的通信方案,主要是因為我們希望對戰雙方能夠實時顯示對方的得分。
本小節詳細介紹了我們在線問答對戰游戲中,具體的websocket通訊方式定義。
本問答游戲規則如下:
用戶打開h5頁面后,輸入自己的昵稱,發送給服務端,服務端將用戶昵稱保存到hashmap,并記錄用戶狀態(空閑,游戲中),接著用戶進入大廳。
大廳中用戶可以互相選擇,一旦某用戶選擇了另一位用戶,將觸發開始游戲,雙方進入答題模式。
答題的兩位用戶各回答10題,每題答對為10分,共100分,左上角頁面顯示自己的分數,右上角顯示對方分數,實時通過websocket接收對方分數。
10題結束,雙方等待對方總分,最后判斷輸贏,顯示結果界面。
所以我們需要設計三個WebSocket協議:
用戶創建昵稱,進入玩家大廳
用戶選擇對手,雙方進入游戲
對戰過程實時顯示雙方分數
接下來詳細介紹這三種WebSocket接口
用戶創建昵稱,進入玩家大廳
打開界面,進入游戲:

我們使用了HashMap存儲用戶狀態,
private?Map<String,?StatusEnum>?userToStatus?=?new?HashMap<>();
用戶狀態分為空閑和游戲中:
public?enum?StatusEnum?{
????IDLE,
????IN_GAME
}
WebSocket接口設計如下:

WebSocket接口代碼如下:
@MessageMapping("/game.add_user")
????@SendTo("/topic/game")
????public?MessageReply?addUser(@Payload?ChatMessage?chatMessage,?SimpMessageHeaderAccessor?headerAccessor)?throws?JsonProcessingException?{
????????MessageReply?message?=?new?MessageReply();
????????String?sender?=?chatMessage.getSender();
????????ChatMessage?result?=?new?ChatMessage();
????????result.setType(MessageTypeEnum.ADD_USER);
????????result.setReceiver(Collections.singletonList(sender));
????????if?(userToStatus.containsKey(sender))?{
????????????message.setCode(201);
????????????message.setStatus("該用戶名已存在");
????????????message.setChatMessage(result);
????????????log.warn("addUser["?+?sender?+?"]:?"?+?message.toString());
????????}?else?{
????????????result.setContent(mapper.writeValueAsString(userToStatus.keySet().stream().filter(k?->?userToStatus.get(k).equals(StatusEnum.IDLE)).toArray()));
????????????message.setCode(200);
????????????message.setStatus("成功");
????????????message.setChatMessage(result);
????????????userToStatus.put(sender,?StatusEnum.IDLE);
????????????headerAccessor.getSessionAttributes().put("username",sender);
????????????log.warn("addUser["?+?sender?+?"]:?"?+?message.toString());
????????}
????????return?message;
????}
用戶選擇對手,雙方進入游戲
在大廳中選擇玩家,隨后會進入對戰:
我們使用了HashMap存儲了正在對戰的用戶,給雙方配對。
private?Map<String,?String>?userToPlay?=?new?HashMap<>();
WebSocket接口設計如下:

WebSocket接口代碼如下:
@MessageMapping("/game.choose_user")
????@SendTo("/topic/game")
????public?MessageReply?chooseUser(@Payload?ChatMessage?chatMessage)?throws?JsonProcessingException?{
????????MessageReply?message?=?new?MessageReply();
????????String?receiver?=?chatMessage.getContent();
????????String?sender?=?chatMessage.getSender();
????????ChatMessage?result?=?new?ChatMessage();
????????result.setType(MessageTypeEnum.CHOOSE_USER);
????????if?(userToStatus.containsKey(receiver)?&&?userToStatus.get(receiver).equals(StatusEnum.IDLE))?{
????????????List?list=new?ArrayList<>();
????????????questionService.getQuestions(limit).forEach(item->{
????????????????QuestionRelayDTO?relayDTO=new?QuestionRelayDTO();
????????????????relayDTO.setTopic_id(item.getId());
????????????????relayDTO.setTopic_name(item.getQuestion());
????????????????List?answers=new?ArrayList<>();
????????????????answers.add(new?Answer(1,item.getId(),item.getOptionA(),item.getResult()==1?1:0));
????????????????answers.add(new?Answer(2,item.getId(),item.getOptionB(),item.getResult()==2?1:0));
????????????????answers.add(new?Answer(3,item.getId(),item.getOptionC(),item.getResult()==3?1:0));
????????????????answers.add(new?Answer(4,item.getId(),item.getOptionD(),item.getResult()==4?1:0));
????????????????relayDTO.setTopic_answer(answers);
????????????????list.add(relayDTO);
????????????});
????????????result.setContent(mapper.writeValueAsString(list));
????????????result.setReceiver(Arrays.asList(sender,?receiver));
????????????message.setCode(200);
????????????message.setStatus("匹配成功");
????????????message.setChatMessage(result);
????????????userToStatus.put(receiver,?StatusEnum.IN_GAME);
????????????userToStatus.put(sender,?StatusEnum.IN_GAME);
????????????userToPlay.put(receiver,sender);
????????????userToPlay.put(sender,receiver);
????????????log.warn("chooseUser["?+?sender?+?","?+?receiver?+?"]:?"?+?message.toString());
????????}?else?{
????????????result.setContent(mapper.writeValueAsString(userToStatus.keySet().stream().filter(k?->?userToStatus.get(k).equals(StatusEnum.IDLE)).toArray()));
????????????result.setReceiver(Collections.singletonList(sender));
????????????message.setCode(202);
????????????message.setStatus("該用戶不存在或已在游戲中");
????????????message.setChatMessage(result);
????????????log.warn("chooseUser["?+?sender?+?"]:?"?+?message.toString());
????????}return?message;
????}
對戰過程實時顯示雙方分數
對戰過程中的演示圖:左邊顯示我方分數,右邊顯示對方分數

WebSocket接口設計如下:

WebSocket接口代碼如下:
@MessageMapping("/game.do_exam")
????@SendTo("/topic/game")
????public?MessageReply?doExam(@Payload?ChatMessage?chatMessage)?throws?JsonProcessingException?{
????????MessageReply?message?=?new?MessageReply();
????????String?sender?=?chatMessage.getSender();
????????String?receiver?=?userToPlay.get(sender);
????????ChatMessage?result?=?new?ChatMessage();
????????result.setType(MessageTypeEnum.DO_EXAM);
????????log.warn("userToStatus:"?+?mapper.writeValueAsString(userToStatus));
????????if?(userToStatus.containsKey(receiver)?&&?userToStatus.get(receiver).equals(StatusEnum.IN_GAME))?{
????????????result.setContent(chatMessage.getContent());
????????????result.setSender(sender);
????????????result.setReceiver(Collections.singletonList(receiver));
????????????message.setCode(200);
????????????message.setStatus("成功");
????????????message.setChatMessage(result);
????????????log.warn("doExam["?+?receiver?+?"]:?"?+?message.toString());
????????}else{
????????????result.setReceiver(Collections.singletonList(sender));
????????????message.setCode(203);
????????????message.setStatus("該用戶不存在或已退出游戲");
????????????message.setChatMessage(result);
????????????log.warn("doExam["?+?sender?+?"]:?"?+?message.toString());
????????}
????????return?message;
????}
進一步
這個只是個兩天趕出來的Demo,當然里成品還有非常大的差距。這里有幾個需要繼續解決的事情:
實現自動匹配/排行榜
WebSocket通訊優化:在某些地方使用點對點通訊,而非全部使用廣播通訊。
我們可以使用convertAndSendToUser()方法,按照名字就可以判斷出來,convertAndSendToUser()方法能夠讓我們給特定用戶發送消息。
spring webscoket能識別帶”/user”的訂閱路徑并做出處理,例如,如果瀏覽器客戶端,訂閱了’/user/topic/greetings’這條路徑,
stompClient.subscribe('/user/topic/greetings',?function(data)?{
????//...
});
就會被spring websocket利用UserDestinationMessageHandler進行轉化成”/topic/greetings-usererbgz2rq”,”usererbgz2rq”中,user是關鍵字,erbgz2rq是sessionid,這樣子就把用戶和訂閱路徑唯一的匹配起來了.
參考文獻
點對點通訊:
https://blog.csdn.net/yingxiake/article/details/51224569
總結
我們在本文中實現了在線多人對戰游戲的服務端WebSocket接口設計,進一步鞏固了對WebSocket的基礎和應用范圍的理解。
本文工程源代碼:
https://github.com/qqxx6661/websocket-game-demo
公眾號文章導航:兩年嘔心瀝血的文章!

長按掃碼可關注獲取?
在看和分享對我非常重要!