Nghiên cứu điển hình - Chuyển đổi Wordico từ Flash thành HTML5

Adam Mark
Adam Mark
Adrian Gould
Adrian Gould

Giới thiệu

Khi chúng tôi chuyển đổi trò chơi ô chữ Wordico từ Flash sang HTML5, nhiệm vụ đầu tiên của chúng tôi là tìm hiểu mọi thứ đã biết về việc tạo trải nghiệm người dùng phong phú trong trình duyệt. Mặc dù Flash cung cấp một API toàn diện, duy nhất cho tất cả các khía cạnh của việc phát triển ứng dụng - từ vẽ vectơ, phát hiện lượt truy cập đa giác đến phân tích cú pháp XML - HTML5 cung cấp hàng loạt thông số kỹ thuật với khả năng hỗ trợ trình duyệt khác nhau. Chúng tôi cũng thắc mắc liệu HTML (một ngôn ngữ dành riêng cho tài liệu) và CSS (một ngôn ngữ tập trung vào hộp) có phù hợp để xây dựng trò chơi hay không. Trò chơi có hiển thị thống nhất trên các trình duyệt như trong Flash không? Trò chơi có hiển thị và hoạt động tốt không? Đối với Wordico, câu trả lời là có.

Victor của bạn muốn dùng vectơ nào?

Chúng tôi phát triển phiên bản gốc của Wordico chỉ sử dụng đồ hoạ vectơ: đường kẻ, đường cong, màu nền và độ dốc. Kết quả vừa nhỏ gọn vừa có thể mở rộng vô hạn:

Khung xương của Wordico
Trong Flash, mọi đối tượng hiển thị đều được tạo thành từ các hình vectơ.

Chúng tôi cũng tận dụng dòng thời gian Flash để tạo các đối tượng có nhiều trạng thái. Ví dụ: chúng ta sử dụng 9 khung hình chính có tên cho đối tượng Space:

Một dấu cách gồm ba chữ cái trong Flash.
Một dấu cách gồm ba chữ cái trong Flash.

Tuy nhiên, trong HTML5, chúng ta sử dụng ảnh sprite dạng bitmap:

Một ảnh sprite PNG hiển thị cả 9 không gian.
Một ảnh sprite PNG hiển thị cả 9 khoảng trắng.

Để tạo bảng điều khiển trò chơi 15x15 từ các không gian riêng lẻ, chúng ta lặp lại một ký hiệu chuỗi gồm 225 ký tự, trong đó mỗi dấu cách được biểu thị bằng một ký tự khác (chẳng hạn như "t" cho chữ cái bộ ba và "T" cho từ bộ ba). Đây là một thao tác đơn giản trong Flash; chúng tôi chỉ đơn giản là đánh dấu các không gian và sắp xếp chúng trong một lưới:

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

Trong HTML5, điều này sẽ phức tạp hơn một chút. Chúng tôi sử dụng phần tử <canvas> (khu vực vẽ bitmap) để vẽ từng hình vuông trên bảng điều khiển trò chơi. Bước đầu tiên là tải nhóm hình ảnh. Sau khi tải, chúng ta lặp lại ký hiệu bố cục, vẽ một phần khác của hình ảnh với mỗi lần lặp lại:

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

Sau đây là kết quả trong trình duyệt web. Lưu ý rằng bản thân canvas có bóng đổ CSS:

Trong HTML5, bảng điều khiển trò chơi là một phần tử canvas duy nhất.
Trong HTML5, bảng điều khiển trò chơi là một thành phần canvas đơn.

Việc chuyển đổi đối tượng thẻ thông tin cũng là bài tập tương tự. Trong Flash, chúng ta đã sử dụng trường văn bản và hình dạng vectơ:

Thẻ thông tin Flash là sự kết hợp giữa trường văn bản và hình dạng vectơ
Thẻ thông tin Flash là sự kết hợp giữa các trường văn bản và hình dạng vectơ.

Trong HTML5, chúng ta kết hợp 3 sprite hình ảnh trên một phần tử <canvas> duy nhất trong thời gian chạy:

Thẻ thông tin HTML là một tổ hợp gồm 3 hình ảnh.
Thẻ thông tin HTML là một tổ hợp gồm ba hình ảnh.

Bây giờ, chúng ta có 100 canvas (một canvas cho mỗi ô) cộng với một canvas cho bảng điều khiển trò chơi. Sau đây là mã đánh dấu cho thẻ thông tin "H":

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

Dưới đây là CSS tương ứng:

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

Chúng tôi áp dụng các hiệu ứng CSS3 khi thẻ thông tin đang được kéo (đổ bóng, độ mờ và điều chỉnh theo tỷ lệ) và khi thẻ thông tin đang nằm trên giá (phản chiếu):

Thẻ thông tin được kéo sẽ lớn hơn một chút, trong suốt một chút và có bóng đổ.
Thẻ thông tin được kéo sẽ lớn hơn một chút, hơi trong suốt và có bóng đổ.

