مطالعه موردی - تبدیل Wordico از Flash به HTML5

معرفی

وقتی بازی جدول کلمات متقاطع Wordico خود را از Flash به HTML5 تبدیل کردیم، اولین وظیفه ما این بود که همه چیزهایی را که در مورد ایجاد یک تجربه کاربری غنی در مرورگر می‌دانستیم، بیاموزیم. در حالی که Flash یک API واحد و جامع را برای همه جنبه‌های توسعه برنامه ارائه می‌کند - از ترسیم برداری گرفته تا تشخیص ضربه چند ضلعی تا تجزیه XML - HTML5 مجموعه‌ای از مشخصات را با پشتیبانی از مرورگرهای مختلف ارائه می‌دهد. ما همچنین تعجب کردیم که آیا HTML، یک زبان خاص سند، و CSS، یک زبان جعبه محور، برای ساخت یک بازی مناسب هستند یا خیر. آیا بازی به طور یکنواخت در مرورگرها نمایش داده می شود، همانطور که در فلش انجام می شود، و آیا ظاهر و رفتار خوبی دارد؟ برای Wordico، پاسخ مثبت بود.

بردار شما چیست ویکتور؟

ما نسخه اصلی Wordico را تنها با استفاده از گرافیک های برداری توسعه دادیم: خطوط، منحنی ها، پرها و گرادیان ها. نتیجه هم بسیار فشرده و هم بی نهایت مقیاس پذیر بود:

Wordico Wireframe
در Flash، هر شیء نمایشی از اشکال برداری ساخته شده بود.

ما همچنین از جدول زمانی Flash برای ایجاد اشیایی با چندین حالت استفاده کردیم. به عنوان مثال، ما از 9 فریم کلیدی با نام برای شی Space استفاده کردیم:

فضای سه حرفی در فلش.
فضای سه حرفی در فلش.

اما در HTML5 از یک sprite بیت مپ شده استفاده می کنیم:

یک sprite PNG که هر نه فاصله را نشان می‌دهد.
یک sprite PNG که هر نه فاصله را نشان می‌دهد.

برای ایجاد گیم‌بورد ۱۵×۱۵ از فضاهای جداگانه، روی یک نماد رشته ۲۲۵ نویسه‌ای تکرار می‌کنیم که در آن هر فاصله با یک کاراکتر متفاوت نشان داده می‌شود (مانند «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> ، یک سطح طراحی bitmap، برای رنگ آمیزی صفحه بازی به صورت مربعی در یک زمان استفاده می کنیم. اولین قدم بارگذاری تصویر اسپرایت است. هنگامی که بارگذاری شد، از طریق نماد طرح بندی تکرار می کنیم و با هر تکرار، بخش متفاوتی از تصویر را ترسیم می کنیم:

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;
}

در اینجا نتیجه در مرورگر وب است. توجه داشته باشید که خود بوم دارای یک سایه CSS است:

در HTML5، گیم‌بورد یک عنصر بوم واحد است.
در HTML5، گیم‌بورد یک عنصر بوم واحد است.

تبدیل شی کاشی تمرین مشابهی بود. در فلش، از فیلدهای متنی و اشکال برداری استفاده کردیم:

کاشی فلش ترکیبی از فیلدهای متنی و اشکال برداری بود
کاشی فلش ترکیبی از فیلدهای متنی و اشکال برداری بود.

در HTML5، سه sprite تصویر را در یک عنصر <canvas> در زمان اجرا ترکیب می‌کنیم:

کاشی HTML ترکیبی از سه تصویر است.
کاشی HTML ترکیبی از سه تصویر است.

اکنون 100 بوم (یکی برای هر کاشی) به اضافه یک بوم برای گیم برد داریم. در اینجا نشانه گذاری برای کاشی "H" آمده است:

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

در اینجا CSS مربوطه است:

