The Hobbit Experience 2014

हॉबिट एक्सपीरियंस में WebRTC गेमप्ले जोड़ना

डैनियल इसाक्सन
डैनियल इसाक्सन

नई हॉबिट फ़िल्म “The Hobbit: The Battle of the Five Armies” के लिए हमने पिछले साल के Chrome प्रयोग, A Journey through मिड- अर्थ में कुछ नया कॉन्टेंट शामिल करने के लिए काम किया है. इस बार मुख्य रूप से WebGL के इस्तेमाल को बढ़ाने पर ध्यान दिया गया, क्योंकि ज़्यादा से ज़्यादा ब्राउज़र और डिवाइस, कॉन्टेंट को देख सकें. साथ ही, Chrome और Firefox में WebRTC की क्षमताओं के साथ काम कर सकें. इस साल के प्रयोग में हमारे तीन लक्ष्य थे:

  • Android के लिए Chrome पर WebRTC और WebGL का इस्तेमाल करते हुए P2P गेमप्ले
  • टच इनपुट के आधार पर, खेलने में आसान और मल्टी-प्लेयर गेम बनाएं
  • Google Cloud Platform पर होस्ट करें

गेम की परिभाषा

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

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

गेम के हिस्से

कई खिलाड़ियों वाले इस गेम को बनाने के लिए हमें कुछ अहम हिस्सों को बनाना था:

  • सर्वर साइड प्लेयर मैनेजमेंट एपीआई, उपयोगकर्ताओं, मैच बनाने, सेशन, और गेम के आंकड़ों को मैनेज करता है.
  • प्लेयर के बीच कनेक्शन बनाने में मदद करने वाले सर्वर.
  • गेम रूम में सभी खिलाड़ियों से कनेक्ट करने और उनसे बातचीत करने के लिए इस्तेमाल किए जाने वाले AppEngine Channels API सिग्नलिंग को हैंडल करने के लिए एक एपीआई.
  • JavaScript गेम इंजन, जो दो खिलाड़ियों या मिलते-जुलते ऐप्लिकेशन के बीच स्टेट और आरटीसी मैसेज को सिंक करने का काम करता है.
  • WebGL गेम व्यू.

प्लेयर मैनेजमेंट

बड़ी संख्या में खिलाड़ियों को मदद करने के लिए, हम हर बैटलग्राउंड के लिए कई समानांतर गेम-रूम इस्तेमाल करते हैं. हर गेम रूम के हिसाब से खिलाड़ियों की संख्या सीमित करने की मुख्य वजह यह है कि नए खिलाड़ियों को सही समय पर लीडरबोर्ड में सबसे ऊपर पहुंचने की सुविधा दी जाए. यह सीमा json ऑब्जेक्ट के साइज़ से भी जुड़ी होती है. इसमें, Channel API से भेजे गए गेम रूम की जानकारी दी जाती है. इसकी सीमा 32 केबी की है. हमें गेम में खिलाड़ियों, कमरों, स्कोर, सेशन, और उनके संबंधों को सेव करना होगा. इसके लिए, हमने पहले इकाइयों के लिए NDB का इस्तेमाल किया और फिर संबंधों को समझने के लिए, क्वेरी इंटरफ़ेस का इस्तेमाल किया. NDB, Google Cloud Datastore का एक इंटरफ़ेस है. शुरुआत में NDB के इस्तेमाल से काफ़ी मदद मिली थी, लेकिन जल्द ही हमें एक समस्या का सामना करना पड़ा कि इसे इस्तेमाल करने की ज़रूरत है. यह क्वेरी, डेटाबेस के "तय किए गए" वर्शन के हिसाब से चलाई गई थी. ज़्यादा जानकारी वाले इस लेख में, एनडीबी राइट्स के बारे में ज़्यादा जानकारी वाले लेख में बताया गया है. इसमें कुछ सेकंड लग सकते हैं. हालांकि, इकाइयों को इतनी देरी नहीं होती, क्योंकि वे सीधे कैश मेमोरी से जवाब देती हैं. आपके लिए, यहां दिए गए उदाहरण में दिए गए कोड का इस्तेमाल करना आसान हो सकता है:

// example code to explain our issue with eventual consistency
def join_room(player_id, room_id):
    room = Room.get_by_id(room_id)
    
    player = Player.get_by_id(player_id)
    player.room = room.key
    player.put()
    
    // the player Entity is updated directly in the cache
    // so calling this will return the room key as expected
    player.room // = Key(Room, room_id)

    // Fetch all the players with room set to 'room.key'
    players_in_room = Player.query(Player.room == room.key).fetch()
    // = [] (an empty list of players)
    // even though the saved player above may be expected to be in the
    // list it may not be there because the query api is being run against the 
    // "committed" version and may still be empty for a few seconds

    return {
        room: room,
        players: players_in_room,
    }

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

