دراسة حالة - تحويل Wordico من Flash إلى HTML5

مقدمة

عندما حوّلنا لعبة الكلمات المتقاطعة Wordico من Flash إلى HTML5، كانت مهمتنا الأولى هي التخلص من كل ما عرفناه حول إنشاء تجربة غنية للمستخدم في المتصفح. وعلى الرغم من أن Flash قد قدم واجهة برمجة تطبيقات واحدة وشاملة لجميع جوانب تطوير التطبيقات - بدءًا من رسم المتجهات إلى اكتشاف النقرات المضلعة وحتى تحليل XML - إلا أن HTML5 يوفر خليطًا من المواصفات مع دعم متنوّع للمتصفح. وتساءلنا أيضًا عما إذا كانت لغة HTML، واللغة الخاصة بالمستندات، وCSS، التي تركز على المربع، مناسبة لتصميم الألعاب. هل سيتم عرض اللعبة بشكل موحّد عبر المتصفحات، كما هو الحال في Flash، وهل ستبدو وتعمل بشكل جيد؟ بالنسبة إلى Wordico، كانت الإجابة نعم.

ما هو الخط المتجه الخاص بك يا فيكتور؟

لقد طورنا النسخة الأصلية من Wordico باستخدام الرسومات المتجهة فقط: الخطوط والمنحنيات والتعبئة والتدرجات. وكانت النتيجة مضغوطة للغاية وقابلة للتطوير بشكل لا نهائي:

الإطار الشبكي Wordico
في برنامج Flash، كان كل عنصر عرض مكوّن من أشكال متّجهات.

استفدنا أيضًا من المخطط الزمني لبرنامج Flash لإنشاء كائنات لها حالات متعددة. على سبيل المثال، استخدمنا تسعة إطارات رئيسية باسم للكائن Space:

مسافة ثلاثية الأحرف في Flash.
مساحة من ثلاثة أحرف في Flash

ومع ذلك، في HTML5، نستخدم مقتطفًا مدمجًا مع الصورة النقطية:

رسم متحرك بتنسيق PNG يعرض جميع المساحات التسعة
صورة متحركة بتنسيق PNG تعرض جميع المسافات التسعة

لإنشاء لوحة ألعاب مقاس 15×15 من مساحات فردية، نكرر تكرارًا على رمز سلسلة مكوَّن من 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;
}

ها هي النتيجة في متصفّح الويب. لاحظ أن اللوحة نفسها بها تظليل قطرات CSS:

في HTML5، تكون لوحة الألعاب عبارة عن عنصر لوحة رسم واحد.
في HTML5، تكون لوحة الألعاب عبارة عن عنصر واحد في لوحة الرسم.

كان تحويل كائن البلاط تمرينًا مشابهًا. في Flash، استخدمنا الحقول النصية وأشكال المتجهات:

كان مربع الفلاش مزيجًا من حقول النص وأشكال المتجهات
كان مربّع الفلاش مزيجًا من حقول النص والأشكال المتّجهة.

في HTML5، ندمج ثلاثة تركيبات صور مدمجة في عنصر <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 عند سحب المربّع (الظل والتعتيم والتحجيم) وعندما يكون المربّع على الرف (الانعكاس):

المربّع الذي يتم سحبه أكبر قليلاً وشفاف قليلاً وبه تظليل قطرات.
المربّع الذي يتم سحبه أكبر قليلاً وشفاف قليلاً وبه تظليل قطرات.

هناك بعض المزايا الواضحة لاستخدام الصور النقطية. أولاً، النتيجة دقيقة بالبكسل. ثانيًا، يمكن تخزين هذه الصور مؤقتًا بواسطة المتصفح. ثالثًا، مع بعض الجهد الإضافي، يمكننا تبديل الصور لإنشاء تصميمات جديدة للبلاط - مثل البلاط المعدني - ويمكن تنفيذ أعمال التصميم هذه في Photoshop بدلاً من Flash.

ما الجانب السلبي؟ وباستخدام الصور، نمنحك إمكانية الوصول الآلي إلى الحقول النصية. في Flash، كانت عملية تغيير اللون أو الخصائص الأخرى للنوع عملية بسيطة؛ في HTML5، يتم دمج هذه الخصائص في الصور نفسها. (لقد جرّبنا استخدام نص HTML، ولكنّه تطلّب الكثير من الترميز وCSS. لقد جربنا أيضًا نص لوحة الرسم، ولكن النتائج كانت غير متسقة عبر المتصفحات).

المنطق الغامض

