Studi Kasus - Mengonversi Wordico dari Flash ke HTML5

Pengantar

Saat mengonversi game teka-teki silang Wordico dari Flash ke HTML5, tugas pertama kami adalah menghilangkan semua yang kami ketahui tentang menciptakan pengalaman pengguna yang kaya di browser. Sementara Flash menawarkan satu API komprehensif untuk semua aspek pengembangan aplikasi - dari gambar vektor hingga deteksi klik poligon hingga penguraian XML - HTML5 menawarkan kumpulan spesifikasi dengan dukungan browser yang beragam. Kami juga bertanya-tanya apakah HTML, bahasa khusus dokumen, dan CSS, bahasa yang berpusat pada kotak, cocok untuk membuat game. Apakah game akan ditampilkan secara seragam di seluruh browser, seperti di Flash, dan apakah game akan terlihat dan berperilaku sama baik? Bagi Wordico, jawabannya adalah ya.

Apa vektormu, Victor?

Kami mengembangkan versi asli Wordico dengan hanya menggunakan grafik vektor: garis, kurva, isian, dan gradien. Hasilnya sangatlah ringkas dan skalabel tanpa batas:

Wireframe Wordico
Pada Flash, setiap objek tampilan dibuat dari bentuk vektor.

Kami juga memanfaatkan linimasa Flash untuk membuat objek yang memiliki beberapa status. Misalnya, kami menggunakan sembilan keyframe bernama untuk objek Space:

Spasi tiga huruf di Flash.
Spasi tiga huruf di Flash.

Namun, dalam HTML5, kami menggunakan sprite bitmap:

Sprite PNG yang menampilkan kesembilan spasi.
Sprite PNG yang menampilkan kesembilan spasi.

Untuk membuat gameboard 15x15 dari setiap spasi, kami melakukan iterasi pada notasi string 225 karakter dengan setiap spasi diwakili oleh karakter yang berbeda (seperti "t" untuk huruf tiga dan "T" untuk tiga kata). Ini adalah operasi yang mudah di Flash; kami hanya menghilangkan spasi dan mengaturnya dalam kisi:

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

Di HTML5, ini sedikit lebih rumit. Kita menggunakan elemen <canvas>, platform gambar bitmap, untuk menggambar gameboard persegi satu per satu. Langkah pertama adalah memuat sprite gambar. Setelah dimuat, kita akan melakukan iterasi melalui notasi tata letak, menggambar bagian gambar yang berbeda pada setiap iterasi:

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

Ini hasilnya di browser web. Perhatikan bahwa kanvas itu sendiri memiliki drop shadow CSS:

Di HTML5, gameboard adalah elemen kanvas tunggal.
Di HTML5, gameboard adalah elemen kanvas tunggal.

Mengonversi objek kartu adalah latihan yang serupa. Di Flash, kami menggunakan kolom teks dan bentuk vektor:

Ubin Flash merupakan kombinasi dari bidang teks dan bentuk vektor
Kartu Flash adalah kombinasi antara kolom teks dan bentuk vektor.

Di HTML5, kita menggabungkan tiga sprite gambar pada satu elemen <canvas> saat runtime:

Petak HTML adalah gabungan dari tiga gambar.
Kartu HTML adalah gabungan dari tiga gambar.

Sekarang kita memiliki 100 kanvas (satu untuk setiap kartu) ditambah kanvas untuk gameboard. Berikut markup untuk kartu "H":

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

Berikut CSS yang sesuai:

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

Kami menerapkan efek CSS3 saat petak diseret (bayangan, opasitas, dan penskalaan) dan saat petak berada di rak (refleksi):

Kartu yang ditarik berukuran sedikit lebih besar, sedikit transparan, dan memiliki drop shadow.
Kartu yang ditarik berukuran sedikit lebih besar, sedikit transparan, dan memiliki drop shadow.

Menggunakan gambar raster memiliki beberapa keuntungan nyata. Pertama, hasilnya adalah presisi piksel. Kedua, gambar ini dapat di-cache oleh browser. Ketiga, dengan sedikit upaya ekstra, kita dapat menukar gambar untuk membuat desain ubin baru - seperti ubin logam - dan pekerjaan desain ini dapat dilakukan dengan Photoshop, bukan di Flash.

