یکی از ویژگی‌های جالب طراحی وب به کمک JavaScript و HTML این است که می‌توانید بازی‌های ساده بنویسید! برنامه‌نویسی یک بازی ساده مثل بازی مار که نقاطی را می‌خورد و طولانی‌تر می‌شود، به زبان جاوااسکریپت چندان پیچیده نیست و می‌توان بازی‌های به مراتب پیچیده‌تر هم تهیه کرد و در صفحات وب‌سایت قرار داد.

در این مقاله با مراحل نوشتن یک بازی ساده به کمک جاوااسکریپت و مقداری HTML و CSS‌ در خدمت شما هستیم. با ما باشید تا طراحی وب را با مثال‌های جالب و واقعی یاد بگیریم.

با Notepad++ کدنویسی کنید

اگر در برنامه‌نویسی به هر زبانی کمی حوصله کنید و خلاقانه به مسأله فکر کنید، توانمندی‌های زبان موردبحث به مراتب بیشتری از چیزی خواهد بود که در ابتدا تصور می‌کنید. در این مقاله می‌خواهیم یک پروژه‌ی کوچک یعنی نوشتن بازی مار و نقطه‌ها را مرحله به مرحله بررسی کنیم و در مراحل کار با برخی توابع و دستورات مفید در جاوااسکریپت آشنا شویم.

ابتدا بهتر است برای ساده‌تر شدن کدنویسی، نرم‌افزار Notepad++‌ را دانلود و نصب کنید چرا که این نرم‌افزار می‌تواند سینتکس‌ها و دستورات مختلف را هایلایت کند و همین‌طور ابتدا و انتهای پرانتزها و گیومه‌ها و تگ‌ها را مشخص کند. بنابراین نوت‌پد پلاس پلاس اشتباهات شما را بسیار کمتر خواهد کرد. البته می‌توانید از نرم‌افزارهای طراحی وب نیز استفاده کنید.

ایجاد صفحه وب یا فایل html

ابتدا فایلی به اسم snake.html روی دستاپ ایجاد کنید و آن را با نوت‌پد پلاس پلاس باز کنید. در فایل ایجاد شده کدهای زیر را پیست کنید که شامل تگ head و body صفحه‌ی وب می‌شود.

ایجاد پالت یا Canvas برای حرکت مار

برای ترسیم کردن عناصر گرافیکی در صفحه‌ی وب، به تگ canvas نیاز داریم. با توجه به اینکه در ادامه قرار است روی این تگ کار کنیم، به آن یک شناسه یا id مثل gameCanvas نیز می‌دهیم. بنابراین تگ مورد بحث این گونه خواهد بود:

و آن را در بخش Body صفحه‌ی وب قرار می‌دهیم:

البته می‌توانید بالای پالت یک جمله‌ی توضیحی مثل نام بازی نیز اضافه کنید:

ترسیم گرافیک در Canvas به کمک JavaScript

پالت ما در حال حاضر خالی است و باید آن را با محتوای گرافیکی پر کنیم. برای اینکه فایل مجزایی به همراه HTML بارگذاری نکنیم، می‌توانید کدهای جاوااسکریپت را نیز در فایل HTML اضافه کنیم. ابتدا تگ script را پس از Canvas اضافه کنید.

<script></script>

توجه کنید که نباید این تگ قبل از canvas قرار بگیرد چرا که کدها کار نخواهند کرد. البته می‌توانید اجرای کد را منوط به تکمیل شدن بارگذاری صفحه بکنید و اسکریپت را در head نیز قرار بدهید. به این ترتیب تمام دستورات زمانی اجرا می‌شوند که صفحه کاملاً لود شده باشد و مشکل برطرف می‌شود.

دستورات زیر را بین تگ script باز شده و بسته قرار دهید تا در ادامه توضیحات بیشتری در مورد آن بدهیم:

و اما دستورات استفاده شده از ابتدا تا انتها:

  • دو ثابت برای مشخص کردن رنگ دور پالت و رنگ پس‌زمینه‌ی پالت که سیاه و سفید است، تعریف شده است.
  • در ادامه برای یافتن پالت در صفحه، از getElementById("gameCanvas") استفاده شده که المانی با شناسه‌ی gameCanvas را پیدا می‌کند و این المان را در متغیر gameCanvas ذخیره می‌کنیم.
  • با توجه به اینکه پالت بازی ما از نوع ۲ بعدی است، از دستور gameCanvas.getContext("2d") برای دوبعدی کردن آن استفاده می‌کنیم و متغیر جدید ctx تعریف می‌شود.
  • در ادامه رنگ حاشیه و پس‌زمینه‌ی ctx با ثابت‌های تعریف‌شده در ابتدای کد مشخص می‌شود.
  • مرحله‌ی آخر ترسیم کردن مستطیلی با ابعاد معادل عرض و طول پالت است که آن را کاملاً می‌پوشاند. برای این کار از fillRect استفاده شده و مختصات نقطه‌ی شروع که ۰ و ۰ است و نقطه‌ی پایان که ۳۰۰ و ۳۰۰ است، به عنوان دو گوشه‌ی مستطیل ذکر شده است.
  • برای ترسیم حاشیه نیز از strokeRect استفاده می‌شود.

