دراسة حالة - Onslaught! ساحة

مقدمة

في حزيران (يونيو) 2010، لاحظنا أن شركة النشر المحلي "zine" Boing Boing كانت تستضيف مسابقة لتطوير الألعاب. لقد رأينا هذا مبررًا مناسبًا تمامًا لإنشاء لعبة سريعة وبسيطة بلغة JavaScript و<canvas>، لذلك بدأنا العمل. بعد المنافسة، بقي لدينا الكثير من الأفكار وأردنا إنهاء ما بدأناه. إليك دراسة الحالة الخاصة بالنتيجة، لعبة صغيرة تسمى Onslaught! الساحة:

المظهر القديم المتقطّع

كان من المهم أن تكون لعبتنا عبارة عن لعبة قديمة من نظام الترفيه Nintendo Entertainment System، بالنظر إلى فكرة المسابقة لتطوير لعبة استنادًا إلى شريحة. لا تفرض معظم الألعاب هذا الشرط، لكنها ما زالت أسلوبًا فنيًا شائعًا (خصوصًا بين مطوّري الألعاب المستقلّة) بسبب سهولة إنشاء مواد العرض وجاذبيتها لدى اللاعبين الذين يحبون ألعابًا قديمة.

هجمة شرسة. أحجام وحدات البكسل في الساحة
يمكن أن تؤدي زيادة حجم البكسل إلى تقليل أعمال تصميم الرسومات.

نظرًا لصغر حجم هذه الإطارات المدمجة، قررنا مضاعفة وحدات البكسل، مما يعني أن الصورة المتحركة أصبحت الآن بحجم 16×16 لتصبح 32×32 بكسل وهكذا. منذ البداية، كنا نضاعف جهودنا في جانب إنشاء الأصول بدلاً من أن نجعل المتصفح يبذل جهدًا كبيرًا. كان هذا ببساطة أسهل في التنفيذ ولكن له أيضًا بعض مزايا المظهر المحددة.

وفي ما يلي سيناريو أخذناه في الاعتبار:

<style>
canvas {
  width: 640px;
  height: 320px;
}
</style>
<canvas width="320" height="240">
  Sorry, your browser is not supported.
</canvas>

ستتكون هذه الطريقة من تركيبات مدمجة 1×1 بدلاً من مضاعفتها على جانب إنشاء مواد العرض. من هناك، سيتولى CSS تغيير حجم اللوحة نفسها. أوضحت مقاييسنا أنّ هذه الطريقة يمكن أن تكون أسرع بنحو ضعف عرض صور أكبر (مضاعفة)، ولكن للأسف يتضمن تغيير حجم CSS تنقيحًا، وهو أمر لم نتمكن من إيجاد طريقة لمنعه.

خيارات تغيير حجم اللوحة
على اليسار: تضاعف عدد مواد العرض الرائعة بوحدات البكسل في برنامج Photoshop. على اليمين: تمت إضافة تأثير ضبابي لتغيير حجم CSS.

كانت هذه إحدى نقاط القوة في لعبتنا نظرًا لأن وحدات البكسل الفردية مهمة جدًا، ولكن إذا كنت بحاجة إلى تغيير حجم اللوحة وكانت التنقيح مناسبًا لمشروعك، فيمكنك التفكير في هذا النهج لأسباب تتعلق بالأداء.

حيل مسلّية في اللوحة

نعلم جميعًا أنّ <canvas> هو الشكل الجديد كليًا، ولكن في بعض الأحيان، يظل المطوّرون ينصحون باستخدام نموذج كائن المستند (DOM). إذا كنت مترددًا بشأن اختيار تلك التي يمكنك استخدامها، إليك مثالاً على الطريقة التي وفرنا بها <canvas> الكثير من الوقت والطاقة.

عندما يتم ضرب عدو في الهجوم! الساحة، يومض باللون الأحمر ويعرض بإيجاز رسمًا متحركًا "ألمًا". للحد من عدد الرسومات التي كان علينا إنشاؤها، نظهر الأعداء فقط في "الألم" في الاتجاه المتجه للأسفل. يبدو أنّ هذه الميزة مقبولة داخل اللعبة، ووفّرت الكثير من الوقت لإنشاء الرموز المتحركة. ولكن بالنسبة إلى الوحوش الوحشية، كان من الصعب رؤية نقش كبير (بدقة 64×64 بكسل أو أكثر) من الوجه الأيسر أو لأعلى إلى أسفل فجأة نحو إطار الألم.