हालांकि, मेमकैश में कुछ खास इंद्रधनुष और यूनिकॉर्न नहीं होते. हालांकि, इसके लिए कुछ सीमाएं मिलती हैं. इनमें सबसे खास है, 1 एमबी की वैल्यू का साइज़ (इसमें किसी युद्ध के मैदान से जुड़े बहुत ज़्यादा कमरे नहीं हो सकते) और कुंजी की समयसीमा खत्म होना या दस्तावेज़ में इसके बारे में बताया गया है:

हमने कम कीमत वाले एक और शानदार स्टोर, Redis का इस्तेमाल करने के बारे में सोचा. हालांकि, उस समय स्केलेबल क्लस्टर सेट अप करना मुश्किल था. इसलिए, हमने सर्वर का रखरखाव करने के बजाय, अनुभव को बेहतर बनाने पर ध्यान दिया. इसलिए, हम उनके अनुभव को बेहतर बनाना चाहते थे. दूसरी ओर, Google Cloud Platform ने हाल ही में एक सामान्य क्लिक-टू-डिप्लॉय सुविधा रिलीज़ की है, जिसमें से एक विकल्प Redis Cluster है. यह एक बहुत ही दिलचस्प विकल्प होता.

आखिर में, हमें Google Cloud SQL मिल गया और हमने संबंधों को MySQL में माइग्रेट कर दिया. यह काफ़ी काम रहा, लेकिन आखिरकार यह काम बेहतर तरीके से हुआ. अपडेट अब पूरी तरह से सामान्य हैं और जांच भी पास हो जाती हैं. इससे मैचमेकिंग और स्कोर बनाने की प्रोसेस को ज़्यादा भरोसेमंद बना दिया गया.

समय के साथ, ज़्यादातर डेटा धीरे-धीरे NDB और मेमकैश से SQL में ट्रांसफ़र हो गया है. हालांकि, आम तौर पर प्लेयर, बैटलग्राउंड, और रूम की इकाइयां अब भी NDB में स्टोर होती हैं. इस दौरान, सेशन और उनके बीच के संबंध को SQL में स्टोर किया जाता है.

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

यह एक से ज़्यादा खिलाड़ियों वाला गेम है, जिसे हम रूम में मौजूद दूसरे खिलाड़ियों को इवेंट के बारे में बताना चाहते हैं. जैसे, “गेम में कौन शामिल हुआ या किसने छोड़ा”, “कौन जीता या हार गया”, और क्या स्वीकार करने की कोई चुनौती है. इसे मैनेज करने के लिए, हमने प्लेयर मैनेजमेंट एपीआई में सूचनाएं पाने की सुविधा दी है.

WebRTC सेट अप करना

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

तीसरे पक्ष की ऐसी कई लाइब्रेरी मौजूद हैं जिनका इस्तेमाल सिग्नलिंग सेवा के लिए किया जा सकता है. इनसे WebRTC को सेट अप करने में भी आसानी होती है. इनमें से कुछ विकल्प हैं PeerJS, SimpleWebRTC, और PubNub WebRTC SDK टूल. PubNub, होस्ट किए गए सर्वर सलूशन का इस्तेमाल करता है. इस प्रोजेक्ट को हम Google Cloud Platform पर होस्ट करना चाहते थे. अन्य दो लाइब्रेरी node.js सर्वर का इस्तेमाल करती हैं, जिसे हम Google Compute Engine पर इंस्टॉल कर सकते थे. हालांकि, हमें यह भी पक्का करना होगा कि वह एक साथ काम करने वाले हज़ारों उपयोगकर्ताओं को मैनेज कर सके. हालांकि, हमें पहले से पता था कि Channel API यह काम कर सकता है.

इस मामले में, Google Cloud Platform का इस्तेमाल करने का एक मुख्य फ़ायदा यह है कि स्केलिंग की जाती है. AppEngine प्रोजेक्ट के लिए ज़रूरी संसाधनों की स्केलिंग Google Developers Console से आसानी से की जा सकती है. साथ ही, Channels API का इस्तेमाल करते समय सिग्नल सेवा को बढ़ाने के लिए अलग से किसी काम की ज़रूरत नहीं होती.

चैनलों का एपीआई बहुत मज़बूत है और इंतज़ार का समय होने को लेकर कुछ चिंता थी. हालांकि, हमने पहले CubeSlam प्रोजेक्ट के लिए इसका इस्तेमाल किया था और उस प्रोजेक्ट में लाखों उपयोगकर्ताओं के लिए यह कारगर साबित हुआ था. इसलिए, हमने इसे फिर से इस्तेमाल करने का फ़ैसला लिया.