تعریف مختصات مار در پالت بازی

برای ترسیم مار، مختصات نقاط بدن مار را با یک آرایه‌ی ساده تعریف می‌کنیم. سر مار در مرکز پالت یعنی مختصات ۱۵۰ و ۱۵۰ است. بنابراین مختصه‌ی x نقاط قبلی را کمتر از ۱۵۰ در نظر می‌گیریم و در هر ۱۰ پیکسل یک نقطه تعریف می‌کنیم.

مختصه‌ی y ثابت است و این یعنی مار در ابتدا افقی است.

ترسیم و ایجاد مار در پالت

برای ترسیم مار با استفاده از نقاط ذکر شده، از تابعی استفاده می‌کنیم که به ازای هر نقطه با دستور forEach، یک مستطیل رسم کند. این کار را در دو مرحله انجام می‌دهیم. مرحله‌ی اول ترسیم مربع روی نقطه‌ای از بدن مار است:

با fillStyle رنگ را مشخص می‌کنیم و با strokestyle نوع خط حاشیه را مشخص می‌کنیم. با fillRect مربعی با نقطه‌ی شروع که یکی از نقاط بدن مار است و نقطه‌ی پایان که ۱۰ پیکسل در جهت x و y به راست و پایین حرکت کرده، رسم می‌کنیم.

و مرحله‌ی دوم تکرار ترسیم مربع‌ها است:

بنابراین در این مرحله کد HTML صفحه‌ی بازی مار به این صورت خواهد بود:

و نتیجه اینگونه است:

تحرک مار در canvas با دستورات جاوااسکریپت

برای فراهم کردن قابلیت حرکت در جهت افقی، با توجه به اینکه فاصله‌ی نقاط و عرض مربع‌های بدن مار را ۱۰ پیکسل فرض کردیم، می‌بایست کاری کنیم که هر بار موقعیت نقاط ۱۰ پیکسل افزایش پیدا کند تا مار به سمت راست حرکت کند و در نتیجه آرایه‌ی نقاط بدن مار کاملاً تغییر می‌کند:

آموزش برنامه‌نویسی بازی مار و نقطه فقط با HTML و JavaScript و CSS

توجه کنید که در تصویر فوق، سرعت تغییرات dx است که ۱۰ پیکسل فرض شده است.

برای این حرکت، تابع advanceSnake را به این صورت تعریف می‌کنیم که نقطه‌ی جدیدی به نوک مار اضافه کند و نقطه‌ی انتهای مار را حذف کند.

و اما توضیحات کد فوق:

  • ابتدا سر مار با ثابت head تعریف شده که مختصه‌ی x و y آن از مختصه‌ی x و y اولین نقطه‌ی آرایه‌ی مار بده دست می‌آید به این صورت که x با dx جمع می‌شود و y تغییری نمی‌کند.
  • با دستور unshift نقطه‌ی جدید که سر جدید مار است را به آرایه اضافه می‌کنیم.
  • با دستور pop آخرین المان آرایه‌ی نقاط مار را حذف می‌کنیم.

به این ترتیب سر جدید به مار اضافه می‌شود و آخرین نقطه‌ی بدن مار حذف می‌شود و در نتیجه به نظر می‌رسد که مار به اندازه‌ی یک مربع به راست حرکت کرده است.

برای حرکت در جهت بالا و پایین نیز روال کار مشابه است و کافی است مختصه‌ی y‌ به اندازه‌ی dy تغییر کند. بنابراین تابع advanceSnake را کمی تغییر می‌دهیم که مختصات سر جدید مار به اندازه‌ی dx و dy با مختصات قبلی فرق کند:

const head = {x: snake[0].x + dx, y: snake[0].y + dy};

برای تست کردن حرکت مار در جهت عمودی، قبل از استفاده از تابع drawSnake که مار را ترسیم می‌کند، کدهای زیر را اضافه می‌کنیم:

بنابراین کد HTML صفحه‌ی بازی مار به این صورت خواهد شد:

برای پاکسازی پالت نیز تابعی به اسم clearCanvas تعریف می‌کنیم که در مراحل بعدی و حرکت کردن مار مفید واقع می‌شود. این تابع شبیه به ترسیم اولیه‌ی پالت و پر کردن آن با مربعی سفید و حاشیه‌ی سیاه عمل می‌کند.

حرکت خودکار مار در پالت

