Přejít k navigační liště

Zdroják » JavaScript » Real-time multiplayer Facebook piškvorky – real-time multiplayer

Real-time multiplayer Facebook piškvorky – real-time multiplayer

Články JavaScript

Pokračujeme ve tvorbě hry pro Facebook v Node.js. V prvním díle jsme vytvořili fungující herní plochu. Dnes zapojíme více hráčů.

Teď se dostane ke slovu Socket.IO a konečně budeme moci hrát po síti. Naše piškvorky budou používat jednoduchý protokol. První hráč se představí serveru svým ID uživatele (událost uid) a pošle zprávu, že čeká na druhého hráče (waitingFor). Až se druhý hráč představí serveru, server vytvoří novou hru a pošle její ID oběma hráčům (game). Ti následně budou posílat své tahy (move). Stav hry si budou udržovat klienti sami a sami také budou rozhodovat, jaké tahy jsou přípustné a kdy je mohou poslat, server je jen bude přeposílat. Server bude pouze po každém tahu čekat 30 sekund a pokud do té doby nedostane další tah, hru zruší.

Pozn. Hráči mohou podvádět, můžete spoluhráči poslat tah na již obsazené pole ;)

Komunikace mezi serverem a klienty

Zdrojové kódy

Stáhnětě si zdrojové kódy 1. dílu:

$ git clone https://github.com/jakubkulhan/xo.git
$ git checkout dil1

Server

Nejdříve se podíváme na server, přidáme kód pro obsluhu Socket.IO:

var waitingFor = {}, games = {};

waitingFor udržuje seznam hráčů čekajících na spoluhráče, kde klíčem je ID uživatele spoluhráče a hodnota je hráčův socket. games jsou aktuálně hrané hry.

io.sockets.on('connection', function (socket) {
    var myUid, opponentUid, gameId;

Událost connection znamená, že se otevřel nový socket. Každý socket si bude pamatovat ID hráče, ID spoluhráče a ID hry.

    socket.on('uid', function (uid) {
        myUid = uid;

        if (waitingFor[myUid]) {
            var opponentSocket = waitingFor[myUid];
            delete waitingFor[myUid];

            games[gameId = String(Date.now() * Math.random())] = {
                X: opponentSocket,
                O: socket,
                timeout: undefined
            };

            opponentSocket.setGameId(gameId);

            opponentSocket.emit('game', gameId, myUid, 'X');
            socket.emit('game', gameId, opponentSocket.getUid(), 'O');

            restart();
        }
    });

Při přijmutí zprávy uid nastavíme hráčovo ID a pokud na něj někdo čeká s hrou, tak ji vytvoříme a oběma pošleme zprávu, že hra byla zahájena.

    socket.getUid = function () {
        return myUid;
    };

    socket.setGameId = function (gid) {
        gameId = gid;
    };

    function restart() {
        stop();

        if (games[gameId]) {
            games[gameId].timeout = setTimeout(function(game) {
                stop();
                game.X.emit('timeout');
                game.O.emit('timeout');
                delete games[gameId];
            }, 30000, games[gameId]);
        }
    }

    function stop() {
        if (games[gameId] && games[gameId].timeout) {
            clearTimeout(games[gameId].timeout);
            games[gameId].timeout = undefined;
        }
    }

setGameId() umožňuje na socketu nastavit ID hry zvenčí. restart() a stop() zapíná, resp. vypíná, odpočítávání. Pokud odpočet vyprší, pošleme oběma hráčům zprávu timeout.

    socket.on('waitingFor', function (uid) {
        opponentUid = uid;
        waitingFor[opponentUid] = socket;
    });

    socket.on('move', function (gameId, row, col, color) {
        if (games[gameId]) {
            games[gameId][color === 'X' ? 'O' : 'X'].emit('move', row, col, color);
            restart();
        }
    });

Při waitingFor uložíme potřebné informace. move se pouze přepošle soupeři.

    socket.on('end', function () {
        stop();
        delete games[gameId];
    });

    socket.on('disconnect', function () {
        if (waitingFor[opponentUid] === socket) {
            delete waitingFor[opponentUid];
        }

        stop();
        delete games[gameId];
    });
});

