HTML, CSS və JS ilə “Snake” oyunu

Yunis Huseynzade
Pragmatech
Published in
9 min readNov 8, 2020

İlan oyunu dünyanın ən məhşur oyunlarından biridir. Bu oyunun bir çox versiyası var və demək olar ki bütün platformalarda oynamaq olur. Bu sevilən oyunu yenidən yaratmaq isə alqoritm qabiliyyətləri üçün yaxşı məşğələdir.

Biz bu məqalədə HTML, CSS və JS-dən istifadə edəcəyik. Həmçinin məqalədə JS və CSS-də bəzi gələcəkdə faydası ola biləcək “trick”-lər də olacaq. Ümid edirəm ki, bu məqalə sizin üçün faydalı bir məqalə olacaq. İndi isə oyunumuza başlayaq.

1.Fayl strukturu

/snake-game
|---js/
---app.js
|---css/
---style.css
|---index.html

Başlanğıc template yuxarıdakı kimidir.

İlk öncə HTML-i yazaq. HTML də oyun sahəsi olacaq bir div olmalıdır. Həmçinin css və script-i də import etməliyik. Bütün kodları yazdıqdan sonra html faylımız belə görünəcək:

Script tag-inə əlavə olaraq iki atribut da əlavə etdim. defer atributu bu faylın ən sonda yüklənməli olduğunu deyir. Yəni body tag-inin sonuna yazmaqla eyni işi görür.type=“module” isə ES6 modullarından istifadə etməyimizə imkan yaradır. ES6 modulları sayəsində, eyni ilə Python və başqa dillər kimi dəyişənləri export və import edə bilərik. Həmçinin kodumuzu bir neçə yerə bölmək development mərhələsinin daha rahat olmasına kömək edir və kodumuz daha anlaşıqlı olur.

2.Stillər

Snake oyunu xanalardan ibarətdir. Bu xanaları yaratmaq üçün isə CSS-də grid adlanan çox əlverişli bir yol var. İlk öncə css faylımıza body üçün bəzi xüsusiyyətlər əlavə edək.

İndi isə bu sətirləri izah edək. 2-ci sətir və 3-cü sətir body-ə bütün ekranı tutmasını deyir. 4, 5 və 6-cı sətirdəki kodlar isə #game-board div-ini ekrana mərkəzləşdirir. Növbəti olaraq #game-board div-inə stil vermək lazımdır.

Böyük ehtimal bu sətirlərdə ilk olaraq vmin keyword-ü diqqətinizi çəkdi. vmin keyword-ü brauzerin eni və ya uzunundan hansı daha kiçik olarsa onun ölçüsünü ifadə edir. 100vmin isə bu ölçünün 100%-i mənasına gəlir. Məsələn brauzerin ekran ölçüsü 1920x1080-dirsə o zaman vmin ən kiçik ölçü olan 1080-i yəni ekranın height-ini götürür. 100vmin isə 1080 x 100%=1080 olur. Mobil brauzerlərdə isə əksi olur, çünki width daha az olur. Həm height həm də width 100vmin olduqda qutu hər zaman kvadrat olur. Bu isə bizim tam olaraq ehtiyacımız olan şeydir. display: grid isə div-i hissələrə bölür. grid-template-rowsgrid-tamplate-columns property-ləri isə bölünməli olan sətir və sütunların sayını bildirir. Chrome DevTools da elementləri inspect edərkən mouse-u #game-board-ın üzərinə gətirsəniz onun hissələrə bölündüyünü görəcəksiniz.

Növbəti olaraq ilan və yemək üçün lazım olan stilləri yazaq.

3.Alqoritm

Artıq proyektin ən maraqlı hissəsi olan JS-ə başlaya bilərik. ilk öncə js folder-i içində constants.js adlanan bir fayl yaradaq və aşağıdakı kodları daxil edək.

export const EXPANSION_RATE = 2
export const GRID_SIZE = 21
export const SNAKE_SPEED = 5

Bu dəyişənlər layihəmizin sabitləri olacaq. EXPANSION_RATE ilanın hər bir yemək yeyəndə artan uzunluğudur. GRID_SIZE x yə y oxu üzrə olacaq xanaların sayıdır. SNAKE_SPEED isə ilanın hərəkət sürətidir. export keyword-ü vasitəsilə bu dəyişənləri başqa .js fayllarında import edərək istifadə edə bilərik. Python-dan fərqli olaraq JS-də yalnız export olunmuş dəyişənlər import edilə bilir. export-un ES6 sintaksı isə belədir. Lakin NodeJS kimi bəzi tool-larda isə aşağıdakı sintaksdan istifadə edilir.