قد يكون الحل الواضح هو رسم إطار عمل لكل رئيس في كل رئيس من الاتجاهات الثمانية، ولكن هذا كان يستغرق وقتًا طويلاً للغاية. بفضل <canvas>، تمكّنا من حلّ هذه المشكلة باستخدام الرمز البرمجي:

الظهير يلحق الضرر في هجوم الهجوم! ساحة
يمكن إنشاء مؤثرات شيّقة باستخدام content.globalCompositeOperation.

نرسم أولاً الوحش على "مخزن احتياطي" <canvas> مخفي، ونضعه باللون الأحمر، ثم نعرض النتيجة على الشاشة مرة أخرى. تبدو التعليمات البرمجية شيئًا مثل هذا:

// Get the "buffer" canvas (that isn't visible to the user)
var bufferCanvas = document.getElementById("buffer");
var buffer = bufferCanvas.getContext("2d");

// Draw your image on the buffer
buffer.drawImage(image, 0, 0);

// Draw a rectangle over the image using a nice translucent overlay
buffer.save();
buffer.globalCompositeOperation = "source-in";
buffer.fillStyle = "rgba(186, 51, 35, 0.6)"; // red
buffer.fillRect(0, 0, image.width, image.height);
buffer.restore();

// Copy the buffer onto the visible canvas
document.getElementById("stage").getContext("2d").drawImage(bufferCanvas, x, y);

حلقة الألعاب

هناك بعض الاختلافات الملحوظة في تطوير الألعاب عن تطوير الألعاب على الويب. في مكدس الويب، من الشائع التفاعل مع الأحداث التي حدثت من خلال أدوات معالجة الأحداث. لذلك قد لا يفعل رمز التهيئة أي شيء سوى الاستماع إلى أحداث الإدخال. يختلف منطق اللعبة، حيث من الضروري تحديث نفسها باستمرار. على سبيل المثال، إذا لم يتحرك اللاعب، فلا ينبغي أن يمنع ذلك من الوصول إليه!

في ما يلي مثال على حلقة الألعاب:

function main () {
  handleInput();
  update();
  render();
};

setInterval(main, 1);

الاختلاف الأول المهم هو أن الدالة handleInput لا تفعل أي شيء على الفور. إذا ضغط المستخدم على مفتاح في تطبيق ويب نموذجي، فمن المنطقي تنفيذ الإجراء المطلوب على الفور. لكن في اللعبة، يجب أن تحدث الأمور بترتيب زمني حتى تتدفق بشكل صحيح.

window.addEventListener("mousedown", function(e) {
  // A mouse click means the players wants to attack.
  // We don't actually do that yet, but instead tell the rest
  // of the program about the request.
  buttonStates[e.button] = true;
}, false);

function handleInput() {
  // Here is where we respond to the click
  if (buttonStates[LEFT_BUTTON]) {
    player.attacking = true;
    delete buttonStates[LEFT_BUTTON];
  }
};

والآن، أصبحنا نعرف معلومات عن الإدخال، ويمكننا أخذه في الاعتبار في دالة update، مع العلم أنّه سيلتزم ببقية قواعد اللعبة.

function update() {
  // Check for collisions, states, whatever else is needed

  // If after that the player can still attack, do it!
  if (player.attacking && player.canAttack()) {
    player.attack();
  }
};

وأخيرًا، بمجرد أن يتم حساب كل شيء، يحين وقت إعادة رسم الشاشة! في DOM-land، يعالج المتصفّح هذه الارتفاع المذهل. ولكن عند استخدام <canvas>، من الضروري إعادة الرسم يدويًا عند حدوث شيء ما (عادةً ما يكون كل إطار على حدة!).

function render() {
  // First erase everything, something like:
  context.clearRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);

  // Draw the player (and whatever else you need)
  context.drawImage(
    player.getImage(),
    player.x, player.y
  );
};

النمذجة المستندة إلى الوقت

