読み書きプログラミング

日常のプログラミングで気づいたことを綴っています

「iPad用HTML5ゲームを作り公開する – PART 2」

http://cubiq.org/build-and-publish-an-html5-game-for-ipad-part-2


HTML5ゲーム開発は着実に進んでいます。先週はウェブsqlとlocalStorage, PhoneGap APIを実験していました。当初見積もったよりも少し複雑だと白状しますが、チャレンジがなければ楽しくないでしょう。

The HTML5 game development holds steady, last week I was experimenting with web sql, localStorage and the PhoneGap APIs. I admit that it is slightly more complicated than I initially estimated, but it wouldn’t be fun if it weren’t challenging.

  • はじめに
  • PhoneGap環境とデータベース(現在ここです)

PHONEGAP環境

この最初の段階では実際にはPhoneGap上では開発していませんが、コードがブラウザとPG WebView上両方で動くようになることを確かめたいです。PG用の開発はブラウザ用の開発とほとんど同様ですが、いくつかの顕著な例外があります。

I’m not actually developing on PhoneGap in this first stage, but I want to be sure that the code is going to work on both the browser and the PG WebView. Developing for PG is almost like developing for the browser, with some notably exceptions.


時々(特にゲームでは)デバイスの向きをロックすることができることが重要です。幸い、PGは、ちょうどそれをしてくれるshouldRotateToOrientationと呼ばれる(ドキュメント化されていない)関数をサポートしています。

Sometimes (especially for games) it’s important to be able to lock the device orientation. Fortunately PG supports a (undocumented) function called shouldRotateToOrientation that does just that.


以下をコードのグローバルにアクセス可能な部分のどこかに加えるだけで、スクリーンをポートレートモードにロックできます。

Adding the following to any globally accessible part of code is all you need to lock the screen into portrait mode:

function shouldRotateToOrientation (rotation) {
    return !(rotation%180);
}


ランドスケープにするには!を削除してください。コードの一番最初(phonegap.jsをインクルードするよりも前)に関数定義を追加しています。PGがコールした時確実に関数が定義されているようにするためです。

Remove the ! for landscape. I’m adding the function definition as the very first thing in my code (even before including the phonegap.js file) so I’m totaly sure that the function is defined when called by PG.


ブラウザとPGの他の違いはdocument ready検出です。

Another difference between browser and PG is document ready detection.


ブラウザがいつ素材をすべてロードしてコードをロックする準備ができたか知りたければ、通常、DOMContentLoadedかロードイベントをリッスンするでしょう。PGはネイティブとJSコードの間に分割されていて、アプリケーションが始動するには両方が準備できて走っている必要があります。

If you want to know when the browser loaded all its stuff and is ready to rock your code you’d usually listen to DOMContentLoaded or load events. PG is split between native and JS code and both need to be up and running for your application to kick start.


このためにPGはdevicereadyイベントと提供します。いくつかの理由のため、このイベントはドキュメントロードイベントの後、コールされるようです。思ったより長い時間ドキュメントを読むのに費やした後、以下のコンフィギュレーションに至りました。とても安定しているようです。

For this purpose PG offers the deviceready event. For some reasons this event seems to be called after the document load event. After spending more time than I wished on the documentation I ended up with the following configuration that seems to be pretty stable.

<head>
<script type="text/javascript">
function loaded () {
	if (window.PhoneGap) document.addEventListener("deviceready", ready, false);
	else ready();
}

function ready () {
	setTimeout(function () {
		Game.init();
	}, 200);
}

</script>
</head>

<body onload="loaded()">
...
</body>

document load上で、もしPhoneGapが定義されていたら(すなわち、標準ブラウザ上でなければ)、devicereadyイベントのリッスンを開始します。そうでないならすぐにアプリケーションを起動できます。

On document load if PhoneGap is defined (ie: if I’m not on the standard browser) we start listening to the deviceready event, otherwise we can fire the application right away.


200m遅らせてからready()関数を実行していることに気がつくでしょう。必ずしも必要ではありませんが、エンジンを2,3ms休ませると予測不可能で不可思議なDOMエラーを回避する助けになると思っています。

You’ll notice that I execute the ready() function with 200ms delay. While not strictly needed, I believe that letting the engine rest for few ms helps to avoid unpredictable and weird DOM errors.


これは特にたくさんの複雑なCSSアニメーションを扱う時に言えることです。私はしばしばアニメーションや遷移を集中させる必要があり、それぞれのステップの終わりにあるコードを実行しています。

This is particularly true when dealing with many complex CSS animations. I often need to concatenate animations or transitions and execute some code at the end of each step.


例: 要素をフェードアウト > コンテンツを更新 > 要素をフェードイン > 要素をx, yに移動 > "finished"変数を設定。プロジェクトが複雑になりたくさんのアニメーションが同時に起こる時、エンジンを2,3ms休ませると、安定性を改善し、与えられたタイムフレーム内でアニメーションを維持する助けとなります。(200msというのは「100msと2秒の間のどれでも」という感じですからね。)

