केस स्टडी - Wordico को Flash से HTML5 में बदलना

परिचय

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

विक्टर, आपका वेक्टर क्या है?

हमने Wordico के ओरिजनल वर्शन को सिर्फ़ वेक्टर ग्राफ़िक का इस्तेमाल करके बनाया है: लाइनें, कर्व, भरने की सुविधा, और ग्रेडिएंट. इसका नतीजा यह हुआ कि यह मॉडल काफ़ी छोटा और स्केलेबल भी था:

Wordico वायरफ़्रेम
Flash में, हर डिसप्ले ऑब्जेक्ट वेक्टर आकार से बना होता था.

हमने एक से ज़्यादा स्टेटस वाले ऑब्जेक्ट बनाने के लिए, फ़्लैश टाइमलाइन का भी फ़ायदा लिया. उदाहरण के लिए, हमने Space ऑब्जेक्ट के लिए, नाम वाले नौ मुख्य फ़्रेम का इस्तेमाल किया:

Flash में तीन अक्षरों वाला स्पेस.
Flash में तीन अक्षरों वाला स्पेस.

हालांकि, HTML5 में हम बिटमैप किए गए स्प्राइट का इस्तेमाल करते हैं:

नौ स्पेस दिखाने वाला PNG स्प्राइट.
नौ स्पेस दिखाने वाला PNG स्प्राइट.

अलग-अलग स्पेस से 15x15 गेमबोर्ड बनाने के लिए, हम 225 वर्णों की स्ट्रिंग नोटेशन पर बार-बार काम करते हैं. इसमें हर स्पेस को किसी अलग वर्ण से दिखाया जाता है. जैसे, तीन अक्षर के लिए "t" और तीन शब्दों के लिए "T". फ़्लैश में यह आसानी से किया जा सकता था. हमने सिर्फ़ स्पेस को स्टैंप किया और उन्हें ग्रिड में व्यवस्थित किया:

var spaces:Array = new Array();

for (var i:int = 0; i < 225; i++) {
  var space:Space = new Space(i, layout.charAt(i));
  ...
  spaces.push(addChild(space));
}

LayoutUtil.grid(spaces, 15);

HTML5 में, यह थोड़ा ज़्यादा मुश्किल है. हम गेमबोर्ड को एक-एक स्क्वेयर करके पेंट करने के लिए, बिटमैप ड्रॉइंग की सतह वाले <canvas> एलिमेंट का इस्तेमाल करते हैं. सबसे पहले, इमेज स्प्राइट को लोड करना होगा. लोड होने के बाद, हम लेआउट नोटेशन को दोहराते हैं. हर बार दोहराने पर, इमेज का एक अलग हिस्सा ड्रॉ किया जाता है:

var x = 0;  // x coordinate
var y = 0;  // y coordinate
var w = 35; // width and height of a space

for (var i = 0; i < 225; i++) {
  if (i && i % 15 == 0) {
    x = 0;
    y += w;
  }

  var imageX = "_dDFtTqQxm".indexOf(layout.charAt(i)) * 70;

  canvas.drawImage("spaces.png", imageX, 0, 70, 70, x, y, w, w);

  x += w;
}

वेब ब्राउज़र में दिखने वाला नतीजा यहां दिया गया है. ध्यान दें कि कैनवस में भी सीएसएस ड्रॉप शैडो है:

HTML5 में, गेमबोर्ड एक कैनवस एलिमेंट होता है.
HTML5 में, गेमबोर्ड एक कैनवस एलिमेंट होता है.

टाइल ऑब्जेक्ट को बदलना भी इसी तरह का काम था. Flash में, हमने टेक्स्ट फ़ील्ड और वेक्टर आकार का इस्तेमाल किया:

फ़्लैश टाइल, टेक्स्ट फ़ील्ड और वेक्टर आकार का कॉम्बिनेशन थी
फ़्लैश टाइल, टेक्स्ट फ़ील्ड और वेक्टर आकृतियों का कॉम्बिनेशन थी.