النمذجة المستندة إلى الوقت هي مفهوم نقل الصور المدمجة بناءً على مقدار الوقت المنقضي منذ آخر تحديث للإطار. تتيح هذه التقنية تشغيل لعبتك بأسرع ما يمكن مع ضمان تحرّك الرموز المتحركة بسرعات متسقة.

لاستخدام النمذجة المستندة إلى الوقت، نحتاج إلى تسجيل الوقت المنقضي منذ رسم الإطار الأخير. سنحتاج إلى تعزيز وظيفة update() في حلقة الألعاب لتتبُّع هذا الأمر.

function update() {

  // NOTE: You'll need to initially seed this.lastUpdate
  // with the current time when your game loop starts
  // this.lastUpdate = Date.now();

  // Calculate elapsed time since last frame
  var now = Date.now();
  var elapsed = (now - this.lastUpdate);
  this.lastUpdate = now;

  // Do stuff with elapsed

};

الآن بعد أن أصبح لدينا الوقت المنقضي، يمكننا حساب المسافة التي يجب أن يتحرك بها شىء معين كل إطار. أولاً، سنحتاج إلى تتبع بعض الأشياء في كائن الرموز المتحركة: الموضع الحالي والسرعة والاتجاه.

var Sprite = function() {

  // The sprite's position relative to the top left of the game world
  this.position = {x: 0, y: 0};

  // The sprite's direction. A positive x value indicates moving to the right
  this.direction = {x: 1, y: 0};

  // How many pixels the sprite moves per second
  this.speed = 50;
};

مع وضع هذه المتغيرات في الاعتبار، إليك كيفية نقل مثيل من فئة الرموز المتحركة أعلاه باستخدام النمذجة المستندة إلى الوقت:

// Determine how far this sprite will move this frame
var distance = (sprite.speed / 1000) * elapsed;

// Apply the movement distance to the sprite's current position
// taking into account its direction
sprite.position.x += (distance * sprite.direction.x);
sprite.position.y += (distance * sprite.direction.y);

يُرجى العلم أنّه يجب تسوية قيمتَي direction.x وdirection.y، ما يعني أنّه يجب أن تقع القيم دائمًا بين -1 و1.

عناصر التحكّم

كانت عناصر التحكّم على الأرجح تمثّل أكبر الصعوبات أثناء تطوير الهجمات! الساحة: دعم الإصدار التجريبي الأول لوحة المفاتيح فقط؛ حيث حرّك اللاعبون الشخصية الرئيسية حول الشاشة باستخدام مفاتيح الأسهم وأطلقوا النار في الاتجاه الذي يواجهه باستخدام مفتاح المسافة. وبالرغم من بساطتها وسهولة فهمها إلى حدٍ ما، فقد جعل ذلك اللعبة غير قابلة للّعب تقريبًا في مستويات أكثر صعوبة. نظرًا لأن عشرات الأعداء والمقذوفات تطير في اتجاه اللاعب في أي وقت، من الضروري أن تكون قادرًا على الربط بين الأشرار أثناء إطلاق النار في أي اتجاه.

وللمقارنة مع الألعاب المماثلة من نوعها، أضفنا دعمًا للماوس للتحكُّم في شبكة الاستهداف التي تستخدمها الشخصية لتوجيه هجماتها. لا يزال من الممكن تحريك الشخصية باستخدام لوحة المفاتيح، ولكن بعد هذا التغيير، يمكنه إطلاق النار في الوقت نفسه بأي اتجاه كامل بزاوية 360 درجة. قد تقدير اللاعبون المتمرسون هذه الميزة ولكن كان لها الآثار الجانبية المؤسفة على مستخدمي لوحة اللمس.

هجمة شرسة. نموذج عناصر التحكّم في الساحة (متوقف)
عنصر تحكّم قديم أو نموذج "كيفية اللعب" في Onslaught! الساحة.

لاستيعاب مستخدمي لوحة اللمس، أعدنا عناصر التحكّم في مفاتيح الأسهم، هذه المرة للسماح بإطلاق النار في الاتجاهات التي تم الضغط عليها. بينما شعرنا أننا نلبي احتياجات كل أنواع اللاعبين، إلا أننا كنا نسبب في هذه اللعبة تعقيدًا كبيرًا بدون قصد. وقد بلغنا لاحقًا أنّ بعض اللاعبين لم يكونوا على دراية بعناصر التحكّم الاختيارية بالماوس (أو لوحة المفاتيح) للهجوم، على الرغم من استخدام أشكال البرامج التعليمية التي تم تجاهلها إلى حدّ كبير.

