HTML5 による 2D ゲームの開発: 重力の実装とサウンドの追加

リアルな落下をアニメーション化し、サウンドトラックとサウンド・エフェクトを追加する

この連載では、HTML5 のエキスパートである David Geary が、HTML5 で 2D テレビ・ゲームを実装する方法について順を追って説明します。今回の記事では、ランナーの落下に重力の影響を組み込む方法について説明し、Snail Bait のメカニズムを完成させます。さらに、サウンドの実装方法について、音楽によるサウンドトラックとサウンド・エフェクトの両方を含めて説明します。

David Geary, Author and speaker, Clarity Training, Inc.

David GearyCore HTML5 Canvas』の著者、David Geary は HTML5 Denver User's Group の共同設立者でもあり、Swing と JavaServer Faces に関するベストセラーの本を含め、Java に関する 8 冊の本の著者でもあります。また彼は、JavaOne、Devoxx、Strange Loop、NDC、OSCON などのカンファレンスで頻繁に講演を行っており、JavaOne Rock Star にも 3 度選ばれています。彼は developerWorks の連載記事、「HTML5 による 2D ゲームの開発」、「JSF 2 の魅力」、そして「GWT の魅力」の著者でもあります。



2013年 7月 18日

Snail Bait は衝突を検出できるようになったので ― 衝突検出は、この連載の前回の記事で実装しました ― 今度は衝突以外の重要な要素の 1 つ、つまりランナーがプラットフォームを踏み外した場合の処理を実装しなければなりません。ランナーはプラットフォームを踏み外すと落下を開始します。今回の記事では、重力の影響を組み込んでリアルな落下を実装する方法について説明します。重力と落下を実装すると、Snail Bait に必要なゲームプレイのメカニズムがひと通り完成します。その説明の後、今度はサウンド (音楽も含む) をゲームに組み込む方法を説明します。この記事で使用するサンプル・コードの全体は「ダウンロード」セクションから入手することができます。

落下

Snail Bait のランナーがプラットフォームの端を通り過ぎてしまった場合、またはプラットフォームに下側から衝突した場合、ランナーは図 1 のように落下します。

図 1. プラットフォームの端を通り過ぎて落下する
ランナーがプラットフォームの端を通り過ぎて落下する様子を示す Snail Bait の画面のスクリーン・キャプチャー

ジャンプして下降した最後にプラットフォームに着地し損なった場合にも、ランナーは図 2 のように落下します。

図 2. ジャンプを終了した時点で落下する
ランナーがジャンプの終了時点で落下する様子を示す Snail Bait の画面のスクリーン・キャプチャー

ランナーの「落下する」ビヘイビア (落下ビヘイビア) によって、ランナーは落下します。リスト 1 に、ランナーのビヘイビアの配列を指定して、ランナー・スプライトをインスタンス化するコードを示します。

リスト 1. ランナーのビヘイビア
Sprite = function () {
   ...
   this.runner = new Sprite('runner',           // type
                            this.runnerArtist,  // artist
                            [ this.runBehavior, // behaviors
                              this.jumpBehavior,
                              this.collideBehavior,
                              this.fallBehavior,
                            ]); 
};

ランナーの落下ビヘイビアは ― スプライトのすべてのビヘイビアと同様に ― ランナー・スプライトが表示されていると、アニメーション・フレームごとに Snail Bait によって呼び出されます。ほとんどの場合、落下ビヘイビアは何もしません。ランナーの falling 属性が true の間、落下ビヘイビアはアニメーション・フレームごとにランナーを少しずつ垂直方向に移動し、あたかもランナーが落下しているかのように見せます。この falling 属性はランナーの fall() メソッドによって設定されます (リスト 2)。

リスト 2. ランナーの fall() メソッド
SnailBait.prototype = {
   ...
   equipRunner: function () {
      ...
      this.equipRunnerForJumping();
      this.equipRunnerForFalling();
   },

   equipRunnerForFalling: function () {
      ...

      this.runner.fallAnimationTimer = new AnimationTimer();

      this.runner.fall = function (initialVelocity) {
         // set attributes for the runner's fall behavior and
         // start the fall animation timer

         this.velocityY = initialVelocity || 0;
         this.initialVelocityY = initialVelocity || 0;
         this.falling = true;

         this.fallAnimationTimer.start();
      }
   },
   ...
};

ランナーの水平方向の速度はどうなのか?

