Pojďme vytvořit hru na kreslení s Node.js

Pravděpodobně jste již slyšeli o node.js. Jedná se o asynchronní webový server postavený na JavaScriptovém enginu Google V8 (stejný, díky kterému je Chrome rychlý). Pomocí node můžete psát škálovatelné webové služby v JavaScriptu, které zvládnou obrovské množství současných připojení, díky čemuž jsou perfektní jako backend pro hry, webové chaty a další úkoly v reálném čase.

Nápad

Dnes vytvoříme jednoduchou online hru na kreslení. Aplikace umožní uživatelům kreslit na stránku tažením a pohybem myší a zobrazí výsledky na velkém prvku plátna. Co se však odlišuje od všech ostatních podobných experimentů, je to, že se lidé při tom uvidí v reálném čase. Abychom toho dosáhli, využijeme knihovnu socket.io pro node.js, která využívá řadu technologií od webových soketů po AJAX long polling, aby nám poskytla datový kanál v reálném čase. Z tohoto důvodu tento příklad funguje ve všech moderních prohlížečích.

Instalace node.js

Pro spuštění hry budete muset nainstalovat node.js. Nemělo by to trvat déle než pár minut a je poměrně jednoduché. Můžete pokračovat a stáhnout si instalační programy z oficiálních stránek. Nebo můžete spustit tuto sadu příkazů, pokud ji chcete nainstalovat z vašeho terminálu v Linuxu nebo OSX (stačí spustit první skript:node-and-npm-in-30-seconds.sh ).

Po dokončení instalace získáte také přístup k npm, správci balíčků uzlů. Pomocí tohoto nástroje můžete nainstalovat užitečné knihovny a kousky kódu, které můžete importovat do skriptů node.js. Pro tento příklad budeme potřebovat knihovnu socket.io, kterou jsem zmínil výše, a node-static, která bude obsluhovat soubory HTML, CSS a JS kreslicí aplikace. Znovu otevřete svůj terminál (nebo nové okno příkazového řádku, pokud používáte Windows) a napište následující příkaz:

npm install [email protected] node-static

Dokončení by nemělo trvat déle než několik minut.

Spuštění aplikace

Pokud chcete soubory pouze uchopit a otestovat aplikaci na svém počítači, budete si muset stáhnout archiv z tlačítka výše a rozbalit jej někam na pevný disk. Poté otevřete příkazový řádek / terminál a přejděte do složky (samozřejmě si pamatujete, jak funguje příkaz cd, že?). Poté zadejte tento příkaz a stiskněte return:

node app.js

Měli byste být přivítáni ladicí zprávou socket.io (jinak je pravděpodobně vaše cesta nesprávná; cvičte s tím příkazem cd!). To znamená, že vše běží! Nyní otevřete http://localhost:8080 a měli byste vidět svou vlastní kopii dema. Pěkné!

Tyto pokyny platí také v případě, že postupujete podle kroků v článku a vytváříte aplikaci od začátku. Což nás přivádí zpět k tutoriálu:

HTML

Prvním krokem je vytvoření nového HTML dokumentu. Uvnitř dáme prvek canvas, na který budou uživatelé kreslit, a div pro držení ukazatelů myši. Každý ukazatel myši bude div s .ukazatelem css třída, která je absolutně umístěna na stránce (v tomto článku nebudeme diskutovat o stylu, otevřete assets/css/styles.css podívat se).

index.html

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <title>Node.js Multiplayer Drawing Game | Tutorialzine Demo</title>

        <!-- The stylesheets -->
        <link rel="stylesheet" href="assets/css/styles.css" />

        <!--[if lt IE 9]>
          <script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script>
        <![endif]-->
    </head>

    <body>
        <div id="cursors">
            <!-- The mouse pointers will be created here -->
        </div>

        <canvas id="paper" width="1900" height="1000">
            Your browser needs to support canvas for this to work!
        </canvas>

        <hgroup id="instructions">
            <h1>Draw anywhere!</h1>
            <h2>You will see everyone else who's doing the same.</h2>
            <h3>Tip: if the stage gets dirty, simply reload the page</h3>
        </hgroup>

        <!-- JavaScript includes. Notice that socket.io.js is served by node.js -->
        <script src="/socket.io/socket.io.js"></script>
        <script src="http://code.jquery.com/jquery-1.8.0.min.js"></script>
        <script src="assets/js/script.js"></script>

    </body>
</html>

Vidíte, že plátno je nastaveno na pevnou šířku 1900px a výšku 1000px, ale uživatelé s menšími displeji uvidí jen jeho část. Možným vylepšením by bylo zvětšení nebo zmenšení plátna ve vztahu k velikosti obrazovky, ale to nechám na vás.

Aby komunikační kanál v reálném čase mezi prohlížečem uživatele a node.js fungoval, musíme zahrnout socket.io knihovnu na obou místech, nenajdete však socket.io.js soubor obsažený ve spodní části index.html v archivu stahování. Je to proto, že socket.io zachycuje požadavky na /socket.io/socket.io.js a obsluhuje jej sám, takže tento soubor nemusíte explicitně nahrávat do vaší aplikace.

Na straně klienta

V jiných výukových programech bychom tuto sekci obvykle pojmenovali JavaScript, ale tentokrát máme JavaScript jak na klientovi (prohlížeči osoby), tak na serveru (node.js), takže je třeba správně rozlišovat.

