केस स्टडी - रेसर बनाना

परिचय

Racer, वेब पर आधारित मोबाइल Chrome एक्सपेरिमेंट है. इसे Active Theory ने डेवलप किया है. ज़्यादा से ज़्यादा पांच दोस्त, हर स्क्रीन पर एक साथ रेस में हिस्सा लेने के लिए अपने फ़ोन या टैबलेट कनेक्ट कर सकते हैं. Google Creative Lab के कॉन्सेप्ट, डिज़ाइन, और प्रोटोटाइप और Plan8 के साउंड की मदद से, हमने I/O 2013 में लॉन्च से पहले आठ हफ़्तों तक, बिल्ड पर काम किया. यह गेम कुछ हफ़्तों से लाइव है. इसलिए, हमने डेवलपर कम्यूनिटी से इस बारे में कुछ सवाल पूछे हैं कि यह गेम कैसे काम करता है. यहां मुख्य सुविधाओं के बारे में जानकारी दी गई है. साथ ही, उन सवालों के जवाब भी दिए गए हैं जो अक्सर हमारे पास आते हैं.

ट्रैक

हमें एक साफ़ तौर पर यह समस्या आ रही थी कि वेब पर आधारित ऐसा मोबाइल गेम कैसे बनाया जाए जो अलग-अलग तरह के डिवाइसों पर अच्छी तरह से काम करे. खिलाड़ियों को अलग-अलग फ़ोन और टैबलेट से रेस बनानी थी. हो सकता है कि एक खिलाड़ी के पास Nexus 4 हो और वह अपने दोस्त के साथ iPad पर रेस करना चाहे. हमें हर रेस के लिए, ट्रैक का एक सामान्य साइज़ तय करने का तरीका ढूंढना था. इस समस्या को हल करने के लिए, रेस में शामिल हर डिवाइस के स्पेसिफ़िकेशन के हिसाब से, अलग-अलग साइज़ के ट्रैक का इस्तेमाल करना पड़ा.

ट्रैक डाइमेंशन का हिसाब लगाना

जब कोई खिलाड़ी शामिल होता है, तो उसके डिवाइस की जानकारी सर्वर पर भेजी जाती है और अन्य खिलाड़ियों के साथ शेयर की जाती है. ट्रैक बनाते समय, इस डेटा का इस्तेमाल ट्रैक की ऊंचाई और चौड़ाई का हिसाब लगाने के लिए किया जाता है. हम सबसे छोटी स्क्रीन की ऊंचाई का पता लगाकर, ऊंचाई का हिसाब लगाते हैं. साथ ही, चौड़ाई सभी स्क्रीन की कुल चौड़ाई होती है. इसलिए, नीचे दिए गए उदाहरण में ट्रैक की चौड़ाई 1,152 पिक्सल और ऊंचाई 519 पिक्सल होगी.

इस उदाहरण में, लाल रंग का हिस्सा ट्रैक की कुल चौड़ाई और ऊंचाई दिखाता है.
लाल रंग का यह हिस्सा, इस उदाहरण में ट्रैक की कुल चौड़ाई और ऊंचाई दिखाता है.
this.getDimensions = function () {
  var response = {};
  response.width = 0;
  response.height = _gamePlayers[0].scrn.h; // First screen height
  response.screens = [];
  
  for (var i = 0; i < _gamePlayers.length; i++) {
    var player = _gamePlayers[i];
    response.width += player.scrn.w;

    if (player.scrn.h < response.height) {
      // Find the smallest screen height
      response.height = player.scrn.h;
    }
      
    response.screens.push(player.scrn);
  }
  
  return response;
}

ट्रैक बनाना

Paper.js एक ओपन सोर्स वेक्टर ग्राफ़िक्स स्क्रिप्टिंग फ़्रेमवर्क है, जो HTML5 कैनवस पर काम करता है. हमें पता चला कि ट्रैक के लिए वेक्टर आकार बनाने के लिए, Paper.js सबसे सही टूल है. इसलिए, हमने <canvas> एलिमेंट पर Adobe Illustrator में बनाए गए SVG ट्रैक को रेंडर करने के लिए, इसकी सुविधाओं का इस्तेमाल किया. ट्रैक बनाने के लिए, TrackModel क्लास, SVG कोड को DOM में जोड़ती है. साथ ही, ओरिजनल डाइमेंशन और पोज़िशनिंग की जानकारी इकट्ठा करती है, ताकि उसे TrackPathView को पास किया जा सके. TrackPathView, ट्रैक को कैनवस पर ड्रॉ करेगी.

