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

शुरुआती जानकारी

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

द ट्रैक

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

ट्रैक डाइमेंशन की गिनती की जा रही है

हर खिलाड़ी के जुड़ने पर, उनके डिवाइस की जानकारी सर्वर पर भेजी जाती है और दूसरे खिलाड़ियों के साथ शेयर की जाती है. जब ट्रैक बनाया जा रहा हो, तो इस डेटा का इस्तेमाल ट्रैक की ऊंचाई और चौड़ाई का हिसाब लगाने के लिए किया जाता है. हम सबसे छोटी स्क्रीन की ऊंचाई का पता लगाकर, उसकी ऊंचाई का पता लगाते हैं. स्क्रीन की चौड़ाई, सभी स्क्रीन की कुल चौड़ाई होती है. इसलिए, नीचे दिए गए उदाहरण में ट्रैक की चौड़ाई 1152 पिक्सल और ऊंचाई 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, ट्रैक के लिए वेक्टर आकार बनाने का सबसे सही टूल है. इसलिए, हमने Adobe Illustrator में <canvas> एलिमेंट पर बनाए गए SVG ट्रैक को रेंडर करने के लिए, अपनी सुविधाओं का इस्तेमाल किया. ट्रैक बनाने के लिए, TrackModel क्लास SVG कोड को DOM में जोड़ती है और 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;

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

var x = 0;

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

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

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

.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, स्क्रीन को दबाता या रिलीज़ करता है, तो सर्वर को तेज़ी से डेटा भेजने की गतिविधि भेजी जाती है. डेटा मिलने के बाद, सर्वर अपना मौजूदा टाइमस्टैंप जोड़ता है और उस डेटा को हर दूसरे प्लेयर के साथ भेजता है.

जब किसी डिवाइस को "एक्सीलरेट चालू है" या "एक्सीलरेट बंद हो जाता है" इवेंट मिलता है, तो हम सर्वर ऑफ़सेट (ऊपर दिया गया कैलकुलेट किया गया) का इस्तेमाल करके, यह पता लगा सकते हैं कि मैसेज मिलने में कितना समय लगा. यह उपयोगी है, क्योंकि हो सकता है कि प्लेयर 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();
  }
}

नतीजा

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