Kód, který vidíte níže, běží v prohlížeči dané osoby. K připojení k serveru používá socket.io a upozorní nás, když dojde k události. Tato událost je zpráva vysílaná ostatními klienty a předaná zpět nám pomocí node.js. Zprávy obsahují souřadnice myši, jedinečné ID uživatele a to, zda právě kreslí nebo ne.

assets/js/script.js

$(function(){

    // This demo depends on the canvas element
    if(!('getContext' in document.createElement('canvas'))){
        alert('Sorry, it looks like your browser does not support canvas!');
        return false;
    }

    // The URL of your web server (the port is set in app.js)
    var url = 'http://localhost:8080';

    var doc = $(document),
        win = $(window),
        canvas = $('#paper'),
        ctx = canvas[0].getContext('2d'),
        instructions = $('#instructions');

    // Generate an unique ID
    var id = Math.round($.now()*Math.random());

    // A flag for drawing activity
    var drawing = false;

    var clients = {};
    var cursors = {};

    var socket = io.connect(url);

    socket.on('moving', function (data) {

        if(! (data.id in clients)){
            // a new user has come online. create a cursor for them
            cursors[data.id] = $('<div class="cursor">').appendTo('#cursors');
        }

        // Move the mouse pointer
        cursors[data.id].css({
            'left' : data.x,
            'top' : data.y
        });

        // Is the user drawing?
        if(data.drawing && clients[data.id]){

            // Draw a line on the canvas. clients[data.id] holds
            // the previous position of this user's mouse pointer

            drawLine(clients[data.id].x, clients[data.id].y, data.x, data.y);
        }

        // Saving the current client state
        clients[data.id] = data;
        clients[data.id].updated = $.now();
    });

    var prev = {};

    canvas.on('mousedown',function(e){
        e.preventDefault();
        drawing = true;
        prev.x = e.pageX;
        prev.y = e.pageY;

        // Hide the instructions
        instructions.fadeOut();
    });

    doc.bind('mouseup mouseleave',function(){
        drawing = false;
    });

    var lastEmit = $.now();

    doc.on('mousemove',function(e){
        if($.now() - lastEmit > 30){
            socket.emit('mousemove',{
                'x': e.pageX,
                'y': e.pageY,
                'drawing': drawing,
                'id': id
            });
            lastEmit = $.now();
        }

        // Draw a line for the current user's movement, as it is
        // not received in the socket.on('moving') event above

        if(drawing){

            drawLine(prev.x, prev.y, e.pageX, e.pageY);

            prev.x = e.pageX;
            prev.y = e.pageY;
        }
    });

    // Remove inactive clients after 10 seconds of inactivity
    setInterval(function(){

        for(ident in clients){
            if($.now() - clients[ident].updated > 10000){

                // Last update was more than 10 seconds ago.
                // This user has probably closed the page

                cursors[ident].remove();
                delete clients[ident];
                delete cursors[ident];
            }
        }

    },10000);

    function drawLine(fromx, fromy, tox, toy){
        ctx.moveTo(fromx, fromy);
        ctx.lineTo(tox, toy);
        ctx.stroke();
    }

});

Základní myšlenkou je, že používáme socket.emit() k odeslání zprávy na server node.js při každém pohybu myši. To může generovat velké množství paketů, takže to omezujeme na jeden paket každých 30 ms (funkce $.now() je definována jQuery a vrací počet milisekund od epochy).

Událost mousemove není volána na každém pixelu pohybu, ale používáme trik na kreslení plných čar místo samostatných bodů - při kreslení na plátno používáme metodu lineTo, takže vzdálenost mezi souřadnicemi myši je spojené přímkou.

Nyní se pojďme podívat na server!

Strana serveru

Po přečtení kódu na straně klienta se můžete obávat, že kód na serveru je ještě delší. Ale budete se mýlit. Kód na straně serveru je mnohem kratší a jednodušší. Poskytuje soubory, když lidé přistupují k adrese URL aplikace ve svých prohlížečích, a předávají socket.io zprávy. Oba tyto úkoly jsou podporovány knihovnami, takže jsou co nejjednodušší.

app.js

// Including libraries

var app = require('http').createServer(handler),
    io = require('socket.io').listen(app),
    static = require('node-static'); // for serving files

// This will make all the files in the current folder
// accessible from the web
var fileServer = new static.Server('./');

// This is the port for our web server.
// you will need to go to http://localhost:8080 to see it
app.listen(8080);

// If the URL of the socket server is opened in a browser
function handler (request, response) {

    request.addListener('end', function () {
        fileServer.serve(request, response); // this will return the correct file
    });
}

// Delete this row if you want to see debug messages
io.set('log level', 1);

// Listen for incoming connections from clients
io.sockets.on('connection', function (socket) {

    // Start listening for mouse move events
    socket.on('mousemove', function (data) {

        // This line sends the event (broadcasts it)
        // to everyone except the originating client.
        socket.broadcast.emit('moving', data);
    });
});

Tímto je naše aplikace pro kreslení kompletní!

Hotovo!

Kreslení je mnohem zábavnější, když vidíte, že to dělají ostatní lidé ve stejnou dobu. Neváhejte si pohrát s příkladem a vylepšit ho! Několik nápadů:bylo by úžasné mít vedle kurzorů různé štětce, gumy, barvy a tvary nebo dokonce vlajky zemí. Zblázněte se!


No