HTML5 में, हम रनटाइम के दौरान एक ही <canvas> एलिमेंट पर तीन इमेज स्प्राइट को जोड़ते हैं:

एचटीएमएल टाइल, तीन इमेज का कंपोज़िट है.
एचटीएमएल टाइल, तीन इमेज का कॉम्बिनेशन होती है.

अब हमारे पास 100 कैनवस (हर टाइल के लिए एक) और गेमबोर्ड के लिए एक कैनवस है. यहां "H" टाइल के लिए मार्कअप दिया गया है:

<canvas width="35" height="35" class="tile tile-racked" title="H-2"/>

यहां उससे जुड़ी सीएसएस दी गई है:

.tile {
  width: 35px;
  height: 35px;
  position: absolute;
  cursor: pointer;
  z-index: 1000;
}

.tile-drag {
  -moz-box-shadow: 1px 1px 7px rgba(0,0,0,0.8);
  -webkit-box-shadow: 1px 1px 7px rgba(0,0,0,0.8);
  -moz-transform: scale(1.10);
  -webkit-transform: scale(1.10);
  -webkit-box-reflect: 0px;
  opacity: 0.85;
}

.tile-locked {
  cursor: default;
}

.tile-racked {
  -webkit-box-reflect: below 0px -webkit-gradient(linear, 0% 0%, 0% 100%,  
    from(transparent), color-stop(0.70, transparent), to(white));
}

हम टाइल को खींचने (शैडो, ओपैसिटी, और स्केलिंग) और रैक पर होने पर (रिफ़्लेक्शन) CSS3 इफ़ेक्ट लागू करते हैं:

खींची गई टाइल थोड़ी बड़ी और थोड़ी पारदर्शी हो जाती है. साथ ही, उसमें ड्रॉप शैडो भी दिखता है.
खींची गई टाइल थोड़ी बड़ी और थोड़ी पारदर्शी हो जाती है. साथ ही, उस पर ड्रॉप शैडो दिखने लगता है.

रेस्टर इमेज का इस्तेमाल करने के कुछ फ़ायदे हैं. सबसे पहले, नतीजा पिक्सल के हिसाब से सटीक होता है. दूसरा, ब्राउज़र इन इमेज को कैश मेमोरी में सेव कर सकता है. तीसरा, थोड़े अतिरिक्त काम से, हम टाइल के नए डिज़ाइन बनाने के लिए इमेज बदल सकते हैं. जैसे, मेटल टाइल. साथ ही, यह डिज़ाइन वाला काम, Flash के बजाय Photoshop में किया जा सकता है.

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

फ़ज़ी लॉजिक

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

सीएसएस स्केलिंग (बाईं ओर) बनाम फिर से ड्रॉ करना (दाईं ओर).
सीएसएस स्केलिंग (बाईं ओर) बनाम फिर से ड्रॉ करना (दाईं ओर).

हमारा सुझाव है कि जब भी उपयोगकर्ता ब्राउज़र का साइज़ बदले, तब गेमबोर्ड, रैक, और टाइल को फिर से ड्रॉ करें:

window.onresize = function (evt) {
...
gameboard.setConstraints(boardWidth, boardWidth);

...
rack.setConstraints(rackWidth, rackHeight);

...
tileManager.resizeTiles(tileSize);
});

इस तरह, हमें किसी भी स्क्रीन साइज़ पर बेहतरीन इमेज और आकर्षक लेआउट मिलते हैं:

गेमबोर्ड, वर्टिकल स्पेस को भरता है; पेज के अन्य एलिमेंट उसके आस-पास फ़्लो करते हैं.
गेमबोर्ड, वर्टिकल स्पेस को भरता है; पेज के अन्य एलिमेंट उसके आस-पास फ़्लो करते हैं.

सीधे मुद्दे पर आएं

