مقدمة
في حزيران (يونيو) 2010، لفت انتباهنا أنّ Boing Boing، وهي مجلة محلية، كانت تقيم
مسابقة لتطوير الألعاب.
ورأينا أنّ هذا هو السبب المثالي لإنشاء لعبة سريعة وبسيطة باستخدام JavaScript
و<canvas>
، لذلك بدأنا العمل على ذلك. بعد انتهاء المسابقة، كان لدينا
ما زال الكثير من الأفكار وأردنا إكمال ما بدأناه. في ما يلي دراسة الحالة التي تتناول
النتيجة، وهي لعبة صغيرة باسم Onslaught! الساحة:
المظهر القديم المقطّع
كان من المهم أن تبدو لعبتنا وتبدو مثل لعبة Nintendo Entertainment System قديمة، نظرًا لفكرة المسابقة التي تقضي بتطوير لعبة مستندة إلى موسيقى قصيرة. لا تفرض معظم الألعاب هذا الشرط، ولكنّه لا يزال أسلوبًا فنيًا شائعًا (خاصةً بين المطوّرين المستقلين) بسبب سهولة إنشاء مواد العرض وجاذبيتها الطبيعية للاعبين الذين يحنون إلى الماضي.
ونظرًا لصغر حجم هذه الصور الرمزية، قرّرنا مضاعفة عدد وحدات البكسل، ما يعني أنّه سيتم الآن استخدام 32×32 بكسل بدلاً من 16×16 بكسل في الصور الرمزية، وهكذا. منذ البداية، كان يتم مضاعفة الجهد المبذول في إنشاء مواد العرض بدلاً من تحميل المتصفّح بالعمل المكثّف. كان هذا الإجراء أسهل في التنفيذ، ولكنه كان يتمتع أيضًا ببعض مزايا المظهر الواضحة.
في ما يلي سيناريو اتّخذناه في الاعتبار:
<style>
canvas {
width: 640px;
height: 320px;
}
</style>
<canvas width="320" height="240">
Sorry, your browser is not supported.
</canvas>
ستتألف هذه الطريقة من صور متحركة بحجم 1×1 بدلاً من مضاعفة حجمها من جهة إنشاء مواد العرض. من هنا، ستتولى خدمة CSS تغيير حجم اللوحة نفسها. أظهرت معايير الأداء أنّ هذه الطريقة يمكن أن تكون أسرع بمرتين تقريبًا من عرض الصور الأكبر حجمًا (المضاعفة)، ولكن للأسف، يشمل تغيير حجم CSS ميزة إزالة التمويه، وهو أمر لم نتمكّن من منع حدوثه.
كان هذا الأمر حاسمًا بالنسبة إلى لعبتنا لأنّ البكسلات الفردية مهمة جدًا، ولكن إذا كنت بحاجة إلى تغيير حجم اللوحة وكان تمويه الحواف مناسبًا لمشروعك، يمكنك التفكير في هذا النهج لأسباب تتعلّق بالأداء.
حيل ممتعة في لوحة الرسم
نعلم جميعًا أنّ <canvas>
هو التنسيق الجديد الرائج، ولكن في بعض الأحيان
يواصل المطوّرون اقتراح استخدام DOM. إذا لم تكن متأكّدًا من الطريقة التي تريد استخدامها، إليك مثالًا على كيفية مساعدة <canvas>
في توفير الكثير من الوقت والجهد.
عندما يُصاب عدو في هجمة خاطفة! Arena، يضيء باللون الأحمر ويُظهر لفترة وجيزة صورة متحركة "للألم". للحد من عدد الرسومات التي كان علينا إنشاؤها، لا نعرض الأعداء في وضع "الألم" إلا في الاتجاه المتّجه للأسفل. تبدو هذه النتيجةمقبولة داخل اللعبة، كما أنّها وفّرت الكثير من الوقت في إنشاء الصور الرمزية. بالنسبة إلى الوحوش الرئيسية، كان من المفاجئ رؤية صورة متحركة كبيرة (بمقاس 64×64 بكسل أو أكثر) تنتقل فجأة من الاتجاه لليسار أو للأعلى إلى الاتجاه للأسفل في إطار الألم.
كان الحلّ الواضح هو رسم إطار ألم لكلّ زعيم في كلّ من
الاتجاهات الثمانية، ولكنّ هذا كان سيستغرق وقتًا طويلاً جدًا. بفضل
<canvas>
، تمكّنا من حلّ هذه المشكلة في الرمز البرمجي:
أولاً، نرسم الوحش في "ذاكرة تخزين مؤقت" مخفية <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، يتولّى المتصفّح هذه المهمة الشاقة. ولكن عند استخدام
<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
.
عناصر التحكّم
كانت عناصر التحكّم هي أكبر عقبة أثناء تطوير Onslaught! الساحة: كان العرض الأول يتيح استخدام لوحة المفاتيح فقط، ويتحرّك اللاعبون بالشخصية الرئيسية حول الشاشة باستخدام مفاتيح الأسهم ويطلقون النار في الاتجاه الذي يواجهونه باستخدام مفتاح المسافة. على الرغم من أنّ هذه الميزات سهلة الاستخدام نوعًا ما وسهلة الفهم، إلا أنّها تجعل اللعبة غير قابلة للعب تقريبًا في المستويات الأكثر صعوبة. مع وجود عشرات الأعداء والقذائف التي تطير باتجاه اللاعب في أي وقت، من الضروري أن يكون بإمكانه التنقّل بين الأشرار أثناء إطلاق النار في أي اتجاه.
للمقارنة مع الألعاب المشابهة في هذا النوع، أضفنا إمكانية استخدام الماوس للتحكّم في شبكة الاستهداف التي يستخدمها البطل لتوجيه هجماته. ظل بإمكان اللاعب تحريك الشخصية باستخدام لوحة المفاتيح، ولكن بعد هذا التغيير، أصبح بإمكانه إطلاق النار في أي اتجاه 360 درجة في الوقت نفسه. وقد أشاد اللاعبون المطوّرون بهذه الميزة، ولكن كان لها تأثير جانبي غير مرغوب فيه على مستخدمي لوحة اللمس.
لتلبية احتياجات مستخدمي لوحة اللمس، أعدنا عناصر التحكّم في مفاتيح الأسهم، وهذه المرة للسماح بإطلاق النار في الاتجاهات التي يتم الضغط عليها. على الرغم من أنّنا كنا نعتقد أنّنا نلبي متطلبات جميع أنواع اللاعبين، إلا أنّنا كنا نضيف أيضًا بدون قصد الكثير من التعقيد إلى لعبتنا. لاحظنا لاحقًا أنّ بعض اللاعبين لم يكونوا على دراية بعناصر التحكّم الاختيارية بالماوس (أو لوحة المفاتيح) للهجوم، على الرغم من نوافذ العرض الإرشادية التي تم تجاهلها إلى حد كبير.
يسرّنا أيضًا أنّ لدينا بعض المعجبين في أوروبا، ولكننا سمعنا من بعضهم شكوى بأنّه قد لا تتوفّر لديهم لوحات مفاتيح QWERTY تقليدية ولا يمكنهم استخدام مفاتيح WASD للتنقّل في الاتجاهات. وقد أعرب اللاعبون العسر عن شكوى مماثلة.
مع هذا المخطط المعقد للتحكّم الذي نفّذناه، هناك أيضًا مشكلة تشغيل الألعاب على الأجهزة الجوّالة. في الواقع، أحد الطلبات الأكثر شيوعًا هو إنشاء Onslaught! تتوفّر Arena على أجهزة 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);
}
يمكن أن تؤدي إزالة عامل التوجيه إلى الأعداء إلى تسهيل اللعبة في بعض الحالات، ولكن نرى أنّ تسهيل الأمور على اللاعب له العديد من المزايا. تظهر استراتيجيات أخرى، مثل الحاجة إلى وضع الشخصية بالقرب من الأعداء الخطيرين لاستهدافهم، وتكون إمكانية استخدام الأجهزة التي تعمل باللمس ميزة قيّمة.
الصوت
من بين عناصر التحكّم والأداء، كانت إحدى أكبر المشاكل التي واجهناها أثناء تطوير
Onslaught! كانت Arena علامة <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) على الويب.
قرّرنا استخدام 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.