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

Adam Mark
Adam Mark
Adrian Gould
Adrian Gould

簡介

Wordico 填字遊戲從 Flash 轉換為 HTML5 後,我們的第一項任務就是瞭解我們所知道,如何在瀏覽器中提供豐富的使用者體驗。雖然 Flash 為應用程式開發的各個方面提供一個全方位的 API (從向量繪圖、多邊形命中偵測到 XML 剖析),HTML5 提供了極為豐富的規格,並且支援不同的瀏覽器。我們還懷疑 HTML、文件專屬的語言,以及 CSS (以方塊為主的語言) 是否適合製作遊戲。遊戲會像在 Flash 中一樣,在不同的瀏覽器上平均顯示嗎?外觀和行為是否與 Flash 相同?Wordico 的答案為「是」

Victor 是什麼媒介?

我們開發的原始版 Wordico 僅使用線條、曲線、填滿和漸層等向量圖形。我們進而成功地做出了十分精簡且可無限擴充的成果:

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

我們也利用 Flash 時間軸來建立擁有多個狀態的物件。舉例來說,我們針對 Space 物件使用了九個命名的主要畫面格:

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

但在 HTML5 中,我們使用點陣圖這個精靈:

顯示全部 9 個空格的 PNG Sprite。
顯示全部 9 個空格的 PNG Sprite。

為了從個別空間建立 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> 元素 (點陣圖繪圖介面) 繪製遊戲板一次繪製一個正方形。第一步是載入圖片 Sprite。載入後,我們會疊代版面配置標記法,每次疊代都會繪製圖片的不同部分:

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 圖塊是文字欄位和向量形狀的組合
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 Box 模型的運作方式。

在本例中是一般的版面配置工作,也就是 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 Player。如果失敗,我們會為每個音檔建立 <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.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>

這些完整細節不在本文的討論範圍內,但建議您參考 GWT 提供的下一個 HTML5 專案。

使用 Java 的好處首先,設為嚴格輸入。動態輸入在 JavaScript 中非常實用 (例如,陣列可保留不同類型的值),但在大型且複雜的專案中可能會令人頭痛。第二,是重構能力想想看,變更 JavaScript 方法簽名在數千行程式碼中的方式 - 不容易!不過,有了優秀的 Java IDE,即可快速完成。最後是測試目的。為 Java 類別撰寫單元測試時的技術超越了「儲存並重新整理」的時間指定技術。

摘要

除了音訊問題外,HTML5 的表現遠遠超乎我們的預期。Wordico 不僅呈現和在 Flash 中一樣美觀的效果,更是流暢快速、反應靈敏。沒有 Canvas 和 CSS3 是我們辦不到的。我們下一個挑戰:針對行動裝置使用者調整 Wordico。