हर टाइल को सटीक तौर पर पोज़िशन किया जाता है और उसे गेमबोर्ड और रैक के साथ सटीक तौर पर अलाइन करना होता है. इसलिए, हमें पोज़िशनिंग के लिए भरोसेमंद सिस्टम की ज़रूरत होती है. हम ग्लोबल स्पेस (एचटीएमएल पेज) में एलिमेंट की जगह को मैनेज करने के लिए, Bounds और Point, दो फ़ंक्शन का इस्तेमाल करते हैं. Bounds, पेज पर मौजूद रेक्टैंगल आकार के एरिया के बारे में बताता है. वहीं, Point, पेज के सबसे ऊपर बाएं कोने (0,0) से x,y निर्देशांक के बारे में बताता है. इसे रजिस्ट्रेशन पॉइंट भी कहा जाता है.

Bounds की मदद से, हम दो रेक्टैंगल एलिमेंट के इंटरसेक्शन का पता लगा सकते हैं. जैसे, जब कोई टाइल रैक को पार करती है. इसके अलावा, यह भी पता लगाया जा सकता है कि रेक्टैंगल एरिया (जैसे, डबल-लेटर स्पेस) में कोई मनमुताबिक पॉइंट (जैसे, टाइल का सेंटर पॉइंट) है या नहीं. यहां बॉउंड लागू करने का तरीका बताया गया है:

// bounds.js
function Bounds(element) {
var x = element.offsetLeft;
var y = element.offsetTop;
var w = element.offsetWidth;
var h = element.offsetHeight;

this.left = x;
this.right = x + w;
this.top = y;
this.bottom = y + h;
this.width = w;
this.height = h;
this.x = x;
this.y = y;
this.midx = x + (w / 2);
this.midy = y + (h / 2);
this.topleft = new Point(x, y);
this.topright = new Point(x + w, y);
this.bottomleft = new Point(x, y + h);
this.bottomright = new Point(x + w, y + h);
this.middle = new Point(x + (w / 2), y + (h / 2));
}

Bounds.prototype.contains = function (point) {
return point.x > this.left &amp;&amp;
point.x < this.right &amp;&amp;
point.y > this.top &amp;&amp;
point.y < this.bottom;
}

Bounds.prototype.intersects = function (bounds) {
return this.contains(bounds.topleft) ||
this.contains(bounds.topright) ||
this.contains(bounds.bottomleft) ||
this.contains(bounds.bottomright) ||
bounds.contains(this.topleft) ||
bounds.contains(this.topright) ||
bounds.contains(this.bottomleft) ||
bounds.contains(this.bottomright);
}

Bounds.prototype.toString = function () {
return [this.x, this.y, this.width, this.height].join(",");
}

हम पेज पर मौजूद किसी भी एलिमेंट या माउस इवेंट के सटीक निर्देशांक (ऊपर बाएं कोने) का पता लगाने के लिए, Point का इस्तेमाल करते हैं. Point में दूरी और दिशा का हिसाब लगाने के तरीके भी शामिल हैं. ये ऐनिमेशन इफ़ेक्ट बनाने के लिए ज़रूरी हैं. Point को लागू करने का तरीका यहां बताया गया है:

// point.js

function Point(x, y) {
this.x = x;
this.y = y;
}

Point.prototype.distance = function (point) {
var a = point.x - this.x;
var b = point.y - this.y;

return Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2));
}

Point.prototype.distanceX = function (point) {
return Math.abs(this.x - point.x);
}

Point.prototype.distanceY = function (point) {
return Math.abs(this.y - point.y);
}

Point.prototype.interpolate = function (point, pct) {
var x = this.x + ((point.x - this.x) * pct);
var y = this.y + ((point.y - this.y) * pct);

return new Point(x, y);
}

Point.prototype.offset = function (x, y) {
return new Point(this.x + x, this.y + y);
}

Point.prototype.vector = function (point) {
return new Point(point.x - this.x, point.y - this.y);
}

Point.prototype.toString = function () {
return this.x + "," + this.y;
}

