간단한 다중 온라인 게임 사용 노드js-제4부분

63520 단어 nodejavascriptwebdev

간단한 소개


이 섹션에서는 다음과 같은 서버 코드를 탐색합니다.
  • server.js - 정적 파일 제공 및 WebSocket
  • 수락을 담당하는 서버의 엔트리 포인트
  • lobby.js - 선수 짝짓기
  • 담당
  • game/ - 모든 뱀 게임 논리가 이 폴더 밑에 있다
  • 서버


    위에서 말한 바와 같이 server.js는 연결과 서비스 정적 파일을 받는 것을 책임진다. 나는 어떤 프레임워크도 사용하지 않지만 ws 모듈을 사용하여WebSocket 연결을 처리한다.

    요청 처리 프로그램


    다음 코드에서 우리는 새로운 http 서버를 만들고 요청 탐지기를 전달하여 요청을 처리했다. 이것은 매우 간단한 코드이다.
    var http = require('http');
    var server = http.createServer(function(req, res) {
        // This is a simple server, support only GET methods
        if (req.method !== 'GET') {
            res.writeHead(404);
            res.end();
            return;
        }
    
        // Handle the favicon (we don't have any)
        if (req.url === '/favicon.ico') {
            res.writeHead(204);
            res.end();
            return;
        }
    
        // This request is for a file
        var file = path.join(DEPLOY_DIR, req.url);
        serveStatic(res, file);
    });
    

    정적 파일 프로세서


    GET 요청 (favicon이 아님) 을 받을 때마다 파일을 대상으로 한다고 가정합니다. serveStatic 방법은 이 파일을 찾아서 클라이언트로 되돌려줍니다.
    코드에서 나는 두 개의 상수 변수를 사용하여 파일을 찾는데 도움을 주었다. 첫 번째는 DEPLOY_DIR이고 실제로는 정적 파일이 있는 루트 폴더이며 두 번째는 DEFAULT_FILE이다. 이것은 URL이 폴더를 가리키도록 요청할 때 제공해야 할 파일 이름이다.
    var DEPLOY_DIR = path.resolve(__dirname, '../client/deploy');
    var DEFAULT_FILE = 'index.html';
    
    그래서 우리가 /var/www/SnakeMatch에 이 프로젝트를 배치했다고 가정하면 DEPLOY_DIR/var/www/SnakeMatch/client/deploy이고 /all.js에 대한 요청은 /var/www/SnakeMatch/client/deploy/all.js을 제공할 것이다.
    다음은 serveStatic 방법의 코드이고 그 중에서 fs는 노드의 fs 모듈이다.
    /**
    * Serves a static file
    * @param {object} res - The response object
    * @param {string} file - The requested file path
    */
    function serveStatic(res, file) {
        // Get the file statistics
        fs.lstat(file, function(err, stat) {
            // If err probably file does not exist
            if (err) {
                res.writeHead(404);
                res.end();
                return;
            }
    
            // If this is a directory we will try to serve the default file
            if (stat.isDirectory()) {
                var defaultFile = path.join(file, DEFAULT_FILE);
                serveStatic(res, defaultFile);
            } else {
                // Pipe the file over to the response
                fs.createReadStream(file).pipe(res);
            }
        });
    }
    

    연락을 받다


    http 서버를 만든 후에 우리는 포트에 귀속해야 한다. 우리는 PORT 환경 변수(Heroku에서 사용)를 사용하고 기본값은 3000이다. 우리가 사용하는 WebSocketws에 대해 WebSocket 연결을 받을 때마다 로비로 보낸다.
    var WebSocketServer = require('ws').Server;
    var port = process.env.PORT || 3000;
    server.listen(port, function () {
        console.log('Server listening on port:', port);
    });
    
    // Create the WebSocket server (it will handle "upgrade" requests)
    var wss = new WebSocketServer({server: server});
    wss.on('connection', function(ws) {
        lobby.add(ws);
    });
    

    서청


    홀에서 새 선수를 받아들이고 짝을 짓는 것을 책임진다.
    새 플러그인이 로비에 추가될 때마다, 먼저 Player 대상을 만들고 (플러그인 패키지를 둘러싸고, 잠시 후에 상세하게 설명할 것) 이벤트를 감청한 다음, 다른 플레이어와 disconnect 맞추려고 시도합니다. 사용할 수 있는 플레이어가 없으면, 플레이어를 Match 사전에 넣습니다.만약 이 유저를 다른 유저와 짝짓기에 성공한다면, Match 대상은 pendingPlayers 사전에 넣고, Match의 activeMatches 이벤트에 등록됩니다.
    Lobby.add = function (socket) {
        // Create a new Player, add it to the pending players dictionary and register to its disconnect event
        var player = new Player(socket);
        pendingPlayers[player.id] = player;
        player.on(Player.Events.Disconnect, Lobby.onPlayerDisconnect);
    
        // Try to pair this player with other pending players, if success we get a "match"
        var match = this.matchPlayers(player);
        if (match) {
            // Register the Match GameOver event and store the match in the active matches dictionary
            match.on(Match.Events.GameOver, Lobby.onGameOver);
            activeMatches[match.id] = match;
    
            // Remove the players in the match from the pending players
            delete pendingPlayers[match.player1.id];
            delete pendingPlayers[match.player2.id];
    
            // Start the match
            match.start();
        } else {
            // No match found for this player, let him know he is Pending
            player.send(protocol.buildPending());
        }
    };
    
    로비의 나머지 코드는 그다지 재미있지 않다. GameOvermatchPlayers 사전에서만 순환하고 다른 끊긴 유저(현재 유저가 아님)를 찾으면 새로운pendingPlayers 대상으로 돌아간다.경기가 끝났을 때 ((Match 사건) 우리는 두 선수의 연결을 끊고 (콘센트를 끄는 것) GameOver 사전에서 경기를 삭제할 수 있다.

    플레이


    현재 우리는 activeMatches 폴더 아래의 코드를 검사할 것입니다. server/game, Player, Match 클래스를 포함합니다.

    선수 등급


    플레이어는 플러그인 종류의 패키지일 뿐, 새로운 데이터가 플러그인에 도착할 때마다 SnakeEngine 이벤트를 일으키고, 플러그인이 닫히면 message 이벤트를 일으키며, 플러그인에 데이터를 쓰는 데 사용되는 disconnect 방법을 공개한다.다음은 ctor 및 send 방법입니다.
    var Emitter = require('events').EventEmitter,
        util = require('util'),
        uuid = require('node-uuid');
    
    function Player(socket) {
        // Make sure we got a socket
        if (typeof socket !== 'object' || socket === null) {
            throw new Error('socket is mandatory');
        }
    
        Emitter.call(this);
    
        this.id = uuid.v1();
        this.index = 0; // The player index within the game (will be set by the Match class)
        this.online = true;
        this.socket = socket;
    
        // Register to the socket events
        socket.on('close', this.onDisconnect.bind(this));
        socket.on('error', this.onDisconnect.bind(this));
        socket.on('message', this.onMessage.bind(this));
    }
    util.inherits(Player, Emitter);
    
    Player.prototype.send = function(msg) {
        if (!msg || !this.online) {
            return;
        }
    
        try {
            this.socket.send(msg);
        } catch (ignore) {}
    };
    

    경기 수업


    이 클래스는 모든 게임의 후방 근무를 책임진다. 100밀리초마다 스노크 엔진을 업데이트하고 클라이언트에게 업데이트를 보내며 클라이언트로부터 메시지를 읽는다.
    주의: Match류는 스노크를 어떻게 하는지 모릅니다. 이것이 바로 우리가 스노크 엔진을 가지고 있는 이유입니다.
    비록 우리가 첫 번째 글에서 이 점을 묘사했지만 뱀이 일치하는 과정을 되돌아보자. 먼저 클라이언트에게 send 메시지를 보내고 모든 게임 정보(바둑판 크기, 뱀의 초기 위치 등)를 포함한다. 그 다음에 3개Ready의 휴식(1초당), 그 다음에 1개Steady의 메시지를 보내고 클라이언트에게 게임이 이미 시작된 신호를 보낸다.그리고 100밀리초 간격으로 일련의 go 메시지를 보내고 마지막은 Update 메시지이다.
    만약 그 중 한 선수가 실패하거나 60초가 지났다면, 60초 후에 스코어가 비슷해지면, 경기는 한 선수가 승리할 때까지 10초를 연장한다.
    이제 Match 클래스가 이 모든 것을 어떻게 완성하는지 살펴보겠습니다. 우선 상수를 정의합니다.
    var MATCH_TIME = 60000; // In milliseconds
    var MATCH_EXTENSION_TIME = 10000; // In milliseconds
    var UPD_FREQ = 100;
    var STEADY_WAIT = 3; // number of steady messages to send
    var BOARD_SIZE = {
        WIDTH: 500,
        HEIGHT: 500,
        BOX: 10
    };
    
    우리가 게임을 초기화한ctor에서 모든 유저가 색인 (player1/player2)에 분배되어 있음을 주의하십시오.
    function Match(player1, player2) {
        Emitter.call(this);
        this.id = uuid.v1();
        this.gameTimer = null;
        this.matchTime = MATCH_TIME; // The match timer (each match is for MATCH_TIME milliseconds)
    
        // Set the players indexes
        this.player1 = player1;
        this.player1.index = 1;
        this.player2 = player2;
        this.player2.index = 2;
    
        // Register to the players events
        this.player1.on(Player.Events.Disconnect, this.onPlayerDisconnect.bind(this));
        this.player2.on(Player.Events.Disconnect, this.onPlayerDisconnect.bind(this));
    
        this.player1.on(Player.Events.Message, this.onPlayerMessage.bind(this));
        this.player2.on(Player.Events.Message, this.onPlayerMessage.bind(this));
    
        // Create the snake game
        this.snakeEngine = new SnakeEngine(BOARD_SIZE.WIDTH, BOARD_SIZE.HEIGHT, BOARD_SIZE.BOX);
    }
    

    준비 다 됐어요?

    GameOverstart 두 가지 방법 모두 준비-안정-시작 프로세스가 나타납니다.
    Match.prototype.start = function() {
        // Build the ready message for each player
        var msg = protocol.buildReady(this.player1.index, this.snakeEngine.board, this.snakeEngine.snake1, this.snakeEngine.snake2);
        this.player1.send(msg);
    
        msg = protocol.buildReady(this.player2.index, this.snakeEngine.board, this.snakeEngine.snake1, this.snakeEngine.snake2);
        this.player2.send(msg);
    
        // Start the steady count down
        this.steady(STEADY_WAIT);
    };
    
    /**
     * Handles the steady count down
     * @param {number} steadyLeft - The number of steady events left
     */
    Match.prototype.steady = function(steadyLeft) {
        var msg;
    
        // Check if steady count down finished
        if (steadyLeft === 0) {
            // Send the players a "Go" message
            msg = protocol.buildGo();
            this.player1.send(msg);
            this.player2.send(msg);
    
            // Starts the update events (this is the actual game)
            this.gameTimer = setTimeout(this.update.bind(this), UPD_FREQ);
            return;
        }
    
        // Sends the players another steady message and call this method again in 1 sec
        msg = protocol.buildSteady(steadyLeft);
        this.player1.send(msg);
        this.player2.send(msg);
        --steadyLeft;
        this.gameTimer = setTimeout(this.steady.bind(this, steadyLeft), 1000);
    };
    

    업데이트 주기

    steady 방법은 100밀리초에 한 번씩 호출됩니다. 이 방법은 매우 간단하지만 update는 결과 대상을 되돌려줍니다. 그 중에는 게임 상태에 대한 정보가 포함되어 있습니다. 더욱 구체적으로 말하면 뱀 한 마리가 잃어버렸는지(자신/경계를 부딪쳐서), 그리고 작은 공이 변했는지(제거/추가)를 알려 줍니다.
    Match.prototype.update = function() {
        // Update the match time, this is not super precise as the "setTimeout" time is not guaranteed,
        // but ok for our purposes...
        this.matchTime -= UPD_FREQ;
    
        // Update the game
        var res = this.snakeEngine.update();
    
        // If no snake lost on this update and there is more time we just reload the update timer
        if (res.loosingSnake < 0 && this.matchTime > 0) {
            this.gameTimer = setTimeout(this.update.bind(this), UPD_FREQ);
            this.sendUpdateMessage(res);
            return;
        }
    
        var msg;
        // If no snake lost it means time's up, lets see who won.
        if (res.loosingSnake < 0) {
            // Check if there is a tie
            if (this.snakeEngine.snake1.parts.length === this.snakeEngine.snake2.parts.length) {
                // We don't like ties, lets add more time to the game
                this.matchTime += MATCH_EXTENSION_TIME;
                this.gameTimer = setTimeout(this.update.bind(this), UPD_FREQ);
                this.sendUpdateMessage(res);
                return;
            }
    
            // No tie, build a GameOver message (the client will find which player won)
            msg = protocol.buildGameOver(protocol.GameOverReason.End, null, this.snakeEngine.snake1, this.snakeEngine.snake2);
        } else {
            // Ok, some snake had a collision and lost, since we have only 2 players we can easily find the winning snake
            var winningPlayer = (res.loosingSnake + 2) % 2 + 1;
            msg = protocol.buildGameOver(protocol.GameOverReason.Collision, winningPlayer);
        }
    
        // Send the message to the players and raise the GameOver event
        this.player1.send(msg);
        this.player2.send(msg);
    
        this.emit(Match.Events.GameOver, this);
    };
    

    클라이언트 메시지 처리


    클라이언트가 메시지를 보낼 때마다 프로토콜 대상을 사용하여 분석을 하고 snakeEngine.update() 요청이 있으면 snake 엔진에 전달합니다. 업데이트할 플레이어 인덱스를 메시지에 저장해서 snake 엔진이 업데이트할 플레이어를 알 수 있도록 합니다.
    Match.prototype.onPlayerMessage = function(player, msg) {
        // Parse the message
        var message = protocol.parseMessage(msg);
        if (!message) {
            return;
        }
    
        switch (message.type) {
            case protocol.Messages.ChangeDirection:
                message.playerIndex = player.index;
                this.snakeEngine.handleDirChangeMessage(message);
                break;
        }
    };
    
    Match 클래스는 이렇습니다. 나머지 코드는 그다지 재미있지 않습니다.

    뱀발동기


    뱀 엔진은 뱀 게임을 한 번ChangeDirection씩 할 때마다 뱀이 자신과 부딪히는지, 경계를 벗어나는지, 작은 공을 하나 먹었는지 등을 검사한다.
    ctor에서 우리는 두 개의 snake 대상을 만들었습니다. 두 개의 snake는 모두 회로판의 첫 줄에서 만들었습니다. 하나는 왼쪽에서 만들었고 다른 하나는 오른쪽에서 만들었습니다.
    회로판은 여러 개의 상자 update 로 나뉘어 상자 인덱스를 가져오고 화면 x/y로 돌아가는 것을 기억하십시오.
    function SnakeEngine(width, height, boxSize) {
        this.board = new Board(width, height, boxSize);
    
        // The first snake is created on the left side and is heading right (very top row, y index = 0)
        var snakeLoc = this.board.toScreen(INITIAL_SNAKE_SIZE - 1);
        this.snake1 = new Snake(snakeLoc.x, snakeLoc.y, boxSize, INITIAL_SNAKE_SIZE, protocol.Direction.Right);
    
        // The second snake is created on the right side and is heading left (very top row, y index = 0)
        snakeLoc = this.board.toScreen(this.board.horizontalBoxes - INITIAL_SNAKE_SIZE);
        this.snake2 = new Snake(snakeLoc.x, snakeLoc.y, boxSize, INITIAL_SNAKE_SIZE, protocol.Direction.Left);
    
        /** @type {Pellet[]} */
        this.pellets = [];
    }
    
    재미있는 방법으로는 Board.toScreen(),updatecheckCollision가 있다.
    업데이트 방법에서, 우리는 모든 뱀에 대해 다음과 같은 조작을 실행합니다. snake 업데이트 방법 (다음 위치로 이동할 것을 알려 줍니다) 을 사용하고, 충돌을 확인하고, 작은 공을 먹었는지 확인하십시오.충돌이 발생하면 우리는 게임이 끝난 후 즉시 멈추고 충돌이 없으면 게임에 새로운 공을 추가하려고 시도합니다.
    SnakeEngine.prototype.update = function() {
        var res = new GameUpdateData();
    
        // Update snake1
        this.snake1.update();
    
        // Check if the snake collides with itself or out-of-bounds
        var collision = this.checkCollision(this.snake1);
        if (collision) {
            res.loosingSnake = 1;
            return res;
        }
    
        // Check if the snake eats a pellet
        res.pelletsUpdate = this.eatPellet(this.snake1);
    
        // Update snake2
        this.snake2.update();
    
        // Check if the snake collides with itself or out-of-bounds
        collision = this.checkCollision(this.snake2);
        if (collision) {
            res.loosingSnake = 2;
            return res;
        }
    
        // Check if the snake eats a pellet
        res.pelletsUpdate = this.eatPellet(this.snake2) || res.pelletsUpdate;
    
        // Finally add new pellet
        res.pelletsUpdate = this.addPellet() || res.pelletsUpdate;
    
        // No one lost (yet...).
        return res;
    };
    
    addPellet에서 우리는 먼저 뱀이 경계를 넘었는지 검사하고 뱀의 머리와 판의 사이즈를 비교하여 검사한다.뱀머리는 직사각형이다. 그 중에서 왼쪽 상단은 x/y로 표시된다. 따라서 우리가 뱀이 상/왼쪽 경계를 통과했는지 검사하고 싶을 때 우리는 x/y를 사용하지만 뱀이 하/오른쪽 경계를 통과했는지 검사하고 싶을 때 우리는 뱀머리의 오른쪽 하단을 사용한다.

    뱀이 자신과 충돌했는지 검사하는 것은 매우 간단하다. 뱀의 모든 부분(머리 포함)을 순환적으로 검사하고 뱀이 머리와 같은지 검사하기만 하면 된다(x/y만 검사한다).
    SnakeEngine.prototype.checkCollision = function(snake) {
        // Check if the head is out-of-bounds
        if (snake.parts[0].location.x < 0 ||
            snake.parts[0].location.y < 0 ||
            snake.parts[0].location.x + snake.parts[0].size > this.board.rectangle.width ||
            snake.parts[0].location.y + snake.parts[0].size > this.board.rectangle.height) {
                return true;
        }
    
        // Check if the snake head collides with its body
        for (var i = 1; i < snake.parts.length; ++i) {
            if (snake.parts[0].location.equals(snake.parts[i].location)) {
                return true;
            }
        }
    
        return false;
    };
    

    입자 추가


    우리가 게임에 새로운 공을 추가할 때, 우리는 우선 허용된 최대 공 수를 초과하지 않았는지 확인한 다음, 바둑판에서 랜덤으로 상자를 선택하고, 이 상자가 비어 있는지 확인한다.checkCollision가 빈번하게 호출되었기 때문에 우리는 필터를 해야 한다. 우리는 무작위 시간에 작은 공을 추가하기를 원하기 때문에 방법의 시작에 우리는 검사addPellet를 한다. 만약 그렇다면 우리는 즉시 되돌아오고 그 어떠한 내용도 추가하지 않기 때문에 평균적으로 10개의 호출 중 8개를 줄일 것이다.
    SnakeEngine.prototype.addPellet = function() {
        // Check if we should add pellets
        if (this.pellets.length >= MAX_PELLETS || Math.random() > 0.2) {
            return false;
        }
    
        // Keep loop until we found a spot for a pellet (theoretically this can turn into an infinite loop, so a solution could
        // be to stop the random search after X times and look for a spot on the board).
        var keepSearch = true;
        while (keepSearch) {
            keepSearch = false;
    
            // Take a random spot on the board
            var boxIndex = Math.floor(Math.random() * this.board.horizontalBoxes * this.board.horizontalBoxes);
            var loc = this.board.toScreen(boxIndex);
    
            // check that this spot is not on snake1
            for (var i = 0; i < this.snake1.parts.length; ++i) {
                if (this.snake1.parts[i].location.equals(loc)) {
                    keepSearch = true;
                    break;
                }
            }
    
            if (!keepSearch) {
                // check that this spot is not on snake2
                for (i = 0; i < this.snake2.parts.length; ++i) {
                    if (this.snake2.parts[i].location.equals(loc)) {
                        keepSearch = true;
                        break;
                    }
                }
            }
    
            if (!keepSearch) {
                // check that this spot is not on existing pellet
                for (i = 0; i < this.pellets.length; ++i) {
                    if (this.pellets[i].location.equals(loc)) {
                        keepSearch = true;
                        break;
                    }
                }
            }
    
            if (!keepSearch) {
                // Hooray we can add the pellet
                this.pellets.push(new Pellet(loc));
            }
        }
    
        return true;
    };
    

    결말


    보수...만약 네가 여기까지 왔다면, 잘했어, 고마워!
    나는 이 시리즈가 너에게 약간의 흥미가 있기를 바란다. 나에게 있어서 이 게임을 작성하는 것은 매우 재미있고 자유롭게 탐색할 수 있다code, 심지어는 그것을 더욱 좋아지게 할 수 있다!!

    좋은 웹페이지 즐겨찾기