end hráč pošle, pokud je hra skončena (někdo vyhrál). disconnect je rezervovaná událost Socket.IO. Podle názvu je jasné, že je vyvolána, když se socket odpojí, zameteme tedy za sebou.

Úpravy na klientu

Předtím jsme vůbec neřešili, zdalipak náhodou jeden z hráčů nevyhrál, přidáme tedy do modelu jednoduchou metodu, která to ověří:

XO.Model.prototype.checkWin = function (color) {
    var relatives = [ [0, 1], [1, 1], [1, 0], [1, -1] ];

    for (var i = 0; i < this.rows; ++i) {
        for (var j = 0; j < this.cols; ++j) {
            if (this.board[i][j] !== color) {
                continue;
            }

            for (var r = 0; r < relatives.length; ++r) {
                var relative = relatives[r], k = i, l = j, inARow = 0;

                while (k < this.rows && l < this.cols && this.board[k][l] === color) {
                    ++inARow;
                    k += relative[0];
                    l += relative[1];
                }

                if (inARow > 4) {
                    return {
                        from: { row: i, col: j },
                        to: { row: k - relative[0], col: l - relative[1] }
                    };
                }
            }
        }
    }

    return null;
};

checkWin() vrátí vítěznou řadu symbolů, kterou našla, a null, když nic nenašla.

Dále budeme potřebovat tuhle vítěznou řadu označit, do vrstvy s vykreslováním křížků a koleček (XO.UI.Squares) přidáme:

// …

    var line = new Kinetic.Line({
        points: [],
        stroke: 'green',
        strokeWidth: 3,
        visible: false
    });

    squares.add(line);

    squares.cross = function (win) {
        line.setPoints([ win.from.col * a + a/2, win.from.row * a + a/2, win.to.col * a + a/2, win.to.row * a + a/2 ]);
        line.setVisible(true);
    };

    squares.uncross = function () {
        line.setVisible(false);
    };

// …

Nejdříve nechceme, aby čára byla vidět, proto ji vytvoříme s visible: false. cross() vezme objekt vrácený z checkWin() a podle toho vykreslí čáru. uncross() ji zase zneviditelní.

Nakonec budeme muset upravit kód xo.html. Pro načtení Socket.IO přidáme následující skript:

<script type="text/javascript" src="/socket.io/socket.io.js"></script>

A změníme controller:

var backgroundImage = new Image();

