Real-time multiplayer Facebook piškvorky

V této sérii článků si ukážeme, zaprvé jak vytvořit jednoduchou real-time hru za použití Kinetic.JS, Socket.IO a Node.JS a zadruhé jak z takové hry udělat Facebookovou aplikaci s JavaScript SDK.

Seriál: Real-time multiplayer Facebook piškvorky (3 díly)

  1. Real-time multiplayer Facebook piškvorky 6.2.2013
  2. Real-time multiplayer Facebook piškvorky – real-time multiplayer 11.2.2013
  3. Real-time multiplayer Facebook piškvorky – Facebook 20.2.2013

Piškvorky určitě každý zná – hra pro dva hráče, jeden má křížky, druhý kolečka, cílem je umístit souvislou řadu pěti stejných symbolů v řádku, sloupci nebo na diagonále. Tak je vytvořme hratelné po síti a ještě na Facebooku!

Každý hráč bude sedět u jiného počítače a jejich tahy se budou posílat po síti. Stav hry se nebude ukládat na serveru, veškerá logika hry se bude odehrávat na klientu, takže server bude jen prostředník mezi dvěma klienty.

Pro začátek si vytvoříme package.json a nainstalujeme závislosti:

$ cat > package.json
{
    "name": "xo",
    "version": "0.0.1",
    "dependencies": {
        "express": "3.0.6",
        "socket.io": "0.9.13"
    }
}
$ npm install

Poté napíšeme jednoduchý server v Node.JS (Node.JS a Express bude bráno jako základ, pro podrobnější informace doporučuji seriál na Zdrojáku):

var express = require('express'),
    app = express(),
    server = require('http').createServer(app),
    io = require('socket.io').listen(server);

Incializujeme Express aplikaci a Socket.IO.

server.listen(process.env.PORT || process.env.VCAP_APP_PORT || 8000);

Zapneme server. Proměnnou prostředí PORT používá Heroku, VCAP_APP_PORT zase Cloud Foundry. Pokud ani jedna proměnná prostředí není nastavena, aplikace bude naslouchat na portu 8000.

app.use(express.static(__dirname + '/public'));

Budeme servírovat statický obsah z adresáře public.

V adresáři public vytvoříme souboru xo.html, který bude základní kostrou aplikace:

<!doctype html>
<title>five in a row</title>

<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/kineticjs/4.3.1/kinetic.min.js"></script>

Když nyní spustíte server:

$ node app.js

Měli byste na adrese http://localhost:8000/xo.html vidět… bílou stránku. Jestli tomu tak je, je všechno v pořádku.

Model

Vytvoříme model – jen hloupý, bude pouze uchovávat stav hry (public/xo/model.js):

var XO = window.XO || {};

XO.Model = function (rows, cols) {
    this.rows = rows;
    this.cols = cols;
    this.board = [];

    for (var i = 0; i < this.rows; ++i) {
        this.board.push([]);
    }

    this.myColor = this.opponentColor = this.lastMoveColor = undefined;
};

XO.Model.prototype.reset = function (myColor) {
    for (var i = 0; i < this.rows; ++i) {
        this.board[i].length = 0;
    }

    this.myColor = myColor;
    this.opponentColor = myColor === 'X' ? 'O' : 'X';
    this.lastMoveColor = undefined;
};

XO.Model.X = 'X';
XO.Model.O = 'O';

A do public/xo.html přidáme:

<script type="text/javascript" src="/xo/model.js"></script>

Kreslíme s Kinetic.JS

Kreslení v Kinetic.JS používá tři základní koncepty – stage (scénu), layers (vrstvy) a shapes (tvary, objekty). Scéna se stará o postupné vykreslení všech vrstev. Vrstva je jednotka k překreslení. Tvary jsou jednotlivé objekty k vykreslení. V jedné vrstvě by měly být věci, které se vykreslují společně.

Kinetic.JS – scéna, vrstvy a tvary

V piškvorkách budeme používat tři vrstvy. Jednu pro pozadí (papír), druhou pro vykreslování jednotlivých křížků a koleček a třetí pro informační „dialog“. Začněme papírem:

