16 KiB
בניית משחק חלל חלק 3: הוספת תנועה
חידון לפני השיעור
משחקים לא ממש כיפיים עד שיש חייזרים שמתרוצצים על המסך! במשחק הזה נשתמש בשני סוגי תנועות:
- תנועה באמצעות מקלדת/עכבר: כאשר המשתמש משתמש במקלדת או בעכבר כדי להזיז אובייקט על המסך.
- תנועה שמונעת על ידי המשחק: כאשר המשחק מזיז אובייקט במרווחי זמן מסוימים.
אז איך מזיזים דברים על המסך? הכל קשור לקואורדינטות קרטזיות: משנים את המיקום (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)); }
האויבים שלכם אמורים להתחיל להתקדם לעבר חללית הגיבור שלכם!
🚀 אתגר
כפי שאתם רואים, הקוד שלכם יכול להפוך ל'קוד ספגטי' כאשר אתם מתחילים להוסיף פונקציות, משתנים ומחלקות. איך תוכלו לארגן את הקוד שלכם בצורה טובה יותר כך שיהיה קריא יותר? שרטטו מערכת לארגון הקוד שלכם, גם אם הוא עדיין נמצא בקובץ אחד.
חידון אחרי השיעור
סקירה ולימוד עצמי
בעוד שאנחנו כותבים את המשחק שלנו ללא שימוש בפריימוורקים, ישנם פריימוורקים רבים מבוססי JavaScript לפיתוח משחקים עם Canvas. הקדישו זמן לקרוא על האפשרויות האלה.
משימה
כתב ויתור:
מסמך זה תורגם באמצעות שירות תרגום מבוסס בינה מלאכותית Co-op Translator. בעוד שאנו שואפים לדיוק, יש לקחת בחשבון שתרגומים אוטומטיים עשויים להכיל שגיאות או אי-דיוקים. המסמך המקורי בשפתו המקורית נחשב למקור הסמכותי. למידע קריטי, מומלץ להשתמש בתרגום מקצועי על ידי מתרגם אנושי. איננו נושאים באחריות לכל אי-הבנה או פרשנות שגויה הנובעת משימוש בתרגום זה.