paper.install(window);
_paper = new paper.PaperScope();
_paper.setup('track_canvas');
                    
var svg = document.getElementById('track');
var layer = new _paper.Layer();

_path = layer.importSvg(svg).firstChild.firstChild;
_path.strokeColor = '#14a8df';
_path.strokeWidth = 2;

ट्रैक बन जाने के बाद, हर डिवाइस को डिवाइस के लाइन-अप क्रम में अपनी पोज़िशन के आधार पर एक्स ऑफ़सेट मिलता है. इसके बाद, ट्रैक को उसी हिसाब से पोज़िशन दी जाती है.

var x = 0;

for (var i = 0; i < screens.length; i++) {
  if (i < PLAYER_INDEX) {
    x += screens[i].w;
  }
}
इसके बाद, ट्रैक का सही हिस्सा दिखाने के लिए x ऑफ़सेट का इस्तेमाल किया जा सकता है.
इसके बाद, ट्रैक के सही हिस्से को दिखाने के लिए, एक्स ऑफ़सेट का इस्तेमाल किया जा सकता है

सीएसएस ऐनिमेशन

Paper.js, ट्रैक की लेन बनाने के लिए सीपीयू प्रोसेसिंग का ज़्यादा इस्तेमाल करता है. इस प्रोसेस में, अलग-अलग डिवाइसों पर ज़्यादा या कम समय लगेगा. इसे मैनेज करने के लिए, हमें एक लोडर की ज़रूरत थी, जो तब तक लूप में चलता रहे, जब तक सभी डिवाइस ट्रैक को प्रोसेस कर लें. समस्या यह थी कि Paper.js के सीपीयू की ज़रूरतों की वजह से, JavaScript पर आधारित कोई भी ऐनिमेशन फ़्रेम को स्किप कर देता था. सीएसएस ऐनिमेशन डालें, जो अलग यूज़र इंटरफ़ेस (यूआई) थ्रेड पर चलते हैं. इससे, "BUILDING TRACK" टेक्स्ट में चमक को आसानी से ऐनिमेट किया जा सकता है.

.glow {
  width: 290px;
  height: 290px;
  background: url('img/track-glow.png') 0 0 no-repeat;
  background-size: 100%;
  top: 0;
  left: -290px;
  z-index: 1;
  -webkit-animation: wipe 1.3s linear 0s infinite;
}

@-webkit-keyframes wipe {
  0% {
    -webkit-transform: translate(-300px, 0);
  }

  25% {
    -webkit-transform: translate(-300px, 0);
  }

  75% {
    -webkit-transform: translate(920px, 0);
  }

  100% {
    -webkit-transform: translate(920px, 0);
  }
}
}

सीएसएस स्प्राइट

इन-गेम इफ़ेक्ट के लिए भी सीएसएस का इस्तेमाल किया गया. मोबाइल डिवाइसों की सीमित पावर का इस्तेमाल, ट्रैक पर दौड़ रही कारों को ऐनिमेशन देने में किया जाता है. इसलिए, हमने गेम में पहले से रेंडर किए गए ऐनिमेशन लागू करने के लिए, स्प्राइट का इस्तेमाल किया. सीएसएस स्प्राइट में, ट्रांज़िशन, चरण-आधारित ऐनिमेशन लागू करते हैं. इससे background-position प्रॉपर्टी बदल जाती है और कार में विस्फोट होता है.

#sprite {
  height: 100px; 
  width: 100px;
  background: url('sprite.jpg') 0 0 no-repeat;
  -webkit-animation: play-sprite 0.33s linear 0s steps(9) infinite;
}

@-webkit-keyframes play-sprite {
  0% {
    background-position: 0 0;
  }

  100% {
    background-position: -900px 0;
  }
}

इस तकनीक की समस्या यह है कि सिर्फ़ एक पंक्ति में बनाई गई स्प्राइट शीट का इस्तेमाल किया जा सकता है. एक से ज़्यादा लाइनों को लूप करने के लिए, एनिमेशन को कई कीफ़्रेम एलान के ज़रिए चेन किया जाना चाहिए.

#sprite {
  height: 100px; 
  width: 100px;
  background: url('sprite.jpg') 0 0 no-repeat;
  -webkit-animation-name: row1, row2, row3;
  -webkit-animation-duration: 0.2s;
  -webkit-animation-delay: 0s, 0.2s, 0.4s;
  -webkit-animation-timing-function: steps(5), steps(5), steps(5);
  -webkit-animation-fill-mode: forwards;
}

@-webkit-keyframes row1 {
  0% {
    background-position: 0 0;
  }

  100% {
    background-position: -500px 0;
  }
}