لقد أردنا الاستفادة بشكل كامل من نافذة المتصفح بأي حجم - وتجنب التمرير. كانت هذه عملية بسيطة نسبيًا في برنامج Flash، حيث تم رسم اللعبة بالكامل في متجهات ويمكن تكبيرها أو تصغيرها دون فقدان الدقة. لكنها كانت أكثر تعقيدًا في 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() مع الموقّت لإنشاء حركة ما قبل سن المراهقة، أو تأثير التخفيف.

شخصية انسيابية

على الرغم من أنّه يسهل إنتاج التنسيقات ذات الحجم الثابت في Flash، فإنّ إنشاء التنسيقات المرنة أسهل بكثير باستخدام HTML ونموذج مربّع CSS. بالنظر إلى عرض الشبكة التالي، مع عرضه وارتفاعه المتغيرين:

ليس لهذا التنسيق أبعاد ثابتة: الصور المصغّرة تتدفق من اليسار إلى اليمين ومن الأعلى إلى الأسفل.
ليس لهذا التنسيق أبعاد ثابتة: الصور المصغّرة تتدفق من اليسار إلى اليمين ومن الأعلى إلى الأسفل.

أو جرِّب لوحة المحادثة. كان إصدار Flash يتطلب معالِجات أحداث متعددة للاستجابة لإجراءات الماوس، وقناع للمنطقة القابلة للتمرير، والرياضيات لحساب موضع التمرير، والكثير من الرموز الأخرى لتثبيتها معًا.

كانت لوحة الدردشة في Flash شديدة التعقيد.
كانت لوحة المحادثة في Flash شديدة التعقيد.

أمّا نسخة HTML، فهي على سبيل المثال <div> مع ارتفاع ثابت وضبط سمة overflow على "مخفية". لا يكلفنا التمرير شيئًا.

نموذج مربّع CSS في العمل
استخدام نموذج مربّع CSS

في مثل هذه الحالات، مثل مهام التنسيق العادية، يتفوق HTML وCSS على Flash.

هل يمكنك أن تسمعني الآن؟

لقد واجهنا صعوبات في استخدام العلامة <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، نحاول اكتشاف مشغّل 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. نأمل أن يستقر المجال على تنسيق واحد في المستقبل القريب.

موضع استطلاع الرأي

ونستخدم الأسلوب نفسه في HTML5 كما فعلنا في Flash لتحديث حالة اللعبة: كل 10 ثوانٍ، يطلب العميل من الخادم التحديثات. إذا تغيرت حالة اللعبة منذ آخر استطلاع، يتلقى العميل التغييرات ويعالجها، وإلا فلن يحدث شيء. ويُقبل أسلوب الاستطلاع التقليدي هذا، إذا لم يكن أنيقًا تمامًا. ومع ذلك، نريد التبديل إلى الاستطلاع الطويل أو WebSockets مع تقدّم اللعبة ويتوقع المستخدمون تفاعلاً في الوقت الفعلي عبر الشبكة. وستقدّم WebSockets على وجه الخصوص العديد من الفرص لتحسين طريقة اللعب.

إنّها أداة رائعة.

لقد استخدمنا مجموعة أدوات الويب من Google (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>

التفاصيل الكاملة خارج نطاق هذه المقالة، ولكننا نشجعك على الاطلاع على GWT لمشروع HTML5 التالي.

لماذا يُنصح باستخدام Java؟ أولاً، للكتابة الصارمة. تُعد الكتابة الديناميكية مفيدة في JavaScript - على سبيل المثال، قدرة الصفيف على الاحتفاظ بقيم من أنواع مختلفة - يمكن أن تمثل صعوبة في المشروعات الكبيرة والمعقدة. ثانيًا، بالنسبة إلى إمكانات إعادة الهيكلة. فكِّر في طريقة تغيير توقيع طريقة JavaScript عبر آلاف سطور الرمز، وليس بسهولة. لكن مع وجود بيئة تطوير متكاملة (IDE) بلغة Java، يسهل عليك الأمر. وأخيرًا، لأغراض الاختبار. فاختبارات وحدة الكتابة لصفوف Java تتفوق على أسلوب "الحفظ وإعادة التحميل" العريق.

ملخّص

باستثناء مشاكل الصوت، تجاوز HTML5 توقعاتنا. لا يبدو Wordico جيدًا كما كان في Flash فحسب، بل إنه أيضًا سلس وسريع الاستجابة. لم يكن من الممكن تنفيذ ذلك بدون Canvas وCSS3. التحدي التالي: تكييف Wordico لاستخدام الأجهزة الجوّالة.