module.exports = {EXPANSION_RATE:2,...}

Biz bu məqalədə ES6 sintaksından istifadə edəcəyik.

Artıq oyunun alqoritmini qurmağa başlaya bilərik.

JS-də oyun yazarkən işinizi asanlaşdıran metodlardan biri də window.requestAnimationFrame metodudur. Bu metod brauzerə sizin bir animasiya göstərmək istədiyinizi və bu animasiya üçün müəyyən bir metodun çağılırmalı olduğunu deyir. Biz bu metodu ekrana oyunu render etmək üçün istifadə edəcəyik. window.requestAnimationFrame bir parametr qəbul edir: Callback funksiya. Callback funksiya brauzer tərəfindən avtomatik olaraq çalışdırılacaq. Həmçinin brauzer tərəfindən callback funksiyaya da timestamp adında parametr ötürülür. Adından da göründüyü kimi bu parametr hazırda olan timestamp-a malikdir. Əgər funksiyanı özümüz manual olaraq çağırsaydıq o zaman timestamplərlə də özümüz məşğul olmalı idik. İndi isə kodu yazaq.

window.requestAnimationFrame metodu funksiyanın işləməsini dayandırmadığına görə onu yuxarıda yaza bilərik. window.requestAnimationFrame-i niyə yuxarıda yazdığımı birazdan görəcəksiniz. Həmçinin hər dəfə main funksiyası çağırılanda update və draw funksiyaları da işləyəcək. Update ilan və yeməyin koordinatları kimi bəzi işləri yerinə yetirəcək. Draw isə update olunmuş kordinatları ekrana render edəcək. Aşağıdakı window.requestAnimationFrame isə dövrün başlanmağı üçündür.

İlanın sürətini dəyişmək və ya azaltmaq üçün update və draw-un müəyyən bir tezliklə çağırılması lazımdır. Ona görə də kodumuza aşağıdakı əlavələri edək.

import keyword-ü ilə SNAKE_SPEED sabitini contants.js faylından import edə bilərik. İndi isə dəyişiklikləri bir-bir izah edək. 4-cü sətirdə elan etdiyimiz lastRenderTime sonuncu dəfə render edilən zaman olan timestamp-ə bəradərdir. 9-cu sətirdəki secondsSinceLastRender sonuncu dəfə renderdən sonra keçmiş zamanı göstərir. 1000-də bölməyimizin səbəbi timestamplərin millisaniyə ilə ifadə olunmasıdır. Millisaniyəni isə saniyəyə çevirmək üçün 1000-ə bölmək lazımdır. 10-cu sətirdəki if statement-ini izah edək. 1 / SNAKE_SPEED hər render üçün lazım olan zaman aralığıdır. 1 / SNAKE_SPEED olması ilə, render zamanı SNAKE_SPEED dən tərs asılı olur və SNAKE_SPEED artdıqca update və draw daha tez-tez çağırılır. Əgər sonuncu render-dən keçən vaxt tələb olunan vaxt aralığından kiçikdirsə, o zaman funksiyanı yarıda kəsir. Əks halda isə davam edir. requestAnimationFrame-in funksiyanın əvvəlində olmasının da səbəbi budur. Əgər funksiya kəsilsə window.requestAnimationFrame çağırılmayacaq və dövr qırılacaq. Böyük ehtimal ekran sadəcə 1 dəfə render olunacaq. Hər render zamanı isə kodumuzun işlədiyindən əmin olmaq üçün 12-ci sətirdəki kimi console-a “Render” sözünü print edə bilərik. Brauzer-i açdıqda isə console-da hər saniyə ərzində 5 dəfə “Render” yazdığını görə bilərsiniz

Artıq dövrü olaraq render məntiqini qurduğumuza görə ilanın funksiyalarını yazmağa başlaya bilərik.

Əvvəlcə snake.js adında bir fayl yaradaq. Daha sonra isə aşağıdakı kodları daxil edək.

3-cü sətirdəki snakeBody array-i ilanın hər bir hissəsinin koordinatlarını saxlayır. İlk indeksi isə ilanın başının kordinatlarıdır. İlanın başlama kordinatını hard-coded bir dəyər ilə ifadə edə bilərik. 11x11 grid sistemin mərkəzidir. update funksiyası ilanın kordinatlarını təzələyəcək. İlan oyununda ilanın hər bir hissəsi hər renderdə özündən əvvəlki hissənin koordinatına malik olur. Bunu isə 8-ci sətirdəki for döngüsü ilə edə bilərik. 12 və 13 sətir isə ilanın başının hərəkət istiqamətini göstərir. İndilik hard-coded bir dəyər verə bilərik. Daha sonra bu dəyəri arrow keylər vasitəsilə dəyişəcəyik. draw funksiyası isə parametr olaraq oyunun olacağı div-i qəbul edəcək. Yəni, #game-board. 18-ci sətirdə isə hər bir ilan hissəsini kordinatlarına görə ekrana yazırıq.