هجمة شرسة. دليل توجيهي لعناصر التحكّم في الساحة
يتجاهل اللاعبون عادةً المحتوى التعليمي الذي يظهر على الشاشة، ويفضّلون اللعب والاستمتاع بوقتهم.

نحن محظوظون أيضًا لوجود بعض المشجعين الأوروبيين، لكننا سمعنا عن انزعاجهم بسبب عدم امتلاكهم لوحات مفاتيح QWERTY العادية، وعدم قدرتهم على استخدام مفاتيح WASD للحركة الاتجاهية. وقد أعرب لاعبو اليد اليسرى عن شكاوى مماثلة.

مع نظام التحكم المعقد هذا الذي نفذناه، توجد أيضًا مشكلة في التشغيل على الأجهزة المحمولة. في الواقع، من أكثر طلباتنا شيوعًا تقديم طلب نسلّح! Arna على Android وiPad وغيرها من الأجهزة التي تعمل باللمس (حيث لا توجد لوحة مفاتيح). تتمثل إحدى نقاط القوة الأساسية لـ HTML5 في سهولة نقله، لذا فإن تنزيل اللعبة على هذه الأجهزة أمر ممكن بالتأكيد، ويتعين علينا حل العديد من المشكلات (على الأغلب، عناصر التحكم والأداء).

لمعالجة هذه المشاكل العديدة، بدأنا اللعب بأسلوب أسلوب لعب أحادي الإدخال يتطلب تفاعلاً بالماوس (أو اللمس) فقط. ينقر اللاعبون على الشاشة أو يلمسونها وتسير الشخصية الرئيسية باتجاه المكان الذي يتم الضغط عليه، وتهاجم تلقائيًا أقرب رجل سيء. تبدو التعليمة البرمجية على النحو التالي:

// Find the nearest hostile target (if any) to the player
var player = this.getPlayerObject();
var hostile = this.getNearestHostile(player);
if (hostile !== null) {
  // Found one! Shoot in its direction
  var shoot = hostile.boundingBox().center().subtract(
    player.boundingBox().center()
  ).normalize();
}

// Move towards where the player clicked/touched
var move = this.targetReticle.position.clone().subtract(
  player.boundingBox().center()
).normalize();
var distance = this.targetReticle.position.clone().subtract(
  player.boundingBox().center()
).magnitude();

// Prevent jittering if the character is close enough
if (distance < 3) {
  move.zero();
}

// Move the player
if ((move.x !== 0) || (move.y !== 0)) {
  player.setDirection(move);
}

يمكن أن تؤدي إزالة العامل الإضافي المتمثل في استهداف الأعداء إلى تسهيل اللعبة في بعض المواقف، إلا أننا نشعر أن تسهيل الأمور له مزايا كثيرة للّاعب. وتظهر استراتيجيات أخرى، مثل وضع الشخصية بالقرب من الأعداء الخطرين لاستهدافهم، والقدرة على دعم الأجهزة التي تعمل باللمس لا تقدر بثمن.

الصوت

إنّ إحدى أهمّ المشاكل التي نواجهها أثناء تطوير عناصر التحكم والأداء هي الهجوم. Arna كانت علامة <audio> لـ HTML5. يحتمل أن يكون أسوأ جانب هو وقت الاستجابة: في جميع المتصفحات تقريبًا، هناك تأخير بين طلب .play() وتشغيل الصوت. يمكن أن يؤدي هذا إلى إفساد تجربة اللاعب، خاصةً عند اللعب بلعبة سريعة الإيقاع مثل لعبتنا.

وتشمل المشاكل الأخرى تعذُّر تنشيط حدث "التقدّم"، ما قد يتسبّب في تعليق تحميل اللعبة إلى أجل غير مسمى. لهذه الأسباب، استخدمنا ما نسميه طريقة "البداية"، حيث إذا فشل تحميل Flash، ننتقل إلى HTML5 Audio. تبدو التعليمة البرمجية على النحو التالي:

/*
This example uses the SoundManager 2 library by Scott Schiller:
http://www.schillmania.com/projects/soundmanager2/
*/

