18 KiB
ساخت یک بازی فضایی بخش ۳: اضافه کردن حرکت
آزمون پیش از درس
بازیها تا زمانی که بیگانگان روی صفحه حرکت نکنند، خیلی جذاب نیستند! در این بازی، از دو نوع حرکت استفاده خواهیم کرد:
- حرکت با صفحهکلید/ماوس: زمانی که کاربر با صفحهکلید یا ماوس تعامل میکند تا یک شیء را روی صفحه حرکت دهد.
- حرکت ایجاد شده توسط بازی: زمانی که بازی یک شیء را با یک بازه زمانی مشخص حرکت میدهد.
پس چگونه میتوانیم اشیاء را روی صفحه حرکت دهیم؟ همه چیز به مختصات دکارتی مربوط میشود: ما مکان (x, y) شیء را تغییر میدهیم و سپس صفحه را دوباره رسم میکنیم.
معمولاً برای انجام حرکت روی صفحه، به مراحل زیر نیاز دارید:
- تنظیم یک مکان جدید برای یک شیء؛ این کار برای درک حرکت شیء ضروری است.
- پاک کردن صفحه؛ صفحه باید بین رسمها پاک شود. میتوانیم این کار را با رسم یک مستطیل که با رنگ پسزمینه پر شده است انجام دهیم.
- رسم مجدد شیء در مکان جدید. با این کار، در نهایت حرکت شیء از یک مکان به مکان دیگر انجام میشود.
نمونهای از این فرآیند در کد به این شکل است:
//set the hero's location
hero.x += 5;
// clear the rectangle that hosts the hero
ctx.clearRect(0, 0, canvas.width, canvas.height);
// redraw the game background and hero
ctx.fillRect(0, 0, canvas.width, canvas.height)
ctx.fillStyle = "black";
ctx.drawImage(heroImg, hero.x, hero.y);
✅ آیا میتوانید دلیلی بیاورید که چرا رسم مجدد قهرمان در هر فریم ممکن است هزینههای عملکردی ایجاد کند؟ درباره جایگزینهای این الگو مطالعه کنید.
مدیریت رویدادهای صفحهکلید
برای مدیریت رویدادها، باید رویدادهای خاصی را به کد متصل کنید. رویدادهای صفحهکلید در کل پنجره فعال میشوند، در حالی که رویدادهای ماوس مانند click
میتوانند به کلیک روی یک عنصر خاص متصل شوند. ما در طول این پروژه از رویدادهای صفحهکلید استفاده خواهیم کرد.
برای مدیریت یک رویداد، باید از متد addEventListener()
پنجره استفاده کنید و دو پارامتر ورودی به آن بدهید. پارامتر اول نام رویداد است، مثلاً keyup
. پارامتر دوم تابعی است که باید در نتیجه وقوع رویداد فراخوانی شود.
نمونهای از این کد:
window.addEventListener('keyup', (evt) => {
// `evt.key` = string representation of the key
if (evt.key === 'ArrowUp') {
// do something
}
})
برای رویدادهای کلید، دو ویژگی در رویداد وجود دارد که میتوانید از آنها برای دیدن کلیدی که فشرده شده استفاده کنید:
key
، این یک نمایش رشتهای از کلید فشرده شده است، مثلاًArrowUp
.keyCode
، این یک نمایش عددی است، مثلاً37
که بهArrowLeft
مربوط میشود.
✅ دستکاری رویدادهای کلید در خارج از توسعه بازی نیز مفید است. چه کاربردهای دیگری برای این تکنیک میتوانید تصور کنید؟
کلیدهای خاص: یک نکته
برخی از کلیدهای خاص وجود دارند که بر پنجره تأثیر میگذارند. این بدان معناست که اگر شما به یک رویداد keyup
گوش دهید و از این کلیدهای خاص برای حرکت قهرمان خود استفاده کنید، ممکن است باعث اسکرول افقی نیز شود. به همین دلیل ممکن است بخواهید این رفتار پیشفرض مرورگر را هنگام ساخت بازی خود غیرفعال کنید. برای این کار به کدی مانند زیر نیاز دارید:
let onKeyDown = function (e) {
console.log(e.keyCode);
switch (e.keyCode) {
case 37:
case 39:
case 38:
case 40: // Arrow keys
case 32:
e.preventDefault();
break; // Space
default:
break; // do not block other keys
}
};
window.addEventListener('keydown', onKeyDown);
کد بالا اطمینان میدهد که کلیدهای جهتدار و کلید فاصله رفتار پیشفرض خود را غیرفعال میکنند. مکانیزم غیرفعال کردن زمانی اتفاق میافتد که ما e.preventDefault()
را فراخوانی میکنیم.
حرکت ایجاد شده توسط بازی
میتوانیم اشیاء را با استفاده از تایمرهایی مانند setTimeout()
یا setInterval()
به صورت خودکار حرکت دهیم. این تایمرها مکان شیء را در هر بازه زمانی بهروزرسانی میکنند. نمونهای از این کد به این شکل است:
let id = setInterval(() => {
//move the enemy on the y axis
enemy.y += 10;
})
حلقه بازی
حلقه بازی مفهومی است که اساساً یک تابع است که در فواصل منظم فراخوانی میشود. به آن حلقه بازی میگویند زیرا همه چیزهایی که باید برای کاربر قابل مشاهده باشند در این حلقه رسم میشوند. حلقه بازی از تمام اشیاء بازی که بخشی از بازی هستند استفاده میکند و همه آنها را رسم میکند، مگر اینکه به دلیلی دیگر نباید بخشی از بازی باشند. برای مثال، اگر یک شیء دشمن توسط یک لیزر مورد اصابت قرار گیرد و منفجر شود، دیگر بخشی از حلقه بازی فعلی نیست (در درسهای بعدی بیشتر در این مورد یاد خواهید گرفت).
نمونهای از حلقه بازی در کد به این شکل است:
let gameLoopId = setInterval(() =>
function gameLoop() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "black";
ctx.fillRect(0, 0, canvas.width, canvas.height);
drawHero();
drawEnemies();
drawStaticObjects();
}, 200);
حلقه بالا هر 200
میلیثانیه برای رسم مجدد بوم فراخوانی میشود. شما میتوانید بهترین بازه زمانی را که برای بازی شما منطقی است انتخاب کنید.
ادامه بازی فضایی
شما کد موجود را گسترش خواهید داد. یا با کدی که در بخش اول تکمیل کردهاید شروع کنید یا از کد بخش دوم - شروعکننده استفاده کنید.
- حرکت قهرمان: شما کدی اضافه خواهید کرد تا بتوانید قهرمان را با استفاده از کلیدهای جهتدار حرکت دهید.
- حرکت دشمنان: همچنین باید کدی اضافه کنید تا دشمنان از بالا به پایین با نرخ مشخصی حرکت کنند.
مراحل پیشنهادی
فایلهایی که برای شما ایجاد شدهاند را در پوشه your-work
پیدا کنید. این پوشه باید شامل موارد زیر باشد:
-| assets
-| enemyShip.png
-| player.png
-| index.html
-| app.js
-| package.json
پروژه خود را در پوشه your_work
با تایپ کردن دستور زیر شروع کنید:
cd your-work
npm start
دستور بالا یک سرور HTTP را در آدرس http://localhost:5000
راهاندازی میکند. یک مرورگر باز کنید و این آدرس را وارد کنید. در حال حاضر، باید قهرمان و همه دشمنان را ببینید؛ اما هنوز هیچ چیزی حرکت نمیکند!
اضافه کردن کد
-
اشیاء اختصاصی برای
hero
،enemy
وgame object
اضافه کنید. این اشیاء باید ویژگیهایx
وy
داشته باشند. (بخش وراثت یا ترکیب را به یاد بیاورید).نکته:
game object
باید شیئی باشد که ویژگیهایx
وy
و توانایی رسم خود روی بوم را داشته باشد.نکته: با اضافه کردن یک کلاس GameObject جدید و تعریف سازنده آن به شکل زیر شروع کنید و سپس آن را روی بوم رسم کنید:
class GameObject { constructor(x, y) { this.x = x; this.y = y; this.dead = false; this.type = ""; this.width = 0; this.height = 0; this.img = undefined; } draw(ctx) { ctx.drawImage(this.img, this.x, this.y, this.width, this.height); } }
حالا این GameObject را گسترش دهید تا Hero و Enemy را ایجاد کنید.
class Hero extends GameObject { constructor(x, y) { ...it needs an x, y, type, and speed } }
class Enemy extends GameObject { constructor(x, y) { super(x, y); (this.width = 98), (this.height = 50); this.type = "Enemy"; let id = setInterval(() => { if (this.y < canvas.height - this.height) { this.y += 5; } else { console.log('Stopped at', this.y) clearInterval(id); } }, 300) } }
-
مدیریتکنندههای رویداد کلید برای مدیریت ناوبری کلید (حرکت قهرمان به بالا/پایین، چپ/راست) اضافه کنید.
به یاد داشته باشید که این یک سیستم دکارتی است، بالا-چپ
0,0
است. همچنین به یاد داشته باشید که کدی برای متوقف کردن رفتار پیشفرض اضافه کنید.نکته: تابع onKeyDown خود را ایجاد کنید و آن را به پنجره متصل کنید:
let onKeyDown = function (e) { console.log(e.keyCode); ...add the code from the lesson above to stop default behavior } }; window.addEventListener("keydown", onKeyDown);
در این مرحله، کنسول مرورگر خود را بررسی کنید و ضربات کلید را مشاهده کنید.
-
الگوی انتشار-اشتراک (Pub sub) را پیادهسازی کنید. این کار کد شما را تمیز نگه میدارد.
برای انجام این بخش، میتوانید:
-
یک شنونده رویداد به پنجره اضافه کنید:
window.addEventListener("keyup", (evt) => { if (evt.key === "ArrowUp") { eventEmitter.emit(Messages.KEY_EVENT_UP); } else if (evt.key === "ArrowDown") { eventEmitter.emit(Messages.KEY_EVENT_DOWN); } else if (evt.key === "ArrowLeft") { eventEmitter.emit(Messages.KEY_EVENT_LEFT); } else if (evt.key === "ArrowRight") { eventEmitter.emit(Messages.KEY_EVENT_RIGHT); } });
-
یک کلاس EventEmitter برای انتشار و اشتراک پیامها ایجاد کنید:
class EventEmitter { constructor() { this.listeners = {}; } on(message, listener) { if (!this.listeners[message]) { this.listeners[message] = []; } this.listeners[message].push(listener); } emit(message, payload = null) { if (this.listeners[message]) { this.listeners[message].forEach((l) => l(message, payload)); } } }
-
ثابتها را اضافه کنید و EventEmitter را تنظیم کنید:
const Messages = { KEY_EVENT_UP: "KEY_EVENT_UP", KEY_EVENT_DOWN: "KEY_EVENT_DOWN", KEY_EVENT_LEFT: "KEY_EVENT_LEFT", KEY_EVENT_RIGHT: "KEY_EVENT_RIGHT", }; let heroImg, enemyImg, laserImg, canvas, ctx, gameObjects = [], hero, eventEmitter = new EventEmitter();
-
بازی را مقداردهی اولیه کنید
function initGame() { gameObjects = []; createEnemies(); createHero(); eventEmitter.on(Messages.KEY_EVENT_UP, () => { hero.y -=5 ; }) eventEmitter.on(Messages.KEY_EVENT_DOWN, () => { hero.y += 5; }); eventEmitter.on(Messages.KEY_EVENT_LEFT, () => { hero.x -= 5; }); eventEmitter.on(Messages.KEY_EVENT_RIGHT, () => { hero.x += 5; }); }
-
-
حلقه بازی را تنظیم کنید
تابع window.onload را بازنویسی کنید تا بازی را مقداردهی اولیه کرده و یک حلقه بازی با یک بازه زمانی مناسب تنظیم کنید. همچنین یک پرتو لیزری اضافه خواهید کرد:
window.onload = async () => { canvas = document.getElementById("canvas"); ctx = canvas.getContext("2d"); heroImg = await loadTexture("assets/player.png"); enemyImg = await loadTexture("assets/enemyShip.png"); laserImg = await loadTexture("assets/laserRed.png"); initGame(); let gameLoopId = setInterval(() => { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = "black"; ctx.fillRect(0, 0, canvas.width, canvas.height); drawGameObjects(ctx); }, 100) };
-
کد اضافه کنید تا دشمنان را در یک بازه زمانی مشخص حرکت دهید.
تابع
createEnemies()
را بازنویسی کنید تا دشمنان را ایجاد کرده و آنها را به کلاس جدید gameObjects اضافه کنید:function createEnemies() { const MONSTER_TOTAL = 5; const MONSTER_WIDTH = MONSTER_TOTAL * 98; const START_X = (canvas.width - MONSTER_WIDTH) / 2; const STOP_X = START_X + MONSTER_WIDTH; for (let x = START_X; x < STOP_X; x += 98) { for (let y = 0; y < 50 * 5; y += 50) { const enemy = new Enemy(x, y); enemy.img = enemyImg; gameObjects.push(enemy); } } }
و یک تابع
createHero()
اضافه کنید تا فرآیند مشابهی را برای قهرمان انجام دهد.function createHero() { hero = new Hero( canvas.width / 2 - 45, canvas.height - canvas.height / 4 ); hero.img = heroImg; gameObjects.push(hero); }
و در نهایت، یک تابع
drawGameObjects()
اضافه کنید تا رسم را شروع کنید:function drawGameObjects(ctx) { gameObjects.forEach(go => go.draw(ctx)); }
دشمنان شما باید شروع به پیشروی به سمت سفینه فضایی قهرمان شما کنند!
🚀 چالش
همانطور که میبینید، کد شما ممکن است با اضافه کردن توابع، متغیرها و کلاسها به یک "کد اسپاگتی" تبدیل شود. چگونه میتوانید کد خود را بهتر سازماندهی کنید تا خواناتر شود؟ یک سیستم برای سازماندهی کد خود طراحی کنید، حتی اگر هنوز در یک فایل قرار دارد.
آزمون پس از درس
مرور و مطالعه شخصی
در حالی که ما بازی خود را بدون استفاده از فریمورکها مینویسیم، فریمورکهای بسیاری مبتنی بر جاوااسکریپت برای توسعه بازی با استفاده از بوم وجود دارند. زمانی را برای مطالعه درباره این فریمورکها اختصاص دهید.
تکلیف
سلب مسئولیت:
این سند با استفاده از سرویس ترجمه هوش مصنوعی Co-op Translator ترجمه شده است. در حالی که ما تلاش میکنیم دقت را حفظ کنیم، لطفاً توجه داشته باشید که ترجمههای خودکار ممکن است شامل خطاها یا نادرستیها باشند. سند اصلی به زبان اصلی آن باید به عنوان منبع معتبر در نظر گرفته شود. برای اطلاعات حساس، توصیه میشود از ترجمه حرفهای انسانی استفاده کنید. ما مسئولیتی در قبال سوء تفاهمها یا تفسیرهای نادرست ناشی از استفاده از این ترجمه نداریم.