socket.io, svelte 및 node를 사용하여 멀티플레이어 영화 퀴즈/퀴즈 게임을 만들어 봅시다. 데블로그 #5

"이륙하세요, 우리는 이륙합니다!"



그래서 어제 계획을 세웠는데, 사건의 흐름을 보여주거나 뭐..

오늘 나는 그것을 구현했거나 아직 게임 중간에 연결을 끊는 플레이어 처리를 구현하지 않았지만 다음이 될 것입니다.
전반적으로 일이 순조롭게 진행되었습니다. :) 순서도를 보고 그것을 구현하는 코드를 작성하세요.

이제 게임 시작부터 끝까지 게임 루프 논리에 대한 첫 번째 작업 프로토타입이 있습니다.

내가 한 일을 살펴보자. Game 클래스부터 시작합니다.

const { nanoid } = require('nanoid');

module.exports = class Game {
    constructor({ maxPlayers = 5, rounds = 2 } = {}) {
        this.id = nanoid();
        this.maxPlayers = maxPlayers;
        this.rounds = rounds;
        this.round = 1;
        this.waitBetweenRounds = 5;
        this.roundTime = 30;
        this.status = 'open';
        this.players = new Map();
        this.roundCountDown = null; //will hold the interval timer for the round
        this.answers = { 1: {}, 2: {}, 3: {} }; //for now just store answers here in hardcoded way, probably wld be better if stored in player object.
    }

    startRoundCountDown(io, func) {
        let count = this.roundTime + 1;
        this.roundCountDown = setInterval(() => {
            count--;
            io.to(this.id).emit('count-down', count);
            if (count === 0) {
                this.clearRoundCountDown();
                func(io, this);
            }
        }, 1000);
    }

    clearRoundCountDown() {
        clearInterval(this.roundCountDown);
    }

    join(player) {
        //check if plyer is allowed to join
        if (this.status === 'open' && this.players.size < this.maxPlayers) {
            this.players.set(player.id, player);
            return true;
        }
        return false;
    }

    leave(playerid) {
        this.players.delete(playerid);
    }

    resetPlayerReady() {
        this.players.forEach((player) => {
            player.ready = false;
        });
    }
    howManyPlayersReady() {
        let ready = 0;
        this.players.forEach((player) => {
            if (player.ready) ready++;
        });
        return ready;
    }
    allPlayersHaveAnswered() {
        let noAnswers = 0;
        this.players.forEach((player) => {
            if (this.answers?.[this.round]?.[player.id] !== undefined) {
                noAnswers++;
            }
        });
        return noAnswers === this.players.size;
    }

    getPublicData() {
        return {
            id: this.id,
            round: this.round,
            rounds: this.rounds,
            status: this.status,
        };
    }

    //easier to do stuff on frontend with players as an array instead of a map
    getPlayersAsArray() {
        let playersArr = [];
        //convert the players map to an array.. this could probably be done cleaner and in one line but I am not used to working with maps
        //this will probably be overhauled later
        this.players.forEach((player) => {
            playersArr.push({ ...player });
        });
        return playersArr;
    }

    compileResults() {
        //later use this to compile the results of the game
        return {};
    }
};


몇 가지 속성을 추가했는데 가장 중요한 속성은 roundCountDown입니다. 이 소품은 라운드를 카운트다운하기 위한 간격 타이머를 보유합니다. 클래스에 넣은 이유는 게임 인스턴스에 연결되어야 하고 이벤트 처리 코드의 다른 위치에서 시작하고 지울 수 있어야 하기 때문입니다.

방법을 자세히 살펴 보겠습니다.

startRoundCountDown(io, func) {
        let count = this.roundTime + 1;
        this.roundCountDown = setInterval(() => {
            count--;
            io.to(this.id).emit('count-down', count);
            if (count === 0) {
                this.clearRoundCountDown();
                func(io, this);
            }
        }, 1000);
    }


그것은 io와 함수를 취하는데, 그것이 취하는 함수는 시간이 다 되었거나 모든 플레이어가 답을 제출했을 때 실행해야 하는 함수입니다. 이 함수는 2개의 인수가 필요합니다. io는 이벤트를 발생시킬 수 있고(메서드에 전달되어 이미 사용 가능함) 다른 하나는 게임입니다. 여기서 "this"는 게임이므로 편리합니다.

Ofc는 모든 플레이어가 응답하기 전에 시간이 다 된 경우에만 실행됩니다. 간격 전에 모든 플레이어가 응답하면 중지되고 제거됩니다. 함수를 트리거할 수 있는 다른 코드는 eventHandler에 있습니다.

아래에서 실행되는 기능을 볼 수 있습니다. 이 기능 ofc는 Game 클래스 외부에 있습니다.

function endRound(io, game) {
    game.round++;
    if (game.round > game.rounds) {
        game.status = 'end-game';
        io.to(game.id).emit('end-game', game.compileResults());
        games.delete(game.id);
    } else {
        game.status = 'end-round';
        io.to(game.id).emit('end-round'); //need to send with some reuslts later
        getReady(io, game);
    }
}


아래에는 게임을 실행하는 코드가 있습니다.
게임 생성, 게임 참여 n soo에 대한 내용을 생략했습니다..

