Hobbit Experience में WebRTC गेमप्ले जोड़ना
हमने पिछले साल के Chrome एक्सपेरिमेंट, मिडल-अर्थ की यात्रा को कुछ नए कॉन्टेंट के साथ पेश किया है. ऐसा, हॉबिट की नई फ़िल्म “द हॉबिट: द बैटल ऑफ़ द फ़ाइव आर्मीज़” के रिलीज़ होने से पहले किया गया है. इस बार, WebGL का इस्तेमाल बढ़ाने पर फ़ोकस किया गया है, ताकि ज़्यादा ब्राउज़र और डिवाइसों पर कॉन्टेंट देखा जा सके. साथ ही, Chrome और Firefox में WebRTC की सुविधाओं के साथ काम किया जा सके. इस साल के एक्सपेरिमेंट के लिए, हमारे तीन लक्ष्य थे:
- Android के लिए Chrome पर, WebRTC और WebGL का इस्तेमाल करके पी2पी गेमप्ले
- ऐसा मल्टीप्लेयर गेम बनाएं जो आसानी से खेला जा सके और टच इनपुट पर आधारित हो
- Google Cloud Platform पर होस्ट
गेम की जानकारी
इस गेम का लॉजिक, ग्रिड-आधारित सेटअप पर आधारित होता है. इसमें सैनिक, गेम बोर्ड पर चलते हैं. इससे, नियम तय करते समय हमें कागज़ पर गेमप्ले को आज़माने में आसानी हुई. ग्रिड-आधारित सेटअप का इस्तेमाल करने से, गेम में टक्कर का पता लगाने में भी मदद मिलती है. इससे गेम की परफ़ॉर्मेंस बेहतर बनी रहती है, क्योंकि आपको सिर्फ़ एक ही या आस-पास की टाइल में मौजूद ऑब्जेक्ट से होने वाली टक्कर की जांच करनी होती है. हमें शुरू से ही पता था कि हमें नए गेम में, मिडल-अर्थ की चार मुख्य सेनाओं, मनुष्यों, बौने, एल्फ़, और ऑर्क के बीच की लड़ाई पर फ़ोकस करना है. यह गेम इतना आसान होना चाहिए कि इसे Chrome एक्सपेरिमेंट में आसानी से खेला जा सके. साथ ही, इसमें सीखने के लिए ज़्यादा इंटरैक्शन न हों. हमने मिडल-अर्थ मैप पर पांच बैटलग्राउंड तय किए हैं. ये गेम-रूम के तौर पर काम करते हैं. यहां कई खिलाड़ी, पीयर-टू-पीयर बैटल में हिस्सा ले सकते हैं. मोबाइल स्क्रीन पर रूम में मौजूद कई खिलाड़ियों को दिखाना और उपयोगकर्ताओं को यह चुनने की सुविधा देना कि उन्हें किससे चैलेंज करना है, यह अपने-आप में एक चुनौती थी. बातचीत और सीन को आसान बनाने के लिए, हमने चैलेंज करने और स्वीकार करने के लिए सिर्फ़ एक बटन का इस्तेमाल किया. साथ ही, इवेंट दिखाने के लिए कमरे का इस्तेमाल किया और कहा कि अभी पहाड़ का राजा कौन है. इस दिशा-निर्देश से, मैच बनाने से जुड़ी कुछ समस्याएं भी हल हो गई हैं. साथ ही, हमें बैटल के लिए सबसे अच्छे खिलाड़ियों को मैच करने में मदद मिली है. Chrome पर Cube Slam गेम के साथ किए गए पिछले एक्सपेरिमेंट से हमें पता चला था कि मल्टीप्लेयर गेम में, लैटेंसी को मैनेज करने में काफ़ी मेहनत लगती है. ऐसा तब होता है, जब गेम का नतीजा लैटेंसी पर निर्भर हो. आपको लगातार यह अनुमान लगाना पड़ता है कि आपके प्रतिद्वंद्वी की स्थिति क्या होगी और वह आपको कहां समझता है. साथ ही, आपको यह अनुमान अलग-अलग डिवाइसों पर ऐनिमेशन के साथ सिंक करना होता है. इस लेख में इन चुनौतियों के बारे में ज़्यादा जानकारी दी गई है. इसे थोड़ा आसान बनाने के लिए, हमने इस गेम को टर्न-आधारित बनाया है.
गेम लॉजिक को ग्रिड पर आधारित सेटअप के आधार पर बनाया गया है. इसमें सेना, गेम बोर्ड पर एक जगह से दूसरी जगह जाती है. इससे हमारे लिए काग़ज़ पर गेमप्ले को आज़माना आसान हो गया, क्योंकि हम नियम तय कर रहे थे. ग्रिड-आधारित सेटअप का इस्तेमाल करने से, गेम में टक्कर का पता लगाने में भी मदद मिलती है. इससे गेम की परफ़ॉर्मेंस बेहतर बनी रहती है, क्योंकि आपको सिर्फ़ एक ही या आस-पास की टाइल में मौजूद ऑब्जेक्ट से होने वाली टक्कर का पता लगाना होता है.
गेम के हिस्से
एक से ज़्यादा खिलाड़ी वाले इस गेम को बनाने के लिए, हमें कुछ अहम हिस्से बनाने होंगे:
- सर्वर साइड प्लेयर मैनेजमेंट एपीआई, उपयोगकर्ताओं, मैच बनाने, सेशन, और गेम के आंकड़ों को मैनेज करता है.
- ऐसे सर्वर जो खिलाड़ियों के बीच कनेक्शन बनाने में मदद करते हैं.
- यह AppEngine Channels API सिग्नल को मैनेज करने वाला एपीआई है जिसका इस्तेमाल गेम रूम में सभी खिलाड़ियों से कनेक्ट करने और उनसे बातचीत करने के लिए किया जाता है.
- JavaScript गेम इंजन, जो दो खिलाड़ियों/पीयर के बीच स्टेटस और आरटीसी मैसेजिंग को सिंक करता है.
- WebGL गेम व्यू.
प्लेयर मैनेजमेंट
ज़्यादा से ज़्यादा खिलाड़ियों को गेम खेलने की सुविधा देने के लिए, हम हर Battleground के लिए कई गेम-रूम का इस्तेमाल करते हैं. हर गेम-रूम में खिलाड़ियों की संख्या सीमित रखने की मुख्य वजह यह है कि नए खिलाड़ी कम समय में लीडरबोर्ड में सबसे ऊपर पहुंच सकें. यह सीमा, Channel API के ज़रिए भेजे गए गेम-रूम की जानकारी देने वाले JSON ऑब्जेक्ट के साइज़ से भी जुड़ी है. इस ऑब्जेक्ट का साइज़ 32 केबी से ज़्यादा नहीं होना चाहिए. हमें गेम में खिलाड़ियों, रूम, स्कोर, सेशन, और उनके बीच के संबंधों की जानकारी सेव करनी होती है. इसके लिए, हमने सबसे पहले इकाइयों के लिए NDB का इस्तेमाल किया और रिलेशनशिप से जुड़ी समस्याओं को हल करने के लिए क्वेरी इंटरफ़ेस का इस्तेमाल किया. NDB, Google Cloud Datastore का एक इंटरफ़ेस है. शुरुआत में एनडीबी का इस्तेमाल करना काफ़ी कारगर साबित हुआ, लेकिन जल्द ही हमें इसके इस्तेमाल में एक समस्या का सामना करना पड़ा. क्वेरी, डेटाबेस के "कमिट किए गए" वर्शन के हिसाब से चलाई गई थी. इस लेख में, 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 मेमकैश में, “तुलना करें और सेट करें” सुविधा का इस्तेमाल करके, कुंजियों के लिए लेन-देन जैसा सिस्टम है. इसलिए, अब टेस्ट फिर से पास हो गए हैं.
माफ़ करें, मेमकैश सभी इंद्रधनुष और यूनिकॉर्न नहीं होते. हालांकि, इसकी कुछ सीमाएं होती हैं. इनमें सबसे ज़्यादा ध्यान जाता है, 1 एमबी का साइज़ (युद्ध के मैदान से जुड़े ज़्यादा कमरे नहीं हो सकते) और कुंजी की समयसीमा, जैसा कि दस्तावेज़ में बताया गया है:
हमने की-वैल्यू वाले एक अन्य शानदार स्टोर, Redis के इस्तेमाल के बारे में सोचा. हालांकि, उस समय स्केलेबल क्लस्टर सेट अप करना थोड़ा मुश्किल था. साथ ही, हम सर्वर को मैनेज करने के बजाय, बेहतर अनुभव देने पर फ़ोकस करना चाहते थे. इसलिए, हमने इस तरीके को अपनाने का फ़ैसला नहीं लिया. वहीं दूसरी ओर, Google Cloud Platform ने हाल ही में क्लिक-टू-डिप्लॉय की एक आसान सुविधा रिलीज़ की है. इसमें Redis Cluster एक विकल्प है. यह बहुत दिलचस्प विकल्प होता.
आखिर में, हमें 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 पर हैंडल किया जाएगा.
कनेक्शन स्थापित करने और NAT और फ़ायरवॉल से निपटने के लिए, हमें STUN और टर्न सर्वर की भी आवश्यकता होती है. WebRTC को सेट अप करने के बारे में ज़्यादा जानने के लिए, HTML5 Rocks के लेख असल दुनिया में WebRTC: STUN, 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 के ज़रिए एक साथी को भेजना है, ताकि सिम्युलेशन को इस तरह से काम करने दिया जाए जैसे कि यह कोई AI-प्लेयर हो. इसके अलावा, यह तय करने के लिए लॉजिक भी है कि किस खिलाड़ी की बारी है. अगर खिलाड़ी पास/अटैक बटन दबाता है, तो पिछले एनीमेशन को देखते समय आने वाले निर्देशों को कतार में लगाता है वगैरह.
अगर सिर्फ़ दो उपयोगकर्ताओं की बारी आती है, तो अपनी बारी आने के बाद दूसरे खिलाड़ियों को बारी-बारी से भेजने की ज़िम्मेदारी दोनों ही साथ देने वालों की होती है, लेकिन इसमें कोई तीसरा खिलाड़ी भी शामिल होता है. जब हमें स्पाइडर और ट्रोल जैसे दुश्मनों को जोड़ना था, तब एआई-सिस्टम फिर से काम का साबित हुआ. यह सिर्फ़ टेस्टिंग के लिए ही नहीं, बल्कि अन्य कामों के लिए भी काम का साबित हुआ. इन्हें बारी-बारी से प्रोसेस करने के लिए, दोनों तरफ़ एक ही तरीके से बनाया और एक्ज़ीक्यूट किया जाना चाहिए. इस समस्या को हल करने के लिए, एक पीयर को टर्न-सिस्टम को कंट्रोल करने और रिमोट पीयर को मौजूदा स्टेटस भेजने की अनुमति दी गई. इसके बाद, जब स्पाइडर की बारी आती है, तो टर्न मैनेजर, एआई-सिस्टम को एक ऐसा निर्देश बनाने देता है जो रिमोट उपयोगकर्ता को भेजा जाता है. गेम-इंजन सिर्फ़ निर्देशों और इकाई-आईडी पर काम कर रहा है, इसलिए गेम दोनों तरफ़ एक जैसा सिम्युलेट किया जाएगा. सभी यूनिट में एआई-कॉम्पोनेंट भी हो सकता है, जो अपने-आप टेस्टिंग की सुविधा को आसान बनाता है.
गेम लॉजिक पर फ़ोकस करते हुए, डेवलपमेंट की शुरुआत में आसान कैनवस-रेंडरर का इस्तेमाल करना सबसे सही था. हालांकि, असल मज़ा तब आया, जब 3D वर्शन लागू किया गया और एनवायरमेंट और ऐनिमेशन के साथ सीन ज़िंदा हो गए. हम 3D-इंजन के तौर पर three.js का इस्तेमाल करते हैं. इसकी बनावट की वजह से, इसे आसानी से चलाया जा सकता है.
माउस की पोज़िशन, रिमोट उपयोगकर्ता को ज़्यादा बार भेजी जाती है. साथ ही, 3D-लाइट की मदद से यह भी बताया जाता है कि कर्सर फ़िलहाल कहां है.