1. 程式人生 > >從零開始實現放置遊戲(十四)——實現戰鬥掛機(5)地圖移動和聊天

從零開始實現放置遊戲(十四)——實現戰鬥掛機(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