מדריך ללא דמעות למשחקי HTML5

Daniel X. Moore
Daniel X. Moore

מבוא

רוצים ליצור משחק באמצעות Canvas ו-HTML5? פשוט פועלים לפי המדריך הזה ומתקדמים תוך זמן קצר.

המדריך מיועד לאנשים שיש להם לפחות ידע בסיסי ב-JavaScript.

אתם יכולים קודם לשחק במשחק או לעבור ישירות לכתבה ולעיין בקוד המקור של המשחק.

יצירת הקנבס

כדי לצייר, נצטרך ליצור לוח. מכיוון שמדובר במדריך ללא דמעות, נשתמש ב-jQuery.

var CANVAS_WIDTH = 480;
var CANVAS_HEIGHT = 320;

var canvasElement = $("<canvas width='" + CANVAS_WIDTH + 
                      "' height='" + CANVAS_HEIGHT + "'></canvas>");
var canvas = canvasElement.get(0).getContext("2d");
canvasElement.appendTo('body');

לולאת משחק

כדי לדמות משחק חלק ורציף, אנחנו רוצים לעדכן את המשחק ולצייר מחדש את המסך מהר יותר ממה שהעין והמוח האנושי יכולים לקלוט.

var FPS = 30;
setInterval(function() {
  update();
  draw();
}, 1000/FPS);

בשלב הזה אפשר להשאיר את השיטות update ו-draw ריקות. חשוב לדעת ש-setInterval() דואג להפעיל אותם מדי פעם.

function update() { ... }
function draw() { ... }

שלום עולם

עכשיו, אחרי שהפעלנו את לולאת המשחק, נעדכן את שיטת הציור כדי שאפשר יהיה לצייר טקסט במסך.

function draw() {
  canvas.fillStyle = "#000"; // Set color to black
  canvas.fillText("Sup Bro!", 50, 50);
}

זה מגניב מאוד לטקסט נייח, אבל מכיוון שכבר הגדרנו לולאת משחק, נוכל לגרום לו לזוז בקלות יחסית.

var textX = 50;
var textY = 50;

function update() {
  textX += 1;
  textY += 1;
}

function draw() {
  canvas.fillStyle = "#000";
  canvas.fillText("Sup Bro!", textX, textY);
}

עכשיו אפשר לנסות. אם אתם עוקבים אחרי ההוראות, הוא אמור לזוז, אבל גם להשאיר את הפעמים הקודמות שבהן הוא צויר במסך. כדאי לנסות לנחש למה זה קורה. הסיבה לכך היא שאנחנו לא מנקים את המסך. עכשיו נוסיף קוד לניקוי המסך לשיטת הציור.

function draw() {
  canvas.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
  canvas.fillStyle = "#000";
  canvas.fillText("Sup Bro!", textX, textY);
}

עכשיו, כשיש טקסט שנע במסך, אתם כבר בדרך למשחק אמיתי. פשוט צריך לשפר את הפקדים, לשפר את הגיימפליי ולשפר את הגרפיקה. אוקיי, אולי רק 1/7 מהדרך ליצירת משחק אמיתי, אבל החדשות הטובות הן שיש עוד הרבה במדריך.

יצירת הנגן

יוצרים אובייקט לאחסון נתוני השחקן, שיהיה אחראי על דברים כמו ציור. כאן אנחנו יוצרים אובייקט של נגן באמצעות אובייקט ליטרלי פשוט ששומר את כל המידע.

var player = {
  color: "#00A",
  x: 220,
  y: 270,
  width: 32,
  height: 32,
  draw: function() {
    canvas.fillStyle = this.color;
    canvas.fillRect(this.x, this.y, this.width, this.height);
  }
};

בשלב הזה אנחנו משתמשים בריבוע צבעוני פשוט כדי לייצג את השחקן. כשנצייר את המשחק, נמחק את הלוח ונצייר את השחקן.

function draw() {
  canvas.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
  player.draw();
}

אמצעי בקרה במקלדת

שימוש במקשות קיצור של jQuery

התוסף jQuery Hotkeys מקל מאוד על ניהול המפתחות בדפדפנים שונים. במקום להתלונן על בעיות לא ניתנות לפענוח ב-keyCode וב-charCode בדפדפנים שונים, אפשר לקשר אירועים באופן הבא:

$(document).bind("keydown", "left", function() { ... });

יתרון גדול הוא שאין צורך לדאוג לגבי הפרטים של המפתחות שיש להם את הקודים האלה. אנחנו רק רוצים להיות מסוגלים לומר דברים כמו "כשהשחקן לוחץ על הלחצן למעלה, מבצעים פעולה כלשהי". מקשי הקיצור של jQuery מאפשרים לעשות זאת בצורה נוחה.

תנועת השחקן