@-webkit-keyframes row2 {
  0% {
    background-position: 0 -100px;
  }

  100% {
    background-position: -500px -100px;
  }
}

@-webkit-keyframes row3 {
  0% {
    background-position: 0 -200px;
  }

  100% {
    background-position: -500px -200px;
  }
}

कारों को रेंडर करना

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

हमने फिर से Paper.js का इस्तेमाल किया, जो गणित से जुड़ी कई सुविधाओं के साथ आता है. हमने इसके कुछ तरीकों का इस्तेमाल करके, कार को रास्ते पर आगे बढ़ाया. साथ ही, हर फ़्रेम में कार की पोज़िशन और रोटेशन को आसानी से अडजस्ट किया.

var trackOffset = _path.length - (_elapsed % _path.length);
var trackPoint = _path.getPointAt(trackOffset);
var trackAngle = _path.getTangentAt(trackOffset).angle;

// Apply the throttle
_velocity.length += _throttle;

if (!_throttle) {
  // Slow down since the throttle is off
  _velocity.length *= FRICTION;
}

if (_velocity.length > MAXVELOCITY) {
  _velocity.length = MAXVELOCITY;
}

_velocity.angle = trackAngle;
trackOffset -= _velocity.length;
_elapsed += _velocity.length;

// Find if a lap has been completed
if (trackOffset < 0) {
  while (trackOffset < 0) trackOffset += _path.length;

  trackPoint = _path.getPointAt(trackOffset);
  console.log('LAP COMPLETE!');
}

if (_velocity.length > 0.1) {
  // Render the car if there is actually velocity
  renderCar(trackPoint);
}

कार की रेंडरिंग को ऑप्टिमाइज़ करते समय, हमें एक दिलचस्प बात पता चली. iOS पर, कार पर translate3d ट्रांसफ़ॉर्म लागू करके सबसे अच्छी परफ़ॉर्मेंस हासिल की गई:

_car.style.webkitTransform = 'translate3d('+_position.x+'px, '+_position.y+'px, 0px)rotate('+_rotation+'deg)';

Android के लिए Chrome पर, मैट्रिक वैल्यू का हिसाब लगाकर और मैट्रिक ट्रांसफ़ॉर्म लागू करके सबसे अच्छी परफ़ॉर्मेंस हासिल की गई:

var rad = _rotation.rotation * (Math.PI * 2 / 360);
var cos = Math.cos(rad);
var sin = Math.sin(rad);
var a = parseFloat(cos).toFixed(8);
var b = parseFloat(sin).toFixed(8);
var c = parseFloat(-sin).toFixed(8);
var d = a;
_car.style.webkitTransform = 'matrix(' + a + ', ' + b + ', ' + c + ', ' + d + ', ' + _position.x + ', ' + _position.y + ')';

डिवाइसों को सिंक करके रखना

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

इंतज़ार के समय का हिसाब लगाना

डिवाइसों को सिंक करने के लिए, यह जानना ज़रूरी है कि Compute Engine रिले से मैसेज मिलने में कितना समय लगता है. मुश्किल बात यह है कि हर डिवाइस की घड़ी कभी भी पूरी तरह से सिंक नहीं होगी. इस समस्या को हल करने के लिए, हमें डिवाइस और सर्वर के बीच के समय के अंतर का पता लगाना था.

डिवाइस और मुख्य सर्वर के बीच के समय के अंतर का पता लगाने के लिए, हम डिवाइस के मौजूदा टाइमस्टैंप के साथ एक मैसेज भेजते हैं. इसके बाद, सर्वर अपने टाइमस्टैंप के साथ-साथ ओरिजनल टाइमस्टैंप का जवाब देगा. हम जवाब का इस्तेमाल करके, समय के असल अंतर का हिसाब लगाते हैं.

var currentTime = Date.now();
var latency = Math.round((currentTime - e.time) * .5);
var serverTime = e.serverTime;
currentTime -= latency;
var difference = currentTime - serverTime;

ऐसा एक बार करने से काम नहीं चलेगा, क्योंकि सर्वर से आने-जाने में लगने वाला समय हमेशा एक जैसा नहीं होता. इसका मतलब है कि सर्वर से जवाब मिलने में ज़्यादा समय लग सकता है, जबकि सर्वर को जवाब देने में कम समय लग सकता है. इस समस्या से बचने के लिए, हम सर्वर से कई बार अनुरोध करते हैं और उनका औसत निकालते हैं. इससे हमें डिवाइस और सर्वर के बीच के असल अंतर के 10 मिलीसेकंड के अंदर पता चल जाता है.

