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)
- Real-time multiplayer Facebook piškvorky 6. 2. 2013
- Real-time multiplayer Facebook piškvorky – real-time multiplayer 11. 2. 2013
- Real-time multiplayer Facebook piškvorky – Facebook 20. 2. 2013
Nálepky:
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ě.
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.
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:
- Kinetic.JS API dokumentace
- Kinetic.JS tutoriály — na každý tvar, události, animace příklad
- základy canvasu a pokročilé věci v 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.
Díky hlavně za ten lehký náhled na KineticJS. Teším se na další díly.
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…
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 :)
Ano, zatím jsou puštěny celé články.
Včera jsem tu celý den hledal možnost vložit komentář a nenašel :(
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.
Vytváření všech properties v konstruktoru objektu je dobré z toho důvodu, že a) při pohledu do konstruktoru vidím, co od objektu očekávat a b) jim pak V8 přiřadí stejnou hidden class – https://developers.google.com/v8/design#prop_access Dalo by se klidně inicializovat na null.
null vs. undefined a chyby – to zní dobře, asi to taky začnu tak používat!
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;
Delete optimalizaci pomocí skrytých tříd rozhodně zabíjí, dobrá poznámka!
V tomto případě to však nevadí, protože config je použit jedinkrát, při inicializaci objektů. XO.* objekty jsou jen továrničky na Kinetic objekty, vezmou si z configu, co je určeno k jejich inicializaci, a zbytek použijí pro inicializaci vytvářeného Kinetic objektu, proto delete.
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/