Kekurangannya? Dengan menggunakan gambar, kami memberikan akses terprogram ke kolom teks. Pada Flash, merupakan operasi sederhana untuk mengubah warna atau properti jenis lainnya; di HTML5, properti ini akan dimasukkan ke dalam gambar itu sendiri. (Kami mencoba teks HTML, tapi itu memerlukan banyak markup tambahan dan CSS. Kami juga mencoba teks kanvas, tetapi hasilnya tidak konsisten di seluruh browser.)

Logika fuzzy

Kami ingin memanfaatkan jendela browser sepenuhnya dalam berbagai ukuran - dan menghindari scroll. Ini adalah operasi yang relatif sederhana di Flash, karena seluruh game digambar dalam vektor dan dapat ditingkatkan atau diturunkan skalanya tanpa kehilangan fidelitas. Namun, lebih rumit lagi di HTML. Kami mencoba menggunakan penskalaan CSS, tetapi hasilnya menjadi kanvas buram:

Penskalaan CSS (kiri) vs. menggambar ulang (kanan).
Penskalaan CSS (kiri) vs. menggambar ulang (kanan).

Solusi kami adalah menggambar ulang gameboard, rak, dan ubin setiap kali pengguna mengubah ukuran browser:

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

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

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

Kita akhirnya akan mendapatkan gambar yang tajam dan tata letak yang menyenangkan pada semua ukuran layar:

Gameboard mengisi ruang vertikal; elemen halaman lain mengalir di sekelilingnya.
Gameboard mengisi ruang vertikal; elemen halaman lainnya mengalir di sekelilingnya.

Sampaikan pesan dengan singkat

Karena setiap kartu benar-benar diposisikan dan harus sejajar dengan gameboard dan rak secara tepat, kami memerlukan sistem penentuan posisi yang andal. Kami menggunakan dua fungsi, Bounds dan Point, untuk membantu mengelola lokasi elemen di ruang global (halaman HTML). Bounds mendeskripsikan area persegi panjang di halaman, sedangkan Point menjelaskan koordinat x,y yang relatif terhadap sudut kiri atas halaman (0,0), atau yang dikenal sebagai titik pendaftaran.

Dengan Bounds, kita dapat mendeteksi perpotongan dua elemen persegi panjang (seperti saat ubin melintasi rak) atau apakah area persegi panjang (seperti spasi dua huruf) berisi titik arbitrer (seperti titik tengah ubin). Berikut adalah implementasi Batas:

// 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(",");
}

Kita menggunakan Point untuk menentukan koordinat absolut (sudut kiri atas) dari setiap elemen di halaman atau peristiwa mouse. Point juga berisi metode untuk menghitung jarak dan arah, yang diperlukan untuk membuat efek animasi. Berikut adalah implementasi 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);
}

Fungsi-fungsi ini menjadi dasar kemampuan tarik lalu lepas dan animasi. Misalnya, kita menggunakan Bounds.intersects() untuk menentukan apakah kartu tumpang-tindih dengan spasi di gameboard; kita menggunakan Point.vector() untuk menentukan arah kartu yang ditarik; dan menggunakan Point.interpolate() bersama timer untuk membuat efek hitung gerakan atau easing.

Santai dan menerima keadaan

Meskipun tata letak ukuran tetap lebih mudah dihasilkan di Flash, tata letak yang mengalir jauh lebih mudah dibuat dengan HTML dan model kotak CSS. Perhatikan tampilan petak berikut, dengan lebar dan tinggi yang bervariasi:

Tata letak ini tidak memiliki dimensi tetap: thumbnail mengalir dari kiri ke kanan, dari atas ke bawah.
Tata letak ini tidak memiliki dimensi tetap: thumbnail mengalir dari kiri ke kanan, dari atas ke bawah.

Atau pertimbangkan panel chat. Versi Flash memerlukan beberapa pengendali peristiwa untuk merespons tindakan mouse, mask untuk area yang dapat di-scroll, matematika untuk menghitung posisi scroll, dan banyak kode lainnya untuk menyatukannya.