.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، این یک عملیات ساده برای تغییر رنگ یا سایر خصوصیات نوع بود. در HTML5، این ویژگی ها در خود تصاویر قرار می گیرند. (ما متن HTML را امتحان کردیم، اما به نشانه‌گذاری و CSS زیادی نیاز داشت. متن بوم را نیز امتحان کردیم، اما نتایج در بین مرورگرها ناسازگار بود.)

منطق فازی

ما می خواستیم از پنجره مرورگر در هر اندازه ای استفاده کامل کنیم - و از پیمایش اجتناب کنیم. این یک عملیات نسبتا ساده در فلش بود، زیرا کل بازی به صورت بردار ترسیم شده بود و می‌توانست بدون از دست دادن وفاداری، آن را کوچک یا بزرگ کند. اما در HTML پیچیده تر بود. ما سعی کردیم از مقیاس‌بندی CSS استفاده کنیم، اما در نهایت با یک بوم تار مواجه شدیم:

مقیاس بندی CSS (چپ) در مقابل ترسیم مجدد (راست).
مقیاس بندی CSS (چپ) در مقابل ترسیم مجدد (راست).

راه حل ما این است که هر زمان که کاربر مرورگر را تغییر اندازه داد، گیم برد، قفسه و کاشی ها را دوباره ترسیم کنیم:

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

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

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

ما در نهایت با تصاویر واضح و طرح‌بندی‌های دلپذیر در هر اندازه صفحه نمایش مواجه می‌شویم:

صفحه بازی فضای عمودی را پر می کند. عناصر دیگر صفحه در اطراف آن جریان دارند.
صفحه بازی فضای عمودی را پر می کند. عناصر دیگر صفحه در اطراف آن جریان دارند.

برو سر اصل مطلب

از آنجایی که هر کاشی کاملاً قرار دارد و باید دقیقاً با صفحه بازی و قفسه هماهنگ باشد، ما به یک سیستم موقعیت یابی قابل اعتماد نیاز داریم. ما از دو تابع Bounds و Point برای کمک به مدیریت مکان عناصر در فضای جهانی (صفحه HTML) استفاده می کنیم. Bounds یک ناحیه مستطیلی را در صفحه توصیف می کند، در حالی که Point مختصات x,y را نسبت به گوشه سمت چپ بالای صفحه (0,0) توصیف می کند که در غیر این صورت به عنوان نقطه ثبت نام شناخته می شود.

با Bounds ، می‌توانیم تقاطع دو عنصر مستطیلی را تشخیص دهیم (مانند زمانی که یک کاشی از قفسه عبور می‌کند) یا اینکه آیا یک ناحیه مستطیلی (مانند یک فضای دو حرفی) دارای یک نقطه دلخواه است (مانند نقطه مرکزی یک کاشی). . در اینجا اجرای 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() در ترکیب با یک تایمر برای ایجاد یک motion tween یا افکت easing استفاده می کنیم.

با جریان برو

در حالی که طرح‌بندی‌های با اندازه ثابت در Flash آسان‌تر تولید می‌شوند، طرح‌بندی‌های سیال با HTML و مدل جعبه CSS بسیار ساده‌تر تولید می‌شوند. نمای شبکه ای زیر را با عرض و ارتفاع متغیر آن در نظر بگیرید:

این طرح‌بندی ابعاد ثابتی ندارد: تصاویر کوچک از چپ به راست، از بالا به پایین جریان دارند.
این طرح‌بندی ابعاد ثابتی ندارد: تصاویر کوچک از چپ به راست، از بالا به پایین جریان دارند.

یا پنل چت را در نظر بگیرید. نسخه فلش به چندین کنترل کننده رویداد برای پاسخ دادن به اقدامات ماوس، یک ماسک برای ناحیه قابل پیمایش، ریاضیات برای محاسبه موقعیت اسکرول و بسیاری از کدهای دیگر برای چسباندن آن به یکدیگر نیاز داشت.

پنل چت در فلش زیبا اما پیچیده بود.
پنل چت در فلش زیبا اما پیچیده بود.

در مقایسه، نسخه HTML فقط یک <div> با ارتفاع ثابت و ویژگی سرریز تنظیم شده روی مخفی است. اسکرول برای ما هیچ هزینه ای ندارد.

