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

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

Real-time multiplayer Facebook piškvorky

Články JavaScript

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.

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.

Komentáře

Subscribe
Upozornit na
guest
10 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
Čelo

Díky hlavně za ten lehký náhled na KineticJS. Teším se na další díly.

Tomá Pácl

Upozorňuji na chybku: v RSS agregaci se objevuje celý článek. Nevím, zda je to chybka „nového zdrojáku“ nebo jen tohoto článku…

Čelo

Já osobně to neberu jako chybu, ale jako velmi vítanou změnu. RSS dogmatici si mě můžou klidně sežrat, ale mě to tak vyhovuje :)

Martin Hassman

Ano, zatím jsou puštěny celé články.

mic362

Včera jsem tu celý den hledal možnost vložit komentář a nenašel :(

Honza Marek

To přiřazování undefined mi přijde zbytečné, protože když čtu hodnotu z nedefinované proměnné, tak mi JS taky vrátí undefined. Pokud bych chtěl inicializovat proměnnou nějakou prázdnou hodnotou, použil bych null. To má ten důsledek, že kdykoliv vidím, že se v mém kódu někde objevuje undefined hodnota, tak vím, že se na 99% jedná o chybu a je potřeba ji opravit.

Pavel Lang

Když jsme u těch skrytých tříd u V8, tohle opravdu nedělá dobře a navíc je to docela ošklivé:
delete config.stage;
delete config.backgroundImage;
delete config.squareSideLength;
delete config.onGridClick;

m4recek

Nieco podobne som napisal asi pred dvoma rokmi ako vikendovy projekt :) Akurat to bolo s php, Raphael.js jQuery…

ukazka: http://www.gomokulive.eu
php websocket: https://github.com/lemmingzshadow/php-websocket (starsia verzia s flash fallbackom https://github.com/m4recek/php-websocket-with-flash-policy-file)
webgl: http://raphaeljs.com/

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.

Pocta C64

Za prvopočátek své programátorské kariéry vděčím počítači Commodore 64. Tehdy jsem genialitu návrhu nemohl docenit. Dnes dokážu lehce nahlédnout pod pokličku. Chtěl bych se o to s vámi podělit a vzdát mu hold.