हमने WebRTC में मदद करने के लिए, तीसरे पक्ष की लाइब्रेरी का इस्तेमाल नहीं किया. इसलिए, हमें अपना प्रोजेक्ट बनाना था. अच्छी बात यह है कि हमने CubeSlam प्रोजेक्ट के लिए बनाए गए अपने काफ़ी कामों का फिर से इस्तेमाल किया है. जब दोनों खिलाड़ी किसी सेशन में शामिल होते हैं, तो सेशन “चालू है” पर सेट हो जाता है. इसके बाद, दोनों खिलाड़ी चैनल एपीआई के ज़रिए पीयर-टू-पीयर कनेक्शन शुरू करने के लिए, उस ऐक्टिव सेशन आईडी का इस्तेमाल करेंगे. इसके बाद, दोनों खिलाड़ियों के बीच होने वाली बातचीत को RTCDataChannel पर हैंडल किया जाएगा.

कनेक्शन बनाने और NAT और फ़ायरवॉल से निपटने में मदद के लिए, हमें STUN और Turn सर्वर की भी ज़रूरत है. HTML5 Rocks में WebRTC सेट अप करने के बारे में ज़्यादा जानकारी के लिए WebRTC in the real world: STUN, TURN, and Signaling लेख पढ़ें.

इस्तेमाल किए गए टर्न सर्वर की संख्या, ट्रैफ़िक के हिसाब से स्केल होनी चाहिए. इसे मैनेज करने के लिए, हमने Google के डिप्लॉयमेंट मैनेजर की जांच की है. इससे हम Google Compute Engine पर संसाधनों को डाइनैमिक तौर पर डिप्लॉय कर पाते हैं और टेंप्लेट का इस्तेमाल करके, टर्न सर्वर इंस्टॉल कर पाते हैं. यह अब भी ऐल्फ़ा में है, लेकिन हमारे मकसद से इसमें कोई गलती नहीं आई है. TURN सर्वर के लिए हम coturn का इस्तेमाल करते हैं, जो STUN/TURN को काफ़ी तेज़, असरदार, और भरोसेमंद लगता है.

चैनल एपीआई

Channel API का इस्तेमाल, क्लाइंट साइड पर मौजूद गेम रूम से सभी कम्यूनिकेशन को भेजने के लिए किया जाता है. हमारा प्लेयर मैनेजमेंट एपीआई, गेम इवेंट के बारे में सूचनाओं के लिए Channel API का इस्तेमाल कर रहा है.

Channels API के साथ काम करने में कुछ स्पीड में कमी आई. इसका एक उदाहरण यह है कि मैसेज बिना क्रम के आ सकते हैं. इसलिए, हमें सभी मैसेज को एक ऑब्जेक्ट में रैप करना होता है और उन्हें क्रम से लगाना होता है. यहां इसके काम करने के तरीके के बारे में कुछ उदाहरण कोड दिए गए हैं:

var que = [];  // [seq, packet...]
var seq = 0;
var rcv = -1;

function send(message) {
  var packet = JSON.stringify({
    seq: seq++,
    msg: message
  });
  channel.send(packet);
}

function recv(packet) {
  var data = JSON.parse(packet);

  if (data.seq <= rcv) {
    // ignoring message, older or already received
  } else if (data.seq > rcv + 1) {
    // message from the future. queue it up.
    que.push(data.seq, packet);
  } else {
    // message in order! update the rcv index and emit the message
    rcv = data.seq;
    emit('message', data.message);

    // and now that we have updated the `rcv` index we 
    // will check the que for any other we can send
    setTimeout(flush, 10);
  }
}

function flush() {
  for (var i=0; i<que.length; i++) {
    var seq = que[i];
    var packet = que[i+1];
    if (data.seq == rcv + 1) {
      recv(packet);
      return; // wait for next flush
    }
  }
}

हम साइट के अलग-अलग एपीआई को भी मॉड्यूलर मोड में रखना चाहते थे. साथ ही, साइट को होस्ट करने वाली साइट से अलग रखना चाहते थे. साथ ही, हमने GAE में पहले से मौजूद मॉड्यूल का इस्तेमाल किया. माफ़ करें, डेवलपर में ये सब करने के बाद हमें पता चला कि Channel API, प्रोडक्शन में चल रहे मॉड्यूल के साथ काम नहीं करता. इसके बजाय, हम अलग GAE इंस्टेंस का इस्तेमाल करने लगे और सीओआरएस से जुड़ी समस्याओं का सामना किया. इसकी वजह से हमें iframe postMessage ब्रिज का इस्तेमाल करना पड़ा.

गेम इंजन

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

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

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

गेम के लॉजिक पर फ़ोकस करते हुए, डेवलपमेंट की शुरुआत में एक आसान कैनवस-रेंडरर होना सही था. हालांकि, गेम का 3D वर्शन लागू होने के बाद, इस गेम का असली आनंद तब शुरू होता था, जब सीन को सीन और एनवायरमेंट के ज़रिए जीवंत कर दिया जाता था. हम three.js का इस्तेमाल 3d-इंजन के तौर पर करते हैं. इसकी आर्किटेक्चर की वजह से इसे आसानी से खेला जा सकता है.

माउस की स्थिति रिमोट उपयोगकर्ता को ज़्यादा बार भेजी जाती है और 3d-लाइट से यह संकेत मिलता है कि कर्सर इस समय कहां है.