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

はじめに

Wordico クロスワード ゲームを Flash から HTML5 に移行した際、まず最初に、ブラウザでリッチなユーザー エクスペリエンスを実現するためにこれまで学んできたことをすべて忘れました。Flash は、ベクター描画からポリゴンのヒット検出、XML 解析まで、アプリケーション開発のあらゆる側面に対応する単一の包括的な API を提供していましたが、HTML5 は、さまざまなブラウザのサポート状況に応じて仕様がバラバラでした。また、ドキュメント固有の言語である HTML と、ボックス中心の言語である CSS がゲームの作成に適しているかどうかも疑問でした。ゲームは Flash と同様に、ブラウザ間で均一に表示されますか?また、見た目や動作は Flash と同様に良好ですか?Wordico の場合、答えは「はい」でした。

Victor 様、ベクトルをお知らせください。

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

Wordico ワイヤーフレーム
Flash では、すべてのディスプレイ オブジェクトはベクター シェイプで作成されていました。

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

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

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

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

個々のスペースから 15 x 15 のゲームボードを作成するために、225 文字の文字列表記を反復処理します。この表記では、各スペースが異なる文字で表されます(3 文字の単語の場合は「t」、3 文字の単語の場合は「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> 要素を使用して、ゲームボードを 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 では、実行時に 3 つの画像スプライトを 1 つの <canvas> 要素に結合します。

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 エフェクトが適用されます。

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

ラスター画像を使用するには、明らかな利点があります。まず、結果はピクセル単位で正確です。2 つ目は、これらの画像がブラウザによってキャッシュに保存される可能性があることです。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);
});

結果として、どの画面サイズでも鮮明な画像と美しいレイアウトが実現します。

ゲームボードが垂直方向のスペースを占有し、他のページ要素がその周囲に配置されます。
ゲームボードが縦方向のスペースを占有し、他のページ要素がその周囲に配置されます。

要点を簡潔に伝える

各タイルは絶対位置で配置され、ゲームボードとラックと正確に位置合わせされる必要があるため、信頼性の高い位置決めシステムが必要です。グローバル空間(HTML ページ)内の要素の位置を管理するために、Bounds 関数と Point 関数を使用します。Bounds はページ上の長方形の領域を表し、Point はページの左上隅(0,0)を基準とした x,y 座標を表します。この座標は登録ポイントとも呼ばれます。

Bounds を使用すると、2 つの長方形要素の交差(タイル レイヤがラックを横切る場合など)や、長方形の領域(2 文字のスペースなど)に任意の点(タイルの中心に位置する点など)が含まれているかどうかを検出できます。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 バージョンは、高さが固定され、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 プレーヤーを検出しようとします。これが失敗した場合は、サウンドファイルごとに <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 を使用する理由まず、厳格な型付けについて説明します。動的型付けは、配列がさまざまな型の値を保持できるなど、JavaScript では便利ですが、大規模で複雑なプロジェクトでは頭痛の種になる可能性があります。2 つ目は、リファクタリング機能です。数千行のコードにわたって JavaScript メソッド シグネチャを変更する方法について考えてみましょう。簡単ではありません。ただし、優れた Java IDE を使用すれば、簡単にできます。最後に、テスト用です。Java クラスの単体テストを作成すると、古くからある「保存して更新」の手法よりも優れた結果が得られます。

概要

音声に関する問題を除き、HTML5 は期待を大きく上回りました。Wordico は Flash 版と同様に美しく、スムーズでレスポンシブな動作を実現しています。Canvas と CSS3 なしでは実現できませんでした。次の課題は、Wordico をモバイル用に適応させることです。