مدل جعبه CSS در حال کار است.
مدل جعبه CSS در حال کار است.

در مواردی مانند این - وظایف طرح بندی معمولی - HTML و CSS از Flash پیشی گرفته اند.

حالا صدایم را می شنوی؟

ما با تگ <audio> مشکل داشتیم - این برچسب به سادگی قادر به پخش مکرر جلوه های صوتی کوتاه در مرورگرهای خاص نبود. ما دو راه حل را امتحان کردیم. ابتدا فایل های صوتی را با هوای مرده پر کردیم تا طولانی تر شوند. سپس پخش متناوب را در چندین کانال صوتی امتحان کردیم. هیچ کدام از این تکنیک ها کاملاً مؤثر یا ظریف نبودند.

در نهایت تصمیم گرفتیم پخش کننده صوتی فلش خودمان را رول کنیم و از صدای 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);

در جاوا اسکریپت، ما سعی می کنیم فلش پلیر تعبیه شده را شناسایی کنیم. اگر این کار انجام نشد، برای هر فایل صوتی یک گره <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 از همان تکنیکی که در Flash انجام دادیم برای به‌روزرسانی حالت بازی استفاده می‌کنیم: هر 10 ثانیه، مشتری از سرور درخواست به‌روزرسانی می‌کند. اگر وضعیت بازی از آخرین نظرسنجی تغییر کرده باشد، مشتری تغییرات را دریافت و مدیریت می کند. در غیر این صورت هیچ اتفاقی نمی افتد این روش سنتی نظرسنجی اگر نه کاملاً ظریف، قابل قبول است. با این حال، مایلیم زمانی که بازی بالغ می‌شود و کاربران انتظار تعامل بی‌درنگ از طریق شبکه را دارند، به رای‌گیری طولانی یا WebSockets روی بیاوریم. وب‌سوکت‌ها، به‌ویژه، فرصت‌های زیادی را برای بهبود بازی ارائه می‌دهند.

چه ابزاری!

ما از Google Web Toolkit (GWT) برای توسعه رابط کاربری جلویی و منطق کنترل پشتیبان (احراز هویت، اعتبارسنجی، ماندگاری و غیره) استفاده کردیم. خود جاوا اسکریپت از کد منبع جاوا کامپایل شده است. به عنوان مثال، تابع 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));
}
...
}

برخی از کلاس‌های UI دارای فایل‌های الگوی متناظری هستند که در آن‌ها عناصر صفحه به اعضای کلاس «پیوند» می‌شوند. برای مثال، 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>

جزئیات کامل خارج از محدوده این مقاله است، اما ما شما را تشویق می کنیم که GWT را برای پروژه بعدی HTML5 خود بررسی کنید.

چرا از جاوا استفاده کنیم؟ اول، برای تایپ دقیق. در حالی که تایپ پویا در جاوا اسکریپت مفید است - به عنوان مثال، توانایی یک آرایه برای نگهداری مقادیر مختلف - می تواند در پروژه های بزرگ و پیچیده دردسرساز باشد. دوم، برای قابلیت‌های بازسازی. در نظر بگیرید که چگونه امضای متد جاوا اسکریپت را در هزاران خط کد تغییر می دهید - نه به راحتی! اما با یک Java IDE خوب، این یک ضربه محکم و ناگهانی است. در نهایت، برای اهداف آزمایشی. نوشتن تست‌های واحد برای کلاس‌های جاوا، تکنیک قدیمی «ذخیره و تازه‌سازی» را شکست می‌دهد.

خلاصه

به جز مشکلات صوتی ما، HTML5 بسیار فراتر از انتظارات ما بود. Wordico نه تنها به خوبی در Flash به نظر می رسد، بلکه به همان اندازه روان و پاسخگو است. ما نمی توانستیم بدون Canvas و CSS3 این کار را انجام دهیم. چالش بعدی ما: تطبیق Wordico برای استفاده از تلفن همراه.