Eg: fade out element > update content > fade in element > move element to x,y > set the “finished” variable. When the project becomes complex and many animations occur at the same time, letting the engine rest for few ms improves stability and helps keeping the animations within the given timeframe (you know, 200ms is more like “anything between 100ms and 2 seconds”).


別の重要な面はローカリゼーションです。ワードゲームを作っているので、言語を変えることができるということが重要です。アプリケーションを国際化する最も簡単な方法はnavigator.languageプロパティを読むことだということに気づきました。それはen-USやit-ITのようなデバイスのローカルを保持しています。他にすることは言語変数に基づいてローカライズされたテキストを含むJSONファイルをロードすることだけです。簡単ですね。

Another important aspect is localization. I’m building a word game, so being able to change language is crucial. I found that the easiest way to internationalize the application is by reading the navigator.language property. It holds the device locale such as en-US or it-IT. All left to do is to load a JSON file with the localized texts based on the language variable. A piece of cake.

AJAXコール

私は、<script>タグでjavascriptファイルを選択的にインクルードするよりもXMLHttpRequestでデータファイルをロードするのが好きです。その時の問題は、PG上のファイルはすべてローカルで、ウェブサーバーが提供するのではないことです。これは、ファイルがロードされた時、200 (OK)や304 (cached)ステータスコードを取得できないことを意味します。返されるステータスはいつも0です。外部のajaxライブラリを使うなら、ライブラリはこの違いに気がつくかもしれませんし気がつかないかもしれません。

I prefer to load data files with XMLHttpRequests instead of selectively including javascript files with the tag. The problem here is that files on PG are all local, they are not served by a web server. This means that you do not get a 200 (OK) or 304 (cached) status codes when the file is loaded. The returned status is instead always 0. If you use an external ajax library it may or may not be aware of this difference.


ajaxコールのとてもぎこちない例はこんな感じです:

A very lame example of ajax call is like so:

var req = new XMLHttpRequest();
req.open('GET', 'filename', true);
req.onreadystatechange = function () {
	if (req.readyState != 4) return;

	if (req.status !== 0 && req.status != 200 && req.status != 304) {
		// Error
		return;
	}

	// Success! Do something here.
}

req.send(null);


私がreq.statusを200, 304と合わせて0もチェックしていることに気がつくでしょう。この方法で、httpリクエストをブラウザとPhoneGap両方で使うことができます。もしあなたのajaxライブラリがPG上で動作しないなら、今ならなぜかわかるし、小さな変更で簡単に微調整できますね。脱線しますが、そう、これら9行のコードがajaxに必要なすべてです。

You’ll notice that I’m checking req.status for 0 together with 200 and 304. This way we can use http requests in both the browser and PhoneGap. If your ajax library doesn’t work on PG you now know why, and you can easily tweak it with a small change. As a side story… yes, those 9 lines of code are pretty much all you need to ajax.


この時点で、ブラウザ上でもデバッグできるPhoneGap対応環境を得たはずです。

At this point I should have a PhoneGap ready environment that I can also debug on the browser.

ウェブ SQL VS. LOCALSTORAGE VS. JS オブジェクト

ワードゲームを開発していて、当然辞書に基づきます。莫大な量のデータを扱うことはJS上では決して容易ではありません、特にアプリケーションがオフラインの場合は。

I’m developing a word game that of course is based on a dictionary. Working with huge amount of data is never easy on JS, especially if your application is offline.


主に3つの選択を考えました: ウェブ sqlと, localStorage, JSオブジェクトです。以下はそれぞれの技術の是非です。

I had mainly three choices: web sql, localStorage and a JS Object. The following are the pros and cons of each technique.

ウェブ SQL

すべてはsqliteデータベースに保持されます。サイズ制限は5MBですが、インデックスされたテーブルはたくさんのスペースを取るので、実際には2MB前後のデータを保存できます。

Everything is held into a sqlite database. The size limit is 5mb but indexed tables take a lot of space, so you can actually store around 2mb of data.


是:

  • 非常にフレキシブルでとても複雑はSQLクエリを実行できます。
  • データベースにデータを保存するのはどういうわけか「自然」に感じます。

Extremely flexible, you can perform very complex SQL queries.
It somehow feels “natural” to store data into a database.


非:

  • DBをメンテしなくてはいけません。リビジョン間でのdbの違いに注意しなければいけません。
  • インデックスされていないデータ上のクエリは遅いです。
  • APIが他の方法より複雑です。
  • DB must be maintained, you have to take care of db differences between revisions.
  • Queries on not indexed data are slow.
  • APIs are more complicated than the other methods.
LOCALSTORAGE

ワードリストを直接localStorageに入れて、正規表現か文字列からオブジェクトへのon-the-fly変換でアクセスします。制限はこれも5MBですが、データはUTF-6で保存されるのでそれぞれの文字は2倍のスペースを取り、トータルの保存可能量は2.5MBになります。

The word list is injected directly into a localStorage and accessed with regular expressions or on-the-fly string to object conversion. The limit is again 5mb but data is saved in UTF-16, so each character takes twice the space, bringing the total storable size to 2.5mb.


