從零開始實現放置遊戲(十四)——實現戰鬥掛機(5)地圖移動和聊天
上一節添加了websocket元件,實現了前後端通訊。後面我們只需要根據遊戲的業務邏輯,逐步實現各種功能即可。
另外,在實現具體業務邏輯時,發現上一章設計的訊息物件有些不合理,由於粒度過粗,導致可以複用的部分很少,且這裡的通訊模型並不是一個請求對應一個響應的模式。比如:玩家a從地圖A移動到地圖B。此時,a傳送移動請求。伺服器返回B地圖的資訊和線上列表給A。同時還要傳送最新的線上列表給地圖B的其他玩家b,c,d....這裡其他玩家並沒有傳送請求,但收到了響應訊息。因此,將訊息型別重構成由客戶端發出的訊息和由服務端發出的訊息兩類,分別以"3000"和"6000"開頭。
const MessageCode = { // 客戶端傳送的訊息型別 CLoadCache: "30000001", // 快取載入 CLogin: "30001001", // 登陸 CLoadMap: "30001002", // 讀取地圖資訊 CLoadOnline: "30001003", // 讀取線上列表 CChat: "30002001", // 聊天 CMove: "30002002", // 地圖移動 // 服務端傳送的訊息型別 SLoadCache: "60000001", // 快取載入 SLoadMap: "60001002", // 讀取地圖資訊 SLoadOnline: "60001003", // 讀取線上列表 SChat: "60002001", // 聊天 };
玩家登陸
進入遊戲主介面,socket建立連線時,即傳送登陸訊息。主要邏輯包括:
1.載入玩家角色資訊(包括所在地圖ID等),將玩家資訊,session資訊等快取到伺服器。
2.載入玩家所在地圖資訊(地圖說明、地圖怪物列表,線上玩家列表等)傳送至客戶端
3.通知玩家所在地圖的其他玩家更新線上列表
地圖移動
玩家在地圖上的移動,這裡客戶端先通過點選圖片上對應的其他地圖位置的錨點來實現。當然後面也可以通過給出列表選單讓玩家選擇來實現。
具體實現程式碼類似如下,給img標籤錨定一組座標,滑鼠點選座標所在圖形範圍,即可觸發事件。這裡錨點的資料,通過定義類MapCoord,配置到後臺,動態讀出。
<!-- 地圖圖片和錨點 --> <img id="mapImg" src="/images/wow/map/${map.name}.jpg" width="100%" height="100%;" style="opacity: 0.8;border-radius: 10px;" usemap="#map-coords"/> <map id="map-coords" name="map-coords"> <area shape="circle" coords="35, 160, 20" onclick="wowClient.move('19');" href="javascript:void(0);" alt="西部荒野" title="西部荒野"/> </map>
關於移動的業務邏輯,以玩家a從地圖A移動到地圖B為例,主要包括以下幾點:
服務端:
1.更資訊伺服器中的快取資料(玩家A的角色資訊資料,所在地圖ID更新 為 地圖B的ID, 地圖A、B的線上玩家列表更新)
客戶端:
1.更新玩家a的地圖資訊到地圖B
2.1)更新玩家a的當前地圖B的線上玩家列表
2.2)更新玩家a的當前地圖B的怪物列表
3.更新地圖A的所有玩家的線上列表(從中移除玩家A)
4.更新地圖B的所有玩家的線上列表(從中新增玩家A)(這一步,地圖B的所有玩家其實已經包含了玩家A,所以2.1可以省略)
後臺訊息處理邏輯主要如下:
private void handleMoveMessage(Session session, CMoveMessage message) { Character character = GameWorld.OnlineCharacter.get(session.getId()); String fromMapId = character.getMapId(); String destMapId = message.getDestMapId(); character.setMapId(destMapId); GameWorld.MapCharacter.get(fromMapId).remove(character); GameWorld.MapCharacter.get(destMapId).add(character); GameWorld.OnlineCharacter.get(session.getId()).setMapId(destMapId); // 通知玩家更新地圖資訊 this.sendLoadMap(session, destMapId); // 通知原地圖玩家更新線上列表 this.sendLoadOnlineToMap(fromMapId); // 通知目標地圖玩家更新線上列表 this.sendLoadOnlineToMap(destMapId); } /** * 傳送載入地圖訊息 * * @param session session * @param mapId 地圖id */ private void sendLoadMap(Session session, String mapId) { WowMessageHeader header = new WowMessageHeader(WowMessageCode.SLoadMap); MapInfoVO mapInfoVO = this.loadMapInfo(mapId); SLoadMapMessage content = new SLoadMapMessage(); content.setMapInfo(mapInfoVO); WowMessage<SLoadMapMessage> wowMessage = new WowMessage<>(header, content); this.sendOne(session, wowMessage); } /** * 傳送載入線上列表訊息給指定地圖的玩家 * * @param mapId 地圖id */ private void sendLoadOnlineToMap(String mapId) { WowMessageHeader header = new WowMessageHeader(WowMessageCode.SLoadOnline); OnlineInfoVO onlineInfoVO = this.loadOnlineInfo(mapId); SLoadOnlineMessage content = new SLoadOnlineMessage(); content.setOnlineInfo(onlineInfoVO); WowMessage<SLoadOnlineMessage> wowMessageLoadOnline = new WowMessage<>(header, content); List<Character> mapChars = GameWorld.MapCharacter.get(mapId); for (Character mapChar : mapChars) { this.sendOne(GameWorld.OnlineSession.get(mapChar.getId()), wowMessageLoadOnline); } }
聊天
目前主要實現3種聊天頻道:【本地】、【世界】、【私聊】。
這裡有一點注意的是,玩家A傳送訊息後,聊天記錄應該立即顯示在A的客戶端上,還是在訊息傳送成功後才顯示。我選擇的是後者,考慮到如果訊息傳送時,B已經下線了,訊息傳送失敗卻仍顯示了聊天記錄,則顯得不合理。
在處理本地、世界頻道聊天邏輯時,A作為本地和世界線上列表的一員,正常接收訊息處理即可。
在處理私聊頻道聊天時,因為訊息是傳送給B的,B的客戶端能正常顯示。但A並未接收任何聊天訊息,所以不會顯示自己發出去的私聊資訊,這裡就需要給A也返回一條訊息,通知客戶端顯示聊天記錄,或者通知其B已下線聊天傳送失敗。
考慮到遇到A給B傳送聊天訊息時,B剛好下線,訊息傳送失敗,這種情況應該有一種錯誤提示的訊息型別和處理邏輯,目前暫未實現,列到todo列表。
聊天訊息的處理邏輯目前如下:
private void handleChatMessage(Session session, CChatMessage message) { Character character = GameWorld.OnlineCharacter.get(session.getId()); WowMessageHeader header = new WowMessageHeader(WowMessageCode.SChat); SChatMessage response = new SChatMessage(); response.setSendId(character.getId()); response.setSendName(character.getName()); response.setRecvId(message.getRecvId()); response.setRecvName(message.getRecvName()); response.setMessage(message.getMessage()); response.setChannel(message.getChannel()); WowMessage wowMessage = new WowMessage<>(header, response); String chatChannel = message.getChannel(); if (chatChannel.equals(GameConst.ChatChannel.Local)) { List<Character> mapChars = GameWorld.MapCharacter.get(character.getMapId()); for (Character mapChar : mapChars) { Session recvSession = GameWorld.OnlineSession.get(mapChar.getId()); if (recvSession != null && recvSession.isOpen()) { this.sendOne(recvSession, wowMessage); } } } else if (chatChannel.equals(GameConst.ChatChannel.World)) { this.sendAll(wowMessage); } else if (chatChannel.equals(GameConst.ChatChannel.Whisper)) { Session recvSession = GameWorld.OnlineSession.get(message.getRecvId()); if (recvSession != null && recvSession.isOpen()) { this.sendOne(session, wowMessage); this.sendOne(recvSession, wowMessage); } else { // todo 傳送錯誤訊息 } } else { // todo 其他頻道聊天待實現 } } /** * 給指定客戶端傳送訊息 * * @param session 客戶端session * @param wowMessage 訊息物件 */ private void sendOne(Session session, WowMessage wowMessage) { try { String message = JSON.toJSONString(wowMessage); session.getBasicRemote().sendText(message); } catch (Exception ex) { logger.error(ex.getMessage(), ex); } } /** * 給所有客戶端傳送訊息 * * @param wowMessage 訊息物件 */ private void sendAll(WowMessage wowMessage) { try { String message = JSON.toJSONString(wowMessage); Collection<Session> sessions = GameWorld.OnlineSession.values(); for (Session session : sessions) { session.getBasicRemote().sendText(message); } } catch (Exception ex) { logger.error(ex.getMessage(), ex); } }
其他
除了業務處理邏輯,本章的程式碼還添加了一個模型對映元件DozerMapper,主要用作模型轉換。
因為之前定義的模型都是資料庫對映模型,包含isDelete, createTime, createUser等一些主要用於系統運維的欄位,不需要在通訊時暴露給客戶端,既增加了通訊的資料量,也可能暴露出潛在的風險。因此,對需要通訊的模型,統一建立VO,檢視模型。轉換後,再發送給客戶端。
關於DozerMapper的使用,可以自行看下官方的文件(推薦),比較全面,只是是英文的,或者其他介紹此元件的部落格。
效果演示
這裡我啟用Chrom和360瀏覽器,登入2個不同的賬號,來測試地圖移動和聊天功能,如下圖。
本章小結
本章主要實現了基本功能 地圖移動 和 聊天,架構上新增的dozerMapper元件。
前端也做了部分重構,但並非重點,在原始碼中能看懂,會修改即可。對於未詳細描述的細節可以參看原始碼。
本章原始碼下載地址:https://545c.com/file/14960372-439875280
本文原文地址:https://www.cnblogs.com/lyosaki88/p/idlewow_14.html
專案交流群:329989095 (歡迎因任何原因加群交流)
&n