Việc sử dụng hình ảnh đường quét có một số lợi ích rõ ràng. Thứ nhất, kết quả sẽ là pixel chính xác. Thứ hai, những hình ảnh này có thể được lưu vào bộ nhớ đệm bởi trình duyệt. Thứ ba, với một chút thao tác, chúng ta có thể hoán đổi hình ảnh để tạo thiết kế thẻ thông tin mới (chẳng hạn như thẻ thông tin kim loại) và công việc thiết kế này có thể được thực hiện trong Photoshop thay vì trong Flash.

Nhược điểm là gì? Bằng cách sử dụng hình ảnh, chúng tôi sẽ từ bỏ quyền truy cập có lập trình vào các trường văn bản. Trong Flash, đó là một thao tác đơn giản để thay đổi màu sắc hoặc các thuộc tính khác của loại; trong HTML5, các thuộc tính này được đưa vào chính hình ảnh. (Chúng tôi đã thử văn bản HTML, nhưng loại văn bản này đòi hỏi nhiều mã đánh dấu và CSS bổ sung. Chúng tôi cũng đã thử dùng văn bản trên canvas, nhưng kết quả không nhất quán trên các trình duyệt.)

Logic mờ

Chúng tôi muốn tận dụng tối đa cửa sổ trình duyệt ở mọi kích thước và tránh phải cuộn. Đây là một thao tác tương đối đơn giản trong Flash, vì toàn bộ trò chơi được vẽ theo vectơ và có thể tăng hoặc giảm tỷ lệ mà không làm mất độ trung thực. Nhưng nó phức tạp hơn trong HTML. Chúng tôi đã thử dùng việc điều chỉnh tỷ lệ CSS nhưng kết quả là canvas bị mờ:

Điều chỉnh tỷ lệ CSS (bên trái) so với vẽ lại (bên phải).
Chia tỷ lệ CSS (bên trái) so với vẽ lại (bên phải).

Giải pháp của chúng tôi là vẽ lại bảng trò chơi, giá và ô bất cứ khi nào người dùng đổi kích thước trình duyệt:

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

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

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

Chúng tôi sẽ tạo ra hình ảnh sắc nét và bố cục đẹp mắt ở mọi kích thước màn hình:

Bảng điều khiển trò chơi lấp đầy không gian dọc; các phần tử trang khác di chuyển xung quanh.
Bảng trò chơi lấp đầy không gian dọc; các phần tử trang khác di chuyển xung quanh.

Vào thẳng trọng tâm

Vì mỗi thẻ thông tin đều được đặt đúng vị trí và phải khớp chính xác với bảng điều khiển và giá đỡ trò chơi, nên chúng ta cần một hệ thống định vị đáng tin cậy. Chúng ta sử dụng hai hàm BoundsPoint để giúp quản lý vị trí của các phần tử trong không gian chung (trang HTML). Bounds mô tả một vùng hình chữ nhật trên trang, trong khi Point mô tả toạ độ x,y tương ứng với góc trên cùng bên trái của trang (0,0), hay còn gọi là điểm đăng ký.

Với Bounds, chúng ta có thể phát hiện giao điểm của hai phần tử hình chữ nhật (chẳng hạn như khi một thẻ thông tin đi qua giá đỡ) hoặc liệu một khu vực hình chữ nhật (chẳng hạn như dấu cách gồm hai chữ cái) có chứa điểm tuỳ ý (chẳng hạn như điểm giữa của thẻ thông tin hay không). Dưới đây là cách triển khai 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(",");
}

Chúng tôi sử dụng Point để xác định toạ độ tuyệt đối (góc trên cùng bên trái) của bất kỳ phần tử nào trên trang hoặc của một sự kiện chuột. Point cũng chứa các phương thức để tính khoảng cách và hướng, cần thiết để tạo hiệu ứng ảnh động. Dưới đây là cách triển khai 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);
}

Các hàm này là nền tảng cho chức năng kéo và thả cũng như chức năng ảnh động. Ví dụ: chúng ta sử dụng Bounds.intersects() để xác định xem một thẻ thông tin có chồng lên một không gian trên bảng điều khiển trò chơi hay không; chúng ta sử dụng Point.vector() để xác định hướng của thẻ thông tin được kéo; và chúng ta sử dụng Point.interpolate() kết hợp với bộ tính giờ để tạo hiệu ứng tween chuyển động hoặc gia tốc.

Thuận theo dòng chảy tự nhiên

Trong khi bố cục có kích thước cố định sẽ dễ tạo hơn trong Flash, bố cục linh hoạt sẽ dễ tạo hơn nhiều bằng HTML và mô hình hộp CSS. Hãy xem xét chế độ xem lưới sau đây với chiều rộng và chiều cao có thể thay đổi:

Bố cục này không có kích thước cố định: hình thu nhỏ di chuyển từ trái sang phải, từ trên xuống dưới.
Bố cục này không có kích thước cố định: hình thu nhỏ di chuyển từ trái sang phải, từ trên xuống dưới.

Hoặc cân nhắc bảng trò chuyện. Phiên bản Flash yêu cầu nhiều trình xử lý sự kiện để phản hồi thao tác bằng chuột, mặt nạ cho khu vực cuộn, toán học để tính toán vị trí cuộn và nhiều mã khác để liên kết chúng lại với nhau.

