事例紹介 - Bouncy Mouse

はじめに

バウンシーネズミ

昨年末に iOS と Android 向けに Bouncy Mouse を公開した後、非常に重要な教訓をいくつか学びました。中でも鍵となったのは、既存の市場に参入するのは難しいことでした。完全に飽和状態にある iPhone 市場では、牽引力を得るのは非常に困難でした。飽和度の低い Android マーケットプレイスでは、進展は容易でしたが、それでも容易ではありませんでした。 この経験から、Chrome ウェブストアで興味深い機会を見つけました。Web Store は決して空きはありませんが、HTML5 ベースの高品質なゲームのカタログは成熟し始めたばかりです。これは、新しいアプリ デベロッパーにとって、ランキング チャートの作成と可視化がはるかに容易になったことを意味します。この機会を念頭に、Bouncy Mouse を HTML5 に移植することにしました。これは、エキサイティングな新規ユーザーベースに最新のゲームプレイ体験を提供できるようにしたいと考えています。 この事例では、Bouncy Mouse を HTML5 に移植する一般的なプロセスについて少し説明してから、興味深い 3 つの分野(オーディオ、パフォーマンス、収益化)について掘り下げます。

C++ ゲームを HTML5 に移植する

Bouncy Mouse は現在、Android(C++)、iOS(C++)、Windows Phone 7(C#)、Chrome(JavaScript)でご利用いただけます。 ここで、「複数のプラットフォームに簡単に移植できるゲームを作るにはどうすればよいのか」という疑問が生じることがあります。皆さんは、ハンドポートに頼ることなく、このレベルの持ち運びができる魔法の薬を望んでいるのだと思います。 残念ながら、そのようなソリューションがまだ存在するかはわかりません(最も近いものはおそらく Google の PlayN フレームワークUnity エンジンでしょうが、どれも私が興味を持ったすべてのターゲットには当てはまりません)。私のアプローチは実はハンドポートでした。最初に iOS/Android 版を C++ で作成し、新しいプラットフォームごとにこのコードを移植しました。大変な作業のように思われるかもしれませんが、WP7 と Chrome のバージョンはそれぞれ 2 週間もかからずに完了しました。 さて、ここで問題となるのは、コードベースを簡単に移植可能にするためにはどうすればよいのかということです。この件に関して、私が行ったことがいくつかあります。

コードベースを小さくする

当然のことのように思えるかもしれませんが、ゲームを迅速に移植できたのはそれが主な理由です。Bouncy Mouse のクライアント コードは、たった約 7,000 行の C++ です。7,000 行のコードは何もとはいえませんが、扱いやすい程度に小さなものです。最終的に、クライアント コードの C# 版と JavaScript 版はどちらもほぼ同じサイズになりました。コードベースを小さくするには、基本的に 2 つの重要なプラクティスが必要でした。余分なコードを記述しないことと、可能な限り前処理(実行時以外の)コードを実行することです。 余計なコードを書かないことは当然のことのように思えますが、私は常に自分自身と戦ってきました。私はしばしば、ヘルパーに組み込めるものについて、ヘルパークラス/関数を記述したくなることがあります。しかし、実際にヘルパーを複数回使用する予定でない限り、通常はコードが肥大化します。Bouncy Mouse では、少なくとも 3 回使用する場合を除き、ヘルパーを作成しないように気をつけていました。ヘルパークラスを作成したときは、それを将来のプロジェクトのためにクリーンで移植可能にし、再利用できるようにしようとしました。一方、再利用される可能性が低い Bouncy Mouse 専用のコードを記述する場合、コードの記述方法が「きれい」でなくても、可能な限り簡単かつ迅速にコーディング タスクを実行することに重点を置きました。コードベースを小さく保つための 2 つ目の重要な部分は、可能な限り前処理ステップをプッシュすることでした。ランタイム タスクを前処理タスクに移動できれば、ゲームの実行が速くなるだけでなく、新しいプラットフォームごとにコードを移植する必要がなくなります。 たとえば、私は当初、レベルのジオメトリ データをかなり未処理な形式で保存し、実行時に実際の OpenGL/WebGL 頂点バッファを組み立てていました。これには、簡単な設定と、数百行のランタイム コードが必要でした。その後、このコードを前処理ステップに移動し、コンパイル時に完全にパックされた OpenGL/WebGL 頂点バッファを書き出しました。実際のコードの量はほぼ同じですが、この数百行が前処理ステップに移動されました。つまり、新しいプラットフォームにそれらを移植する必要がなくなりました。Bouncy Mouse にはこれを示す例が数多くあり、できることはゲームによって異なりますが、実行時には必要のないものについては注意してください。

不要な依存関係を使用しない

Bouncy Mouse が移植しやすいもう一つの理由は、依存関係がほとんどないからです。次のグラフは、Bouncy Mouse のプラットフォームごとの主なライブラリ依存関係をまとめたものです。

Android iOS HTML5 WP7
グラフィック OpenGL ES OpenGL ES WebGL XNA
サウンド OpenSL ES OpenAL ウェブ オーディオ XNA
物理学 Box2D Box2D Box2D.js Box2D.xna

以上です。すべてのプラットフォームで移植可能な Box2D 以外に、大きなサードパーティ ライブラリは使用されていません。グラフィックについては、WebGL と XNA はどちらも OpenGL とほぼ 1:1 でマッピングされるため、これは大きな問題ではありませんでした。サウンドの領域でのみ、実際のライブラリが異なっていました。しかし、Bouncy Mouse のサウンドコードは小さいため(プラットフォーム固有のコードが約 100 行)、大きな問題ではありませんでした。Bouncy Mouse に移植できない大きなライブラリを使用しないということは、言語が変更されても、ランタイム コードのロジックがバージョン間でほぼ同じになることを意味します。さらに、ポータブルでないツールチェーンにロックインされるのも防ぎます。OpenGL/WebGL に対してコーディングを直接行うと、Cocos2DUnity などのライブラリを使用する場合と比べて複雑さが増すかという質問がありました(WebGL ヘルパーもあります)。実際、私はその逆を信じています。ほとんどのスマートフォン / HTML5 ゲーム(Bouncy Mouse などのゲーム)は非常にシンプルです。ほとんどの場合、ゲームではいくつかのスプライトとテクスチャのあるジオメトリを描画するだけです。Bouncy Mouse の OpenGL 固有のコードの合計は、おそらく 1,000 行未満になります。ヘルパー ライブラリを使用することで、実際にこの数を減らせるとしたら驚くでしょう。たとえこの数字が半分に減ったとしても、500 行のコードを節約するためだけに、新しいライブラリやツールの習得にかなりの時間を費やさなければなりません。さらに、興味のあるすべてのプラットフォームに移植できるヘルパー ライブラリがまだ見つかっていないため、そのような依存関係を使用すると、ポータビリティが大幅に低下する可能性があります。 もしライトマップ、ダイナミック LOD、スキン アニメーションなどを必要とする 3D ゲームを作成しているなら、私の答えは確実に変わるでしょう。この場合、OpenGL に対してエンジン全体を手動コーディングするために一から作り直すことになります。ここでのポイントは、ほとんどのモバイル/HTML5 ゲームはこのカテゴリに(まだ)含まれていないため、必要になる前に複雑にする必要はありません。

言語間の類似点を過小評価しない

C++ コードベースを新しい言語に移植する時間を大幅に短縮できた最後の秘訣は、コードの大部分が各言語間でほぼ同一であることを認識したことです。重要な要素は変わるかもしれませんが、変わらないものよりはるかに少ないものです。実際、多くの関数では、C++ から JavaScript に移行するには、C++ コードベースで正規表現を置換するだけで済みます。

移植の結論

移行プロセスについては以上です。以降のセクションで、HTML5 固有の課題をいくつか取り上げますが、要点は、コードをシンプルにしておけば、移植は悪夢ではなく、小さな頭痛の種になるということです。

音声

私(そしておそらく誰もが)問題を引き起こしたのは、音声に関することでした。iOS と Android では、さまざまなオーディオの選択肢(OpenSL、OpenAL)を利用できますが、HTML5 の世界では、状況は落ち着いていたように見えました。HTML5 オーディオも使用できますが、ゲームで使用すると大きな問題があることがわかりました。最新のブラウザでも ときどきおかしな動作が発生しましたたとえば Chrome では、同時に作成できる音声要素(ソース)の数に上限があるようです。また 音声が再生されても 不自然に歪むことがあります全体として、少し不安でした。オンラインで検索したところ、ほとんどの人が同じ問題を抱えていることがわかりました。私が最初に見つけたソリューションは、SoundManager2 という API でした。この API では、使用可能な場合は HTML5 Audio を使用し、難しい場合は Flash にフォールバックします。このソリューションは機能していましたが、まだバグがあり、予測不可能でした(純粋な HTML5 オーディオよりはわずかです)。 リリースから 1 週間後、Google の助けを借りて、Webkit の Web Audio API を紹介してくれました。もともとこの API を使うことを検討していましたが (私にとっては)不必要な複雑さのために遠ざけていました少しだけ効果音を再生したかったのですが、HTML5 オーディオでは数行の JavaScript で処理できます。 しかし、ウェブ オーディオをざっと見てみると、その巨大な仕様(70 ページ)、ウェブのサンプルが少数であること(新しい API では一般的)、仕様のどこにも「再生」、「一時停止」、「停止」の機能がないことに驚きました。 私の懸念はしっかりと定着していないという Google の保証の下、再び API について掘り下げました。いくつかの例を見てさらに調査を重ねた結果、Google の判断が正しかったことがわかりました。この API は間違いなく私のニーズを満たし、他の API を悩ませるバグもなく、それを満たすことができます。特に便利なのは、Web Audio API スタートガイドの記事です。API についてより深く理解したい場合は、こちらをおすすめします。私の本当の問題は、API を理解して使用した後でも、「音を少し再生するだけの API ではない」ように見えることです。 この不安を解消するために、小さなヘルパークラスを作成しました。私はこの思いどおりに API を使用して、サウンドの再生、一時停止、停止、クエリを行うことができます。 このヘルパークラスを AudioClip と名付けました。完全なソースは、Apache 2.0 ライセンスの下で GitHub から入手できます。クラスの詳細については後述します。その前に、Web Audio API について簡単に説明します。

ウェブ オーディオのグラフ

Web Audio API を HTML5 Audio 要素よりも複雑かつ強力にしている点の 1 つ目は、ユーザーに出力する前に音声を処理 / ミックスできることです。オーディオの再生にはグラフが伴うという強力な反面、単純なシナリオでは物事が少し複雑になります。次のグラフを使用して、Web Audio API の能力を説明します。

基本的なウェブ オーディオのグラフ
基本的なウェブ オーディオ グラフ

上記の例は Web Audio API の能力を示していますが、私のシナリオではそのような機能のほとんどは必要ありませんでした。音を鳴らしたかっただけです。これにはやはりグラフが必要ですが、グラフは非常にシンプルです。

シンプルなグラフ

Web Audio API を HTML5 Audio 要素よりも複雑かつ強力にしている点の 1 つ目は、ユーザーに出力する前に音声を処理 / ミックスできることです。オーディオの再生にはグラフが伴うという強力な反面、単純なシナリオでは物事が少し複雑になります。次のグラフを使用して、Web Audio API の能力を説明します。

シンプルなウェブ音声のグラフ
Trivial Web Audio Graph

上記の簡単なグラフにより、サウンドの再生、一時停止、停止に必要なすべてのことを達成できます。

グラフのことは気にしない

グラフを理解するのは良いことですが、音を鳴らすたびに対処したくはありません。そのため、シンプルなラッパークラス「AudioClip」を作成しました。このクラスは、このグラフを内部で管理しますが、ユーザー向け API ははるかにシンプルです。

AudioClip
AudioClip

このクラスは単なるウェブ オーディオ グラフとヘルパー状態にすぎません。各サウンドを再生するウェブ オーディオ グラフを作成するよりも、はるかにシンプルなコードを使用できます。

// At startup time
var sound = new AudioClip("ping.wav");

// Later
sound.play();

実装の詳細

ヘルパークラスのコードを簡単に見てみましょう。 コンストラクタ - コンストラクタは、XHR を使用して音声データの読み込みを処理します。わかりやすくするためにここでは示していませんが、HTML5 Audio 要素をソースノードとして使用することもできます。これは、サンプルの数が多い場合に特に役立ちます。Web Audio API では、このデータを「配列バッファ」としてフェッチする必要があります。データを受け取ったら、このデータからウェブ オーディオ バッファを作成します(元の形式からランタイム PCM 形式にデコードします)。

/**
* Create a new AudioClip object from a source URL. This object can be played,
* paused, stopped, and resumed, like the HTML5 Audio element.
*
* @constructor
* @param {DOMString} src
* @param {boolean=} opt_autoplay
* @param {boolean=} opt_loop
*/
AudioClip = function(src, opt_autoplay, opt_loop) {
// At construction time, the AudioClip is not playing (stopped),
// and has no offset recorded.
this.playing_ = false;
this.startTime_ = 0;
this.loop_ = opt_loop ? true : false;

// State to handle pause/resume, and some of the intricacies of looping.
this.resetTimout_ = null;
this.pauseTime_ = 0;

// Create an XHR to load the audio data.
var request = new XMLHttpRequest();
request.open("GET", src, true);
request.responseType = "arraybuffer";

var sfx = this;
request.onload = function() {
// When audio data is ready, we create a WebAudio buffer from the data.
// Using decodeAudioData allows for async audio loading, which is useful
// when loading longer audio tracks (music).
AudioClip.context.decodeAudioData(request.response, function(buffer) {
    sfx.buffer_ = buffer;
    
    if (opt_autoplay) {
    sfx.play();
    }
});
}

request.send();
}

再生 - サウンドを再生するには、再生グラフを設定し、グラフのソースで「noteOn」のバージョンを呼び出すという 2 つのステップを行います。ソースは 1 回しか再生できないため、プレイするたびにソース/グラフを再作成する必要があります。この関数の複雑さのほとんどは、一時停止したクリップを再開するために必要な要件(this.pauseTime_ > 0)に起因します。一時停止したクリップの再生を再開するには、noteGrainOn を使用します。これにより、バッファのサブ領域を再生することができます。残念ながら、このシナリオでは、noteGrainOn はループ処理を適切に処理できません(バッファ全体ではなく、サブリージョンがループします)。 そのため、noteGrainOn を使用してクリップの残りの部分を再生し、ループを有効にしてクリップを最初から再生し直すことで、この問題を回避する必要があります。

/**
* Recreates the audio graph. Each source can only be played once, so
* we must recreate the source each time we want to play.
* @return {BufferSource}
* @param {boolean=} loop
*/
AudioClip.prototype.createGraph = function(loop) {
var source = AudioClip.context.createBufferSource();
source.buffer = this.buffer_;
source.connect(AudioClip.context.destination);

// Looping is handled by the Web Audio API.
source.loop = loop;

return source;
}

/**
* Plays the given AudioClip. Clips played in this manner can be stopped
* or paused/resumed.
*/
AudioClip.prototype.play = function() {
if (this.buffer_ && !this.isPlaying()) {
// Record the start time so we know how long we've been playing.
this.startTime_ = AudioClip.context.currentTime;
this.playing_ = true;
this.resetTimeout_ = null;

// If the clip is paused, we need to resume it.
if (this.pauseTime_ > 0) {
    // We are resuming a clip, so it's current playback time is not correctly
    // indicated by startTime_. Correct this by subtracting pauseTime_.
    this.startTime_ -= this.pauseTime_;
    var remainingTime = this.buffer_.duration - this.pauseTime_;

    if (this.loop_) {
    // If the clip is paused and looping, we need to resume the clip
    // with looping disabled. Once the clip has finished, we will re-start
    // the clip from the beginning with looping enabled
    this.source_ = this.createGraph(false);
    this.source_.noteGrainOn(0, this.pauseTime_, remainingTime)

    // Handle restarting the playback once the resumed clip has completed.
    // *Note that setTimeout is not the ideal method to use here. A better 
    // option would be to handle timing in a more predictable manner,
    // such as tying the update to the game loop.
    var clip = this;
    this.resetTimeout_ = setTimeout(function() { clip.stop(); clip.play() },
                                    remainingTime * 1000);
    } else {
    // Paused non-looping case, just create the graph and play the sub-
    // region using noteGrainOn.
    this.source_ = this.createGraph(this.loop_);
    this.source_.noteGrainOn(0, this.pauseTime_, remainingTime);
    }

    this.pauseTime_ = 0;
} else {
    // Normal case, just creat the graph and play.
    this.source_ = this.createGraph(this.loop_);
    this.source_.noteOn(0);
}
}
}

効果音として再生 - 上記の再生機能では、音声クリップを重複して再生することはできません(2 回目の再生は、クリップの終了時または停止時のみ可能です)。サウンドの再生が完了するのを待たずにサウンドを何度も再生したい場合があります(ゲーム内でコインが収集されるなど)。これを可能にするため、AudioClip クラスには playAsSFX() メソッドがあります。複数の再生が同時に行われる可能性があるため、playAsSFX() からの再生は AudioClip と 1:1 でバインドされません。そのため、再生の停止、一時停止、状態の照会はできません。また、この方法で再生されるループ音声を停止する方法がないため、ループも無効になります。

/**
* Plays the given AudioClip as a sound effect. Sound Effects cannot be stopped
* or paused/resumed, but can be played multiple times with overlap.
* Additionally, sound effects cannot be looped, as there is no way to stop
* them. This method of playback is best suited to very short, one-off sounds.
*/
AudioClip.prototype.playAsSFX = function() {
if (this.buffer_) {
var source = this.createGraph(false);
source.noteOn(0);
}
}

停止、一時停止、状態の照会 - 残りの関数はとても簡単で、説明も必要ありません。

/**
* Stops an AudioClip , resetting its seek position to 0.
*/
AudioClip.prototype.stop = function() {
if (this.playing_) {
this.source_.noteOff(0);
this.playing_ = false;
this.startTime_ = 0;
this.pauseTime_ = 0;
if (this.resetTimeout_ != null) {
    clearTimeout(this.resetTimeout_);
}
}
}

/**
* Pauses an AudioClip. The offset into the stream is recorded to allow the
* clip to be resumed later.
*/
AudioClip.prototype.pause = function() {
if (this.playing_) {
this.source_.noteOff(0);
this.playing_ = false;
this.pauseTime_ = AudioClip.context.currentTime - this.startTime_;
this.pauseTime_ = this.pauseTime_ % this.buffer_.duration;
this.startTime_ = 0;
if (this.resetTimeout_ != null) {
    clearTimeout(this.resetTimeout_);
}
}
}

/**
* Indicates whether the sound is playing.
* @return {boolean}
*/
AudioClip.prototype.isPlaying = function() {
var playTime = this.pauseTime_ +
            (AudioClip.context.currentTime - this.startTime_);

return this.playing_ && (this.loop_ || (playTime < this.buffer_.duration));
}

音声のまとめ

このヘルパークラスが、私と同じ音声の問題に悩まされているデベロッパーのお役に立てば幸いです。また、Web Audio API のより強力な機能を追加する必要がある場合でも、このようなクラスを出発点として最適であると考えられます。いずれにせよ、このソリューションは Bouncy Mouse のニーズを満たし、文字列のない真の HTML5 ゲームになりました。

パフォーマンス

JavaScript ポートに関してもう一つ懸念を抱いた点はパフォーマンスでした。ポートの v1 を終了した後、クアッドコア デスクトップではすべてが正常に動作していることがわかりました。残念ながら、ネットブックや Chromebook では少し問題がありました。この場合、Chrome のプロファイラは、プログラムのすべての時間が費やされている場所を正確に示し、私を救いました。 私の経験では、最適化を行う前にプロファイリングを行うことが重要です。Box2D の物理特性かレンダリング コードが速度低下の主な原因になると予想していましたが、実際には時間の大部分が Matrix.clone() 関数に費やされていました。私のゲームは数学が多用されるため、行列の作成やクローン作成をたくさん行うことはわかっていましたが、これがボトルネックになるとは思いもしませんでした。最終的に、非常にシンプルな変更により、ゲームの CPU 使用率を 3 倍以上削減でき、パソコンの CPU 使用率は 6 ~ 7% から 2% に減らすことができました。JavaScript のデベロッパーにとっては一般的な知識かもしれませんが、C++ のデベロッパーにとってこの問題に驚いたので、もう少し詳しく説明します。基本的に、私の元の行列クラスは 3x3 行列でした。3 つの要素の配列で、各要素に 3 つの要素の配列が含まれます。残念ながら、マトリックスのクローンを作成するとき、4 つの新しい配列を作成する必要がありました。必要な変更は、このデータを単一の 9 つの要素の配列に移動し、それに応じて計算を更新するだけでした。この 1 つの変更が原因で、CPU が 3 倍に削減されました。この変更後、私のパフォーマンスはすべてのテストデバイスで許容範囲内になりました。

さらなる最適化

パフォーマンスは許容範囲内でしたが、軽微な問題がいくつかありました。プロファイリングをしてみたら、JavaScript のガベージ コレクションが原因であったことがわかりました。アプリが 60 fps で実行されていたため、各フレームの描画時間は 16 ミリ秒でした。残念ながら、より遅いマシンでガベージ コレクションが開始されると、最大で 10 ミリ秒かかることがありました。ゲームがフルフレームを描画するのに約 16 ms が必要で、これにより数秒間のスタッタが発生しました。 ゴミが大量に生成されていた理由を把握するために、Chrome のヒープ プロファイラを使用しました。非常に失望しましたが、ゴミの大部分(70% 以上)は Box2D によって生成されていることがわかりました。JavaScript でゴミをなくすのは難しい作業ですが、Box2D を書き直すのは難しいことです。そのため、手に負えないことに気づきました。幸いなことに、この本の中でも最も古いトリックの一つが残っています。それは、「60fps にならないときは 30fps で走る」というものです。安定した 30 fps で実行する方が、ジッターを伴う 60 fps で実行するよりもはるかに優れているという意見は十分に意見が一致しています。実際、ゲームが 30 fps で実行されるという苦情やコメントは 1 件も受けていません(2 つのバージョンを並べて比較しないと判断するのは非常に困難です)。1 フレームあたり 16 ミリ秒という余分な時間があるため、不適切なガベージ コレクションが行われた場合でも、フレームをレンダリングするのに十分な時間があります。30 fps での実行は、私が使用した Timing API(WebKit の優れた requestAnimationFrame)では明示的には有効になりませんが、非常に簡単な方法で実現できます。明示的な API ほど洗練されていないかもしれませんが、30 fps は、RequestAnimationFrame の間隔がモニターの VSYNC(通常は 60 fps)に揃えられることを把握することで実現できます。つまり、他のすべてのコールバックを無視するだけです。基本的に、「RequestAnimationFrame」が呼び出されるたびに呼び出されるコールバック「Tick」がある場合は、次のように実行できます。

var skip = false;

function Tick() {
skip = !skip;
if (skip) {
return;
}

// OTHER CODE
}

特に注意したい場合は、起動時にコンピュータの VSYNC が 30 fps 以下になっていないことを確認し、このときのスキップを無効にしてください。しかし、テストしたデスクトップ/ノートパソコンの構成では、まだこのような現象は見られません。

配信と収益化

Chrome への Bouncy Mouse ポートについて驚いた最後の点は、収益化でした。このプロジェクトに参加するにあたって、私は HTML5 ゲームは新技術を学ぶための興味深い実験だと思いました。当時、私はこのポートが膨大な数の視聴者にリーチし、大きな収益化の可能性を秘めているとは知りませんでした。

Bouncy Mouse は 10 月末に Chrome ウェブストアでリリースされました。Chrome ウェブストアでリリースすることで、モバイル プラットフォームで慣れ親しんだ既存のシステムを活用して、見つけやすさ、コミュニティ エンゲージメント、ランキングなどの機能を実現できました。驚いたのは、店舗がこれほど広範に取り扱っていることでした。リリースから 1 か月足らずでインストール数が 40 万近くに達し、すでにコミュニティ エンゲージメント(バグレポート、フィードバック)の恩恵を受けています。もう一つ驚いたのは、ウェブアプリには収益化の可能性があることです。

Bouncy Mouse では、ゲーム コンテンツの横にバナー広告が表示されるという、シンプルな収益化方法を採用しています。しかし、ゲームのリーチが広範に及ぶことから、このバナー広告はかなりの収入を生み出すことができ、繁忙期には、最も成功したプラットフォームである Android に匹敵する収益を生み出していることがわかりました。その要因の 1 つとして、HTML5 バージョンで表示される大きな AdSense 広告は、Android で表示される小さい AdMob 広告よりもインプレッションあたりの収益が大幅に高いことがあげられます。さらに、HTML5 版のバナー広告は Android 版に比べてかなり邪魔にならず、すっきりとしたゲームプレイが可能になっています。全体として、この結果にはとても嬉しい驚きがありました。

正規化された収益の推移。
正規化された収益の推移

このゲームの収益は予想をはるかに上回るものでしたが、Chrome ウェブストアのリーチは、Android マーケットのような成熟したプラットフォームに比べ、依然として少ないことは注目に値します。Bouncy Mouse はすぐに Chrome ウェブストアで最も人気のあるゲームに昇格できましたが、サイトの新規ユーザーの割合は最初のリリース以降著しく低下しました。とはいえ、ゲームは着実に成長を続けており、プラットフォームが今後どのような発展を遂げるのか楽しみです。

おわりに

Bouncy Mouse の Chrome への移植は予想以上にスムーズになりました。音声やパフォーマンスに関する軽微な問題以外は、Chrome は既存のスマートフォン向けゲームに完全に対応できるプラットフォームであることがわかりました。この経験を躊躇しているデベロッパーには、ぜひお試しいただきたいと思います。移行プロセスだけでなく、HTML5 ゲームを導入したことで新たなゲーム ユーザーを獲得できたことにも満足しています。 ご不明な点がございましたら、お気軽にお問い合わせください。または、下にコメントしてください。定期的に確認いたします。