個案研究 - 將 Wordico 從 Flash 轉換為 HTML5

Adam Mark
Adam Mark
Adrian Gould
Adrian Gould

簡介

當我們將 Wordico 填字遊戲從 Flash 轉換為 HTML5 時,第一個任務就是忘掉所有關於在瀏覽器中打造豐富使用者體驗的知識。雖然 Flash 提供單一且全面的 API,可用於所有應用程式開發作業 (從向量繪圖到多邊形命中偵測,再到 XML 剖析),但 HTML5 則提供一堆規格,並支援各種瀏覽器。我們也想知道,HTML 這類文件專用語言,以及 CSS 這類以方塊為主的語言,是否適合用於建構遊戲。遊戲是否會在各瀏覽器中顯示一致的內容,就像在 Flash 中一樣,且外觀和運作方式是否一樣良好?Wordico 的答案是「是」

你的向量是什麼,Victor?

我們開發原始版本的 Wordico 時,只使用了線條、曲線、填充和漸層等向量圖形。結果不僅非常精簡,還可無限擴充:

Wordico 線框
在 Flash 中,每個顯示物件都是由向量圖形組成。

我們也利用 Flash 時間軸建立具有多個狀態的物件。舉例來說,我們為 Space 物件使用了九個命名關鍵影格:

Flash 中的三字母空格。
Flash 中的三字母空格。

不過,在 HTML5 中,我們使用位階圖片方塊:

顯示所有九個空格的 PNG 圖像。
顯示所有九個區塊的 PNG 圖像集。

為了從個別空格建立 15x15 的遊戲板,我們會重複執行 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 中使用文字欄位和向量圖形:

閃光資訊方塊是文字欄位和向量圖形的組合
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);
});

最終結果是清晰的圖片和任何螢幕尺寸都適合的版面配置:

遊戲板會填滿垂直空間,其他頁面元素會環繞在遊戲板周圍。
遊戲板會填滿垂直空間,其他網頁元素會圍繞在周圍。

切中要點

由於每個方塊都會以絕對位置定位,且必須與遊戲板和機架精準對齊,因此我們需要可靠的定位系統。我們使用 BoundsPoint 這兩個函式,協助管理全域空間 (HTML 網頁) 中的元素位置。Bounds 會描述網頁上的矩形區域,而 Point 會描述相對於網頁左上角 (0,0) 的 x,y 座標,也就是註冊點。

透過 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>。捲動畫面不會產生任何費用。

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。特別是 WebSocket,可提供許多機會來提升遊戲體驗。

真是太好用了!

我們使用 Google Web Toolkit (GWT) 開發前端使用者介面和後端控制邏輯 (驗證、驗證、持久性等)。JavaScript 本身是從 Java 原始碼編譯而來。例如,Point 函式是從 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));
}
...
}

部分 UI 類別具有對應的範本檔案,其中網頁元素會「繫結」至類別成員。例如,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>

完整詳細資訊不在本文的討論範圍內,但我們建議您在下一個 HTML5 專案中使用 GWT。

為什麼要使用 Java?首先,針對嚴格型別。雖然動態類型在 JavaScript 中相當實用 (例如陣列可保留不同類型的值),但在大型複雜專案中可能會造成困擾。第二個原因是為了重構功能。想想如何在成千上萬行的程式碼中變更 JavaScript 方法簽名,這可不是件容易的事!但只要使用優質的 Java IDE,就能輕鬆完成。最後,為測試目的,為 Java 類別編寫單元測試,比起傳統的「儲存並重新整理」技巧更為優異。

摘要

除了音訊問題,HTML5 的表現遠遠超出我們的預期。Wordico 不僅外觀與 Flash 版本相同,其流暢度和回應速度也與 Flash 版本不相上下。沒有 Canvas 和 CSS3,我們也無法完成這項工作。接下來要面對的挑戰:將 Wordico 改編成行動應用程式。