ランナーの落下ビヘイビアにおける唯一の関心事は、ランナーの垂直方向の配置です。落下ビヘイビアはランナーの水平方向の速度を変更する必要がありません。なぜなら、ランナーは左から右へ (そして右から左へ) 移動しているように見えますが、実際にはランナーは水平方向にまったく移動しないからです。ランナーではなく、ランナーの背後にある背景を移動することにより、ランナーが水平方向に動いているように見せているのです。

ゲームが開始されると、Snail Bait は equipRunner() メソッドを呼び出します。equipRunner() メソッドは、ランナーのさまざまな準備を行う中で、ランナーがジャンプおよび落下をするための準備も行います。equipRunnerForFalling() メソッドには、ランナーの fall() メソッドの実装が含まれています。ランナーの fall() メソッドはランナーの初期速度を設定して falling 属性を true に設定し、アニメーション・タイマーを開始することで、ランナーが落下を開始してからの経過時間を測定します。

ランナーの fall() メソッドはランナーの falling 属性を true に設定すると、ランナーの落下ビヘイビアをトリガーします。すると、ゲームによってランナーの falling 属性が false に設定されるまで、ランナーは落下します。ここから先では、この落下ビヘイビアの実装に焦点を絞って重力について説明します。


重力の影響を組み込む

重力は特殊なケース

この連載の第 7 回に相当する記事「HTML5 による 2D ゲームの開発: 時間を操作する、第 2 回」では、重力によって生じるリニアでない動きについて説明しました。その記事では、イーズイン、イーズアウトというトゥイーン機能を利用して重力を近似するタイム・トランスデューサーを使用することで、リニアではないジャンプを実装しています。これらのタイム・トランスデューサーを変更することにより、ありとあらゆるリニアではない動きを作り出すことができます。つまり重力はリニアではない動き全般の中の特殊なケースということになります。

地表の近くで落下する物体は、重力によって 1 秒ごとに 9.81 メートル/秒 (m/s) の割合で加速されます。つまり何かが落下すると、その物体の速度は 1 秒ごとに約 10 m/s (32 ft/s) ずつ増加します。つまり重力があることから、ゲーム開発者はスプライトの速度に応じてスプライトの位置を計算するだけではなく、スプライトが落下しているときのスプライトの速度も計算する必要があります。

重力の影響下で速度を計算するための計算式は単純です。スプライトが落下を開始してからの経過時間を重力に掛け、スプライトが落下を開始した時点の初期速度にその掛け算の結果を加えればよいのです。計算式によくあることですが、この場合に混乱しがちなのは、計算そのものよりも単位です。この計算式によって得られるのは、メートル/秒 (m/s) という単位での結果です。この数値を意味のあるものにするために、Snail Bait は以下のアルゴリズムでスプライトの位置を計算し、この数値をピクセル/秒に変換します。

  • ゲームの開始時時点では以下のようにします。
    1. ゲームの画面の幅をピクセルで定義します。
    2. ゲームの画面の幅をメートルで適当な値に定義します。
    3. ピクセル数で表現した幅を、メートルで表現した幅で割り、ピクセル/メートルの比を得ます。
  • その後、アニメーション・フレームごとに以下のようにします。
    1. 重力加速度 (9.81 m/s/s) を使用してメートル/秒 (m/s) で速度を計算します。
    2. m/s で表現した速度にステップ 3 で計算したピクセル/メートルの比を掛け、ピクセル/秒の値を得ます。
    3. ピクセル/秒で表現した速度を基に位置を計算します。

では、上記のアルゴリズムをコードにします。最初のステップでは、重力を定義するとともに、ゲームのピクセル/メートルの比も定義します (リスト 3)。

リスト 3. 重力と落下に関連する定数
var SnailBait = { // SnailBait constructor function
   ...
  
   this.GRAVITY_FORCE = 9.81,
   this.PIXELS_PER_METER = this.canvas.width / 10; // 10 meters, randomly selected width
   ...
}

ランナーがプラットフォームの端を通り過ぎてしまった場合、または下側からプラットフォームに衝突した場合、ランナーは垂直方向の速度ゼロで落下を開始します。ただしジャンプの最後でランナーがプラットフォームに着地しなかった場合には、ランナーはジャンプによる下降の終了時点の垂直方向の速度で落下を開始します。リスト 4 は、リスト 3 で定義された GRAVITY_FORCE 定数と PIXELS_PER_METER 定数を使用して、ランナーのジャンプ・ビヘイビアの中で落下の初期速度を計算する方法を示しています。

