Giới thiệu
Khi 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à quên đi mọi kiến thức đã biết về việc tạo trải nghiệm phong phú cho người dùng trong trình duyệt. Mặc dù Flash cung cấp một API toàn diện cho mọi khía cạnh của quá trình phát triển ứng dụng – từ vẽ vectơ đến phát hiện lượt nhấn đa giác cho đến phân tích cú pháp XML – nhưng HTML5 lại cung cấp một 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 tự hỏi 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 để tạo trò chơi hay không. Trò chơi có hiển thị đồng nhất trên các trình duyệt như trong Flash không, đồng thời có giao diện và hoạt động tốt như vậy không? Đối với Wordico, câu trả lời là có.
Victor, vectơ của bạn là gì?
Chúng tôi đã phát triển phiên bản gốc của Wordico chỉ bằng đồ hoạ vectơ: đường thẳng, đườ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:
Chúng tôi cũng tận dụng tiến trình 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
:
Tuy nhiên, trong HTML5, chúng ta sử dụng sprite bitmap:
Để tạo bảng 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 không gian được biểu thị bằng một ký tự khác nhau (chẳng hạn như "t" cho chữ cái ba lần và "T" cho từ ba lần). Đây là một thao tác đơn giản trong Flash; chúng ta chỉ cần đóng dấu các khoảng trống và sắp xếp chúng theo 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, việc này phức tạp hơn một chút. Chúng ta sử dụng phần tử <canvas>
, một nền tảng vẽ bitmap, để vẽ từng ô trên bảng trò chơi. Bước đầu tiên là tải sprite hình ảnh. Sau khi tải, chúng ta lặp lại qua 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;
}
Dưới đây là kết quả trong trình duyệt web. Xin lưu ý rằng chính canvas cũng có bóng đổ CSS:
Việc chuyển đổi đối tượng thẻ thông tin cũng tương tự như vậy. Trong Flash, chúng ta đã sử dụng trường văn bản và các hình dạng vectơ:
Trong HTML5, chúng ta kết hợp ba sprite hình ảnh trên một phần tử <canvas>
duy nhất trong thời gian chạy:
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 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 ta áp dụng hiệu ứng CSS3 khi thẻ thông tin đang được kéo (bóng, độ mờ và tỷ lệ) và khi thẻ thông tin nằm trên giá đỡ (hiệu ứng phản chiếu):
Việc sử dụng hình ảnh đường quét có một số ưu điểm rõ ràng. Trước tiên, kết quả có độ chính xác đến từng pixel. Thứ hai, trình duyệt có thể lưu các hình ảnh này vào bộ nhớ đệm. Thứ ba, với một chút công sứ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? Khi sử dụng hình ảnh, chúng ta 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, việc thay đổi màu sắc hoặc các thuộc tính khác của loại hình ảnh là một thao tác đơn giản; trong HTML5, các thuộc tính này được tích hợp sẵn vào hình ảnh. (Chúng tôi đã thử văn bản HTML, nhưng cần thêm nhiều mã đánh dấu và CSS. Chúng tôi cũng đã thử 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 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ẽ bằng vectơ và có thể được tăng hoặc giảm tỷ lệ mà không làm mất độ trung thực. Nhưng việc này phức tạp hơn trong HTML. Chúng tôi đã thử sử dụng tính năng điều chỉnh theo tỷ lệ CSS nhưng kết quả là canvas bị mờ:
Giải pháp của chúng tôi là vẽ lại bảng trò chơi, giá đỡ và thẻ thông tin bất cứ khi nào người dùng thay đổi kích thước trình duyệt:
window.onresize = function (evt) {
...
gameboard.setConstraints(boardWidth, boardWidth);
...
rack.setConstraints(rackWidth, rackHeight);
...
tileManager.resizeTiles(tileSize);
});
Cuối cùng, chúng ta có được hình ảnh sắc nét và bố cục bắt mắt ở mọi kích thước màn hình:
Vào thẳng vấn đề
Vì mỗi thẻ thông tin được định vị tuyệt đối và phải căn chỉnh chính xác với bảng trò chơi và giá đỡ, 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 Bounds
và Point
để giúp quản lý vị trí của các phần tử trong không gian toàn cục (trang HTML). Bounds
mô tả một vùng hình chữ nhật trên trang, còn 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), 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 cắt ngang giá đỡ) hoặc liệu một vùng hình chữ nhật (chẳng hạn như khoảng trống hai chữ cái) có chứa một đ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 &&
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(",");
}
Chúng ta sử dụng Point
để xác định toạ độ tuyệt đối (góc trên 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 toán 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 của các tính năng kéo và thả cũng như ả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 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ộ hẹn giờ để tạo hiệu ứng chuyển động tween hoặc hiệu ứng làm dịu.
Thuận theo dòng chảy tự nhiên
Mặc dù bố cục có kích thước cố định dễ tạo hơn trong Flash, nhưng bố cục linh hoạt lại 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 biến đổi:
Hoặc cân nhắc sử dụng 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 các thao tác của chuột, mặt nạ cho vùng có thể cuộn, toán học để tính toán vị trí cuộn và nhiều mã khác để kết hợp các thành phần này với nhau.
So sánh với phiên bản HTML, phiên bản này chỉ là một <div>
có chiều cao cố định và thuộc tính tràn được đặt thành ẩn. Chúng tôi không mất phí khi cuộn.
Trong những trường hợp như thế này – các tác vụ bố cục thông thường – HTML và CSS vượt trội so với 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>
– thẻ này đơn giản là không thể phát hiệu ứng âm thanh ngắn nhiều lần trong một số trình duyệt nhất định. Chúng tôi đã thử hai giải pháp. Trước tiên, chúng tôi đã thêm khoảng lặng vào các tệp âm thanh để kéo dài thời lượng. Sau đó, chúng tôi thử luân phiên phát trên nhiều kênh âm thanh. Cả hai kỹ thuật này đều không hoàn toàn hiệu quả hoặc tinh tế.
Cuối cùng, chúng tôi quyết định phát triển 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 ta cố gắng phát hiện trình phát Flash được nhúng. Nếu 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();
}
}
Xin lưu ý rằng tính năng này chỉ hoạt động với tệp MP3 – chúng tôi chưa bao giờ hỗ trợ OGG. Chúng tôi hy vọng ngành công nghiệp này sẽ thống nhất một định dạng trong tương lai gần.
Vị trí cuộc thăm dò ý kiến
Chúng ta sử dụng cùng một kỹ thuật trong HTML5 như trong Flash để làm mới trạng thái trò chơi: cứ 10 giây, ứng dụng 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ừ lần thăm dò ý kiến gần đây nhất, thì ứng dụng sẽ nhận và xử lý các thay đổi đó; nếu không, sẽ không có gì xảy ra. Bạn có thể chấp nhận kỹ thuật thăm dò ý kiến truyền thống này, mặc dù không được tinh tế lắm. Tuy nhiên, chúng tôi muốn chuyển sang thăm dò ý kiến dài hạn hoặc WebSockets khi trò chơi phát triển và người dùng mong đợi tương tác theo thời gian thực qua mạng. Đặc biệt, WebSockets sẽ mang lại nhiều cơ hội để nâng cao trải nghiệm chơi 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 phía trước và logic kiểm soát phía sau (xác thực, xác thực, lưu 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 thành phần trang được "liên kết" với các thành viên 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 tìm hiểu 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 kiểu nhập nghiêm ngặt. Mặc dù tính năng nhập động rất hữu ích trong JavaScript (ví dụ: khả năng của một mảng lưu giữ các giá trị thuộc nhiều loại), nhưng tính năng này có thể gây phiền toái trong các dự án lớn, phức tạp. Thứ hai, để có các tính năng tái cấu trúc. Hãy cân nhắc cách bạn thay đổi chữ ký phương thức JavaScript trên hàng nghìn dòng mã – không dễ dàng! Nhưng với một IDE Java tốt, việc này rất dễ dàng. Cuối cùng, cho mục đích kiểm thử. Việc viết mã kiểm thử đơn vị cho các lớp Java sẽ hiệu quả hơn so với kỹ thuật "lưu và làm mới" đã có từ lâu.
Tóm tắt
Ngoại trừ vấn đề về âm thanh, HTML5 đã vượt xa mong đợi của chúng tôi. Wordico không chỉ trông đẹp như trong Flash mà còn mượt mà và thích ứng. Chúng tôi không thể làm được điều này nếu không có Canvas và CSS3. Thử thách tiếp theo của chúng ta: điều chỉnh Wordico để sử dụng trên thiết bị di động.