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

Odebírat
Upozornit na
guest
0 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
Zobrazit všechny komentáře

Přístupnost není jen o splnění norem: nový pohled na inkluzivní design

Přístupnost a inkluze možná nepatří mezi nejžhavější témata digitálního světa – dokud o nich nezačne mluvit Vitaly Friedman. Na WebExpo 2024 předvedl, že inkluzivní design není jen o splněných checkboxech, ale hlavně o lidech. S energií sobě vlastní obrátil zažité přístupy naruby a ukázal, že skutečně přístupný web je nejen možný, ale i nezbytný.

Efektivnější vývoj UI nebo API: Co si odnést z WebExpo 2025?

Různé
Komentáře: 0
Jak snadno implementovat moderní uživatelské rozhraní? Které funkce brzdí rychlost vašeho webu? A kdy raději sami přibrzdit, abychom využitím AI nepřekročili etické principy? Debatu aktuálních dev témat rozdmýchá sedmnáctý ročník technologické konference WebExpo, která proběhne v Praze od 28. do 30. května. Který talk či workshop si rozhodně nenechat ujít? Toto je náš redakční výběr z vývojářských hroznů.

Zapřáhněte AI jako nikdy předtím. Květnová konference WebExpo přivítá hvězdy technologického světa

Od 28. do 30. května 2025 promění pražský Palác Lucerna na tři dny technologická konference WebExpo. Na programu je více než 80 přednášek a workshopů od expertů z celého světa. WebExpo tradičně propojuje vývojáře, designéry, marketéry i byznysové lídry a nabízí praktické dovednosti, strategické myšlení a přináší nejnovější trendy nejen v oblasti AI.