iT邦幫忙

2022 iThome 鐵人賽

DAY 23
0

今日目標,「準備」功能。

今天我們要透過 WebSocket 更新房間內成員的準備狀態,聽起來很簡單,但還是有些細節要注意

  1. 判斷準備的依據,準備的時候要變成沒準備,沒準備要變成準備
  2. 房主應該一直都是未準備狀態,直到房間人數滿,且所有人都準備就開始遊戲
  3. 房主點了開始遊戲的時候,如果有人還沒準備或成員沒滿,應該要有錯誤訊息的回饋
  4. 有任何人的準備狀態改變,都需要將最新的狀態傳給該房間的所有人

再來就實作吧~~
對於房主的「開始」,小弟依然當作「準備」在處理,但比較不同的是,如果人數沒有滿或有人沒準備,那房主的準備狀態就會被取消掉,當所有人都準備(包含房主),才會開始遊戲。

  1. 定義 Client 傳遞過來的資料格式,在 room package 底下建立一個 java class,名稱為 UserReadyMessage,完整內容為:
    package com.example.room;
    
    import lombok.Getter;
    import lombok.Setter;
    
    @Getter @Setter
    public class UserReadyMessage {
        private boolean ready;
    }
    
    • 用 True or False 來決定使用者現在的狀態
  2. 定義 Server 回傳的資料格式,在 room package 底下建立一個 java class,名稱為 UserReadyResponse,完整內容為:
    package com.example.room;
    
    import lombok.Getter;
    import lombok.Setter;
    
    import java.util.HashMap;
    import java.util.Map;
    
    public class UserReadyResponse {
        @Getter
        private final Map<String, Boolean> userReadyStatus = new HashMap<>();
    
        @Getter @Setter
        private boolean allReady = false;
    
        @Getter @Setter
        private String message = "";
    
        public void add(String username, boolean isReady) {
            this.userReadyStatus.put(username, isReady);
        }
    }
    
    • userReadyStatus:儲存使用者的準備狀態
    • allReady:是否全部人都準備
    • message:訊息回饋
  3. 在 RoomWsController 加入 readyToPlay(),加入的片段程式碼為:
    @MessageMapping("/ready")
    public void readyToPlay(UserReadyMessage readyMessage, Principal principal) {
        // 設定使用者的準備狀態
        String username = principal.getName();
        this.userStatus.setUserReady(username, readyMessage.isReady());
    
        // 取出所有房間內的成員
        String roomId = this.userStatus.getUserRoomId(username);
        Room room = roomList.getRoomById(roomId);
        ArrayList<String> roomMembers = room.getAllMembers();
    
        UserReadyResponse response = new UserReadyResponse();
    
        // counter 用於確定是否全部都準備了,如果不是就會給予回饋
        int counter = 0;
        for (String member : roomMembers) {
            boolean isReady = this.userStatus.isUserReady(member);
            if (isReady) {
                counter += 1;
            }
            response.add(member, isReady);
        }
    
        // 如果沒有全部都準備好,就把房主的準備狀態取消
        String owner = room.getOwner();
        if (counter == 4) {
            response.setAllReady(true);
        }
        else {
            this.userStatus.setUserReady(owner, false);
            response.add(owner, false);
        }
    
        // 設定回饋給所有成員的訊息
        for (String member : roomMembers) {
            response.setMessage("");
            if (counter != 4 && member.equals(username)) {
                if (member.equals(owner)) {
                    if (room.count() == 4) {
                        response.setMessage("尚有人未準備好開始遊戲!");
                    }
                    else {
                        response.setMessage("人數不足!");
                    }
                }
                else {
                    if (!readyMessage.isReady()) {
                        response.setMessage("請快點準備!");
                    }
                }
            }
    
            // 個別發送訊息
            simpMessagingTemplate.convertAndSendToUser(member, "/queue/ready", response);
        }
    }
    
    • @MessageMapping("/ready"):定義接收資料的 url 為 ready
    • 參數 Principal:其實是前面 SecurityContext 的 Authentication 的 Principal,用於保存登入資訊,所以在 RoomController 同樣可以使用這個參數來取得目前的使用者名稱
  4. 修改 room.js 內容,訂閱 /user/queue/ready (因為要做單播),並定義 ready() 的行為,完整內容為:
    var websocket = new WebSocket();
    var myUsername = $("#my-username").val();
    var roomId;
    var owner;
    
    websocket.connect('/connect', () => {
        subscribeRoomInfo();
        subscribeReady();  // 記得加入
        websocket.send(`/room-info`, {});
    });
    
    
    function subscribeRoomInfo() {
        websocket.subscribe("/user/queue/room-info", (response) => {
            response = JSON.parse(response.body);
            let roomInfo = response.roomInfo;
            owner = roomInfo.owner;
            roomId = roomInfo.roomId;
            $("#room-id").text(roomId);
    
            let roomMembers = [owner, ...roomInfo.guests]
            if (!roomMembers.includes(myUsername)) {
                window.location.href = `/rooms/`;
            }
    
            let userStatus = response.userStatus;
            $("#room .card").each((index, element) => {
                let name = roomMembers[index];
                if (name === myUsername) {
                    $(element).addClass("shadow rounded");
                }
    
                let closeButton = "";
                if (owner === myUsername  && name !== myUsername) {
                    closeButton = `
                    <button type="button" class="close close-button" onclick="quitRoom('${name}')">
                        <i class="bi bi-x-circle-fill"></i>
                    </button>
                    `;
                }
    
                let readyText = "";
                if (userStatus[name]) {
                    readyText = "準備";
                }
                if (name === owner) {
                    readyText = "房主";
                }
    
                $(element).prop("id", `user-${name}`);
                $(element).empty();
                if (index < roomMembers.length) {
                    $(element).html(`
                    ${closeButton}
                    <img src="https://picsum.photos/200/200" class="card-img-top" alt="...">
                    <div class="card-body">
                        <h5 class="card-title">${name}</h5>
                        <h4 class="card-body user-ready">${readyText}</h4>
                    </div>
                    `);
                }
                else {
                    $(element).html(`
                    <div class="card-body">
                        <h5 class="card-title"></h5>
                    </div>
                    `);
                }
            })
    
            $("#ready").text(owner === myUsername ? '開始' : '準備');
        })
    }
    
    function subscribeReady() {
        // 訂閱
        websocket.subscribe("/user/queue/ready", (response) => {
            response = JSON.parse(response.body);
            let userReadyStatus = response.userReadyStatus;
    
            // 顯示準備狀態的文字,房主額外處理
            for (let user in userReadyStatus) {
                let readyText = userReadyStatus[user] ? '準備' : '';
                if (user === owner) {
                    readyText = "房主";
                }
                $(`#user-${user} .user-ready`).text(readyText);
            }
    
            if (response.allReady) {
                // TODO: 開始遊戲
            }
        })
    }
    
    function ready() {
        let myUsername = $("#my-username").val();
    
        // 判斷依據: 看是不是「準備」
        let status = $(`#user-${myUsername} .user-ready`).text() === '準備' ? true : false;
        websocket.send(`/ready`, {
            ready : !status,
        });
    }
    
  5. 來去試試吧~~ 一樣用兩個瀏覽器,一個建立房間,另一個加入,點「準備」看是不是有出現和取消 (ps. 目前還沒處理訊息回饋,所以只能從 console 查看)

準備功能也好了,整個遊戲已經完成一半了~~ 加油,只剩下一週!
/images/emoticon/emoticon18.gif


上一篇
Day 21 - 即時更新房間資訊
下一篇
Day 23 - 滾出我的房間!
系列文
Spring Boot... 深不可測31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言