Panel chat di Flash cukup rumit.
Panel chat di Flash cukup menarik, tetapi rumit.

Sebagai perbandingan, versi HTML hanyalah <div> dengan tinggi tetap dan properti tambahan yang disetel ke tersembunyi. Scroll tidak dikenai biaya.

Model kotak CSS di tempat kerja.
Model kotak CSS di tempat kerja.

Dalam kasus seperti ini - tugas tata letak biasa - HTML dan CSS mengungguli Flash.

Apakah Anda bisa mendengar saya sekarang?

Kami kesulitan menggunakan tag <audio> - tag tersebut tidak dapat memutar efek suara pendek berulang kali di browser tertentu. Kami mencoba dua solusi. Pertama, kami menambahkan file suara dengan udara mati untuk membuatnya lebih panjang. Kemudian, kami mencoba mengubah pemutaran di beberapa saluran audio. Tidak ada teknik yang benar-benar efektif atau elegan.

Pada akhirnya, kami memutuskan untuk meluncurkan pemutar audio Flash sendiri dan menggunakan audio HTML5 sebagai pengganti. Berikut ini kode dasar di 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);

Pada JavaScript, kami berupaya mendeteksi pemutar Flash yang disematkan. Jika gagal, kita akan membuat node <audio> untuk setiap file suara:

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

Perlu diperhatikan bahwa cara ini hanya berfungsi untuk file MP3 - kami tidak perlu repot-repot mendukung OGG. Kami berharap industri ini akan memanfaatkan satu format dalam waktu dekat.

Posisi polling

Kami menggunakan teknik yang sama di HTML5 seperti yang kami lakukan di Flash untuk menyegarkan status game: setiap 10 detik, klien akan meminta update ke server. Jika status game telah berubah sejak polling terakhir, klien akan menerima dan menangani perubahan; jika tidak, tidak akan ada yang terjadi. Teknik polling tradisional ini dapat diterima, jika tidak cukup elegan. Namun, kami ingin beralih ke pemungutan suara panjang atau WebSockets seiring dengan semakin berkembangnya game dan pengguna mengharapkan interaksi real-time melalui jaringan. WebSockets, khususnya, akan memberikan banyak peluang untuk meningkatkan gameplay.

Alat yang bagus!

Kami menggunakan Google Web Toolkit (GWT) untuk mengembangkan antarmuka pengguna front-end dan logika kontrol back-end (autentikasi, validasi, persistensi, dan sebagainya). JavaScript sendiri dikompilasi dari kode sumber Java. Misalnya, fungsi Point diadaptasi dari 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));
}
...
}

Beberapa class UI memiliki file template yang sesuai di mana elemen halaman "terikat" ke anggota class. Misalnya, ChatPanel.ui.xml sesuai dengan 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>

Detail lengkapnya berada di luar cakupan artikel ini, tetapi sebaiknya lihat GWT untuk proyek HTML5 Anda berikutnya.

Mengapa menggunakan Java? Pertama, untuk pengetikan ketat. Meskipun pengetikan dinamis berguna dalam JavaScript - misalnya, kemampuan array untuk menyimpan nilai dari berbagai jenis - hal ini dapat memusingkan Anda dalam project yang besar dan kompleks. Kedua, untuk memfaktorkan ulang kemampuan. Pertimbangkan bagaimana Anda akan mengubah tanda tangan metode JavaScript pada ribuan baris kode - tidak mudah! Namun, dengan IDE Java yang bagus, semuanya akan mudah. Terakhir, untuk tujuan pengujian. Menulis pengujian unit untuk class Java akan mengalahkan teknik "simpan dan muat ulang" yang lazim.

Ringkasan

Kecuali untuk masalah audio, HTML5 jauh melebihi harapan kami. Wordico tidak hanya terlihat sebagus di Flash, tetapi juga lancar dan responsif. Kami tidak mungkin melakukannya tanpa Canvas dan CSS3. Tantangan kami berikutnya: mengadaptasi Wordico untuk penggunaan seluler.