XO.UI.Paper = function (config) {
    var stage = config.stage,
        backgroundImage = config.backgroundImage,
        a = config.squareSideLength,
        onGridClick = config.onGridClick || function () {};

    delete config.stage;
    delete config.backgroundImage;
    delete config.squareSideLength;
    delete config.onGridClick;

    var paper = new Kinetic.Layer(config);

Papír budeme vytvářet pomocí new XO.UI.Paper({ stage: …, … }). Budeme potřebovat scénu, obrázek, který se použije jako pozadí pod mřížku (nějakou texturu papíru), jakou mají mít jednotlivá políčka výšku/šířku a callback pro kliknutí na políčko.

    var background = new Kinetic.Rect({
        x: 0,
        y: 0,
        width: stage.getWidth(),
        height: stage.getHeight(),
        fillPatternImage: backgroundImage
    });

    paper.add(background);

Vytvořili jsme pozadí jako obdélník (Kinetic.Rect) vyplněné obrázkem (fillPatternImage) a přidali ho do vrstvy. Souřadnice tvarů ve vrstvě se počítají relativně vzhledem k pozici vrstvy ve scéně. Pro všechny možnosti při vytváření obdélníku konzultujte dokumentaci. Více tvarů najdete v API Kinetic.JS.

Jak vytvořit mřížku? Mohli bychom využít obdélníky (Kinetic.Rect) anebo čáry (Kinetic.Line). Ovšem zdá se mi, že nejjednodušší je použít API canvasu.

    var grid = new Kinetic.Shape({
        drawFunc: function (canvas) {
            var ctx = canvas.getContext();

            ctx.beginPath();

            for (var x = a; x < canvas.width; x += a) {
                ctx.moveTo(x, 0);
                ctx.lineTo(x, canvas.height);
            }

            for (var y = a; y < canvas.height; y += a) {
                ctx.moveTo(0, y);
                ctx.lineTo(canvas.width, y);
            }

            ctx.closePath();

            canvas.stroke(this);
        },

        stroke: '#79E2F7',
        strokeWidth: 1
    });

    paper.add(grid);

Kinetic.JS bude volat pro náš objekt danou drawFunc. Předaný argument není samotný element canvas, ale je to objekt canvas obalující. getContext() vrátí již vytvořený kontext, do kterého byste měli kreslit, aby všechno fungovalo. Kdybyste potřeboval přímo element, dostanete ho z canvas.getElement().

    paper.on('click', function () {
        var mousePosition = stage.getMousePosition();
        onGridClick(Math.floor(mousePosition.y / a), Math.floor(mousePosition.x / a));
    });

    return paper;
};

Nakonec jsem nastavili obsluhu události click. Z pozice myši získáme řádek a sloupec a zavoláme onGridClick callback.

Vrstvu s křížky a kolečky vytvoříme podobně. Využijeme zase API canvasu, jeden tvar bude vykreslovat všechny křížky a druhý všechna kolečka. Samozřejmě by šlo použít dvě Kinetic.Line pro křížek a Kinetic.Circle pro kolečko, avšak při mřížce 38×30 by se vytvářelo až 1710 Kinetic.JS tvarů. To vůbec není ohleduplné vůči paměti a už vůbec ne vůči procesoru, který to bude všechno muset postupně projít a poslat k vykreslení. Kdyby musely být vytvořené jednotlivě objekty klikatelné, bylo by to hned něco jiného – to by se použití Kinetic.JS tvarů a jejich událostí vyplatilo.

Poslední vrstva bude sloužit k informování uživatele a bude mít klikatelné tlačítko:

XO.UI.Overlay = function (config) {
    var stage = config.stage,
        onButtonClick = config.onButtonClick;

    delete config.stage;
    delete config.onButtonClick;

    var overlay = new Kinetic.Layer(config);

    // var background = new Kinetic.Rect({ … })
    // var text = new Kinetic.Text({ … })

    var button = new Kinetic.Group({
        x: stage.getWidth() / 2,
        y: stage.getHeight() / 5 * 3
    });

Kinetic.Group slouží k seskupování objektů ve vrstvě. Souřadnice objektů se poté počítají relativně od polohy skupiny.

    button.add(new Kinetic.Circle({
        x: 0,
        y: 0,
        radius: 60,
        fill: 'red'
    }));

    var buttonText = new Kinetic.Text({
        x: 0,
        y: 0,
        fontFamily: 'Trebuchet MS',
        fontSize: 20,
        fill: 'white',
        text: ''
    });

    button.add(buttonText);

    button.on('click', onButtonClick);

    overlay.add(button);

Vytvořili jsme jednoduché tlačítko a navěsili na událost kliknutí callback.

    function centerText(text) {
        text.setOffset({
            x: text.getWidth() / 2,
            y: text.getHeight() / 2
        });
    }

    overlay.setText = function (s) {
        text.setText(s);
        centerText(text);
        overlay.draw();
    };

    // overlay.showText = function (callback) { … }
    // overlay.hideText = function (callback) { … }
    // overlay.setButtonText = function (s) { … }
    // overlay.showButton = function (callback) { … }
    // overlay.hideButton = function (callback) { … }

Pomocná funkce centerText() vystředí text horizontálně i vertikálně vzhledem k jeho souřadnicím. Při každé změně textu se musí text znovu vycentrovat. Dalším způsobem je použít align, což nastaví horizontální zarovnání. Možnost vertikálního zarovnání v Kinetic.JS zatím chybí.

Po každé akci, která změní, co nebo jak se vrstvě bude vykreslovat, je nutno zavolat draw(), aby se vrstva překreslila.

    overlay.show = function (callback) {
        overlay.transitionTo({
            x: 0,
            duration: 1,
            easing: 'ease-in-out',
            callback: callback
        });
    };

    overlay.hide = function (callback) {
        overlay.transitionTo({
            x: stage.getWidth(),
            duration: 1,
            easing: 'ease-in-out',
            callback: callback
        });
    };

    return overlay;
};

