Hobbit Experience में WebRTC गेमप्ले जोड़ना
हमने पिछले साल के Chrome एक्सपेरिमेंट, मिडल-अर्थ की यात्रा को कुछ नए कॉन्टेंट के साथ पेश किया है. ऐसा, हॉबिट की नई फ़िल्म “द हॉबिट: द बैटल ऑफ़ द फ़ाइव आर्मीज़” के रिलीज़ होने से पहले किया गया है. इस बार, WebGL का इस्तेमाल बढ़ाने पर फ़ोकस किया गया है, ताकि ज़्यादा ब्राउज़र और डिवाइसों पर कॉन्टेंट देखा जा सके. साथ ही, Chrome और Firefox में WebRTC की सुविधाओं के साथ काम किया जा सके. इस साल के एक्सपेरिमेंट के लिए, हमारे तीन लक्ष्य थे:
- Android के लिए Chrome पर, WebRTC और WebGL का इस्तेमाल करके पी2पी गेमप्ले
- ऐसा मल्टीप्लेयर गेम बनाएं जो आसानी से खेला जा सके और टच इनपुट पर आधारित हो
- Google Cloud Platform पर होस्ट करना
गेम के बारे में जानकारी
इस गेम का लॉजिक, ग्रिड-आधारित सेटअप पर आधारित है. इसमें सैनिक, गेम बोर्ड पर चलते हैं. इससे, नियम तय करते समय हमें कागज़ पर गेमप्ले को आज़माने में आसानी हुई. ग्रिड-आधारित सेटअप का इस्तेमाल करने से, गेम में टक्कर का पता लगाने में भी मदद मिलती है. इससे गेम की परफ़ॉर्मेंस बेहतर बनी रहती है, क्योंकि आपको सिर्फ़ एक ही या आस-पास की टाइल में मौजूद ऑब्जेक्ट से होने वाली टक्कर का पता लगाना होता है. हमें शुरू से ही पता था कि हमें नए गेम में, मिडल-अर्थ की चार मुख्य सेनाओं, मनुष्यों, बौने, एल्फ़, और ऑर्क के बीच की लड़ाई पर फ़ोकस करना है. यह गेम इतना आसान होना चाहिए कि इसे Chrome एक्सपेरिमेंट में आसानी से खेला जा सके. साथ ही, इसमें सीखने के लिए ज़्यादा इंटरैक्शन न हों. हमने Middle-earth के मैप पर पांच बैटलग्राउंड तय किए हैं. ये गेम-रूम के तौर पर काम करते हैं. इनमें कई खिलाड़ी, एक-दूसरे के साथ मुकाबला कर सकते हैं. मोबाइल स्क्रीन पर रूम में मौजूद कई खिलाड़ियों को दिखाना और उपयोगकर्ताओं को यह चुनने की सुविधा देना कि उन्हें किससे चैलेंज करना है, यह अपने-आप में एक चुनौती थी. इंटरैक्शन और सीन को आसान बनाने के लिए, हमने चैलेंज और स्वीकार करने के लिए सिर्फ़ एक बटन जोड़ा है. साथ ही, हमने रूम का इस्तेमाल सिर्फ़ इवेंट दिखाने और यह बताने के लिए किया है कि 'किंग ऑफ़ द हिल' कौन है. इस दिशा-निर्देश से, मैच बनाने से जुड़ी कुछ समस्याएं भी हल हुई हैं. साथ ही, हमें बैटल के लिए सबसे अच्छे खिलाड़ियों को मैच करने में मदद मिली है. Chrome पर Cube Slam गेम के साथ किए गए पिछले एक्सपेरिमेंट से हमें पता चला था कि अगर मल्टीप्लेयर गेम का नतीजा, इंतज़ार के समय पर निर्भर करता है, तो उसे मैनेज करने में काफ़ी मेहनत लगती है. आपको लगातार यह अनुमान लगाना होता है कि आपके प्रतिद्वंद्वी की स्थिति क्या होगी और वह आपको कहां समझता है. साथ ही, आपको यह अनुमान अलग-अलग डिवाइसों पर ऐनिमेशन के साथ सिंक करना होता है. इस लेख में इन चुनौतियों के बारे में ज़्यादा जानकारी दी गई है. इसे थोड़ा आसान बनाने के लिए, हमने इस गेम को टर्न-आधारित बनाया है.
इस गेम का लॉजिक, ग्रिड-आधारित सेटअप पर आधारित है. इसमें सैनिक, गेम बोर्ड पर चलते हैं. इससे, नियम तय करते समय हमें कागज़ पर गेमप्ले को आज़माने में आसानी हुई. ग्रिड-आधारित सेटअप का इस्तेमाल करने से, गेम में टक्कर का पता लगाने में भी मदद मिलती है. इससे गेम की परफ़ॉर्मेंस बेहतर बनी रहती है, क्योंकि आपको सिर्फ़ एक ही या आस-पास की टाइल में मौजूद ऑब्जेक्ट से होने वाली टक्कर का पता लगाना होता है.
गेम के हिस्से
इस मल्टी-प्लेयर गेम को बनाने के लिए, हमें कुछ अहम चीज़ें बनानी पड़ीं:
- सर्वर साइड प्लेयर मैनेजमेंट एपीआई, उपयोगकर्ताओं, मैच-मेकिंग, सेशन, और गेम के आंकड़ों को मैनेज करता है.
- ऐसे सर्वर जो खिलाड़ियों के बीच कनेक्शन बनाने में मदद करते हैं.
- AppEngine चैनलों के एपीआई सिग्नल को मैनेज करने के लिए एपीआई. इसका इस्तेमाल, गेम रूम में सभी खिलाड़ियों से कनेक्ट करने और उनसे बातचीत करने के लिए किया जाता है.
- JavaScript गेम इंजन, जो दो खिलाड़ियों/पीयर के बीच स्टेटस और आरटीसी मैसेजिंग को सिंक करता है.
- WebGL गेम व्यू.
प्लेयर मैनेजमेंट
ज़्यादा से ज़्यादा खिलाड़ियों को गेम खेलने की सुविधा देने के लिए, हम हर Battleground के लिए कई गेम-रूम का इस्तेमाल करते हैं. हर गेम-रूम में खिलाड़ियों की संख्या सीमित करने की मुख्य वजह यह है कि नए खिलाड़ी कम समय में लीडरबोर्ड में सबसे ऊपर पहुंच सकें. यह सीमा, Channel API के ज़रिए भेजे गए गेम-रूम की जानकारी देने वाले JSON ऑब्जेक्ट के साइज़ से भी जुड़ी है. इस ऑब्जेक्ट का साइज़ 32 केबी से ज़्यादा नहीं होना चाहिए. हमें गेम में खिलाड़ियों, रूम, स्कोर, सेशन, और उनके बीच के संबंधों की जानकारी सेव करनी होती है. इसके लिए, हमने सबसे पहले इकाइयों के लिए NDB का इस्तेमाल किया. साथ ही, रिलेशनशिप से जुड़ी जानकारी पाने के लिए क्वेरी इंटरफ़ेस का इस्तेमाल किया. एनडीबी, Google Cloud Datastore का इंटरफ़ेस है. शुरुआत में, NDB का इस्तेमाल करना बहुत अच्छा रहा, लेकिन हमें जल्द ही इस बात की समस्या हुई कि इसका इस्तेमाल कैसे किया जाए. क्वेरी, डेटाबेस के "कमिट किए गए" वर्शन के हिसाब से चलाई गई थी. इस लेख में, NDB Writes के बारे में पूरी जानकारी दी गई है. इसमें कुछ सेकंड की देरी हो सकती है. हालांकि, इकाइयों को इस तरह की देरी नहीं होती, क्योंकि वे सीधे कैश मेमोरी से जवाब देती हैं. उदाहरण के तौर पर दिए गए कोड की मदद से, इसे समझना थोड़ा आसान हो सकता है:
// 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 memcache में, “compare and set” सुविधा का इस्तेमाल करके, कुंजियों के लिए लेन-देन जैसा सिस्टम है. इसलिए, अब टेस्ट फिर से पास हो गए.
माफ़ करें, memcache में कुछ सीमाएं हैं. इनमें सबसे अहम, वैल्यू का साइज़ 1 एमबी होना (बैटलग्राउंड से जुड़े बहुत ज़्यादा रूम नहीं हो सकते) और पासकोड की समयसीमा खत्म होना है. इसके बारे में दस्तावेज़ में इस तरह बताया गया है:
हमने Redis, एक और बेहतरीन की-वैल्यू स्टोर का इस्तेमाल करने पर विचार किया था. हालांकि, उस समय स्केलेबल क्लस्टर सेट अप करना थोड़ा मुश्किल था. साथ ही, हम सर्वर को मैनेज करने के बजाय, बेहतर अनुभव देने पर फ़ोकस करना चाहते थे. इसलिए, हमने इस तरीके को अपनाने का फ़ैसला नहीं लिया. दूसरी ओर, Google Cloud Platform ने हाल ही में एक आसान क्लिक करके डिप्लॉय करें सुविधा लॉन्च की है. इसमें Redis क्लस्टर एक विकल्प है. इसलिए, यह एक बहुत ही दिलचस्प विकल्प होता.
आखिरकार, हमें Google Cloud SQL मिला और हमने रिलेशनशिप को MySQL में ले गए. इसमें काफ़ी काम करना पड़ा, लेकिन आखिर में यह बहुत अच्छा काम कर गया. अपडेट अब पूरी तरह से एटॉमिक हैं और टेस्ट अब भी पास हो रहे हैं. इससे मैच बनाने और स्कोर को ज़्यादा भरोसेमंद तरीके से लागू करने में भी मदद मिली.
समय के साथ, ज़्यादातर डेटा धीरे-धीरे NDB और memcache से SQL पर चला गया है. हालांकि, आम तौर पर खिलाड़ी, बैटलग्राउंड, और रूम की इकाइयां अब भी NDB में सेव की जाती हैं. वहीं, इन सभी के बीच के सेशन और संबंधों को SQL में सेव किया जाता है.
हमें यह भी ट्रैक करना था कि कौन किसके साथ खेल रहा है. साथ ही, खिलाड़ियों को एक-दूसरे के साथ जोड़ने के लिए, मैचिंग मैकेनिज्म का इस्तेमाल करना था. इस मैकेनिज्म में, खिलाड़ियों के कौशल के लेवल और अनुभव को ध्यान में रखा जाता था. हमने मैच बनाने की सुविधा को ओपन-सोर्स लाइब्रेरी Glicko2 पर आधारित किया है.
यह एक मल्टी-प्लेयर गेम है. इसलिए, हम कमरे में मौजूद अन्य खिलाड़ियों को “कौन शामिल हुआ या बाहर हुआ”, “कौन जीता या हारा” जैसे इवेंट के बारे में बताना चाहते हैं. साथ ही, यह भी बताना चाहते हैं कि कोई चैलेंज स्वीकार किया जा सकता है या नहीं. इसे मैनेज करने के लिए, हमने Player Management API में सूचनाएं पाने की सुविधा जोड़ी है.
WebRTC सेट अप करना
जब दो खिलाड़ियों को एक-दूसरे के साथ बैटल के लिए जोड़ा जाता है, तो सिग्नल सेवा का इस्तेमाल किया जाता है. इससे, दोनों खिलाड़ी एक-दूसरे से बात कर पाते हैं और एक-दूसरे से कनेक्ट हो पाते हैं.
सिग्नल सेवा के लिए, तीसरे पक्ष की कई लाइब्रेरी का इस्तेमाल किया जा सकता है. इनसे WebRTC को सेट अप करना भी आसान हो जाता है. इसके कुछ विकल्प हैं: PeerJS, SimpleWebRTC, और PubNub WebRTC SDK. PubNub, होस्ट किए गए सर्वर के समाधान का इस्तेमाल करता है. इस प्रोजेक्ट के लिए, हम Google Cloud Platform पर होस्ट करना चाहते थे. दूसरी दो लाइब्रेरी, node.js सर्वर का इस्तेमाल करती हैं. इन्हें Google Compute Engine पर इंस्टॉल किया जा सकता था. हालांकि, हमें यह भी पक्का करना होता कि ये एक साथ हजारों उपयोगकर्ताओं को हैंडल कर सकें. हमें पहले से पता था कि Channel API ऐसा कर सकता है.
इस मामले में, Google Cloud Platform का इस्तेमाल करने का एक मुख्य फ़ायदा स्केलिंग है. Google Developers Console की मदद से, AppEngine प्रोजेक्ट के लिए ज़रूरी संसाधनों को आसानी से स्केल किया जा सकता है. साथ ही, Channels API का इस्तेमाल करते समय, सिग्नल भेजने और पाने की सेवा को स्केल करने के लिए, कोई अतिरिक्त काम करने की ज़रूरत नहीं होती.
हमें लैटेंसी और Channels API के काम करने के तरीके को लेकर कुछ चिंताएं थीं. हालांकि, हमने पहले CubeSlam प्रोजेक्ट के लिए इसका इस्तेमाल किया था और उस प्रोजेक्ट में यह लाखों उपयोगकर्ताओं के लिए काम कर चुका था. इसलिए, हमने इसका फिर से इस्तेमाल करने का फ़ैसला लिया.
हमने WebRTC के लिए, तीसरे पक्ष की लाइब्रेरी का इस्तेमाल नहीं किया. इसलिए, हमें खुद की लाइब्रेरी बनानी पड़ी. अच्छी बात यह है कि हमने CubeSlam प्रोजेक्ट के लिए किए गए बहुत से काम का फिर से इस्तेमाल किया. जब दोनों खिलाड़ी किसी सेशन में शामिल हो जाते हैं, तो सेशन को “चालू” पर सेट कर दिया जाता है. इसके बाद, दोनों खिलाड़ी Channel API की मदद से, पीयर-टू-पीयर कनेक्शन शुरू करने के लिए, उस चालू सेशन आईडी का इस्तेमाल करेंगे. इसके बाद, दोनों खिलाड़ियों के बीच की सारी बातचीत RTCDataChannel के ज़रिए मैनेज की जाएगी.
कनेक्शन बनाने और एनएटी और फ़ायरवॉल से निपटने के लिए, हमें STUN और TURN सर्वर की भी ज़रूरत होती है. WebRTC को सेट अप करने के बारे में ज़्यादा जानने के लिए, HTML5 Rocks के लेख असल दुनिया में WebRTC: STUN, TURN, और सिग्नल पढ़ें.
इस्तेमाल किए जाने वाले TURN सर्वर की संख्या भी ट्रैफ़िक के हिसाब से बढ़नी चाहिए. इसे ठीक करने के लिए, हमने Google Deployment Manager की जांच की. इसकी मदद से, Google Compute Engine पर संसाधनों को डाइनैमिक तौर पर डिप्लॉय किया जा सकता है. साथ ही, टेंप्लेट का इस्तेमाल करके TURN सर्वर इंस्टॉल किए जा सकते हैं. यह अब भी अल्फा वर्शन में है, लेकिन हमारे काम के लिए यह बिना किसी रुकावट के काम कर रहा है. TURN सर्वर के लिए, हम coturn का इस्तेमाल करते हैं. यह STUN/TURN को लागू करने का एक तेज़, असरदार, और भरोसेमंद तरीका है.
Channel API
Channel API का इस्तेमाल, क्लाइंट साइड पर गेम रूम में और उससे सभी कम्यूनिकेशन भेजने के लिए किया जाता है. हमारा Player Management 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 के ज़रिए दूसरे पीयर को ऐक्शन-कमांड का कॉन्फ़िगरेशन भेजना है और सिम्युलेशन को एआई-प्लेयर की तरह काम करने देना है. इसके अलावा, यह तय करने के लिए लॉजिक भी है कि किस खिलाड़ी की बारी है. साथ ही, अगर खिलाड़ी पास/अटैक बटन दबाता है, तो निर्देशों को सूची में जोड़ने की सुविधा भी है. यह सुविधा तब काम करती है, जब खिलाड़ी पिछले एनीमेशन को देख रहा हो.
अगर सिर्फ़ दो उपयोगकर्ताओं के बीच बारी-बारी से खेला जा रहा था, तो दोनों उपयोगकर्ता अपनी बारी खत्म होने पर, अपने विरोधी को बारी देने की ज़िम्मेदारी शेयर कर सकते थे. हालांकि, अब तीसरे खिलाड़ी की वजह से ऐसा नहीं किया जा सकता. जब हमें स्पाइडर और ट्रोल जैसे दुश्मनों को जोड़ना था, तब एआई-सिस्टम फिर से काम का साबित हुआ. यह सिर्फ़ टेस्टिंग के लिए ही नहीं, बल्कि अन्य कामों के लिए भी मददगार साबित हुआ. टर्न-आधारित फ़्लो में उन्हें फ़िट करने के लिए, उन्हें दोनों तरफ़ एक ही तरह से स्पॉन और लागू करना पड़ा. इस समस्या को हल करने के लिए, एक पीयर को टर्न-सिस्टम को कंट्रोल करने और रिमोट पीयर को मौजूदा स्टेटस भेजने की अनुमति दी गई. इसके बाद, जब स्पाइडर की बारी आती है, तो टर्न मैनेजर, एआई-सिस्टम को एक ऐसा निर्देश बनाने देता है जो रिमोट उपयोगकर्ता को भेजा जाता है. गेम-इंजन सिर्फ़ निर्देशों और इकाई-आईडी पर काम कर रहा है, इसलिए गेम को दोनों तरफ़ एक जैसा सिम्युलेट किया जाएगा. सभी यूनिट में एआई-कॉम्पोनेंट भी हो सकता है, जो अपने-आप टेस्टिंग की सुविधा को आसान बनाता है.
गेम लॉजिक पर फ़ोकस करते हुए, डेवलपमेंट की शुरुआत में आसान कैनवस-रेंडरर का इस्तेमाल करना सबसे सही था. हालांकि, असल मज़ा तब आया, जब 3D वर्शन लागू किया गया और एनवायरमेंट और ऐनिमेशन के साथ सीन ज़िंदा हो गए. हम 3D-इंजन के तौर पर three.js का इस्तेमाल करते हैं. इसकी बनावट की वजह से, इसे आसानी से चलाया जा सकता है.
माउस की पोज़िशन, रिमोट उपयोगकर्ता को ज़्यादा बार भेजी जाती है. साथ ही, 3D-लाइट की मदद से यह भी बताया जाता है कि कर्सर फ़िलहाल कहां है.