برای حرکت کردن مار در پالت، می‌توانیم تابع advanceSnake را چند بار پشت‌سرهم اجرا کنیم. به عنوان مثال برای ۵ خانه حرکت به سمت راست، ۵ بار این تابع اجرا می‌شود و سپس تابع ترسیم مار یعنی drawSnake اجرا می‌شود.

حرکت در یک مرحله صورت می‌گیرد و پرشی خواهد بود. نتیجه را مشاهده کنید:

آموزش برنامه‌نویسی بازی مار و نقطه فقط با HTML و JavaScript و CSS

برای پله‌پله کردن حرکت مار، از تابع setTimeout جاوااسکریپت استفاده می‌کنیم تا اجرا کردن توابع حرکت و ترسیم مار، با ۱۰۰ میلی‌ثانیه تأخیر انجام شود و البته این توابع را در یک تابع جدید به اسم onTick‌ قرار می‌دهیم:

دقت کنید که وجود clearCanvas در هر مرحله‌ی ترسیم مار لازم است چرا که باید مار قبلی را از Canvas پاک کند. در غیر این صورت طول مار بیشتر و بیشتر می‌شود.

آموزش برنامه‌نویسی بازی مار و نقطه فقط با HTML و JavaScript و CSS

اما یک مشکل بزرگ دیگر در کدها باقی مانده و آن این است که پس از ۱۰۰ میلی‌ثانیه همه‌ی پله‌های حرکت مار انجام می‌شود و باز هم مار ناگهان پرش می‌کند. برای حل کردن این مشکل، تابعی به اسم stepOne را تعریف می‌کنیم که مرحله‌ی اول حرکت و ترسیم مار را انجام می‌دهد و در آن تابع دوم به اسم stepTwo نیز فراخوانی می‌شود. بنابراین دومین حرکت مار با تأخیر ۱۰۰ میلی‌ثانیه‌ای پس از اولین حرکت صورت می‌گیرد.

اما این روش هم جالب نیست چرا که مجبوریم هزاران تابع تعریف کنیم! روش بهتر این است که یک تابع حرکت و رسم مار تعریف کنیم و در این تابع، با تأخیری کوتاه، یک بار دیگر خودش را فراخوانی کنیم. به این ترتیب هر ۱۰۰ میلی‌ثانیه یک مرتبه تابع اجرا می‌شود. بنابراین تابع main به این صورت خواهد بود:

و نتیجه را مشاهده کنید:

آموزش برنامه‌نویسی بازی مار و نقطه فقط با HTML و JavaScript و CSS

بله، هنوز هم شرط و شروطی برای حرکات مار لازم است تا مار از پالت خارج نشود.

تغییر جهت حرکت مار در پالت کلیدهای جهت کیبورد جاوااسکریپت

با فشار دادن هر کلید که کد خاص و معادلی دارد، یک رویداد یا event اتفاق می‌افتد و سرعت حرکت مار که قبلاً برای سادگی فقط در جهت x بوده، عوض می‌شود. بنابراین از if استفاده می‌کنیم و هر بار بررسی می‌کنیم که کدام کلید جهت فشار داده شده و متناسب با آن، مقدار dx و dy را تعریف می‌کنیم.

دقت کنید که dy مثبت، به معنی حرکت به سمت پایین است.

هر بار زمانی که کلید فشار داده شده را چک می‌کنیم، یک شرط دیگر هم باید بررسی شود و آن جهت حرکت فعلی مار است چرا که مار نباید ناگهان ۱۸۰ درجه تغییر مسیر بدهد و برگردد.

آموزش برنامه‌نویسی بازی مار و نقطه فقط با HTML و JavaScript و CSS

برای این شرط از عبارت مخالف یعنی ! استفاده می‌کنیم و می‌بایست ثابت‌های goingUp و goingDown و goingLeft و goingDown تعریف شود. بنابراین کد به این صورت خواهد شد:

و در نهایت برای اتصال کیبورد به بازی، از addEventListener استفاده می‌کنیم و در صورت فشار دادن کلیدها، تابع تعریف‌شده یعنی changeDirection اجرا می‌شود.

ایجاد نقاط غذای مار به صورت تصادفی

غذای مار به صورت نقاطی با x و y تصادفی تعریف می‌شود. برای این کار تابع randomTen را تعریف می‌کنیم که دو عدد را دریافت کرده و عددی تصادفی یا رندم بینشان تحویل می‌دهد. تابع بعدی createFood است که مختصه‌ی افقی و عمودی غذا یعنی foodx‌ و foody را تولید می‌کند. به عنوان مثال با ارسال ۰ که عدد شروع است و عرض پالت منهای ۱۰ پیکسل که حداکثر عرض است به تابع randomTen، مختصه‌ی افقی به صورت تصادفی ایجاد می‌شود.

 در نهایت در تابع createFood‌ در یک حلقه‌ی forEach‌ بررسی می‌کنیم که نقطه‌ی غذا روی یکی از نقاط بدن مار نباشد.