リスト 4. ジャンプによる下降の終了時点で落下する場合
this.jumpBehavior = {
   ...

   finishDescent: function (sprite) {
      sprite.stopJumping();

      if (snailBait.isOverPlatform(sprite) !== -1) {
         sprite.top = sprite.verticalLaunchPosition;
      }
      else {
         // The argument passed to the sprite's fall() method
         // is the sprite's initial velocity when it begins to fall
   
         sprite.fall(snailBait.GRAVITY_FORCE *
                     (sprite.descendAnimationTimer.getElapsedTime()/1000) *
                     snailBait.PIXELS_PER_METER);
      }
   },
};

ジャンプによる下降の終了時点でのランナーの垂直方向の速度は、下降を開始してからの経過時間を重力に掛け、それにゲームのピクセル/メートルの比を掛けることで得られます。

(9.81 m/s/s) * (下降経過時間の秒数) * (800 ピクセル / 10 m)

この計算の結果はピクセル/秒になり、ジャンプによる下降の終了時点でのランナーの垂直方向の速度を表します。(ジャンプ・ビヘイビアの他の部分の実装については、この連載の第 6 回に相当する記事「HTML5 による 2D ゲームの開発: 時間を操作する、第 1 回」を参照してください。)

Snail Bait は、ランナーの落下ビヘイビアの中でも GRAVITY_FORCEPIXELS_PER_METER を使用してランナーの速度を計算します (リスト 5)。

リスト 5. 現在のフレームに対するスプライトの速度の設定と、ランナーの垂直落下速度の計算
this.fallBehavior = {
   ...
   setSpriteVelocity: function (sprite) {
      sprite.velocityY = sprite.initialVelocityY + 

                         snailBait.GRAVITY_FORCE *
                         (sprite.fallAnimationTimer.getElapsedTime()/1000) *
                         snailBait.PIXELS_PER_METER;
   },

   calculateVerticalDrop: function (sprite, fps) {
      return sprite.velocityY / fps;
   },
};

落下ビヘイビアの setSpriteVelocity() メソッドは、ランナーが落下を開始してからの時間の長さに応じてランナーの速度を設定します。このメソッドは、リスト 2 のジャンプ・ビヘイビアによってランナーの初期速度が設定されている場合も考慮するように、抜かりなく作られています。

calculateVerticalDrop() メソッドは、(連載の第 2 回に相当する記事の「タイム・ベースで動かす」セクションで説明した) タイム・ベースの動きを使用しており、setSpriteVelocity() と現在のフレーム・レートで計算される速度に基づいてランナーの垂直落下速度を計算します。

連載第 5 回に相当する記事「HTML5 による 2D ゲームの開発: スプライトのビヘイビアを実装する」で詳細に説明したとおり、Snail Bait はアニメーション・フレームごとにすべてのスプライトに対して繰り返し処理を行います。表示されている各スプライトに対し、Snail Bait はスプライトのビヘイビアに繰り返し処理を行い、各ビヘイビアの execute() メソッドを順次呼び出します。リスト 6 はランナーの落下ビヘイビアの execute() メソッドを示しています。

リスト 6. 落下ビヘイビアの execute() メソッド
this.fallBehavior = {
      execute: function (sprite, time, fps) { // sprite is the runner
         var deltaY;

         if (sprite.jumping) {
            return;
         }

         if (this.isOutOfPlay(sprite) || sprite.exploding) {
            if (sprite.falling) {
               sprite.stopFalling();
            }
            return;
         }
         
         if (!sprite.falling) {
            if (!sprite.exploding && !this.isPlatformUnderneath(sprite)) {
               sprite.fall();
            }
            return;

         }

         this.setSpriteVelocity(sprite);
         deltaY = this.calculateVerticalDrop(sprite, fps);
               
         if (!this.willFallBelowCurrentTrack(sprite, deltaY)) {
            sprite.top += deltaY;
         }
         else { // will fall below current track
            if (this.isPlatformUnderneath(sprite)) {
               this.fallOnPlatform(sprite);
               sprite.stopFalling();
            }
            else {
               sprite.top += deltaY;
               sprite.track--;
            }
         }
      }
   },

落下ビヘイビアの execute() メソッドは、ランナーがジャンプ中であるかどうか、ゲームの表示領域外まで落下してしまっているかどうか、あるいは爆発中であるかどうかなどに応じて、そのままリターンしたり、落下を開始または停止してリターンしたりするなど、処理を分岐します。ランナーがジャンプ中のときは、そのままリターンし、ランナーがゲームの表示領域外まで落下してしまったときや、落下中に爆発しているときには、ランナー・スプライトの stopFalling() メソッドを呼び出してリターンします。ランナーが落下中ではなく、現在爆発中でもなく、しかもランナーの新しい水平位置では真下にプラットフォームがない場合には、ランナー・スプライトの fall() メソッドを呼び出してリターンします。

