從零開始實現放置遊戲(十四)——實現戰鬥掛機(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組件。
前端也做了部分重構,但並非重點,在源碼中能看懂,會修改即可。對於未詳細描述的細節可以參看源程式碼。
本章源碼下載地址://545c.com/file/14960372-439875280
本文原文地址://www.cnblogs.com/lyosaki88/p/idlewow_14.html
項目交流群:329989095 (歡迎因任何原因加群交流)