從零開始實現放置遊戲(十四)——實現戰鬥掛機(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 (歡迎因任何原因加羣交流)

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章