केस स्टडी - शुरू हो गया है! एरिना

परिचय

जून 2010 में, हमें पता चला कि स्थानीय पब्लिशिंग "ज़ीन" Boing Boing में गेम डेवलपमेंट प्रतियोगिता का आयोजन किया जा रहा है. हमने इसे JavaScript और <canvas> में एक आसान और तेज़ गेम बनाने का एक अच्छा मौका माना. इसलिए, हमने काम शुरू कर दिया. प्रतियोगिता के बाद भी हमारे पास कई आइडिया थे और हमने जो शुरू किया था उसे पूरा करना चाहते थे. यहां इस नतीजे की केस स्टडी दी गई है. यह Onslaught! Arena.

पिक्सलेट वाला रेट्रो लुक

चिपट्यून पर आधारित गेम डेवलप करने के लिए, प्रतियोगिता के आधार के हिसाब से, यह ज़रूरी था कि हमारा गेम रेट्रो Nintendo Entertainment System गेम की तरह दिखे और उसी तरह का अनुभव दे. ज़्यादातर गेम में ऐसा करना ज़रूरी नहीं है. हालांकि, यह अब भी एक आम कलात्मक स्टाइल है. खास तौर पर, इंडी डेवलपर के बीच यह स्टाइल काफ़ी लोकप्रिय है. इसकी वजह यह है कि एसेट बनाना आसान होता है और यह पुराने गेम खेलने वालों को पसंद आता है.

Onslaught! एरिना के पिक्सल साइज़
पिक्सल साइज़ बढ़ाने से, ग्राफ़िक डिज़ाइन का काम कम हो सकता है.

इन स्प्राइट के छोटे होने की वजह से, हमने अपने पिक्सल को दोगुना करने का फ़ैसला लिया है. इसका मतलब है कि 16x16 स्प्राइट अब 32x32 पिक्सल का हो जाएगा. हमने शुरू से ही, ब्राउज़र को ज़्यादा काम करने के बजाय, एसेट बनाने पर ज़्यादा ध्यान दिया है. इसे लागू करना आसान था. साथ ही, इसकी दिखने की क्वालिटी भी बेहतर थी.

हमने इस स्थिति पर विचार किया है:

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

इस तरीके में, एसेट बनाने के दौरान स्प्राइट को दोगुना करने के बजाय, 1x1 स्प्राइट का इस्तेमाल किया जाएगा. इसके बाद, सीएसएस कैनवस का साइज़ अपने-आप बदल देगी. हमारे बेंचमार्क से पता चला है कि यह तरीका, बड़ी (दोगुनी) इमेज को रेंडर करने के मुकाबले करीब दोगुना तेज़ हो सकता है. हालांकि, माफ़ करें, सीएसएस के ज़रिए साइज़ बदलने की प्रोसेस में ऐंटी-ऐलिऐसिंग शामिल होती है. हमने इस प्रोसेस को रोकने का कोई तरीका नहीं ढूंढा.

कैनवस का साइज़ बदलने के विकल्प
बाईं ओर: Photoshop में पिक्सल-परफ़ेक्ट एसेट को दोगुना किया गया. दाईं ओर: सीएसएस का साइज़ बदलने से, इमेज धुंधली हो गई.

यह हमारे गेम के लिए एक समस्या थी, क्योंकि अलग-अलग पिक्सल बहुत ज़रूरी होते हैं. हालांकि, अगर आपको अपने कैनवस का साइज़ बदलना है और ऐंटी-ऐलिऐसिंग आपके प्रोजेक्ट के लिए सही है, तो परफ़ॉर्मेंस की वजहों से इस तरीके को आज़माया जा सकता है.

कैनवस से जुड़ी मज़ेदार तरकीबें

हम सभी जानते हैं कि <canvas> एक नया और लोकप्रिय टूल है. हालांकि, कभी-कभी डेवलपर अब भी DOM का इस्तेमाल करने का सुझाव देते हैं. अगर आपको यह तय करने में मुश्किल आ रही है कि किसका इस्तेमाल करना है, तो यहां एक उदाहरण दिया गया है. इसमें बताया गया है कि <canvas> ने हमारा कितना समय और ऊर्जा बचाई.

ओनस्लॉट! Arena में, यह लाल रंग में चमकता है और कुछ समय के लिए "दर्द" वाला ऐनिमेशन दिखाता है. हमें जितने ग्राफ़िक बनाने थे उनकी संख्या कम करने के लिए, हमने दुश्मनों को सिर्फ़ नीचे की ओर "दर्द" में दिखाया है. यह स्प्राइट, गेम में ठीक लगती है. साथ ही, इसे बनाने में काफ़ी समय भी नहीं लगता. हालांकि, बॉस मॉन्स्टर के लिए, 64x64 पिक्सल या उससे ज़्यादा के बड़े स्प्राइट को दर्द के फ़्रेम के लिए, बाईं या ऊपर की ओर से अचानक नीचे की ओर मुड़ते हुए देखना परेशान करने वाला था.