İndi isə app.js-ə ilan üçün xüsusiyyətləri əlavə edək.

2-ci sətirdəki import ilə ilanın update və draw funksiyalarını import edirik. Amma funksiyaların qarışmaması üçün as keyword-ü ilə funksiyaların adını dəyişirik. 6-cı sətirdə gameBoard div-ini seçirik. 24-cü sətirdə updateSnake funksiyasını çağırırıq artıq ilanın kordinatları dəyişəcək. 29-cü sətirdəki drawSnake funksiyasına isə bayaq parametr kimi qəbul etdiyimiz gameBoard-ı ötürürük. 28-ci sətirdəki kodun səbəbi isə hər dəfə ekranı təmizləməkdir. əgər təmizləməsən ekranda ilan daim genişləyər. Bu kodu comment-ə alaraq səbəbini daha yaxşı anlaya bilərsiniz.

İndi isə input.js adında bir fayl yaradıb içinə aşağıdakı kodları daxil edək.

inputDirection hal hazırda ilanın hərəkət etdiyi istiqaməti saxlayır. lastInputDriection isə sonuncu istiqaməti. 10-cu sətir ilə arrow key-ləri handle edirəm. Böyük ehtimal 13-cü sətirdəki kod və bənzərləri diqqətinizi çəkdi. Bu kodun səbəbi ilanın geri dönə bilməməsi üçündür. Çünki ilan oyununda sağa gedən bir ilə öz üstündən sola getməyə başlaya bilməz. export etdiyimiz getInputDirection isə bizə bu directionları ilana tətbiq etmə şansı yaradır.

İndi isə kontrol funksiyası əlavə etmək üçün snake.js-də dəyişikliklər edək.

3-cü sətirdə import etdiyimiz getInputDirection metodu ilə 10-cu sətirdə gedilən istiqaməti əldə edirik. Və onu hard-code etdiyimiz 16 və 17-ci sətirlərə əlavə edirik. Artıq brauzerdə ilanı hərəkət etdirə bildiyinizi görəcəksiniz.

Növbəti olaraq food.js faylını yaradaraq yemək üçün lazım olan kodları yaza bilərik.

2-ci sətirdəki food dəyişəni yeməyin x və y koordinatlarını saxlayır. Hələlik koordinatlara hard-coded bir dəyər vermişik. update funksiyasına daha sonra əlavələr edəcəyik. draw funksiyası isə ilandakı kimi gameBoard parametri qəbul edir və yeməyi müəyyən koordinatlara render edir.

Növbədə isə eyni ilə yeməyi də oyuna əlavə edək.

3-cü sətirdə funksiyaları import edirik və update və draw funksiyalarına əlavə edirik.

Oyunda əsas məqsəd ilanın yeməkləri yeyib böyüməsidir. Ona görə də ilanın yeməyi yeməsi üçün alqoritm yazmalıyıq.

İlk öncə hər hansı bir kordinatın ilanın üstündə olub olmamasını tapmalıyıq. Bunun üçün snake.js faylına aşağıdakı dəyişiklikləri etmək lazımdır.

onSnake funksiyası bir məcburi, bir optional parametr qəbul edir. Position parametri x və y key-ləri olan obyekt olmalıdı. İkinci parametr haqqında daha sonra danışacağıq. Array.some metodu ilə üst-üstə düşən bir koordinatın olub olmamasını yoxlayırıq. Yoxlamaq üçün isə equalPositions funksiyasından istifadə olunur. 32-ci sətiri isə ignoreHead ilə birlikdə izah edəcəm. Növbəti olaraq isə onSnake metodunu food.js-ə import edib yoxlamaq lazımdır.

Brauzerdə yoxlasanız, hər şeyin yaxşı işlədiyini görəcəksiniz. Lakin hələ də çatışmayan cəhətlər var.

4 və 9-cu sətirə hard-coded dəyərlər verdik. Lakin indi bu dəyərləri dəyişmək lazımdır. İlan oyununda yemək ilanın üstündə və oyun xaricində olmayan yerlərdə random olaraq yaranır. İndi isə random üçün lazım olan kodları yazaq.

grid.js faylı yaradın və içərisinə aşağıdakı kodları daxil edin.

