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

דניאל X. Moore
Daniel X. מור

מבוא

רוצה ליצור משחק באמצעות לוח הציור ו-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);

בינתיים, אפשר להשאיר את העדכון ולצייר שיטות ריקות. מה שחשוב לדעת הוא ש-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);
}

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

יצירת הנגן מתבצעת

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

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 Hotkeys

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

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

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

תנועת השחקן

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

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

החדשות הטובות הן שכללתי wrapper של 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 = [];

בשלב הבא צריך ה-constructor כדי ליצור מופעי תבליטים.

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. תבליטים של שחקן => ספינות אויב
  2. הנגן => ספינות אויב

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

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 פועל ללא הפרעות, הוספת צלילים היא כרגע הדרך המהירה ביותר לקרוס את האפליקציה. יכול להיות שצלילים חותכים או מסירים את כל הכרטיסייה בדפדפן, אז כדאי להכין את הרקמות.

פרידה

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

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

קובצי עזר