是:

  • 簡単なAPI, 管理がとても簡単。
  • 少量のデータではとても速い。
  • Simple API, very easy to manage.
  • Very fast with small chunks of data.


非:

  • 文字列だけが保存できる。(オブジェクトや配列は現時点ではサポートされていない。)
  • 大量のデータでは遅い。
  • You can store only strings (objects and arrays are not supported at this time).
  • Slow with lots of data.
JSオブジェクト

全データベースをJS配列かJSオブジェクトにロードする。

  • Load the whole database into a JS array/object.


是:

  • 速ーい!
  • 管理や単語の検索が超簡単。
  • Faaaaaaaaaast!
  • Super easy to manage and search for words


非:

  • デバイスメモリをすべて飲み込む。
  • It sucks up all the device memory


JSオブジェクトの道を進む唯一の理由は「対CPU」モードを提供することです。他の方法ではiPad 1で1ラウンドにつき10から30秒必要で、受け入れられません。問題はブラウザ上でどれだけのメモリをあてがうことができるか決して知ることができないことです。今まで数えきれないテストをしてきて、デバイスの限られたメモリにそんなにたくさんのデータを入れることを確かに気持ちよくは感じません。プロファイリングしたところ、トライ木圧縮された単語リストはRAMを30MBまで消費しました。いくじなしと呼んでください。でもWebViewに絶対の信頼を持っているわけではなく、私のバカなゲームにそんなにも多くのリソースを吸わせるつもりはありません。

The only reason to go the JS Object path is to offer a “versus CPU” mode. The other methods need 10 to 30 seconds per round on iPad 1, that is not acceptable. The problem is that you never know how much memory you can allocate on the browser. I’ve made countless tests and I don’t really feel comfortable putting so much data into the device limited memory. With some profiling a Trie compressed word list took up to 30mb of RAM. Call me coward, but I don’t have all that faith in the WebView, and I’m not going to leech so many resources for my stupid game.


たぶんデータベースの小さな部分をメモリに持たせ、残りをディスクに保存することができますが、それはコードに複雑さを増やします。一方で私はことを簡単で速いままにしておこうとしています。なのでlocalStorageにすることに決めました。時間(回数?)の90%でユーザーは6文字未満の単語を扱っているはずなので、データベースを2つに分けました: 6文字未満と6文字以上 (単語1つ当たりの最大文字数は8)。

You can probably carry a small portion of the database into memory and keep the rest on disk, but that would add complexity to the code while I’m trying to keep it simple and fast. So I decided to go localStorage. 90% of the times the user will be dealing with less than 6 letters words, therefore I split the database into two chunks: less than 6 letters and more than 6 letters (the max letter count per word is 8).


対CPUモードを提供する予定はありません。(あなたは時間と戦うことになるでしょう。)なので速度は問題ではないのですが、6文字単語ファイルはたった100KBでそれを正規表現検索するのは相当速いです。

I’m not going to offer a versus CPU mode (you’ll be fighting against time), so speed is not an issue, but the 6 letters words file takes only 100kb and regexing it is rather fast.


もちろんワードリストは少し圧縮されますが、まだ正規表現でクエリしやすいです。圧縮と正規表現速度の間のいい妥協点を見つけるために相当のテストをしました。ファイルはこんな風です:

Of course the words list is slightly compressed, but it’s still easy to query with RegExp. I made quite a few tests to find a good compromise between compression and regexp speed. The file looks like this:

...;PAL,ACE,AIS,APA,ATE,E,EA,EAE,EAL,ED,ELY,ER,ES,EST,ET,ETS,IER,ING,ISH,L,LED,LET,LIA,LID,LOR,LS,LY,M,MAR,MED,MER,MS,MY,P,PAL,PED,PI,PS,PUS,S,SY,TER,TRY,Y;...


ゲームの最も短い単語は3文字の長さなので、ルートとして三つ組を取り、同じ前置を持つ次の単語からそのルートを削除しました。上のリストは以下のように翻訳されます:

Since the game shortest word is 3 letters long, I take a triplet as root and remove that root from subsequent words with the same prefix. The above list translates into:

PAL
PALACE
PALAIS
PALAPA
PALATE
PALE
...


この構文法でファイルは容易に探索可能なまま40%近く圧縮できます。

This syntax keeps the file easily searchable with a compress ratio close to the 40%.

次の内容

最初のHTML5実験として後知恵でワードゲームを選んだわけではありません。オフラインブラウザでたくさんのデータを扱うことは簡単な仕事ではありませんが、この経験について投稿しようと決めたことを嬉しく思っています。おかげで投げ出したりせず動機を維持するのに役立っています。

With hindsight I wouldn’t have chosen a word game as the first HTML5 experiment. Dealing with lots of data in the offline browser is no easy task, but I’m glad I decided to post about this experience, it helps me to stay on track and motivated.


次回はたぶんゲームのデモか少なくともスクリーンショットをお見せするでしょう。チャンネルはそのまま。

Next time I’ll probably show you a demo or at least a screencast of the game. So stay tuned.

  • はじめに
  • PhoneGap環境とデータベース(現在ここです)