// Default to sm2 (Flash)
var api = "sm2";

function initAudio (callback) {
  switch (api) {
    case "sm2":
      soundManager.onerror = (function (init) {
        return function () {
          api = "html5";
          init(callback);
        };
      }(arguments.callee));
      break;
    case "html5":
      var audio = document.createElement("audio");

      if (
        audio
        && audio.canPlayType
        && audio.canPlayType("audio/mpeg;")
      ) {
        callback();
      } else {
        // No audio support :(
      }
      break;
  }
};

قد يكون من المهم أيضًا أن تتوافق الألعاب مع المتصفّحات التي لا تتيح تشغيل ملفات MP3 (مثل Mozilla Firefox). في هذه الحالة، يمكن اكتشاف الدعم والتبديل إلى شيء مثل Ogg Vorbis، باستخدام رمز مثل هذا:

/*
Note: you could instead use "new Audio()" here,
but the client will throw an error if it doesn't support Audio,
which makes using "document.createElement" a safer approach.
*/

var audio = document.createElement("audio");

if (audio && audio.canPlayType) {
  if (!audio.canPlayType("audio/mpeg;")) {
    // Here you know you CANNOT use .mp3 files
    if (audio.canPlayType("audio/ogg; codecs=vorbis")) {
      // Here you know you CAN use .ogg files
    }
  }
}

جارٍ حفظ البيانات

لا يمكنك إطلاق النار باستخدام نمط الألعاب الكلاسيكية بدون تسجيل نقاط عالية. كنا نعرف أننا بحاجة إلى بعض بيانات الألعاب لاستمرارها، وعلى الرغم من أنه كان بإمكاننا استخدام شيء قديم مثل ملفات تعريف الارتباط، إلا أننا أردنا التعمق في تقنيات HTML5 الجديدة والممتعة. لا يوجد بالتأكيد نقص في الخيارات، بما في ذلك التخزين المحلي وتخزين الجلسة وقواعد بيانات SQL على الويب.

ALT_TEXT_HERE
يتم حفظ أعلى النتائج بالإضافة إلى مكانك في اللعبة بعد هزيمة كل وحش.

قرّرنا استخدام السمة localStorage لأنّها جديدة ورائعة وسهلة الاستخدام. وهو يتيح حفظ أزواج المفتاح/القيمة الأساسية التي تشكّل كل ما نحتاج إليه من ألعاب بسيطة. فيما يلي مثال مباشر على كيفية استخدامها:

if (typeof localStorage == "object") {
  localStorage.setItem("foo", "bar");
  localStorage.getItem("foo"); // Value is "bar"
  localStorage.removeItem("foo");
  localStorage.getItem("foo"); // Value is now null
}

هناك بعض "المشكلات" التي ينبغي أن تكون على دراية بها. بغض النظر عما تمرره، يتم تخزين القيم كسلاسل، مما قد يؤدي إلى بعض النتائج غير المتوقعة:

localStorage.setItem("foo", false);
typeof localStorage.getItem("foo"); // Value is "false" (a string literal)
if (localStorage.getItem("foo")) {
  // It's true!
}

// Don't pass objects into setItem
localStorage.setItem("bar", {"key": "value"});
localStorage.getItem("bar"); // Value is "[object Object]" (a string literal)

// JSON stringify and parse when dealing with localStorage
localStorage.setItem("json", JSON.stringify({"key": "value"}));
typeof localStorage.getItem("json"); // string
JSON.parse(localStorage.getItem("json")); // {"key": "value"}

ملخّص

استخدام HTML5 أمر رائع. تتعامل معظم عمليات التنفيذ مع كل ما يحتاجه مطور اللعبة، بدءًا من الرسومات إلى حفظ حالة اللعبة. وبالرغم من تزايد المشاكل (مثل مشاكل علامات <audio>)، إلا أنّ مطوّري البرامج يتحركون بسرعة وفي ظلّ الأمور الرائعة كما هي، يبدو المستقبل مشرقًا للألعاب المستندة إلى HTML5.

هجمة شرسة. ساحة فيها شعار HTML5 مخفي
يمكنك الحصول على درع HTML5 من خلال كتابة "html5" عند تشغيل لعبة Onslaught. الساحة.