İlk sətirdə məqalənin əvvəlində təyin etdiyimiz sabitlərdən birini import edirik. 3-cü sətirdə isə parametr qəbul etməyib, bizə random dəyər qaytaran bir funksiya elan edirik. 5 və 6-cı sətir isə [1:21] aralığında random bir ədəd qaytarır. Math.random metodunun özü [0:1] aralığında dəyər qaytarır və 0 koordinatının grid-dən kənarda olduğuna görə + 1 əlavə edirik. İndi isə bunu food.js-ə əlavə edək.

getRandomGridPosition funksiyası həmçinin ilanın üstündə olan koordinatlar da return edə bilər. Ona görə də 22-ci sətirdə ilanın üstündə olmayan bir koordinat return edən getRandomFoodPosition funksiyası yaratmalıyıq.

Artıq brauzer-də ilanın hər dəfə yemək yeyəndən sonra yeməyin random bir koordinata getdiyini görə bilərsiniz.

Növbəti bir problem ilanın yemək yeyəndən sonra böyüməməsidir. Bunun üçün isə snake.js-ə aşağıdakı kodları daxil etməliyik.

Yeni bir new segments dəyişəni elan etdik. Bu, hər dəfə əlavə olunmalı olan segmentlərin sayıdır. Bu dəyər ona 46-cı sətirdə amount parametri ilə mənimsədilir. amount parametri food.js-dən göndəriləcək. addSegments funksiyası isə newSegments-in sayı qədər ilanın quyruğunun kordinatında olan xana push edir. Və təzədən yeni hissələrin əlavə olunmaması üçün newSegments-i 0-a bərabər edirik. İndi isə food.js-ə qayıdıb aşağıdakı kodları əlavə edək.

Artıq oyunu yoxlasanız, ilanın yemək yedikcə böyüdüyünü görəcəksiniz.

Artıq oyunun demək olar hamısı hazırdır. Lakin əsas bir hissə qalıb. Təbii ki, game over. Game over üçün 2 şərt var. İlanın özünü kəsməsi və kənara toxunması.

snake.js-ə sonuncu dəfə aşağıdakı kodu daxil edin.

getSnakeHead funksiyası adından da göründüyü kimi ilanın başının koordinatlarını geri qaytarır. snakeIntersection isə bool tipində məlumat qaytarır və ilanın başı ilə digər hər hansı bir hissəsinin koordinatlarının üst-üstə düşüb düşməməsini bilməyimiz üçün vacibdir (ilanın özü ilə kəsişməsi). İndi ignoreHead parametrinin nə işə yaradığı haqqında danışa bilərik. 62-ci sətirdə ignoreHead olmasının səbəbi, ilanın başının koordinatının hər zaman başının koordinatı ilə eyni olmasıdır. Və əgər biz bu parametri ötürməsək, onSnake funksiyası həmçinin başı da nəzərə alacaq və oyun başladığı an game over olacaq.

Növbəti game over səbəbi isə oyundan kənara çıxmaqdır. Buna görə də grid.js-ə aşağıdakı kodları daxil edin.

outsideGrid bizə ilanın başının sahədə olub olmamasını deyir.

İndi isə proyektdə son dəyişiklikləri edək. app.js-i açın və aşağıdakı kodları daxil edin.

9-cu sətirdə oyunun game over olub olmamasını saxlayan bir global dəyişən elan etdik. 14–19-cu sətirlərdə isə əgər bu dəyişən true-dursa oyunu dayandırıb, istifadəçinin yenidən oynamağı istəyib istəmədiyini soruşur. Əgər cancel olsa o zaman oyun dayanır.18-ci sətirdəki return cancel zamanı oyunun davam etməsinin qarşısını alır. Çünki 22-ci sətirdəki window.requestAnimationFrame çalışmır və render dövrü qırılır. İstifadəçi ok klikləyərsə, səhifə refresh olunur və oyun təzədən başlayır. 47-ci sətirdəki funksiya isə game over ehtimallarının olub olmamasını yoxlayır və əgər hansısa ehtimal baş versə, gameOver-i true edərək oyunu dayandırır.

Brauzer-i açıb oyunu yoxlayın.

Məqaləni oxuduğunuz üçün təşəkkürlər. Bu məqaləni yazarkən özüm də zövq aldım. Ümid edirəm ki, maraqlı və faydalı bir məqalə oldu. Aşağıda sizə bu məqalədəki bəzi hissələri anlamağınız üçün kömək edə biləcək linklər qoyacam. Əgər hər hansı bir sualınız olarsa şərh yaza və ya yunisdev.04@gmail.com email-i ilə əlaqə saxlaya bilərsiniz.

Online demo

Qaynaq kodu

--

--