これらの前提条件に当てはまらなかった場合、落下ビヘイビアの execute() メソッドはランナーの現在の速度と新しい位置を計算します。その計算の結果、ランナーの新しい垂直位置が現在対象としているプラットフォーム・トラックよりも上のままになる場合には、その位置までランナーを移動させます。ランナーの新しい位置が現在対象としているプラットフォーム・トラックと同じ高さか、それより下になる場合には、現在対象としているトラックにおいてランナーの新しい水平位置に相当する位置にプラットフォームがあるかどうかを確認し、プラットフォームがある場合には、そのプラットフォーム上にランナーを配置して、落下を停止します。プラットフォームがない場合には、ランナーを新しい位置まで移動させ、ランナーが対象とするトラックを現在よりも値が 1 小さいトラックへと変更します。

落下ビヘイビアの execute() メソッドは 4 つのコンビニエンス・メソッドを使用します。リスト 7 の 2 つのメソッドは、ランナーがゲームの表示領域外に落ちてしまっているのかどうかを判断するメソッドと、ランナーが現在対象としているトラックよりも下に落ちることになるのかどうかを判断するメソッドです。

リスト 7. ランナーがゲームの表示領域外に落ちてしまっているのかどうかを判断するメソッドと、ランナーが現在対象としているトラックよりも下に落ちることになるのかどうかを判断するメソッド
this.fallBehavior = {
   isOutOfPlay: function (sprite) {
      return sprite.top > snailBait.TRACK_1_BASELINE;
   },

   willFallBelowCurrentTrack: function (sprite, deltaY) {
      return sprite.top + sprite.height + deltaY >
             snailBait.calculatePlatformTop(sprite.track);
   },
   ...
};

リスト 8 のコンビニエンス・メソッドは、ランナーの真下にプラットフォームがあるかどうかを判断するメソッドと、ランナーをプラットフォームに着地させるメソッドです。

リスト 8. ランナーの真下にプラットフォームがあるかどうかを判断するメソッドと、ランナーをプラットフォームに着地させるメソッド
this.fallBehavior = {
   isPlatformUnderneath: function (sprite) {
      return snailBait.isOverPlatform(sprite) !== -1;
   },

   fallOnPlatform: function (sprite) {
      sprite.top = snailBait.calculatePlatformTop(sprite.track) - sprite.height;
      sprite.stopFalling();
   },
   ...
};

重要な点として、ランナーの falling 属性が true の場合にのみ、落下ビヘイビアの execute() メソッドはランナーを垂直方向に移動するという点に注意してください。ランナーの stopFalling() メソッドは falling 属性を false に設定する他、ランナーの垂直方向の速度を 0 に設定し、ランナーの落下用アニメーション・タイマーを停止します (リスト 9)。

リスト 9. 落下ビヘイビアの stopFalling() メソッド
SnailBait.prototype = {
   ...

   equipRunnerForFalling: function () {
      ...

      this.runner.stopFalling = function () {
         this.falling = false;
         this.velocityY = 0;
         this.fallAnimationTimer.stop();
      }
   },
   ...
};

ランナーの落下中にビヘイビアを一時停止する

この連載の第 7 回に相当する記事の「ビヘイビアを一時停止する」セクションで説明したように、ゲーム全体の動作に合わせて一時停止と一時停止解除ができるように、ビヘイビアは pause() メソッドと unpause() メソッドを実装する必要があります。ランナーの落下ビヘイビアは、その要件を満たしています (リスト 10)。

リスト 10. 落下ビヘイビアの一時停止と一時停止解除
 var SnailBait = function () {
   ...

   this.fallBehavior = {
      pause: function (sprite) {
         sprite.fallAnimationTimer.pause();
      },

      unpause: function (sprite) {
         sprite.fallAnimationTimer.unpause();
      },
   }
   ...
}

落下ビヘイビアは、ランナーの落下用アニメーション・タイマーを使用して、落下の経過時間を追跡します。つまり落下ビヘイビアの pause() メソッドと unpause() メソッドは、単純にそのタイマーを一時停止し、また一時停止解除します。

これで Snail Bait がランナーの落下をどのように処理しているかを理解できたので、今度は落下とまったく関連のない要素である、ゲームのサウンドについて説明しましょう。