show() a hide() dostanou vrstvu na scénu, resp. ze scény (vedle scény, takže nebude vidět). Metoda transitionTo() jde aplikovat na všechny vrstvy, skupiny a tvary a postupnou animací změní nějaký atribut(y) daného objektu. V tomto případě měníme souřadnici X.

Na scéně

Nyní máme všechny vrstvy a můžeme zkompletovat scénu:

var XO = window.XO || {};

XO.UI = function (config) {
    var model = config.model,
        backgroundImage = config.backgroundImage,
        onGridClick = config.onGridClick || function (row, col) {},
        onOverlayButtonClick = config.onOverlayButtonClick || function () {};

    delete config.model;
    delete config.backgroundImage;
    delete config.onGridClick;
    delete config.onOverlayButtonClick;

    var stage = new Kinetic.Stage(config);

    var squareSideLength = Math.round(Math.min(stage.getHeight() / model.rows, stage.getWidth() / model.cols));

    stage.paper = new XO.UI.Paper({
        stage: stage,
        backgroundImage: backgroundImage,
        squareSideLength: squareSideLength,
        onGridClick: onGridClick
    });

    stage.add(stage.paper);

    stage.squares = new XO.UI.Squares({
        stage: stage,
        model: model,
        squareSideLength: squareSideLength
    });

    stage.add(stage.squares);

    stage.overlay = new XO.UI.Overlay({
        stage: stage,
        onButtonClick: onOverlayButtonClick
    });

    stage.add(stage.overlay);

    return stage;
};

Abychom si vyzkoušeli, jestli to, co jsme právě spáchali, vůbec funguje, musíme do public/xo.html přidat všechny potřebné skripty, <div>, který použije Kinetic.JS ke kreslení, a controller:

<script type="text/javascript" src="/js/xo/ui.js"></script>
<script type="text/javascript" src="/js/xo/ui/paper.js"></script>
<script type="text/javascript" src="/js/xo/ui/squares.js"></script>
<script type="text/javascript" src="/js/xo/ui/overlay.js"></script>

<div id="container"></div>

<script type="text/javascript">
    var backgroundImage = new Image();

    backgroundImage.onload = function () {
        var model = new XO.Model(30, 38), clicks = 0;

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

            onGridClick: function (row, col) {
                if (!model.board[row][col]) {
                    model.board[row][col] = clicks++ % 2 === 0 ? XO.Model.X : XO.Model.O;
                    ui.squares.draw();
                } 
            },

            onOverlayButtonClick: function () {
                ui.overlay.hide();
            }
        });

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

    backgroundImage.src = '/img/paper.png';
</script>

Zatím můžeme hrát sami proti sobě. Jej! Ale zatím vůbec neověřujeme, jestli náhodou někdo nevyhrál, a po pár hrách nás to proti nám samým přestane bavit.

Screenshot piškvorek

Závěr

Kinetic.JS výrazně zjednodušuje práci s canvasem… Až na ty případy, kdy ji ztěžuje. Když se budete držet základních tvarů (čtverečků, obdélníčků, koleček, hvězdiček atd.), je to paráda. Když budete chtít něco složitějšího, použijte Kinetic.JS na organizaci kódu, ale kreslete to radši přes canvas API. Dobré zdroje ohledně Kinetic.JS a canvasu:

Kompletní zdrojové kódy můžete stáhnout z GitHubu:

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

V dalším díle se podíváme na hru s protihráčem.

Autor programuje v Javascriptu, PHP, Javě, Golangu… ve všem možném. Ve volném čase probádává nejrůznější zákoutí světa programovacích jazyků a databází a všeho kolem nich.

Věděli jste, že nám můžete zasílat zprávičky? (Jen pro přihlášené.)

Komentáře: 10

Přehled komentářů

Čelo
Tomá Pácl RSS
Čelo Re: RSS
Martin Hassman Re: RSS
mic362 heh
Honza Marek Přiřazování undefined
Jakub Kulhan Re: Přiřazování undefined
Pavel Lang Re: Přiřazování undefined
Jakub Kulhan Re: Přiřazování undefined
m4recek Piskvorky s php, websocket a webgl
Zdroj: https://www.zdrojak.cz/?p=6905