برای ترسیم نقاط غذا از تابع دیگری استفاده می‌کنیم که مستطیلی قرمز با مرز سیاه در نقاط غذا ترسیم کند:

و در نهایت باید تابع createFood را در حلقه‌ای که مار را متحرک می‌کرد، یعنی تابع main، قبل از تکرار اجرای main فراخوانی و اجرا کنیم:

افزایش طول مار با خوردن نقاط غذا

ساده‌ترین الگوریتم برای شبیه‌سازی خوردن غذا و درازتر شدن مار این است که غذا را به سر جدید مار تبدیل کنیم و مار قبلی را ثابت نگه داریم. به عبارت دیگر از تابع pop‌ برای حذف کردن آخرین نقطه‌ی بدن مار استفاده نکنیم. بنابراین یک دستور شرطی نیاز داریم و باید تابع advanceSnake را اندکی تغییر بدهیم.

به جای تعریف کردن دستور شرطی، ابتدا یک ثابت تعریف می‌کنیم که در آن از && که معادل "و" یا AND است، استفاده می‌شود تا بررسی شود که آیا مختصه‌ی افقی و عمودی سر مار با مختصات افقی و عمومی نقطه‌ی غذا یکسان است یا خیر. در ادامه اگر این عدد ثابت معادل ۱ یا True بود، تابع ایجاد غذا اجرا می‌شود و اگر اینگونه نبود، تابع pop اجرا می‌شود تا آخرین نقطه‌ی مار را حذف کند و به این ترتیب مار یک نقطه حرکت کند.

نمایش امتیاز بازی مار و نقطه

بازی بدون امتیاز بی‌معنی است. بنابراین یک div با id مشخصی مثل score در صفحه‌ی html اضافه می‌کنیم و هر بار که مار نقطه‌ی غذای جدیدی را جذب کرد، امتیاز را یک واحد افزایش می‌دهیم. امتیاز اولیه صفر است، بنابراین تگ‌های بخش body به این صورت خواهد بود:

و در تابع advanceSnake تغییر دیگری ایجاد می‌کنیم که متغیر score هر بار با برخورد سر مار به نقطه‌ی غذا، ۱۰ واحد بیشتر شود و سپس مقدار المانی با شناسه‌ی score، با دستور innerHTML تغییر کند.

شرط پایان بازی

اگر سر مار با بدنه برخورد کند، بازی تمام می‌شود. با توجه به اینکه سر مار نمی‌تواند با خودش و همین‌طور ۳ نقطه‌ی بعدی تلاقی داشته باشد، حلقه‌ای با شروع از عدد ۴ می‌سازیم و بررسی می‌کنیم که آیا سر مار با بدن تلاقی دارد یا خیر.

شرط بعدی پایان بازی نیز تلاقی کردن با دیوار است که برای چهار دیوار مختلف می‌بایست بررسی شود.

اگر تابع didGameEnd مقدار True داشته باشد، در تابع main برگشت انجام می‌شود.

به این ترتیب کد کامل صفحه‌ی HTML بازی مار به این صورت خواهد شد:

دیباگ کردن کد

اگر کد فعلی را استفاده کنید و کمی مشغول بازی شوید، متوجه می‌شوید که گاهی بازی ناگهان بدون دلیل خاصی متوقف می‌شود. در این موارد برای دیباگ کردن نرم‌افزار، می‌بایست باگ را بازسازی کنید و ببینید دقیقاً در چه شرایطی مشکل ایجاد می‌شود.

مشکل فعلی حرکت مار این است که اگر کاربر قبل از ۱۰۰ میلی‌ثانیه، کلید جهت را فشار دهد، موقعیت جدید مار محاسبه می‌شود و اگر شرط تلاقی که موجب باختن وی می‌شود، برقرار باشد، بازی تمام است! برای جلوگیری از چنین اتفاقی، یک متغیر جدید به اسم changingDirection تعریف می‌کنیم و مقدار آن را در زمانی که تابع تغییر جهت فراخوانی شده، True در نظر می‌گیریم و در حالتی که مار در حرکت است و تابع advanceSnake اجرا می‌شود، False تعریف می‌کنیم. به این ترتیب می‌توانیم با یک شرط اضافی، باگ را برطرف کنیم.

در نهایت کد ما با اضافه کردن کمی استایل اضافی در تگ style که ظاهر صفحه‌ی بازی را زیباتر می‌کند، به این صورت خواهد شد:

اگر سرعت بازی زیاد است، ۱۰۰ میلی‌ثانیه را در تابع حرکت مار افزایش دهید.