backgroundImage.onload = function () {
    var uid = prompt('Enter your user ID:'), opponentUid, gameId;

    var model = new XO.Model(30, 38);

Na začátku se uživatele zeptáme na jeho ID.

    function tryWin(color) {
        var win = model.checkWin(color);

        if (win) {
            socket.emit('end');

            ui.squares.cross(win);

            ui.overlay.show(function () {
                alert(color === model.myColor ? 'you won :)' : 'you lost :(');
            });
        }
    }

tryWin() vyzkouší, jestli předaná barva vyhrála. Jestli ano, ukončíme hru (socket.emit('end')), zaškrtneme vítězné postavení a oznámíme to uživateli.

    var ui = new XO.UI({
        container: 'container',
        width: 760,
        height: 600,
        model: model,
        backgroundImage: backgroundImage,

        onOverlayButtonClick: function () {
            opponentUid = prompt('Enter opponent user ID:');

            ui.overlay.setText('waiting…');
            ui.overlay.hideButton();

            socket.emit('waitingFor', opponentUid);
        },

Po kliknutí na tlačítko play uživatel zadá ID protihráče a pošleme serveru zprávu, že hráč čeká na hru.

        onGridClick: function (row, col) {
            if (model.lastMoveColor === model.myColor || model.board[row][col]) {
                return;
            }

            socket.emit('move', gameId, row, col, model.myColor);

            model.board[row][col] = model.myColor;
            model.lastMoveColor = model.myColor;

            tryWin(model.myColor);
            ui.squares.draw();
        }
    });

    ui.overlay.setText('five in a row');
    ui.overlay.setButtonText('play');

Tah povolíme pouze tehdy, jestli je hráč na tahu (tzn. pokud netáhl minulý tah) a není-li políčko obsazeno. Jestliže je všechno v pořádku, pošleme tah na server, upravíme data v modelu a vyzkoušíme, jestli jsme nevyhráli (tryWin(model.myColor); v piškvorkách může po našem tahu těžko vyhrát soupeř).

Pozn. Schválně si zkuste podmínku zajišťující regulérnost hry odmazat – superpiškvorky! Nemůžete prohrát, musíte být pouze rychlejší než soupeř.

Nyní se dosteneme konečně ke komunikaci se serverem.

    var socket = io.connect();

    socket.emit('uid', uid);

Připojili jsme se a identifikovali se serveru.

    socket.on('game', function (gid, oid, myColor) {
        gameId = gid;
        opponentUid = oid;
        model.reset(myColor);

        ui.squares.uncross();
        ui.squares.draw();

        ui.overlay.hide(function () {
            ui.overlay.setText('five in a row');
            ui.overlay.setButtonText('play');
            ui.overlay.showButton();
        });
    });

    socket.on('timeout', function () {
        ui.overlay.show(function () {
            alert('timeout :|');
        });
    });

Když začne nová hra, uložíme si její ID a zresetujeme model do výchozího stavu. V případě, že hra skončila, server pošle timeout. Pouze to oznámíme uživateli a nabídneme možnost hrát novou hru.

    socket.on('move', function (row, col, opponentColor) {
        model.board[row][col] = model.opponentColor;
        model.lastMoveColor = model.opponentColor;

        tryWin(model.opponentColor);
        ui.squares.draw();
    });
};

backgroundImage.src = '/img/paper.png';

Když obdržíme tah od soupeře, zaznemenáme ho a vyzkoušíme, jestli soupeř nevyhrál.

Zatím můžeme hrát proti soupeři po síti. Jej! Ale zadávat pořád svoje ID a protihráčovo ID je hodně nepohodlné.

Závěr

Piškvorkoklienti zatím mají dost naivní přístup k ověřování správnosti tahů. Určitě by se toto ověření mělo přesunout do modelu a probíhat nejen při posílání tahu, ale taky při přijímání od soupeře.

Použití Socket.IO je velmi jednoduché, prakticky se stačí připojit, nastavit handlery přijímaných zpráv a emit()ovat odchozí zprávy. O dalších šikovných věcech se můžete dočíst zde:

Pokud aplikaci se Socket.IO budete chtít hostovat na Heroku nebo AppFogu, ti bohužel nepodporují WebSocket. Aby se Socket.IO zbytečně nepokoušelo spojit přes WebSocket, je nejlepší použít spojení přes xhr-polling:

io.configure(function () {
    io.set('transports', [ 'xhr-polling' ]);
    io.set('polling duration', 10);
});

Kompletní kód hry po tomto díle:

$ git checkout dil2

V příštím článku se podíváme na to, jak začlenit hru jako aplikaci na Facebook.

Komentáře

Subscribe
Upozornit na
guest
0 Komentářů
Inline Feedbacks
View all comments

Enum a statická analýza kódu

Mám jednu univerzální radu pro začínající programátorty. V učení sice neexistují rychlé zkratky, ovšem tuhle radu můžete snadno začít používat a zrychlit tak tempo učení. Tou tajemnou ingrediencí je statická analýza kódu. Ukážeme si to na příkladu enum.