事例紹介 - Wordico を Flash から HTML5 に変換する

はじめに

Wordico のクロスワード ゲームを Flash から HTML5 に変換したときの第一の仕事は、ブラウザで豊かなユーザー エクスペリエンスを生み出すことについて知っていたすべての知識を捨てることでした。Flash は、ベクター描画からポリゴンヒット検出、XML 解析に至るまで、アプリケーション開発のあらゆる側面に対応する単一の包括的な API を提供していましたが、HTML5 はさまざまなブラウザ サポートによって仕様がばらばらでした。また、ドキュメント固有の言語の HTML とボックス中心の言語である CSS がゲーム開発に適しているかどうかも検討しました。Flash の場合と同様に、ゲームはブラウザ間で均一に表示され、見た目や動作も同じように損なわれませんか?Wordico の場合、答えは「はい」でした。

Victor、あなたのベクトルは?

Wordico のオリジナル バージョンは、線、曲線、塗りつぶし、グラデーションのベクター グラフィックのみを使用して開発されました。その結果、非常にコンパクトになり、無制限にスケーラブルになりました。

Wordico ワイヤーフレーム
Flash では、すべての表示オブジェクトがベクター図形で形成されていました。

また、Flash タイムラインを活用して、複数の状態を持つオブジェクトを作成しました。たとえば、Space オブジェクトには 9 つの名前付きキーフレームを使用しました。

Flash の 3 文字スペース。
Flash の 3 文字のスペース。

一方、HTML5 ではビットマップ スプライトを使用します。

9 つのスペースすべてを表示する PNG スプライト。
9 つのスペースすべてを表示する PNG スプライト

個々のスペースから 15x15 のゲームボードを作成するために、各スペースを異なる文字で表した 225 文字の文字列表記を反復処理します(たとえば、「t」は 3 文字、「T」は 3 単語を表します)。これは 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> 要素を使用して、ゲームボードを 1 マスずつ描画します。最初のステップは、画像スプライトの読み込みです。読み込まれた後、レイアウト表記を反復して、反復ごとに画像の別の部分を描画します。

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 では、実行時に 1 つの <canvas> 要素に次の 3 つの画像スプライトを組み合わせます。

HTML タイルは、3 つの画像を組み合わせたものです。
HTML タイルは、3 つの画像を組み合わせたものです。

これで、キャンバスが 100 個(タイルごとに 1 つ)とゲームボード用のキャンバスができました。「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 効果を適用します。

ドラッグしたタイルは少し大きく、やや透明で、ドロップ シャドウが付いています。
ドラッグしたタイルは少し大きく、やや透明で、ドロップ シャドウが付いています。

ラスター画像の使用には明らかなメリットがあります。第 1 に、結果はピクセル精度です。次に、これらの画像をブラウザでキャッシュに保存できます。第 3 に、少し追加作業で、画像を入れ替えて、金属タイルなどの新しいタイル デザインを作成できます。このデザイン作業は、Flash ではなく Photoshop で行えます。

欠点は、画像を使用すると、テキスト フィールドにプログラムからアクセスできなくなります。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 という 2 つの関数を使用して、グローバル空間(HTML ページ)内の要素の位置を管理します。Bounds はページ上の長方形領域を表します。Point はページの左上隅(0,0)を基準とする x,y 座標(登録ポイントとも呼ばれます)を表します。

Bounds を使用すると、2 つの長方形要素の交差(タイルがラックを横切るなど)や、長方形の領域(2 文字のスペースなど)に任意の点(タイルの中心点など)が含まれているかどうかを検出できます。境界の実装は次のとおりです。

// 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 バージョンは、高さが固定され、overflow プロパティが hidden に設定された <div> にすぎません。スクロールには費用は発生しません。

CSS ボックスモデルの仕組み
CSS ボックスモデルの仕組み

通常のレイアウト タスクでは、HTML と CSS は Flash よりも優れています。

聞こえますか?

一部のブラウザでは短い効果音を繰り返し再生できないことが単純で、<audio> タグには苦労していました。次の 2 つの回避策を試しました。まず、沈黙時間でサウンド ファイルをパディングして長くしました。次に、複数の音声チャンネルで交互に再生してみました。どちらのテクニックも、完全に効果的でも洗練されたものでもありません。

最終的には、独自の 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 はサポートしていません。近い将来、業界が 1 つのフォーマットに定着することを願っています。

アンケートの位置

ゲーム ステータスの更新には、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.xmlChatPanel.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 を使用する理由1 つ目は厳密な入力ですJavaScript では動的入力(たとえば配列がさまざまな型の値を保持できる)は便利ですが、大規模で複雑なプロジェクトでは頭痛の種になります。2 つ目は機能のリファクタリングですJavaScript のメソッド シグネチャを、何千行ものコードにわたって変更する方法を考えてみましょう。これは簡単ではありません。優れた Java IDE を使用すれば簡単に行えます。最後に、テストを目的としています。Java クラスの単体テストの作成は、古くからある「保存して更新」の手法よりも優れています。

まとめ

音声の問題を除けば、HTML5 は期待を大きく上回るものでした。Wordico は、Flash と同じくらい見栄えが良いだけでなく、滑らかで応答性に優れています。これは、Canvas と CSS3 なしでは実現できなかったでしょう。次の課題は、Wordico をモバイル対応にすることです。