Pengantar
Saat mengonversi game teka-teki silang Wordico dari Flash ke HTML5, tugas pertama kami adalah melupakan semua yang kami ketahui tentang cara menciptakan pengalaman pengguna yang kaya di browser. Meskipun Flash menawarkan satu API komprehensif untuk semua aspek pengembangan aplikasi - mulai dari gambar vektor hingga deteksi hit poligon hingga penguraian XML - HTML5 menawarkan kumpulan spesifikasi dengan dukungan browser yang bervariasi. Kita juga bertanya-tanya apakah HTML, bahasa khusus dokumen, dan CSS, bahasa yang berfokus pada kotak, cocok untuk membuat game. Apakah game akan ditampilkan secara seragam di seluruh browser, seperti yang dilakukan di Flash, dan apakah game akan terlihat dan berperilaku dengan baik? Untuk Wordico, jawabannya adalah ya.
Apa vektor Anda, Victor?
Kami mengembangkan Wordico versi asli hanya menggunakan grafik vektor: garis, kurva, isian, dan gradien. Hasilnya sangat ringkas dan skalabel tanpa batas:
Kami juga memanfaatkan linimasa Flash untuk membuat objek yang memiliki beberapa status. Misalnya, kita menggunakan sembilan keyframe bernama untuk objek Space
:
Namun, di HTML5, kita menggunakan sprite bitmap:
Untuk membuat papan game 15x15 dari setiap ruang, kita melakukan iterasi pada notasi string 225 karakter yang setiap ruangnya diwakili oleh karakter yang berbeda (seperti "t" untuk tiga huruf dan "T" untuk tiga kata). Ini adalah operasi yang mudah di Flash; kita cukup mencetak ruang dan mengaturnya dalam petak:
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, prosesnya sedikit lebih rumit. Kita menggunakan elemen <canvas>
, platform gambar bitmap, untuk melukis papan game satu kotak pada satu waktu. Langkah pertama adalah memuat sprite gambar. Setelah dimuat, kita akan melakukan iterasi melalui notasi tata letak, menggambar bagian gambar yang berbeda dengan 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;
}
Berikut adalah hasilnya di browser web. Perhatikan bahwa kanvas itu sendiri memiliki bayangan jatuh CSS:
Mengonversi objek kartu adalah latihan yang serupa. Di Flash, kita menggunakan kolom teks dan bentuk vektor:
Di HTML5, kita menggabungkan tiga sprite gambar pada satu elemen <canvas>
saat runtime:
Sekarang kita memiliki 100 kanvas (satu untuk setiap kartu) ditambah kanvas untuk papan game. Berikut adalah markup untuk kartu "H":
<canvas width="35" height="35" class="tile tile-racked" title="H-2"/>
Berikut adalah 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 kartu ditarik (bayangan, opasitas, dan penskalaan) dan saat kartu berada di rak (refleksi):
Penggunaan gambar raster memiliki beberapa keunggulan yang jelas. Pertama, hasilnya presisi piksel. Kedua, gambar ini dapat di-cache oleh browser. Ketiga, dengan sedikit pekerjaan tambahan, kita dapat menukar gambar untuk membuat desain kartu baru - seperti kartu logam - dan pekerjaan desain ini dapat dilakukan di Photoshop, bukan di Flash.
Kekurangannya? Dengan menggunakan gambar, kita melepaskan akses terprogram ke kolom teks. Di Flash, mengubah warna atau properti lain dari jenis tersebut adalah operasi yang sederhana; di HTML5, properti ini diintegrasikan ke dalam gambar itu sendiri. (Kami mencoba teks HTML, tetapi memerlukan banyak markup dan CSS tambahan. Kami juga mencoba teks kanvas, tetapi hasilnya tidak konsisten di seluruh browser.)
Logika fuzzy
Kami ingin memanfaatkan jendela browser sepenuhnya dalam ukuran apa pun - dan menghindari scroll. Ini adalah operasi yang relatif sederhana di Flash, karena seluruh game digambar dalam vektor dan dapat diskalakan naik atau turun tanpa kehilangan fidelitas. Namun, hal ini lebih rumit di HTML. Kami mencoba menggunakan penskalaan CSS, tetapi akhirnya mendapatkan kanvas yang buram:
Solusi kami adalah menggambar ulang papan permainan, rak, dan kartu setiap kali pengguna mengubah ukuran browser:
window.onresize = function (evt) {
...
gameboard.setConstraints(boardWidth, boardWidth);
...
rack.setConstraints(rackWidth, rackHeight);
...
tileManager.resizeTiles(tileSize);
});
Kita akan mendapatkan gambar yang tajam dan tata letak yang menarik di semua ukuran layar:
Langsung ke intinya
Karena setiap kartu diposisikan secara mutlak dan harus sejajar dengan papan permainan dan rak, kita memerlukan sistem pemosisian yang andal. Kita 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
mendeskripsikan koordinat x,y relatif terhadap sudut kiri atas halaman (0,0), atau dikenal sebagai titik pendaftaran.
Dengan Bounds
, kita dapat mendeteksi persimpangan dua elemen persegi panjang (seperti saat kartu melintasi rak) atau apakah area persegi panjang (seperti spasi huruf ganda) berisi titik arbitrer (seperti titik tengah kartu). 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 &&
point.x < this.right &&
point.y > this.top &&
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 elemen apa pun 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 ini membentuk dasar kemampuan animasi dan tarik lalu lepas. Misalnya, kita menggunakan Bounds.intersects()
untuk menentukan apakah kartu tumpang-tindih dengan ruang di papan game; kita menggunakan Point.vector()
untuk menentukan arah kartu yang ditarik; dan kita menggunakan Point.interpolate()
yang dikombinasikan dengan timer untuk membuat tween gerakan, atau efek easing.
Santai dan menerima keadaan
Meskipun tata letak berukuran tetap lebih mudah dibuat di Flash, tata letak fleksibel jauh lebih mudah dibuat dengan HTML dan model kotak CSS. Pertimbangkan tampilan petak berikut, dengan lebar dan tinggi variabelnya:
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 menggabungkannya.
Sebagai perbandingan, versi HTML hanyalah <div>
dengan tinggi tetap dan properti overflow disetel ke tersembunyi. Scrolling tidak dikenakan biaya.
Dalam kasus seperti ini - tugas tata letak biasa - HTML dan CSS lebih unggul dari Flash.
Apakah Anda bisa mendengar saya sekarang?
Kami mengalami kesulitan dengan tag <audio>
- tag ini tidak dapat memutar efek suara singkat berulang kali di browser tertentu. Kami mencoba dua solusi. Pertama, kami menambahkan file suara dengan dead air untuk membuatnya lebih panjang. Kemudian, kami mencoba pemutaran bergantian di beberapa saluran audio. Tidak satu pun teknik yang sepenuhnya efektif atau elegan.
Pada akhirnya, kami memutuskan untuk meluncurkan pemutar audio Flash kami sendiri dan menggunakan audio HTML5 sebagai penggantian. Berikut kode dasarnya 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);
Dalam JavaScript, kita mencoba mendeteksi Flash player 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();
}
}
Perhatikan bahwa ini hanya berfungsi untuk file MP3 - kami tidak pernah mendukung OGG. Kami berharap industri akan menyepakati satu format dalam waktu dekat.
Posisi polling
Kami menggunakan teknik yang sama di HTML5 seperti yang kami lakukan di Flash untuk memuat ulang status game: setiap 10 detik, klien meminta update dari server. Jika status game telah berubah sejak polling terakhir, klien akan menerima dan menangani perubahan tersebut; jika tidak, tidak akan terjadi apa pun. Teknik polling tradisional ini dapat diterima, meskipun tidak cukup elegan. Namun, kita ingin beralih ke long polling atau WebSockets seiring perkembangan game dan pengguna mengharapkan interaksi real-time melalui jaringan. WebSocket, khususnya, akan memberikan banyak peluang untuk meningkatkan kualitas gameplay.
Alat yang luar biasa!
Kami menggunakan Google Web Toolkit (GWT) untuk mengembangkan antarmuka pengguna frontend dan logika kontrol backend (autentikasi, validasi, persistensi, dan sebagainya). JavaScript itu sendiri dikompilasi dari kode sumber Java. Misalnya, fungsi Titik 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 tempat elemen halaman "terikat" dengan anggota class. Misalnya, ChatPanel.ui.xml
berkaitan 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 Anda melihat GWT untuk project HTML5 berikutnya.
Mengapa menggunakan Java? Pertama, untuk jenis data yang ketat. Meskipun berguna dalam JavaScript - misalnya, kemampuan array untuk menyimpan nilai dari berbagai jenis - pengelompokan dinamis dapat menjadi masalah dalam project besar dan kompleks. Kedua, untuk kemampuan pemfaktoran ulang. Pertimbangkan cara Anda mengubah tanda tangan metode JavaScript di ribuan baris kode - tidak mudah! Namun, dengan IDE Java yang baik, hal ini sangat mudah. Terakhir, untuk tujuan pengujian. Menulis pengujian unit untuk class Java mengalahkan teknik "simpan dan muat ulang" yang sudah lama digunakan.
Ringkasan
Kecuali masalah audio, HTML5 jauh melampaui ekspektasi kami. Wordico tidak hanya terlihat bagus seperti di Flash, tetapi juga lancar dan responsif. Kami tidak dapat melakukannya tanpa Canvas dan CSS3. Tantangan berikutnya: menyesuaikan Wordico untuk penggunaan seluler.