サウンド・エフェクトと音楽を制御する

Snail Bait は、サウンドトラックとサウンド・エフェクトを同時に再生することができます。図 3 の左下にある、サウンド用と音楽用のチェックボックスは、ユーザーがゲームのサウンド・エフェクトと音楽のオン/オフを別々に制御するためのチェックボックスです。

図 3. サウンド用と音楽用のコントロール
サウンドと音楽を制御するためのチェックボックスを表示した Snail Bait の画面のスクリーン・キャプチャー

リスト 11 は、このチェックボックスの HTML を示しています。

リスト 11. Snail Bait のサウンド用と音楽用のチェックボックス
<div id='sound-and-music'>
   <div class='checkbox-div'>
      Sound <input id='sound-checkbox' type='checkbox' checked/>
   </div>
          
   <div class='checkbox-div'>
      Music <input id='music-checkbox' type='checkbox'/>
   </div>
</div>

チェックボックスの checked 属性 (値はありません) を使用すると、そのチェックボックスに最初にチェックを入れるかどうかを制御することができます。図 3リスト 11 を見るとわかるように、この属性がある場合には、始めからチェックボックスにチェックが入り、ない場合には、チェックが入りません。

リスト 12 で、Snail Bait は 2 つのチェックボックス要素にプログラムでアクセスし、soundOnmusicOn という 2 つの変数を保持します。この 2 つの変数とチェックボックスは、イベント・ハンドラーによって同期状態が維持されます。さらに、音楽用のチェックボックスのイベント・ハンドラーは、(常時再生されるわけではないサウンド・エフェクトとは異なり) 常時バックグラウンドで再生されるゲームの音楽のサウンドトラックを再生または一時停止する処理を行います。

リスト 12. サウンド用と音楽用のチェックボックスに対するイベント・ハンドラー
var SnailBait = function () {
   ...
   this.soundCheckbox = document.getElementById('sound-checkbox');
   this.musicCheckbox = document.getElementById('music-checkbox');

   this.soundOn = this.soundCheckbox.checked;
   this.musicOn = this.musicCheckbox.checked;
   ... 
};
...

snailBait.soundCheckbox.onchange = function (e) {
   snailBait.soundOn = snailBait.soundCheckbox.checked;
};

snailBait.musicCheckbox.onchange = function (e) {
   snailBait.musicOn = snailBait.musicCheckbox.checked;

   if (snailBait.musicOn) {
      snailBait.soundtrack.play();
   }
   else {
      snailBait.soundtrack.pause();
   }
};

音楽用のチェックボックスにチェックが入っていると、Snail Bait の startGame() メソッドはサウンドトラックを再生します (リスト 13)。

