Muszę się do czegoś przyznać. Tak naprawdę, nigdy nie napisałem żadnej konkretnej aplikacji używając modułów node’a. Znam teorię, wiem jak działają ale nie mam najważniejszego – doświadczenia. Dlatego zanim rozpocznę właściwą pracę nad projektem Daj Się Poznać, chciałem stworzyć mini projekt testowy. Tak na rozgrzewkę.
Początkowo nie miałem opisywać go na blogu. Jednak gdy skończyłem, doszedłem do wniosku, że lepiej podzielę się ze światem wynikami mojej pracy. Głównie dlatego, że nie wiem czy robię to dobrze 🙂 . A tak może ktoś zwróci uwagę na ewentualne błędy lub potwierdzi, że wszystko jest ok.
Celem projektu było stworzenie prostej implementacji klasycznej gry snake. Chciałem popracować z modułami node’a w połączeniu z najważniejszym dla mojego projektu elementem canvas. Napisanie prostej gry było idealnym rozwiązaniem.
Gra jest bardzo prosta i posiada tylko jeden stan. Wąż porusza się, reaguje na przyciskanie strzałek, zjada pojawiające się losowo jabłka, rośnie i przyśpiesza. Jeśli nastąpi kolizja węża z jego ogonem lub z krawędzią pola gry, gra resetuje się.
Tym razem udostępniam tylko paczkę z kodem. Wewnątrz są wszystkie pliki i moduły używane podczas tworzenia programu (łącznie z moim package.json). Aby zagrać wystarczy uruchomić w przeglądarce plik index.html. Gra nie jest działająca w stu procentach. Jednym z problemów jest na przykład to, że można bardzo szybko przycisnąć odpowiednie strzałki, co powoduje, że wąż włazi sam w siebie. Możliwe, że są też inne. Ale nie o to chodziło, żeby było idealnie. Chodziło to żeby działało 🙂
Projekt podzieliłem na moduł wejściowy (main.js), główny moduł gry (game.js), moduły konfiguracyjne (canvas.js, helpers.js, keys.js) oraz moduły obiektów gry (snake.js, apple.js).
Wszystko to za pomocą Browserify upycham do pliku bundle.js, który z kolei dodawany jest do index.html.
Zacznę od pliku wejściowego czyli main.js:
1 2 3 |
var game = require('./game'); game.init(); |
Jak widać, nic wielkiego. Po prostu pobieram moduł game i uruchamiam jego funkcję init.
Kolejny moduł to właśnie game. Jest to główny obiekt gry. To tutaj znajduje się pętla odpowiadająca za przebieg rozgrywki:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
var c = require('./Config/canvas'); var keys = require('./Config/keys'); var helpers = require('./Config/helpers'); var apple = require('./Actors/apple'); var snake = require('./Actors/snake'); startloop = function(){ window.requestAnimationFrame(startloop,c.canvas); updateGame(); drawGame() } initGame = function() { keys.init(); snake.init(); startloop(); } drawGame = function() { c.ctx.clearRect(0,0,c.width,c.height); if(apple.isOnScreen()) { apple.draw(c.ctx); }; snake.draw(c.ctx); } updateGame = function() { if(keys.isPressed('UP') && !keys.isPressed('DOWN') && snake.getDirection() != 'down'){ snake.setDirection('up'); } if(keys.isPressed('DOWN') && !keys.isPressed('UP') && snake.getDirection() != 'up'){ snake.setDirection('down'); } if(keys.isPressed('LEFT') && !keys.isPressed('RIGHT') && snake.getDirection() != 'right'){ snake.setDirection('left'); } if(keys.isPressed('RIGHT') && !keys.isPressed('LEFT') && snake.getDirection() != 'left'){ snake.setDirection('right'); } if(!apple.isOnScreen()){ apple.spawn(); apple.setOnScreen(true); } snake.update(); if(helpers.checkApple(snake.getBody(),snake.getLength(),apple.getPosition())){ apple.setOnScreen(false); snake.grow(); snake.quickenSnake(); } if(helpers.checkCollisions(snake.getBody(),snake.getLength(),snake.getSize(),c)){ snake.reset(); apple.setOnScreen(false); } } module.exports = { init: function(){ initGame(); } } |
Na początku ładuję wszystkie moduły i przypisuję je do odpowiednich zmiennych. Dalej znajdują się funkcje nie będące częścią obiektu exports. Pierwsza z nich to startLoop. Wywołuje ona rAF, w którym z kolej wywoływane są funkcje updateGame oraz drawGame.
Następna funkcja to initGame. W niej inicjalizowane są moduły keys oraz snake. Każdy z nich ma własną funkcję init. Przypominam, że aby można było użyć funkcji z zewnętrznego modułu, musi ona znajdować się w obiekcie exports tego modułu 🙂 Będzie to ładnie widoczne gdy przejdę do konkretnych modułów. w funkcji initGame wywoływana jest też startLoop, co wprawia grę w ruch.
Kolejna funkcja to drawGame. Najpierw czyszci pole elementu canvas. Następnie, jeżeli funkcja isOnScreen modułu apple zwróci true, wywoływana jest funkcja draw tego samego modułu. Na koniec zawsze wywoływana jest funkcja draw modułu snake.
Ostatnia ‚prywatna’ funkcja to updateGame. Jest ona najdłuższa ale wcale nie bardziej skomplikowana. Pierwsza część funkcji to cztery wyrażenia warunkowe. W każdym z nich sprawdzam za pomocą funkcji isPressed modułu keys czy wciśnięta jest któraś ze strzałek. Jeżeli tak i jeżeli nie jest wciśnięta przeciwna strzałka i jeżeli wąż nie porusza się w przeciwnym kierunku (funkcja getDirection modułu snake), zmieniam kierunek poruszania się węża (funkcja setDirection modułu snake). W kolejnej części funkcji przy pomocy isOnScreen modułu apple, sprawdzam czy jabłko znajduje się na planszy. Jeżeli nie, wywoływane są funkcje spawn i setOnScreen tego samego modułu. Ta druga funkcja jako parametr otrzymuje wartość boolowską true. Gdy jest to już gotowe aktualizuję węża wywołując metodę update modułu snake.
Pozostała część kodu updateGame to wywołanie dwóch funkcji modułu helpers – checkApple oraz checkCollision. Pierwsza sprawdza, czy wąż nie zjadł przypadkiem jabłka a druga, czy nie wszedł w swój ogon. Jeżeli warunek pierwszej funkcji zostanie spełniony, jabłko znika z planszy (funkcja setOnScreen(false) modułu apple) a wąż rośnie (funkcja grow modułu snake) i przyśpiesza (funkcja quickenSnake modułu snake). Jeżeli warunek funkcji checkCollisions będzie prawdziwy, wąż jest resetowany a jabłko znika z planszy.
Na końcu modułu game znajduje się obiekt exports, który otrzymuje tylko jedno pole – init. Jest to funkcja w który wywoływane jest initGame. I to cały moduł :).
Teraz przyjrzę się modułom z folderu config. Pierwszy z nich to canvas
1 2 3 4 5 6 7 8 |
var canvas = document.getElementById('canvas'); module.exports = { canvas: canvas, ctx: canvas.getContext('2d'), width: canvas.width, height: canvas.height, }; |
W tym module po prostu pobieram element canvas z DOMu a następnie w obiekcie exports ‚upubliczniam’ jego najczęściej używane właściwości. Znajdują się tu wymiary płótna, kontekst, oraz bezpośredni odnośnik do płótna.
Kolejny moduł konfiguracyjny to keys:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
var pressedKeys = {}; var keys = { SPACE: 32, UP: 38, DOWN: 40, LEFT: 37, RIGHT: 39, } module.exports = { init: function(){ window.addEventListener("keydown", function keydown(e) { pressedKeys[e.keyCode] = true; },false) window.addEventListener("keyup", function keydown(e) { delete pressedKeys[e.keyCode]; },false) }, isPressed: function(key){ if(pressedKeys[keys[key]]){ return true; } else { return false; } } } |
Ten moduł służy do obsługi przyciskanych na klawiaturze klawiszy. Posiada dwie zmienne, pierwsza z nich to obiekt pressedKeys, który na początku jest pusty. Druga zmienna to keys. Ona też zawiera obiekt, którego pola to nazwy przycisków a właściwości to ich kody.
Moduł posiada dwie funkcje przekazywane są do obiektu exports. Pierwsza z nich to init. Sprawia ona, że po wciśnięciu klawisza jego kod trafia do obiektu pressedKeys. Po puszczeniu klawisza, odpowiednie pole wspomnianego wcześniej obiektu jest kasowane. Nic nowego, ten mechanizm pojawiał się w moich programach już wiele razy.
Druga eksportowana funkcja to isPressed. Przyjmuje ona jeden parametr. Będzie to nazwa przyciśniętego klawisza. Jeżeli w obiekcie pressedKeys, znajduje się kod, który pasuje do tej nazwy, zwracane jest true, w przeciwnym razie, funkcja zwraca false.
Ostatni moduł konfiguracyjny to helpers. Trafiły do niego wszystkie funkcje użytkowe, których nie mogłem za bardzo umieścić nigdzie indziej a nie pasowały mi w module game. Oto treść helpers:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
module.exports = { checkApple: function(snake,snakeLength,apple){ var snakeHead = snakeBody[snakeLength-1]; if(snakeHead.x == apple.x && snakeHead.y == apple.y){ return true; } else { return false; } }, checkCollisions: function(snake,sLength,sSize,canvas){ var sneakHead = snake[sLength-1]; if(sneakHead.x*sSize >= canvas.width || sneakHead.x < 0){ return true; } else if(sneakHead.y*sSize >= canvas.height || sneakHead.y < 0){ return true; } for(var i = 0; i < sLength-1; i++){ if(sneakHead.x == snake[i].x && sneakHead.y == snake[i].y){ return true; } } return false; } } |
Ten moduł to tak naprawdę tylko dwie funkcje, obie znajdują się w obiekcie exports. Pierwsza z nich, checkApple, przyjmuje w parametrach potrzebne jej właściwości węża i jabłka a następnie sprawdza czy nie następuje między nimi kolizja. Jeżeli tak zwracane jest true, w przeciwnym wypadku false.
Druga funkcja checkCollisions, również otrzymuje jako argumenty potrzebne jej dane. Sprawdza czy ‚głowa’ węża nie koliduje z którymś z pozostałych jego elementów oraz czy nie znalazła się poza granicami płótna. Jeśli któryś z powyższych warunków zostanie spełniony, funkcja zwraca true, W przeciwnym razie zwracane jest false.
Ostatnie dwa moduły przedstawiają ‚bohaterów’ gry. Pierwszy z nich to snake:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
var body = []; var segmentSize = 25; var length = 3; var currDirection = 'right'; var framesToMove = 20; var lastMove = 0; moveSnake = function(){ var tempx = body[length-1].x; var tempy = body[length-1].y; var prevX = tempx; var prevY = tempy; switch(currDirection){ case "right": body[length-1].x++; break; case "down": body[length-1].y++; break; case "left": body[length-1].x--; break; case "up": body[length-1].y--; break; } for(var i = length-2; i>=0;i--) { tempx = body[i].x; tempy = body[i].y; body[i].x = prevX; body[i].y = prevY; prevX = tempx; prevY = tempy; } } module.exports = { init: function(){ for (var i=0; i < length; i++) { body.push({x:i,y:0}); }; }, getDirection: function(){ return currDirection; }, getLength: function(){ return length; }, getSize: function(){ return segmentSize; }, getBody: function(){ return body; }, setDirection: function(direction) { currDirection = direction; }, draw: function(ctx){ ctx.fillStyle = "#000"; for(var i=0; i < length; i++) { ctx.fillRect(body[i].x*segmentSize,body[i].y*segmentSize,segmentSize,segmentSize); }; }, update: function(){ if(framesToMove === lastMove) { moveSnake(); lastMove = 0; } else { lastMove++; } }, grow: function(){ length++; body.unshift({x:-1,y:-1}); }, quickenSnake: function(){ framesToMove -= 2; if(framesToMove < 5){ framesToMove = 5; }; }, reset: function(){ length = 3; currDirection = 'right'; framesToMove = 20; lastMove = 0; body = []; for (var i=0; i < length; i++) { body.push({x:i,y:0}); }; } } |
To najdłuższy moduł programu ale tak jak i poprzednie nie zawiera żadnej skomplikowanej logiki.
Na początku znajdują się zmienne związane z wężem:
- body – tablica przechowująca obiekty reprezentujące segmenty węża.
- segmentSize – rozmiar węża w pikselach.
- length – ilość segmentów węża.
- currDirection – aktualny kierunek, w którym podąża wąż.
- framesToMove – liczba klatek co którą wąż się porusza.
- lastMove – licznik zawierający informacje ile klatek wąż się nie poruszył.
Następnie pojawia się jedna funkcja ‚prywatna’ moveSnake. Zawiera ona logikę poruszania się węża. Nie będę jej opisywał dokładnie ponieważ już raz to zrobiłem. Jest ona identyczna jak w mojej poprzedniej implementacji węża 🙂 .
W obiekcie exports znajduje się dość sporo funkcji. Pierwsza z nich to init. Wypełnia ona tablicę body odpowiednią ilością obiektów reprezentujących segmenty ciała węża. Obiekty otrzymują pola x oraz y reprezentujących położenie danego segmentu. Początkowo wąż znajduje się w lewym górnym rogu i porusza się w prawo.
Następne cztery funkcje to zwykłe ‚getter’y’, które zwracają wartości prywatnych zmiennych modułu. Są to getDirection, getLength, getSize oraz getBody. Kolejna funkcja to ‚setter’ – setDirection. Ustawia ona wartość zmiennej direction na tę którą otrzyma jako parametr.
Kolejna funkcja draw wyrysowuje wszystkie elementy węża na płótnie w odpowiednich pozycjach. Wskaźnik do kontekstu płótna przekazywany jest w argumencie funkcji.
Funkcja update wywołuje prywatną funkcję moveSnake jeżeli wartość lastMove równa jest framesToMove. W przeciwnym razie wartość lastMove jest inkrementowana.
Funkcje grow oraz quickenSnake wywoływane są gdy wąż zje jabłko. Pierwsza dodaje nowy segment do ciała węża a druga zmniejsza wartość framesToMove o dwa (z tym, że wartość ta nie może spaść poniżej 5).
Ostatnia publiczna funkcja reset odpowiedzialna jest za zresetowanie węża po tym jak gracz skusi. Ustawia ona wszystkie zmienne modułu snake do wartości początkowych i na nowo inicjalizuje węża.
I tak dotarłem do ostatniego modułu czyli apple. Oto jego zawartość:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
var size = 25; var onScreen = false; var appleCorrect = true; var snake = require('./snake'); var c = require('../Config/canvas'); var color = '#F00'; var x = -1; var y = -1; module.exports = { isOnScreen: function(){ return onScreen; }, getPosition: function(){ return {'x':x,'y':y}; }, setOnScreen: function(bool){ onScreen = bool; }, spawn: function(){ snakeBody = snake.getBody(); do { appleCorrect = true; x = Math.floor(Math.random() * (c.width/size)); y = Math.floor(Math.random() * (c.height/size)); for(var i = 0; i < snake.getLength(); i++){ if(x == snakeBody[i].x && y == snakeBody[i].y ){ appleCorrect = false; break; } } } while(!appleCorrect); }, draw: function(ctx){ ctx.fillStyle = color; ctx.fillRect(x*size,y*size,size,size); } } |
Nie będę już dokładnie opisywał tego ostatniego modułu. Jeżeli działanie poprzednich modułów było dla czytelnika jasne, to i tu nie powinno być problemu. Tym bardziej, że logika nie różni się zbytnio od mojej poprzedniej implementacji węża.
Jest tu za to inna ciekawostka. W tym module trochę poeksperymentowałem. Nie przekazuję właściwości innych modułów w argumentach publicznych funkcji. Zamiast tego ‚rekłajeruję’ je bezpośrednio do zmiennych. Czy to dobra praktyka? Może wszędzie powinienem tak robić? Nie wiem (patrz tytuł posta), ale zadziałało 🙂 . Może są wśród czytelników ludzie bardziej otrzaskani z nodem i coś mi podpowiedzą? Byłbym bardzo wdzięczny.
I to tyle jeśli chodzi o dzisiejszy post. Nie wiem czy dobrze operuję modułami noda, ale wiem, że bardzo przyjemnie mi się z nim pracuje. Jak tylko opublikuję ten wpis, zabieram się za pisanie konkursowego projektu. Tak, w następnym wpisie w końcu pojawią się pierwsze podejścia do implementacji mojej własnej platformówki 🙂
Tymczasem zachęcam wszystkich tych, którzy jeszcze tego nie zrobili do polubienia mojej strony na facebooku. To dobre miejsce na kontakt ze mną. Zamieszczam tam też informacje o wszystkich nowościach na blogu, więc warto tam zaglądać aby być na bieżąco.
Kolejny ciekawy wpis. Ostatnio ogarniałem nodejs i jakby zalety jego uzywania wydaję się w miare klarowne, ale zastanawia mnie jedna rzecz i ciekawi mnie co Ty o tym uważasz 🙂 Otóż moduły node posiadają wiele zależności od innych modułów, finalnie pobierając paczkę kilku modułów do niedużego projektu, i finalnie projekt waży 100 albo 200 MB i ma w sobie kilka tysięcy plików (których nawet usuwanie trwa z 1-2 min), gdzie projekt w czystym JS lub + jquery można by napisać (co prawda większym nakładem pracy i większą ilością własnego kodu) tak aby ważył np. 50 KB. Dobrych kilka lat temu na studiach uczono mnie jak ważne jest trzymanie czystości kodu, aby on mało ważył – generalnie aby robić wszystko optymalnie, a tu nagle do prostego projektu potrzebuję tysięcy plików ważących w sumie kilkaset MB.
Jak Ty, jako bardziej doświadczony programista się na to zapatrujesz?
Tu też ciekawy tekst o tym: https://medium.com/friendship-dot-js/i-peeked-into-my-node-modules-directory-and-you-wont-believe-what-happened-next-b89f63d21558#.31hlyjoxh
NPM to ‚miejsce’, gdzie publikować może każdy a wiadomo jakie to może nieść za sobą konsekwencje. Osobiście uważam, że jeżeli jesteś w stanie napisać coś sam i nie zajmie Ci to więcej niż kilka godzin, to lepiej zrobić to samemu. Oczywiście jest sporo ‚pewnych’ paczek i nic nie stoi na przeszkodzie żeby z nich korzystać. Ale zawsze warto jest się zastanowić czy faktycznie jest mi potrzebny cały moduł. Co do podlinkowanego artykułu, nie jestem pewny czy to nie jest mały trolling :), ale dobrze sygnalizuje problem instalowania co popadnie. Najgłośniejszy rezultat takiego podejścia to afera z left-padem: http://www.theregister.co.uk/2016/03/23/npm_left_pad_chaos/
Tego projektu nie da się uruchomić na serwerze, racja? Tylko przez index.html. Mam przez to wrażenie, że pokazałeś tu w zasadzie możliwości JS, a nie node’a.