// static
Point.fromElement = function (element) {
return new Point(element.offsetLeft, element.offsetTop);
}

// static
Point.fromEvent = function (evt) {
return new Point(evt.x || evt.clientX, evt.y || evt.clientY);
}

ये फ़ंक्शन, खींचकर छोड़ने और ऐनिमेशन की सुविधाओं का आधार हैं. उदाहरण के लिए, हम Bounds.intersects() का इस्तेमाल करके यह तय करते हैं कि कोई टाइल, गेमबोर्ड पर मौजूद किसी स्पेस को ओवरलैप करती है या नहीं. हम Point.vector() का इस्तेमाल करके, खींची गई टाइल की दिशा तय करते हैं. साथ ही, मोशन ट्वीन या आसानी से होने वाले बदलाव का इफ़ेक्ट बनाने के लिए, टाइमर के साथ Point.interpolate() का इस्तेमाल करते हैं.

हवा के रुख के साथ चलने वाला

फ़्लैश में, तय साइज़ के लेआउट आसानी से बनाए जा सकते हैं. वहीं, एचटीएमएल और सीएसएस बॉक्स मॉडल की मदद से, फ़्ल्यूइड लेआउट जनरेट करना काफ़ी आसान है. नीचे दिए गए ग्रिड व्यू की चौड़ाई और ऊंचाई अलग-अलग हो सकती है:

इस लेआउट का कोई तय डाइमेंशन नहीं होता: थंबनेल, बाएं से दाएं और ऊपर से नीचे की ओर फ़्लो करते हैं.
इस लेआउट के डाइमेंशन तय नहीं होते: थंबनेल, बाएं से दाएं और ऊपर से नीचे की ओर फ़्लो करते हैं.

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

Flash में चैट पैनल अच्छा था, लेकिन जटिल था.
Flash में चैट पैनल अच्छा था, लेकिन जटिल था.

तुलना करने पर, एचटीएमएल वर्शन सिर्फ़ एक <div> है, जिसकी ऊंचाई तय होती है और ओवरफ़्लो प्रॉपर्टी 'छिपाया गया' पर सेट होती है. स्क्रोल करने पर हमें कोई शुल्क नहीं देना पड़ता.

सीएसएस बॉक्स मॉडल काम करता हुआ.
सीएसएस बॉक्स मॉडल के काम करने का तरीका.

इस तरह के मामलों में - सामान्य लेआउट टास्क - एचटीएमएल और सीएसएस, फ़्लैश से बेहतर होते हैं.

क्या अब मुझे सुना जा सकता है?

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

आखिर में, हमने अपना Flash ऑडियो प्लेयर रोल आउट करने का फ़ैसला लिया और फ़ॉलबैक के तौर पर HTML5 ऑडियो का इस्तेमाल किया. फ़्लैश में बुनियादी कोड यहां दिया गया है:

var sounds = new Array();

function playSound(path:String):void {
var sound:Sound = sounds[path];

if (sound == null) {
sound = new Sound();
sound.addEventListener(Event.COMPLETE, function (evt:Event) {
    sound.play();
});
sound.load(new URLRequest(path));
sounds[path] = sound;
}
else {
sound.play();
}
}

ExternalInterface.addCallback("playSound", playSound);

JavaScript में, हम एम्बेड किए गए Flash प्लेयर का पता लगाने की कोशिश करते हैं. अगर ऐसा नहीं होता है, तो हम हर साउंड फ़ाइल के लिए एक <audio> नोड बनाते हैं:

function play(String soundId) {
var src = "/audio/" + soundId + ".mp3";

// Flash
try {
var swf = window["swfplayer"] || document["swfplayer"];
swf.playSound(src);
}
// or HTML5 audio
catch (e) {
var sound = document.getElementById(soundId);
if (sound == null || sound == undefined) {
    var sound = document.createElement("audio");
    sound.id = soundId;
    sound.src = src;
    document.body.appendChild(sound);
}
sound.play();
}
}