इसका सबसे आसान तरीका यह होता कि हर बॉस के लिए, आठों दिशाओं में एक-एक पेन फ़्रेम बनाया जाता. हालांकि, इसमें काफ़ी समय लगता. <canvas> की मदद से, हम कोड में इस समस्या को हल कर पाए:

ओनस्लॉट में, Beholder को नुकसान पहुंच रहा है! एरिना
context.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 के बीच होनी चाहिए.

कंट्रोल

Onslaught! Arena. सबसे पहले डेमो में सिर्फ़ कीबोर्ड का इस्तेमाल किया गया था. इसमें खिलाड़ी, ऐरो बटन की मदद से मुख्य किरदार को स्क्रीन पर इधर-उधर ले जाते थे और स्पेस बार की मदद से उस दिशा में गोली चलाते थे जिस दिशा में वह देख रहा था. यह गेम आसानी से समझ में आता है और इसे खेलना भी आसान है. हालांकि, ज़्यादा मुश्किल लेवल पर इसे खेलना मुश्किल हो जाता है. किसी भी समय, खिलाड़ी पर दर्जनों दुश्मन और प्रोजेक्टाइल फ़ायर किए जाते हैं. इसलिए, जब भी किसी भी दिशा में फ़ायर किया जा रहा हो, तब दुश्मनों के बीच से बचना ज़रूरी है.

इस गेम की तुलना, इसकी शैली के मिलते-जुलते गेम से करने के लिए, हमने टारगेटिंग रेटिकल को कंट्रोल करने के लिए माउस की सुविधा जोड़ी है. इस रेटिकल का इस्तेमाल, गेम का किरदार अपने हमलों के लिए करता है. हालांकि, कीबोर्ड की मदद से अब भी किरदार को घुमाया जा सकता था, लेकिन इस बदलाव के बाद वह एक साथ 360 डिग्री की किसी भी दिशा में गोली चला सकता था. गेमिंग के शौकीनों ने इस सुविधा की सराहना की, लेकिन इसका असर ट्रैकपैड इस्तेमाल करने वालों पर पड़ा.

Onslaught! एरिना कंट्रोल वाला मॉडल (अब काम नहीं करता)
Onslaught में पुराने कंट्रोल या "खेलने का तरीका" मोडल! Arena.

ट्रैकपैड का इस्तेमाल करने वाले लोगों के लिए, हमने ऐरो बटन के कंट्रोल को वापस लाया है. इस बार, हमने ऐरो बटन को दबाने पर, उस दिशा में फ़ायर करने की सुविधा जोड़ी है. हमें लगता था कि हम सभी तरह के प्लेयर के लिए गेम बना रहे हैं. हालांकि, अनजाने में हमने अपने गेम को बहुत मुश्किल बना दिया था. हमें बाद में पता चला कि कुछ खिलाड़ियों को हमले के लिए, माउस (या कीबोर्ड!) के कंट्रोल के बारे में पता नहीं था. ऐसा, ट्यूटोरियल मॉडल के बावजूद हुआ, जिन्हें ज़्यादातर खिलाड़ियों ने अनदेखा किया.

Onslaught! एरिना गेम के कंट्रोल के बारे में ट्यूटोरियल
खेलते समय, खिलाड़ी ट्यूटोरियल ओवरले को अनदेखा करते हैं. वे गेम खेलना और मज़े करना चाहते हैं!

हमें यूरोप के कुछ प्रशंसकों का भी साथ मिला है. हालांकि, हमें उनसे यह शिकायत मिली है कि उनके पास 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, HTML5 का <audio> टैग था. शायद सबसे खराब बात यह है कि इसमें देरी होती है: .play() को कॉल करने और आवाज़ के असल में चलने के बीच, करीब-करीब सभी ब्राउज़र में देरी होती है. इससे गेमर को गेम खेलने का बेहतर अनुभव नहीं मिल पाता. खास तौर पर, हमारे जैसे तेज़ी से चलने वाले गेम खेलते समय.

अन्य समस्याओं में, "प्रगति" इवेंट ट्रिगर न होना शामिल है. इसकी वजह से, गेम लोड होने में अनलिमिटेड समय लग सकता है. इन वजहों से, हमने "फ़ॉल-फ़ॉरवर्ड" तरीका अपनाया है. इस तरीके के तहत, अगर फ़्लैश लोड नहीं होता है, तो हम HTML5 ऑडियो पर स्विच कर देते हैं. कोड कुछ ऐसा दिखता है:

/*
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;
  }
};

यह भी ज़रूरी हो सकता है कि गेम ऐसे ब्राउज़र के साथ काम करे जो एमपी3 फ़ाइलें नहीं चलाते, जैसे कि 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 टेक्नोलॉजी का इस्तेमाल करना चाहते थे. डेटा को सेव करने के लिए, कई विकल्प उपलब्ध हैं. जैसे, लोकल स्टोरेज, सेशन स्टोरेज, और वेब एसक्यूएल डेटाबेस.

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 पर बनाए गए गेम के लिए आने वाला समय काफ़ी अच्छा दिख रहा है.

Onslaught! छिपे हुए HTML5 लोगो वाला अखाड़ा
Onslaught खेलते समय, "html5" टाइप करके HTML5 शील्ड पाया जा सकता है! Arena.