リスト 13. ゲームを開始する
SnailBait.prototype = {
SnailBait.prototype = {
   ...

   startGame: function () {
      if (this.musicOn) {
         this.soundtrack.play();
      }
      requestNextAnimationFrame(this.animate);
   },
   ...

リスト 14では、ゲームの togglePaused() メソッドも変更し、ゲームが一時停止されているかどうかに応じてサウンドトラックを一時停止または再生しています。

リスト 14. 音楽を一時停止する
SnailBait.prototype = {
   ...

   togglePaused: function () {
      ...

      if (this.paused && this.musicOn) {
         this.soundtrack.pause();
      }
      else if ( ! this.paused && this.musicOn) {
         this.soundtrack.play();
      }
   },
};

サウンド・エフェクトを実装する

Snail Bait はゲームのさまざまな時点で (例えば、ランナーが蜂やコウモリと衝突したときなど) サウンド・エフェクトを再生し、場合によると複数のサウンドを同時に再生する必要があります。Snail Bait は HTML5 の audio 要素を使用してサウンド・エフェクトを実装しています。

HTML の audio 要素

Snail Bait は、ゲームの各サウンドに対して audio 要素を作成します (リスト 15)。

リスト 15. Snail Bait の audio 要素
<!DOCTYPE html>
<html>
   <head>
      <title>Snail Bait</title>
      <link rel='stylesheet' href='game.css'/>
   </head>
 
   <body>
      <audio id='soundtrack'>
        <source src='sounds/soundtrack.mp3' type='audio/mp3'>
        <source src='sounds/soundtrack.ogg' type='audio/ogg'>
      </audio>

      <audio id='plop-sound' >
        <source src='sounds/plop.mp3' type='audio/mp3'>
        <source src='sounds/plop.ogg' type='audio/ogg'>
      </audio>

      <audio id='chimes-sound' >
        <source src='sounds/chimes.mp3' type='audio/mp3'>
        <source src='sounds/chimes.ogg' type='audio/ogg'>
      </audio>

      <audio id='whistle-down-sound' >
        <source src='sounds/whistledown.mp3' type='audio/mp3'>
        <source src='sounds/whistledown.ogg' type='audio/ogg'>
      </audio>

      <audio id='thud-sound' >
        <source src='sounds/thud.mp3' type='audio/mp3'>
        <source src='sounds/thud.ogg' type='audio/ogg'>
      </audio>

      <audio id='jump-sound' >
        <source src='sounds/jump.mp3' type='audio/mp3'>
        <source src='sounds/jump.ogg' type='audio/ogg'>
      </audio>

      <audio id='coin-sound' >
        <source src='sounds/coin.mp3' type='audio/mp3'>
        <source src='sounds/coin.ogg' type='audio/ogg'>
      </audio>

      <audio id='explosion-sound' >
        <source src='sounds/explosion.mp3' type='audio/mp3'>
        <source src='sounds/explosion.ogg' type='audio/ogg'>
      </audio>
      ...

  </body>
</html>

上記コードでは、各 audio 要素の中で 2 つのサウンド・ファイルを異なるフォーマットで指定しており、ブラウザーはこの中から再生可能なフォーマットを選択します。最近のブラウザーのベースはどれもが MP3 フォーマットと OGG フォーマットに対応しています。HTML5 の audio フォーマットについては「参考文献」を参照してください。

また、Snail Bait は document の getElementById() メソッドを使用して JavaScript で各 audio 要素にアクセスします (リスト 16)。

リスト 16. Snail Bait の audio 要素に JavaScript でアクセスする
SnailBait = function () {
   ...

   this.coinSound           = document.getElementById('coin-sound'),
   this.chimesSound         = document.getElementById('chimes-sound'),
   this.explosionSound      = document.getElementById('explosion-sound'),
   this.fallingWhistleSound = document.getElementById('whistle-down-sound'),
   this.plopSound           = document.getElementById('plop-sound'),
   this.jumpWhistleSound    = document.getElementById('jump-sound'),
   this.soundtrack          = document.getElementById('soundtrack'),
   this.thudSound           = document.getElementById('thud-sound'),
   ...

};

音量

Snail Bait は再生する各サウンドに対し、0.0 (無音) から 1.0 (最大音量) までの音量レベルを定義しています。リスト 17 は Snail Bait で定義されている音量に対する定数を示しており、この定数は私が実験的に決定したものです。

リスト 17. Snail Bait のサウンドの音量レベルを定義する
SnailBait = function () {
   // Sound-related constants

   this.COIN_VOLUME            = 1.0,
   this.CHIMES_VOLUME          = 1.0,
   this.EXPLOSION_VOLUME       = 0.25,
   this.FALLING_WHISTLE_VOLUME = 0.10,
   this.JUMP_WHISTLE_VOLUME    = 0.05,
   this.PLOP_VOLUME            = 0.20,
   this.SOUNDTRACK_VOLUME      = 0.12,
   this.THUD_VOLUME            = 0.20,
   ...

};

Snail Bait は audio 要素への参照、そして音量レベルを表現する定数を用意した後、ゲーム開始時にリスト 18 のように audio 要素を初期化します。

リスト 18. Snail Bait のサウンドの音量レベルを設定する
SnailBait.prototype = {
   ...

   initializeSounds: function () {
      this.coinSound.volume           = this.COIN_VOLUME;
      this.chimesSound.volume         = this.CHIMES_VOLUME;
      this.explosionSound.volume      = this.EXPLOSION_VOLUME;
      this.fallingWhistleSound.volume = this.FALLING_WHISTLE_VOLUME;
      this.plopSound.volume           = this.PLOP_VOLUME;
      this.jumpWhistleSound.volume    = this.JUMP_WHISTLE_VOLUME;
      this.soundtrack.volume          = this.SOUNDTRACK_VOLUME;
      this.thudSound.volume           = this.LANDING_VOLUME;
   },

   start: function () {
      this.createSprites();
      this.initializeImages();
      this.initializeSounds();
      this.equipRunner();
      this.splashToast('Good Luck!');
      ...
   },
   ...   
   
};

複数のサウンドを同時に再生する

HTML5 の audio 要素には、以下のメソッドを含む、わかりやすい API があり、Snail Bait はこれらの API を使用してサウンドを再生します。

  • play()
  • pause()
  • load()

また Snail Bait は audio 要素の属性として以下の 2 つを使用します。

  • currentTime
  • ended

(リスト 12リスト 14 で使用した pause() を除き) これらのメソッドと属性の使い方はすべて、リスト 19 を見るとわかります。

リスト 19. Snail Bait のオーディオ・トラックを使用してサウンドを再生する
SnailBait = function () {
   ...
   this.soundOn = true,

   this.audioTracks = [ // 8 tracks is more than enough
      new Audio(), new Audio(), new Audio(), new Audio(), 
      new Audio(), new Audio(), new Audio(), new Audio()
   ],
   ...

   // Playing sounds.......................................................

   soundIsPlaying: function (sound) {
      return !sound.ended && sound.currentTime > 0;
   },

   playSound: function (sound) {
      var track, index;

      if (this.soundOn) {
         if (!this.soundIsPlaying(sound)) {
            sound.play();
         }
         else {
            for (i=0; index < this.audioTracks.length; ++index) {
               track = this.audioTracks[index];
            
               if (!this.soundIsPlaying(track)) {
                  track.src = sound.currentSrc;
                  track.load();
                  track.volume = sound.volume;
                  track.play();

                  break;
               }
            }
         }              
      }
   },
   ...
};

複数のサウンドを同時に再生するために、Snail Bait は 8 つの audio 要素からなる配列を作成します。Snail Bait の playSound() メソッドはその配列に繰り返し処理を行い、現在再生中ではない最初の audio 要素を使用してサウンドを再生します。

Snail Bait がリスト 15 の HTML で指定される元の audio 要素を使用してサウンド・エフェクトを再生するわけではないことに注意してください。Snail Bait はリスト 19 のようにプログラムで作成される 8 つの audio 要素を使用してサウンド・エフェクトを再生します。Snail Bait はプログラムで作成された要素に元の audio 要素のサウンドをロードし、そのプログラムで作成された要素を再生します。


Snail Bait のサウンド・エフェクトを再生する

リスト 20 からリスト 23 に示すのは、Snail Bait のさまざまな時点でサウンド・エフェクトを再生するコードの抜粋です。これらのリストのコードはすべて、この連載の前回までの記事で説明しているので、ここでは説明を繰り返しません。これらの抜粋したコードには、それぞれのコードのコンテキストがわかるように、playSound() を呼び出す前のロジックを十分に残してあります。

ランナーがジャンプすると、Snail Bait はヒューッというサウンドを再生します (リスト 20)。

リスト 20. ランナーがジャンプすると発せられるサウンド
var SnailBait = function () {
   ...

   this.equipRunnerForJumping: function() {
       ...
      this.runner.jump = function () {
         if (this.jumping) // 'this' is the runner
            return;

         this.runAnimationRate = 0;
         this.jumping = true;
         this.verticalLaunchPosition = this.top;
         this.ascendAnimationTimer.start();

         snailBait.playSound(snailBait.jumpWhistleSound);
      };
   },

スプライトが爆発すると、Snail Bait は explosionSound を再生します (リスト 21)。

リスト 21. 爆発
SnailBait.prototype = {
   ...

   explode: function (sprite, silent) {
      if (sprite.runAnimationRate === 0) {
         sprite.runAnimationRate = this.RUN_ANIMATION_RATE;
      }
               
      sprite.exploding = true;

      this.playSound(this.explosionSound);
      this.explosionAnimator.start(sprite, true);  // true means sprite reappears
   },
   ...
};

スプライトがプラットフォームに着地するとドスンという音を出し、トラックよりも下に落ちると、(ジャンプしたときとは異なる) ヒューッという音を出します。

リスト 22. 落下に関連するサウンド
SnailBait = function () {
   ...

   this.fallBehavior = {
      ...

      fallOnPlatform: function (sprite) {
         sprite.top = snailBait.calculatePlatformTop(sprite.track) - sprite.height;
         sprite.stopFalling();
         snailBait.playSound(snailBait.thudSound);
      },

      execute: function (sprite, time, fps) {
         var deltaY;

         if (!this.willFallBelowCurrentTrack(sprite, deltaY)) {
            sprite.top += deltaY;
         }
         else { // will fall below current track
            if (this.isPlatformUnderneath(sprite)) {
               this.fallOnPlatform(sprite);
               sprite.stopFalling();
            }
            else {
               sprite.track--;

               sprite.top += deltaY;

               if (sprite.track === 0) {
                  snailBait.playSound(snailBait.fallingWhistleSound);
               }
            }
         }
         ...
      }
   };
   ...
};

スプライト同士が衝突すると、その衝突したスプライトに応じて、さまざまなサウンドを再生します (リスト 23)。

リスト 23. 衝突した場合のサウンド
var SnailBait =  function () {
   ...

   this.collideBehavior = {
      ...

      processCollision: function (sprite, otherSprite) {
         if (otherSprite.value) { // Modify Snail Bait sprites so they have values
            // Keep score...
         }

         if ('coin'  === otherSprite.type    ||
             'sapphire' === otherSprite.type ||
             'ruby' === otherSprite.type     || 
             'button' === otherSprite.type   ||
             'snail bomb' === otherSprite.type) {
            otherSprite.visible = false;

            if ('coin' === otherSprite.type) {
               snailBait.playSound(snailBait.coinSound);
            }
            if ('sapphire' === otherSprite.type || 'ruby' === otherSprite.type) {
               snailBait.playSound(snailBait.chimesSound);
            }
         }

         if ('bat' === otherSprite.type   ||
             'bee' === otherSprite.type   ||
             'snail' === otherSprite.type || 
             'snail bomb' === otherSprite.type) {
            snailBait.explode(sprite);
         }

         if (sprite.jumping && 'platform' === otherSprite.type) {
            this.processPlatformCollisionDuringJump(sprite, otherSprite);
         }
      },
   },

   processPlatformCollisionDuringJump: function (sprite, platform) {
      var isDescending = sprite.descendAnimationTimer.isRunning();

      sprite.stopJumping();

      if (isDescending) {
         sprite.track = platform.track; 
         sprite.top = snailBait.calculatePlatformTop(sprite.track) - sprite.height;
      }
      else { // Collided with platform while ascending
         snailBait.playSound(snailBait.plopSound);
         sprite.fall(); 
      }
   },
   ...
};

次回は

次回の記事では、この連載「HTML5 による 2D ゲームの開発」の締めくくりとして、残機の数が変化したときの遷移やゲーム終了時のアニメーションなど、ゲームプレイの最後の部分とゲームの仕上げについて説明します。


ダウンロード

内容ファイル名サイズ
Sample codecode.zip11.9MB

参考文献

学ぶために

  • HTML5 の音声フォーマット: 公式のドキュメントを読んでください。
  • Core HTML5 Canvas』(David Geary 著、Prentice Hall、2012年): Canvas API とゲーム開発について広範にわたって説明している David Geary の著書です。関連する Web サイトとブログにもアクセスしてください。
  • Snail Bait: HTML5 対応の任意のブラウザーで Snail Bait をオンラインでプレイしてみてください (Chrome のバージョン 18 またはそれ以降のバージョンが最適です)。
  • HTML5 Canvas を使用した目を見張るアプリケーション: Strange Loop 2011 での David Geary のプレゼンテーションを見てください。
  • HTML5 Game Development」: NDC (Norwegian Developer's Conference) 2011 での David Geary のプレゼンテーションを見てください。
  • Platform games」: Wikipedia でプラットフォーム・ゲームについての説明を読んでください。
  • 「横スクロール」テレビ・ゲーム: ウィキペディアで横スクロール・テレビ・ゲームについての説明を読んでください。
  • Strategy パターン」: ウィキペディアで Strategy デザイン・パターンの項目を調べてください。
  • HTML5 の基礎」: developerWorks の Knowledge path で HTML5 の基本について学んでください。
  • developerWorks の Web development ゾーン: Web ベースのさまざまなソリューションを解説した記事が豊富に用意されています。Web development 技術文書一覧に用意された、さまざまな技術記事やヒント、チュートリアル、技術標準、IBM Redbooks をご覧ください。

製品や技術を入手するために

  • レプリカアイランド: Android 用として人気の、オープンソースのプラットフォーム・ゲームのソースをダウンロードしてください。Snail Bait のスプライトの大部分は (許可を得て) レプリカアイランドのスプライトを使用しています。
  • IBM 製品の評価版: IBM 製品の評価版をダウンロードして、DB2、Lotus、Rational、Tivoli、WebSphere などが提供するアプリケーション開発ツールやミドルウェア製品を試してみてください。

議論するために

  • developerWorks コミュニティーに参加してください。ここでは他の developerWorks ユーザーとのつながりを持てる他、開発者によるブログ、フォーラム、グループ、Wiki を調べることができます。

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Web development, Java technology
ArticleID=936968
ArticleTitle=HTML5 による 2D ゲームの開発: 重力の実装とサウンドの追加
publish-date=07182013