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

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

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

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

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

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

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

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

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

PNG स्प्राइट में सभी नौ स्पेस दिख रहे हैं.
PNG स्प्राइट में नौ खाली जगहें दिख रही हैं.

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

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 इफ़ेक्ट लागू करते हैं (रिफ़्लेक्शन):

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

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

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

फ़ज़ी लॉजिक

हम ब्राउज़र विंडो का किसी भी साइज़ का पूरा इस्तेमाल करना चाहते थे - और स्क्रोल करने से बचना चाहते थे. यह Flash में आसान ऑपरेशन था, क्योंकि पूरा गेम वेक्टर से बनाया गया था और उसे बिना किसी फ़िडेलिटी के ऊपर या नीचे की ओर बढ़ाया या घटाया जा सकता है. हालांकि, एचटीएमएल में यह ज़्यादा मुश्किल था. हमने सीएसएस स्केलिंग का इस्तेमाल करने की कोशिश की, लेकिन आखिर में हमें धुंधला कैनवस मिला:

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

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

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 ऑडियो को फ़ॉलबैक के तौर पर इस्तेमाल करने का फ़ैसला किया. यह रहा Flash में बेसिक कोड:

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 में, हम एम्बेड किए गए फ़्लैश प्लेयर का पता लगाने की कोशिश करते हैं. अगर यह काम नहीं करता, तो हम हर साउंड फ़ाइल के लिए एक <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 के साथ काम करने की कोशिश नहीं की. हमें उम्मीद है कि आने वाले समय में यह इंडस्ट्री सिर्फ़ एक फ़ॉर्मैट पर काम करेगी.

मतदान की स्थिति

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

क्या टूल है!

हमने फ़्रंट-एंड यूज़र इंटरफ़ेस और बैक-एंड कंट्रोल लॉजिक (पुष्टि करना, पुष्टि करना, परसिस्टेंस वगैरह) दोनों को डेवलप करने के लिए, Google Web Toolkit (GWT) का इस्तेमाल किया. JavaScript को ही Java सोर्स कोड से कंपाइल किया जाता है. उदाहरण के लिए, पॉइंट फ़ंक्शन को 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 हमारी उम्मीदों से कहीं ज़्यादा बेहतर है. वर्डिको न सिर्फ़ Flash जितना अच्छा दिखता है, उतना ही यह तरल और जवाबदेह भी है. हम कैनवस और CSS3 के बिना यह काम नहीं कर पाते. हमारी अगली चुनौती: Wordico को मोबाइल पर इस्तेमाल करने के लिए अपनाना.