त्वरण/मंदी

जब प्लेयर 1, स्क्रीन को दबाता है या छोड़ता है, तो सर्वर पर 'ऐक्सेलरेशन इवेंट' भेजा जाता है. डेटा मिलने के बाद, सर्वर अपना मौजूदा टाइमस्टैंप जोड़ता है. इसके बाद, वह डेटा को हर दूसरे खिलाड़ी को भेजता है.

जब किसी डिवाइस पर "accelerate on" या "accelerate off" इवेंट मिलता है, तो हम सर्वर ऑफ़सेट (ऊपर कैलकुलेट किया गया) का इस्तेमाल करके यह पता लगा सकते हैं कि उस मैसेज को मिलने में कितना समय लगा. यह जानकारी इसलिए काम की है, क्योंकि हो सकता है कि प्लेयर 1 को मैसेज 20 मिलीसेकंड में मिल जाए, लेकिन प्लेयर 2 को 50 मिलीसेकंड लगें. इससे कार दो अलग-अलग जगहों पर दिखेगी, क्योंकि डिवाइस 1, कार को पहले ही तेज़ी से दौड़ाना शुरू कर देगा.

हम इवेंट मिलने में लगने वाले समय को फ़्रेम में बदल सकते हैं. 60fps पर, हर फ़्रेम 16.67 मि॰से॰ का होता है. इसलिए, हम कार के फ़्रेम को ध्यान में रखते हुए, उस पर ज़्यादा वेग (त्वरण) या घर्षण (मंदी) जोड़ सकते हैं.

var frames = time / 16.67;
var onScreen = this.isOnScreen() && time < 75;

for (var i = 0; i < frames; i++) {
  if (onScreen) {
    _velocity.length += _throttle * Math.round(frames * .215);
  } else {
    _this.render();
  }
}}

ऊपर दिए गए उदाहरण में, अगर प्लेयर 1 की स्क्रीन पर कार है और उसे मैसेज मिलने में 75 मिलीसेकंड से कम समय लगता है, तो यह कार की रफ़्तार में बदलाव करेगा. साथ ही, अंतर को पूरा करने के लिए, कार की रफ़्तार बढ़ा देगा. अगर डिवाइस स्क्रीन पर नहीं है या मैसेज को पढ़ने में बहुत ज़्यादा समय लगा, तो यह रेंडर फ़ंक्शन चलाएगा और कार को उस जगह पर ले जाएगा जहां उसे जाना है.

कारों को सिंक रखना

ऐक्सेलरेशन में लगने वाले समय को ध्यान में रखकर भी, कार सिंक न हो सकती है और एक साथ कई स्क्रीन पर दिख सकती है. ऐसा खास तौर पर, एक डिवाइस से दूसरे डिवाइस पर ट्रांज़िशन करते समय होता है. ऐसा होने से रोकने के लिए, अपडेट इवेंट बार-बार भेजे जाते हैं, ताकि सभी स्क्रीन पर कारों को ट्रैक पर एक ही पोज़िशन में रखा जा सके.

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

this.getValues = function () {
  _values.p = _position.clone();
  _values.r = _rotation;
  _values.e = _elapsed;
  _values.v = _velocity.length;
  _values.pos = _this.position;

  return _values;
}

this.setValues = function (val, time) {
  _position.x = val.p.x;
  _position.y = val.p.y;
  _rotation = val.r;
  _elapsed = val.e;
  _velocity.length = val.v;

  var frames = time / 16.67;

  for (var i = 0; i < frames; i++) {
    _this.render();
  }
}

नतीजा

जब हमें Racer के कॉन्सेप्ट के बारे में पता चला, तब हमें पता था कि यह एक बहुत खास प्रोजेक्ट हो सकता है. हमने तुरंत एक प्रोटोटाइप बनाया, जिससे हमें इंतज़ार के समय और नेटवर्क की परफ़ॉर्मेंस से जुड़ी समस्याओं को हल करने का एक अनुमान मिला. यह एक चुनौती भरा प्रोजेक्ट था. इसकी वजह से, हमें रातों को और वीकेंड के दौरान काम करना पड़ा. हालांकि, जब गेम का आकार लेना शुरू हुआ, तो हमें बहुत अच्छा लगा. आखिर में, हम इस नतीजे से काफ़ी खुश हैं. Google Creative Lab के कॉन्सेप्ट ने ब्राउज़र टेक्नोलॉजी को मज़ेदार तरीके से आगे बढ़ाया है. डेवलपर के तौर पर, हम इससे ज़्यादा कुछ नहीं चाहते.