האופן שבו JavaScript מטפל באירועי מקלדת מבוסס לחלוטין על אירועים. כלומר, אין שאילתה מובנית לבדיקה אם מפתח מושבת, ולכן נצטרך להשתמש בשאילתה משלו.

יכול להיות שתתהו "למה לא משתמשים בדרך מבוססת-אירועים לטיפול במפתחות?" הסיבה לכך היא ששיעור החזרה של המקלדת משתנה בין מערכות ולא קשור לתזמון של לולאת המשחק, כך שסגנון המשחק עשוי להשתנות מאוד בין מערכות שונות. כדי ליצור חוויה עקבית, חשוב לשלב בצורה הדוקה את זיהוי האירועים במקלדת עם לולאת המשחק.

החדשות הטובות הן שצירפתי מעטפת JS בת 16 שורות שתאפשר להריץ שאילתות על אירועים. הקובץ נקרא key_status.js, ותוכלו לבדוק את הסטטוס של מפתח בכל שלב באמצעות keydown.left וכו'.

עכשיו, כשיש לנו אפשרות לשלוח שאילתה כדי לבדוק אם מקשים מופעלים, אנחנו יכולים להשתמש בשיטת העדכון הפשוטה הזו כדי להזיז את הנגן.

function update() {
  if (keydown.left) {
    player.x -= 2;
  }

  if (keydown.right) {
    player.x += 2;
  }
}

קדימה, אפשר לנסות.

יכול להיות שתבחינו בכך שאפשר להזיז את הנגן מחוץ למסך. נצמיד את המיקום של השחקן כדי שהוא יישאר בתוך הגבולות. בנוסף, נראה שהנגן פועל באיטיות, אז נאיץ גם את המהירות.

function update() {
  if (keydown.left) {
    player.x -= 5;
  }

  if (keydown.right) {
    player.x += 5;
  }

  player.x = player.x.clamp(0, CANVAS_WIDTH - player.width);
}

הוספת עוד מקורות קלט תהיה קלה באותה מידה, אז נוסיף כמה פריטים מונחים.

function update() {
  if (keydown.space) {
    player.shoot();
  }

  if (keydown.left) {
    player.x -= 5;
  }

  if (keydown.right) {
    player.x += 5;
  }

  player.x = player.x.clamp(0, CANVAS_WIDTH - player.width);
}

player.shoot = function() {
  console.log("Pew pew");
  // :) Well at least adding the key binding was easy...
};

הוספת אובייקטים נוספים למשחק

כלי נשק

עכשיו נוסיף את הקלעים. קודם כול, אנחנו צריכים אוסף שבו נאחסן את כולם:

var playerBullets = [];

בשלב הבא, נצטרך קונסטרוקטור כדי ליצור מכונות של תווי פסיק.

function Bullet(I) {
  I.active = true;

  I.xVelocity = 0;
  I.yVelocity = -I.speed;
  I.width = 3;
  I.height = 3;
  I.color = "#000";

  I.inBounds = function() {
    return I.x >= 0 && I.x <= CANVAS_WIDTH &&
      I.y >= 0 && I.y <= CANVAS_HEIGHT;
  };

  I.draw = function() {
    canvas.fillStyle = this.color;
    canvas.fillRect(this.x, this.y, this.width, this.height);
  };

  I.update = function() {
    I.x += I.xVelocity;
    I.y += I.yVelocity;

    I.active = I.active && I.inBounds();
  };

  return I;
}

כשהשחקן יורה, צריך ליצור מופע של כדור ולהוסיף אותו לאוסף הכדורים.

player.shoot = function() {
  var bulletPosition = this.midpoint();

  playerBullets.push(Bullet({
    speed: 5,
    x: bulletPosition.x,
    y: bulletPosition.y
  }));
};

player.midpoint = function() {
  return {
    x: this.x + this.width/2,
    y: this.y + this.height/2
  };
};

עכשיו צריך להוסיף את העדכון של הנקודות לפונקציית שלב העדכון. כדי למנוע את האפשרות שהאוסף של הנקודות יתמלא ללא הגבלת זמן, אנחנו מסננים את רשימת הנקודות כך שתכלול רק את הנקודות הפעילות. כך אנחנו יכולים גם להסיר כדורים שהתנגש ביריבים.

function update() {
  ...
  playerBullets.forEach(function(bullet) {
    bullet.update();
  });

  playerBullets = playerBullets.filter(function(bullet) {
    return bullet.active;
  });
}

השלב האחרון הוא לצייר את הנקודות:

function draw() {
  ...
  playerBullets.forEach(function(bullet) {
    bullet.draw();
  });
}

אויבים

עכשיו הגיע הזמן להוסיף אויבים באותו אופן שבו הוספנו את הכדורים.

  enemies = [];