Bảng trò chuyện trong Flash khá nhưng phức tạp.
Bảng trò chuyện trong Flash khá nhưng phức tạp.

So sánh, phiên bản HTML chỉ là một <div> có chiều cao cố định và thuộc tính mục bổ sung được đặt thành ẩn. Thao tác cuộn không tốn phí.

Mô hình hộp CSS hoạt động.
Mô hình hộp CSS đã hoạt động.

Trong những trường hợp như thế này – tác vụ bố cục thông thường – HTML và CSS vượt trội hơn Flash.

Bạn có nghe thấy tôi nói không?

Chúng tôi gặp khó khăn với thẻ <audio> – đơn giản là thẻ này không thể phát nhiều lần các hiệu ứng âm thanh ngắn trong một số trình duyệt. Chúng tôi đã thử hai giải pháp. Trước tiên, chúng tôi đã chèn khoảng thời gian chết vào các tệp âm thanh để làm cho tệp dài hơn. Sau đó, chúng tôi đã thử phát luân phiên trên nhiều kênh âm thanh. Không có kỹ thuật nào hoàn toàn hiệu quả hoặc thoải mái.

Cuối cùng, chúng tôi quyết định chạy trình phát âm thanh Flash của riêng mình và sử dụng âm thanh HTML5 làm phương án dự phòng. Dưới đây là mã cơ bản trong 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);

Trong JavaScript, chúng tôi cố gắng phát hiện trình phát Flash được nhúng. Nếu cách đó không thành công, chúng ta sẽ tạo một nút <audio> cho mỗi tệp âm thanh:

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

Lưu ý rằng tính năng này chỉ hoạt động với các tệp MP3 – chúng tôi không bao giờ bận tâm đến việc hỗ trợ OGG. Chúng tôi hy vọng ngành quảng cáo sẽ chuyển sang một định dạng duy nhất trong tương lai gần.

Vị trí cuộc thăm dò ý kiến

Chúng tôi sử dụng cùng một kỹ thuật trong HTML5 như đã làm trong Flash để làm mới trạng thái trò chơi: cứ 10 giây một lần, ứng dụng khách sẽ yêu cầu máy chủ cập nhật. Nếu trạng thái trò chơi thay đổi kể từ cuộc thăm dò ý kiến gần đây nhất, thì ứng dụng sẽ nhận được và xử lý các thay đổi; nếu không, sẽ không có gì xảy ra. Kỹ thuật thăm dò ý kiến truyền thống này cũng được chấp nhận, nếu không thực sự hiệu quả. Tuy nhiên, chúng ta muốn chuyển sang sử dụng tính năng thăm dò ý kiến dài hoặc WebSockets khi trò chơi hoàn thiện và người dùng kỳ vọng tương tác theo thời gian thực qua mạng. Cụ thể, WebSockets sẽ mang lại nhiều cơ hội để nâng cao trải nghiệm chơi trong trò chơi.

Thật là một công cụ!

Chúng tôi đã sử dụng Bộ công cụ web của Google (GWT) để phát triển cả giao diện người dùng giao diện người dùng và logic điều khiển ở phần phụ trợ (xác thực, xác thực, duy trì, v.v.). Bản thân JavaScript được biên dịch từ mã nguồn Java. Ví dụ: hàm Point được điều chỉnh từ 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));
}
...
}

Một số lớp giao diện người dùng có các tệp mẫu tương ứng, trong đó các phần tử trang được "liên kết" với các thành phần của lớp. Ví dụ: ChatPanel.ui.xml tương ứng với 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>

Thông tin chi tiết đầy đủ nằm ngoài phạm vi của bài viết này, nhưng bạn nên xem GWT cho dự án HTML5 tiếp theo của mình.

Tại sao nên sử dụng Java? Trước tiên, đối với thao tác nhập nghiêm ngặt. Mặc dù tính năng nhập động rất hữu ích trong JavaScript, chẳng hạn như khả năng của một mảng để lưu giữ các giá trị thuộc nhiều loại khác nhau. Tuy nhiên, việc này có thể gây khó khăn cho các dự án lớn, phức tạp. Thứ hai, đối với khả năng tái cấu trúc. Hãy cân nhắc xem bạn có thể thay đổi chữ ký phương thức JavaScript trên hàng nghìn dòng mã – thật không dễ dàng! Tuy nhiên, với một IDE Java tốt, việc này sẽ diễn ra rất nhanh chóng. Cuối cùng, dành cho mục đích thử nghiệm. Việc viết mã kiểm thử đơn vị cho các lớp Java sẽ đánh bại kỹ thuật "lưu và làm mới" theo thời gian.

Tóm tắt

Ngoại trừ sự cố về âm thanh, HTML5 vượt quá sự mong đợi của chúng tôi. Wordico không chỉ trông đẹp như trong Flash mà còn linh hoạt và phản hồi nhanh. Chúng tôi không thể làm được điều này nếu không có Canvas và CSS3. Thách thức tiếp theo của chúng ta: điều chỉnh Wordico cho phù hợp với việc sử dụng trên thiết bị di động.