ध्यान दें कि यह सुविधा सिर्फ़ MP3 फ़ाइलों के लिए काम करती है. हमने OGG फ़ाइलों के लिए इसे उपलब्ध नहीं कराया है. हमें उम्मीद है कि आने वाले समय में, इंडस्ट्री एक ही फ़ॉर्मैट पर सेटल हो जाएगी.

पोल की स्थिति

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

वाह, क्या टूल है!

हमने Google वेब टूलकिट (GWT) का इस्तेमाल, फ़्रंट-एंड यूज़र इंटरफ़ेस और बैक-एंड कंट्रोल लॉजिक, दोनों को डेवलप करने के लिए किया है. जैसे, पुष्टि करना, पुष्टि करना, बनाए रखना वगैरह. JavaScript को Java सोर्स कोड से कंपाइल किया जाता है. उदाहरण के लिए, Point फ़ंक्शन को Point.java से लिया गया है:

package com.wordico.client.view.layout;

import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.event.dom.client.DomEvent;

public class Point {
public double x;
public double y;

public Point(double x, double y) {
this.x = x;
this.y = y;
}

public double distance(Point point) {
double a = point.x - this.x;
double b = point.y - this.y;

return Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2));
}
...
}

कुछ यूज़र इंटरफ़ेस (यूआई) क्लास में, मिलती-जुलती टेंप्लेट फ़ाइलें होती हैं. इनमें पेज एलिमेंट, क्लास के सदस्यों से "बाउंड" होते हैं. उदाहरण के लिए, ChatPanel.ui.xml, ChatPanel.java से मेल खाता है:

<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">

<ui:UiBinder
xmlns:ui="urn:ui:com.google.gwt.uibinder"
xmlns:g="urn:import:com.google.gwt.user.client.ui"
xmlns:w="urn:import:com.wordico.client.view.widget">

<g:HTMLPanel>
<div class="palette">
<g:ScrollPanel ui:field="messagesScroll">
    <g:FlowPanel ui:field="messagesFlow"></g:FlowPanel>
</g:ScrollPanel>
<g:TextBox ui:field="chatInput"></g:TextBox>
</div>
</g:HTMLPanel>

</ui:UiBinder>

इस लेख में पूरी जानकारी नहीं दी गई है. हालांकि, हमारा सुझाव है कि अपने अगले HTML5 प्रोजेक्ट के लिए, GWT को आज़माएं.

Java का इस्तेमाल क्यों करना चाहिए? सबसे पहले, स्ट्रिक्ट टाइपिंग के लिए. डाइनैमिक टाइपिंग, JavaScript में काम की होती है. उदाहरण के लिए, किसी अरे में अलग-अलग तरह की वैल्यू हो सकती हैं. हालांकि, बड़े और जटिल प्रोजेक्ट में यह परेशानी का सबब बन सकती है. दूसरा, रीफ़ैक्टर करने की सुविधाओं के लिए. सोचें कि कोड की हज़ारों लाइनों में, JavaScript के किसी मेथड के हस्ताक्षर को कैसे बदला जा सकता है. यह आसान नहीं है! हालांकि, किसी अच्छे Java IDE की मदद से, यह काम आसानी से किया जा सकता है. आखिर में, टेस्टिंग के लिए. Java क्लास के लिए यूनिट टेस्ट लिखना, "सेव और रीफ़्रेश करें" की पुरानी तकनीक से बेहतर है.

खास जानकारी

ऑडियो से जुड़ी समस्याओं को छोड़कर, HTML5 ने हमारी उम्मीदों को काफ़ी बेहतर तरीके से पूरा किया. Wordico, फ़्लैश में दिखने जैसा ही अच्छा दिखता है. साथ ही, यह उतना ही आसान और रिस्पॉन्सिव है. हम Canvas और CSS3 के बिना ऐसा नहीं कर पाते. हमारा अगला चैलेंज: Wordico को मोबाइल पर इस्तेमाल करने के लिए अडैप्ट करना.