function Enemy(I) {
  I = I || {};

  I.active = true;
  I.age = Math.floor(Math.random() * 128);

  I.color = "#A2B";

  I.x = CANVAS_WIDTH / 4 + Math.random() * CANVAS_WIDTH / 2;
  I.y = 0;
  I.xVelocity = 0
  I.yVelocity = 2;

  I.width = 32;
  I.height = 32;

  I.inBounds = function() {
    return I.x >= 0 && I.x <= CANVAS_WIDTH &&
      I.y >= 0 && I.y <= CANVAS_HEIGHT;
  };

  I.draw = function() {
    canvas.fillStyle = this.color;
    canvas.fillRect(this.x, this.y, this.width, this.height);
  };

  I.update = function() {
    I.x += I.xVelocity;
    I.y += I.yVelocity;

    I.xVelocity = 3 * Math.sin(I.age * Math.PI / 64);

    I.age++;

    I.active = I.active && I.inBounds();
  };

  return I;
};

function update() {
  ...

  enemies.forEach(function(enemy) {
    enemy.update();
  });

  enemies = enemies.filter(function(enemy) {
    return enemy.active;
  });

  if(Math.random() < 0.1) {
    enemies.push(Enemy());
  }
};

function draw() {
  ...

  enemies.forEach(function(enemy) {
    enemy.draw();
  });
}

טעינת תמונות וציור שלהן

כיף לראות את כל התיבות האלה מרחפות, אבל עדיף שיהיה להן תמונה. טעינת תמונות וציור שלהן על קנבס הם בדרך כלל חוויה מרה. כדי למנוע את הכאב והסבל האלה, אפשר להשתמש בכיתה שימושית פשוטה.

player.sprite = Sprite("player");

player.draw = function() {
  this.sprite.draw(canvas, this.x, this.y);
};

function Enemy(I) {
  ...

  I.sprite = Sprite("enemy");

  I.draw = function() {
    this.sprite.draw(canvas, this.x, this.y);
  };

  ...
}

זיהוי התנגשויות

יש לנו את כל הדברים האלה שמרחפים במסך, אבל הם לא מקיימים אינטראקציה ביניהם. כדי לדעת מתי להפעיל את הפיצוץ, נצטרך להוסיף סוג כלשהו של זיהוי התנגשויות.

נשתמש באלגוריתם פשוט לזיהוי התנגשויות ריבועיות:

function collides(a, b) {
  return a.x < b.x + b.width &&
         a.x + a.width > b.x &&
         a.y < b.y + b.height &&
         a.y + a.height > b.y;
}

יש כמה התנגשויות שאנחנו רוצים לבדוק:

  1. Player Bullets => Enemy Ships
  2. שחקן => ספינות אויב

נוצר method לטיפול בהתנגשויות, שאפשר להפעיל מ-update method.

function handleCollisions() {
  playerBullets.forEach(function(bullet) {
    enemies.forEach(function(enemy) {
      if (collides(bullet, enemy)) {
        enemy.explode();
        bullet.active = false;
      }
    });
  });

  enemies.forEach(function(enemy) {
    if (collides(enemy, player)) {
      enemy.explode();
      player.explode();
    }
  });
}

function update() {
  ...
  handleCollisions();
}

עכשיו צריך להוסיף את שיטות הפיצוץ לשחקן ולאויבים. הפעולה הזו תסמן אותם להסרה ותוסיף פיצוץ.

function Enemy(I) {
  ...

  I.explode = function() {
    this.active = false;
    // Extra Credit: Add an explosion graphic
  };

  return I;
};

player.explode = function() {
  this.active = false;
  // Extra Credit: Add an explosion graphic and then end the game
};

צליל

כדי להשלים את החוויה, נוסיף כמה אפקטים קוליים נעימים. שימוש בצלילים, כמו שימוש בתמונות, יכול להיות קצת קשה ב-HTML5, אבל הודות לנוסחה הקסומה שלנו ללא דמעות, sound.js, אפשר להוסיף צלילים בקלות רבה.

player.shoot = function() {
  Sound.play("shoot");
  ...
}

function Enemy(I) {
  ...

  I.explode = function() {
    Sound.play("explode");
    ...
  }
}

ה-API עכשיו ללא בעיות, אבל הוספת צלילים היא כרגע הדרך המהירה ביותר לגרום לקריסה של האפליקציה. לא פעם קורה שהצלילים נתקעים או שהם גורמים לכרטיסייה של הדפדפן כולה להשתבש, אז כדאי להכין מראש את המגבונים.

Farewell

שוב, הנה הדגמה של משחק עובד מלא. אפשר גם להוריד את קוד המקור כקובץ zip.

אני מקווה שנהניתם ללמוד את היסודות של יצירת משחק פשוט ב-JavaScript וב-HTML5. כשיוצרים קוד ברמת האבסטרקציה המתאימה, אפשר להתנתק מהחלקים הקשים יותר של ממשקי ה-API, וגם להיות עמידים יותר מול שינויים עתידיים.

קובצי עזר