따라서 로비에 있는 플레이어가 게임을 시작할 준비가 되면 '플레이어 준비' 이벤트가 전송됩니다.

        socket.on('player-ready', (gameId) => {
            const game = games.get(gameId);

            //maybe we need to do something here later except reurn but probably not, this is a safeguard if socket reconnects n start sending shit when game is in another state
            if (game.status !== 'open' && game.status !== 'waiting-for-start') return;

            //when player is ready shld.. change the ready variable of player
            game.players.get(socket.id).ready = true;
            if (game.status !== 'waiting-for-start') game.status = 'waiting-for-start'; //now we do not accept any new players

            //if half of players are not ready then just return
            if (game.howManyPlayersReady() < game.players.size / 2) return;
            //here shld run a function that is reused everytime a new round starts
            getReady(io, game);
        });



보시다시피 마지막으로 발생하는 것은 getReady 기능을 실행하는 것입니다.
이렇게 하면 게임 시작을 위한 카운트다운이 시작되고 완료되면 '준비 완료'가 표시됩니다.

이 코드는 각 라운드가 완료된 후에도 실행되며 새 라운드에서 계산됩니다.

function getReady(io, game) {
    game.status = 'get-ready';
    game.resetPlayerReady();
    let count = game.waitBetweenRounds + 1;
    const counter = setInterval(countdown, 1000, game.id);

    function countdown(gameId) {
        count--;
        console.log(count);
        io.to(gameId).emit('count-down', count);
        if (count == 0) {
            clearInterval(counter);
            io.to(gameId).emit('ready-round'); //here neeed to send with some junk later.. like question n metadata about it
        }
    }
}


다음으로 발생하는 일은 모든 플레이어 클라이언트가 준비가 되었음을 인정할 때까지 기다리는 것입니다. 그들은 '플레이어 준비 라운드' 이벤트를 전송하여 이를 수행합니다.

아래 코드에서 처리됩니다. 모든 플레이어로부터 준비가 완료되면
'round-start'가 출력되고 처음에 썼던 카운트다운 간격이 시작됩니다.

        socket.on('player-ready-round', (gameId) => {
            const game = games.get(gameId);
            if (game.status !== 'get-ready' && game.status !== 'waiting-for-ready') return;
            if (game.status !== 'waiting-for-ready') game.status = 'waiting-for-ready';
            game.players.get(socket.id).ready = true;
            if (game.howManyPlayersReady() !== game.players.size) return;
            game.status = 'waiting-for-answer';
            io.to(gameId).emit('round-start');
            game.startRoundCountDown(io, endRound);
        });


이제 우리는 라운드를 마칠 때까지 모든 플레이어가 응답하거나 시간이 다하기를 기다립니다(동일한 endRound() 함수는 내가 조금 더 길게 게시했습니다). 이 endRound 함수는 'end-round'를 방출하여 이 라운드를 종료하고 다음 라운드를 준비할지(이전과 동일한 getReady 함수) 또는 'end-game'을 방출하여 게임을 종료할지 결정합니다.

socket.on('answer', (gameId, answer) => {
            const game = games.get(gameId);
            if (game.status !== 'waiting-for-answer') return;
            //store the answer.. for now it's stored in the game object as an object
            game.answers[game.round][socket.id] = answer;
            //check if all players have answered
            if (game.allPlayersHaveAnswered() == false) return;
            //clear the interval for counting down as we now ends the round as all players have answered
            game.clearRoundCountDown();
            //run endRound logic
            endRound(io, game);
        });


그리고 그래, 그게 다야.. 내가 그 차트를 만들었다니 다행이야, 맞아!

프런트엔드 코드는 너무 간단해서 지금은 보여줄 가치조차 없는 것 같지만 여기에 있습니다.

socket.on('count-down', (count) => {
        currentCount = count;
    });

    socket.on('ready-round', () => {
        socket.emit('player-ready-round', $gameProps.id);
    });

    socket.on('round-start', () => {
        $activeComponent = 'question';
    });

    socket.on('end-round', () => {
        $activeComponent = 'roundresult';
    });

    socket.on('end-game', () => {
        $activeComponent = 'gameresult';
    });


대부분은 표시되어야 하는 구성 요소에 대한 저장소를 변경합니다.
모든 카운트다운은 '카운트다운' 리스너에 의해 처리되며 변수를 값으로만 ​​설정합니다. 이 변수는 필요한 구성 요소로 전달됩니다.

나중에 이것을 저장소 변수로 변경할 수 있습니다. 그렇게 하면 모든 소켓 논리를 자체 일반 Javascript 파일로 추출할 수 있어야 합니다. 그러나 라운드, 게임 및 질문의 결과와 같이 나중에 전달되는 더 많은 데이터가 있으므로 Svelte 구성 요소에 유지하는 것이 합리적일 수 있습니다.

다음 작업은 서버의 일부 이벤트 핸들러를 좀 더 세분화하여 플레이어가 게임 중간에 나갈 경우 이를 처리할 수 있도록 하는 것입니다.

그 후에는 이것을 플레이할 수 있는 실제 게임으로 만드는 작업을 계속할 때입니다.

좋은 웹페이지 즐겨찾기