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年 6月 20日

衝突検出とスプライトのアニメーションは、すべてのテレビ・ゲームに不可欠な要素です。この連載で皆さんが作成しているゲーム、Snail Bait も例外ではありません。図 1 は Snail Bait のランナーが左上隅で蜂と衝突した後に爆発している様子を示しています。

図 1. 衝突検出の実際
Snail Bait でランナーと蜂が衝突した後に爆発する様子を示すスクリーン・ショット

この記事では以下の内容を説明します。

  • 衝突を検出する方法
  • HTML5 の Canvas コンテキストを使用して衝突を検出する方法
  • スプライトのビヘイビアとして衝突検出を実装する方法
  • 衝突を処理する方法
  • 爆発など、スプライトのアニメーションを実装する方法

衝突検出のプロセス

衝突検出のプロセスには以下の 4 つのステップがあり、このうちの 1 つのステップで実際に衝突を検出します。

  1. ゲームのスプライトに対して繰り返し処理を行う
  2. 衝突検出の候補以外のスプライトを排除する
  3. 候補のスプライトとの衝突を検出する
  4. 衝突を処理する

衝突検出は計算負荷が高くなる可能性があるため、衝突する可能性のないスプライトでは衝突検出を行わないようにすることが極めて重要です。例えば Snail Bait のランナーは、爆発している他のスプライトの中を走り抜けます。スプライトが爆発中かどうかを確認する方が、衝突検出を実行するよりも、処理に要する時間が短くてすむため、Snail Bait では爆発中のスプライトを衝突検出から排除しています。

では、衝突検出手法の概要について説明します。

衝突検出手法

スプライト同士の衝突は、いくつかの方法で検出することができます。よく使われる 3 つの手法を、高度さと複雑さの低いものから順に挙げると、以下のようになります。

  • バウンディング領域 (3D ゲームのバウンディング・ボリューム)
  • レイ・キャスティング
  • 分割軸原理 (Separating Axis Theorem: SAT)

バウンディング領域による衝突検出は、円またはポリゴン同士の交差を検出します。図 2 の例では、小さい円は 1 つのスプライト (小さいボール) を表すバウンディング領域であり、大きな円はボールよりも大きなバケツのスプライトのバウンディング領域です。この 2 つの円によるバウンディング領域が交差すると、ボールはバケツの中に入ります。

図 2. バウンディング領域: 2 つの円の衝突
円同士の衝突検出の基本原理を説明した図

2 つの円の衝突を検出する方法は、あらゆる衝突検出手法のうちで最も単純なものです。2 つの円の中心間の距離が 2 つの円の半径の合計よりも小さくなると、2 つの円が交差し、スプライト同士が衝突したことになります。

バウンディング領域による衝突検出は単純ですが、バウンディング領域が小さすぎる場合やバウンディング領域の移動速度が速すぎる場合は失敗する場合があります。どちらの場合も、1 つのアニメーション・フレーム内でスプライト同士が互いのそばを通り過ぎてしまうために、衝突が検出されない可能性があります。

高速で移動する小さなスプライトの場合に信頼性の高い手法は、図 3 に示すレイ・キャスティングです。レイ・キャスティングでは 2 つのスプライトの速度ベクトルの交差を検出します。図 3 の 5 つの各フレームで、ボールの速度ベクトルは、青で描画した斜めの直線であり、バケツの速度ベクトルは、水平方向に引かれた赤の直線です (バケツは水平方向に移動します)。この 2 つのベクトルの交差がバケツ上部の開口部内で交差し、図 3 右端のスクリーン・ショットのようにボールがバケツの開口部よりも下になると、ボールはバケツの中に入ります。

図 3. レイ・キャスティング
レイ・キャスティングによる衝突検出の原理を説明した図

事前または事後の衝突検出

衝突検出の方法には、衝突が発生する前 (事前) に検出する方法と、衝突が発生した後 (事後) に検出する方法があります。発生前に衝突を検出する場合には、スプライトの将来の位置を予測する必要があります。発生後に衝突を検出する場合には、衝突したスプライト同士を分離する必要があるのが通常です。いずれかの方法が、他方よりも明らかに優れているとか単純であるとかいうことはありません。

レイ・キャスティングは、図 2 でボールがバケツに入る場合のように、2 つの単純な形状同士が衝突したことを、それらの速度ベクトルの交差によって容易に判断できる状況では有効です。

もっと複雑なシナリオ (例えばサイズや形状が任意のポリゴン同士が衝突するなど) の場合、分割軸原理 (Separating Axis Theorem: SAT) は最も信頼性の高い (そして最も複雑な) 衝突検出手法の 1 つです。分割軸原理は、図 4 に示すように 2 つのポリゴンに異なる角度から光を当てることと、数学的には同じです。ポリゴンの背後にある壁に映る影に隙間があれば、ポリゴン同士は衝突していません。

図 4. 分割軸原理
分割軸原理を使用した衝突検出の原理を説明した図

この記事では、レイ・キャスティングと分割軸原理についてはこれ以上説明しません。この 2 つの手法の詳細については、『Core HTML5 Canvas』(2012年 Prentice Hall 刊) を読んでください (「参考文献」のリンクを参照)。


Snail Bait の衝突検出

Snail Bait の衝突検出では、ゆっくりとした速度で移動する比較的大きなスプライトを扱うため、バウンディング・ボックスを使用します。図 5 は Snail Bait のバウンディング・ボックスを示しています。

図 5. Snail Bait の衝突検出のためのバウンディング・ボックス
Snail Bail の各スプライトに長方形のバウンディング・ボックスが重ねて表示された様子を示す画面のスクリーン・ショット

Snail Bait では、スプライトのアクティビティー (走る、ジャンプする、爆発するなど) をスプライトのビヘイビアとして実装します (詳細については、developerWorks の記事「HTML5 による 2D ゲームの開発: スプライトのビヘイビアを実装する」(2013年 6月) を参照)。衝突検出についても同じことが言え、衝突はビヘイビアとして実装されます。Snail Bait を作成している現時点で、ランナーには 3 つのビヘイビアがあります。つまりランナーは走ること、ジャンプすること、そして他のスプライトと衝突することが可能です。リスト 1 では、この 3 つのビヘイビアを持つランナー・スプライトをインスタンス化しています。

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

リスト 2 はランナーの collideBehavior のコードです。

リスト 2. ランナーの「衝突する」ビヘイビア
var SnailBait =  function () {
   ...

   // Runner's collide behavior...............................................

   this.collideBehavior = {
execute: function (sprite, time, fps, context) {  // sprite is the runner
         var otherSprite;

         for (var i=0; i < snailBait.sprites.length; ++i) { 
            otherSprite = snailBait.sprites[i];

            if (this.isCandidateForCollision(sprite, otherSprite)) {
               if (this.didCollide(sprite, otherSprite, context)) { 
                  this.processCollision(sprite, otherSprite);
               }
            }
         }
      },
      ...
      
   };
};

collideBehavior オブジェクトはスプライトのビヘイビアであるため、Snail Bait ではアニメーション・フレームごとに collideBehavior オブジェクトの execute() メソッドを呼び出します。また collideBehavior オブジェクトはランナーと関連付けられているため、Snail Bait が execute() メソッドに渡すスプライトは常にランナーです。スプライトのビヘイビアについての詳細は、「スプライトのビヘイビアを実装する」の「ビヘイビアの基本」セクションを参照してください。

collideBehavior オブジェクトの execute() メソッドには、先ほど説明した衝突検出のための 4 つのステップがカプセル化されています。そのうちの終わりの 3 つのステップは以下の collideBehavior メソッドで表現されます。

  • isCandidateForCollision(sprite, otherSprite)
  • didCollide(sprite, otherSprite, context)
  • processCollision(sprite, otherSprite)

以下のセクションでは、これらの各メソッドの実装について説明します。


衝突検出の候補を選択する

Snail Bait のスプライトがランナー・スプライトと衝突する候補となるのは以下の場合です。

  • スプライトがランナーではないとき
  • そのスプライトとランナーの両方が表示されているとき
  • そのスプライトもランナーも爆発中ではないとき

このロジックを実装しているのが collideBehavior オブジェクトの isCandidateForCollision() メソッド (リスト 3) です。

リスト 3. 衝突検出の候補を選択する: isCandidateForCollision()
var SnailBait =  function () {
   ...

isCandidateForCollision: function (sprite, otherSprite) {
      return sprite !== otherSprite &&                       // not same
             sprite.visible && otherSprite.visible &&        // visible
             !sprite.exploding && !otherSprite.exploding;    // not exploding
   }, 
   ...
};

次に、候補となり得るスプライトとの衝突を検出する方法について説明します。


ランナーと他のスプライトとの衝突を検出する

collideBehavior オブジェクトの didCollide() メソッドは、ランナーが別のスプライトと衝突したかどうかを判断します。このメソッドをリスト 4 に示します。

リスト 4. 衝突したかどうかを確認する: didCollide()
var SnailBait =  function () {
   ...

didCollide: function (sprite,      // runner
                          otherSprite, // candidate for collision
                          context) {   // for context.isPointInPath()
      var left = sprite.left + sprite.offset,
          right = sprite.left + sprite.offset + sprite.width,
          top = sprite.top,
          bottom = sprite.top + sprite.height,
          centerX = left + sprite.width/2,
          centerY = sprite.top + sprite.height/2;

      // All the preceding variables -- left, right, etc. -- pertain to the runner sprite.

      if (otherSprite.type !== 'snail bomb') {
         return this.didRunnerCollideWithOtherSprite(left, top, right, bottom,
                                                         centerX, centerY,
                                                         otherSprite, context);
      }
      else {
         return this.didSnailBombCollideWithRunner(left, top, right, bottom,
                                                       otherSprite, context);
      }
   },

didCollide() メソッドは、ランナーのバウンディング・ボックスの四隅とその中心の座標を計算し、それらの計算結果を、衝突検出候補のスプライトに応じた (2 つのメソッドのうちの一方の) メソッドに渡します。

衝突検出候補のスプライトがカタツムリ爆弾ではない場合には、didCollide()リスト 5 に示す didRunnerCollideWithOtherSprite() を呼び出します。

リスト 5. didRunnerCollideWithOtherSprite()
didRunnerCollideWithOtherSprite: function (left, top, right, bottom,
                                              centerX, centerY,
                                              otherSprite, context) {
   // Determine if either of the runner's four corners or its
   // center lies within the other sprite's bounding box. 

   context.beginPath();
   context.rect(otherSprite.left - otherSprite.offset, otherSprite.top,
                otherSprite.width, otherSprite.height);
         
   return context.isPointInPath(left,    top)     ||
          context.isPointInPath(right,   top)     ||

          context.isPointInPath(centerX, centerY) ||

          context.isPointInPath(left,    bottom)  ||
          context.isPointInPath(right,   bottom);
},

didRunnerCollideWithOtherSprite() は、ランナーのバウンディング・ボックスとその中心の座標が渡されるので、バウンディング・ボックスの四隅のいずれか、またはその中心が、衝突検出候補のスプライトのバウンディング・ボックス内にあるかどうかを確認します。

Canvas コンテキストはグラフィックス以上のことをします

Canvas コンテキストの isPointInPath() メソッドは、ある 1 つのポイントが現在のパスにあるかどうかを検出します。Snail Bait では、ある 1 つのポイントが長方形の内部にあるかどうかを isPointInPath() メソッドを使用して判断します。しかし isPointInPath() が本当に力を発揮するのは、パスが不規則な形状の場合です。ある 1 つのポイントが不規則な形状の内部にあるかどうかを手計算で判断するのは簡単ではありません。

ある 1 つのポイントが長方形の内部にあるかどうかを判断する上で、数学的センスに満ちあふれている必要はありません。HTML5 の canvas 要素の 2D コンテキストを使用すると、ある 1 つのポイントが Canvas コンテキストの現在のパス内にある場合に true を返す isPointInPath() メソッドを使用できるため、その判断が一層容易になります。

didRunnerCollideWithOtherSprite() メソッドは、beginPath()rect() を呼び出すことにより、衝突検出候補のスプライトのバウンディング・ボックスを表す長方形のパスを作成します。続いて isPointInPath() を呼び出し、ランナー内の 5 つのポイントのいずれかが、衝突検出候補のスプライトのバウンディング・ボックスの中にあるかどうかを判断します。

didRunnerCollideWithOtherSprite() メソッドは、基本的にランナーと他のすべてのスプライトとの衝突を適切に検出しますが、カタツムリ爆弾との衝突 (図 6) については別です。

図 6. ランナーとカタツムリ爆弾
Snail Bait のランナーがカタツムリ爆弾と衝突しそうなところのスクリーン・ショット

このメソッドでは、カタツムリ爆弾との衝突検出はうまくいきません。それは、たまたまカタツムリ爆弾が非常に小さく、ランナーのバウンディング・ボックスの四隅またはその中心のいずれとも接触せずに、ランナーを通り抜けてしまうためです。ランナーとカタツムリ爆弾のサイズの比率が、あいにくこのメソッドでは衝突検出がうまくいかない値であるため、リスト 4didCollide() メソッドは、衝突検出候補のスプライトがカタツムリ爆弾である場合には、リスト 6 に示す didSnailBombCollideWithRunner() を呼び出します。

リスト 6. didSnailBombCollideWithRunner() メソッド
didSnailBombCollideWithRunner: function (left, top, right, bottom,
                                             snailBomb, context) {
   // Determine if the center of the snail bomb lies within
   // the runner's bounding box  

   context.beginPath();
   context.rect(left, top, right - left, bottom - top); // runner's bounding box

   return context.isPointInPath(
                 snailBomb.left - snailBomb.offset + snailBomb.width/2,
                 snailBomb.top + snailBomb.height/2);
},

didSnailBombCollideWithRunner() メソッドは、didRunnerCollideWithOtherSprite() とは逆の処理を行います。didRunnerCollideWithOtherSprite() では、ランナー内のポイントが衝突検出候補のスプライト内にあるかどうかを確認しますが、didSnailBombCollideWithRunner() では、衝突検出候補のスプライト (カタツムリ爆弾) の中心がランナー内にあるかどうかを確認します。

ここまで、バウンディング・ボックスを使用した衝突検出の実装方法を見てきましたが、この手法をもっと正確でパフォーマンスの高いものにすることができます。以降のセクションでは、ランナーのバウンディング・ボックスを変更し、さらにゲームの空間を分割することで Snail Bait の衝突検出を改善する方法について説明します。


バウンディング・ボックスを改善する

図 5 を見るとわかるように、衝突検出のバウンディング・ボックスは、そのボックスで表現されるスプライトを囲んでいます。しかしこれらのバウンディング・ボックスの四隅に近い辺りでは、ボックスの内部は多くの場合透明です。それはランナー・スプライトの場合も同じです (図 7)。これらの透明な領域によって誤った衝突が発生する可能性があり、2 つの透明な領域同士が衝突した場合には特にそれが目立ってしまいます。

図 7. ランナーの当初のバウンディング・ボックス
Snail Bait のランナーの当初のバウンディング・ボックスとランナーとが重ねて表示されている画面のスクリーン・ショット

四隅の透明な領域によって発生する誤った衝突をなくすための 1 つの方法は、スプライトのバウンディング・ボックスのサイズを小さくすることです (図 8)。

図 8. ランナーの変更後のバウンディング・ボックス
Snail Bait のランナーの変更後のバウンディング・ボックスが表示されている画面のスクリーン・ショット

Snail Bait では、リスト 7 のように変更した didCollide() メソッドを使用して、ランナーのバウンディング・ボックスのサイズを小さくします。

リスト 7. ランナーのバウンディング・ボックスのサイズを変更する
var SnailBait =  function () {
...

didCollide: function (sprite,      // runner
                       otherSprite, // candidate for collision
                       context) {   // for context.isPointInPath()
      var MARGIN_TOP = 10,
MARGIN_LEFT = 10,
MARGIN_RIGHT = 10,
MARGIN_BOTTOM = 0,
          left = sprite.left + sprite.offset + MARGIN_LEFT,
          right = sprite.left + sprite.offset + sprite.width - MARGIN_RIGHT,
          top = sprite.top + MARGIN_TOP,
          bottom = sprite.top + sprite.height - MARGIN_BOTTOM,
          centerX = left + sprite.width/2,
          centerY = sprite.top + sprite.height/2;
       ...
   },
   ...
};

ランナーのバウンディング・ボックスを小さくすると、誤った衝突がなくなるため、Snail Bait の衝突検出の精度が高くなります。次は、衝突検出処理のパフォーマンスを高める方法を説明します。


空間分割

空間分割についての詳細

Snail Bait での空間分割は最も基本的な方式です。もっと複雑な空間分割の実装には、衝突検出セルが大量にある場合に適した八分木空間分割やバイナリー空間分割などがあります。空間分割についての詳細は「参考文献」を参照してください。

空間分割とは、ゲームの空間をセルに分割することであり、これによって同じセル内のスプライト同士のみが衝突できるようにします。異なるセルのスプライトに対する衝突検出を排除することで、空間分割によって多くの場合はパフォーマンスが大幅に向上します。Snail Bait でも、そうした大幅なパフォーマンスの向上を実現するために、図 9 のように空間を分割しています。

図 9. Snail Bait の空間分割
Snail Bait での空間分割を示すスクリーン・ショット。左側の区画 (画面の約 10分の1 を占めている部分) 内にあるスプライトのみがランナーと衝突することができます。右側のもっと大きな区画内にあるスプライトはランナーと衝突することができません。

リスト 8 を見るとわかるように、Snail Bait では図 9 の右側領域内にあるスプライトをすべて衝突検出の候補から除外しているため、実行する衝突検出の計算処理量は大幅に少なくなっています。

リスト 8. 衝突検出の対象となるスプライトの選択を改善する
this.isCandidateForCollision: function (sprite, otherSprite) {
   return sprite !== otherSprite &&
          sprite.visible && otherSprite.visible &&
          !sprite.exploding && !otherSprite.exploding &&
otherSprite.left - otherSprite.offset < sprite.left + sprite.width;


},

ここまでで、効率的に衝突を検出する方法を見てきたので、今度は Snail Bait がどのように衝突を処理するのかを見ていきましょう。


衝突を処理する

衝突を検出したら、その衝突に対して何らかの処理をしなければなりません。リスト 9 を見るとわかるように、Snail Bait の processCollision() はランナーと他のスプライトとの衝突を処理します。

リスト 9. 衝突を処理する: processCollision()
var SnailBait =  function () {
processCollision: function (sprite, otherSprite) {
      if ('coin'  === otherSprite.type    ||  // bad guys
          'sapphire' === otherSprite.type ||
          'ruby' === otherSprite.type     ||
          'button' === otherSprite.type   ||
          'snail bomb' === otherSprite.type) {
         otherSprite.visible = false;
      }

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

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

上記コードでは、ランナーがアイテム (コイン、サファイア、ルビー、ボタン) やカタツムリ爆弾にぶつかると、その衝突相手のスプライトの visible 属性を false に設定することによって、そのスプライトを非表示にします。

一方、ランナーが敵 (コウモリ、蜂、カタツムリ、カタツムリ爆弾) にぶつかると、snailBait オブジェクトの explode() メソッドを呼び出すことで、ランナーを爆発させます。現状では、explode() メソッドはコンソールに「BOOM」と出力するのみです。この連載の次回の記事では、explode() メソッドの最終的な実装について説明します。

最後に processPlatformCollisionDuringJump() メソッド (リスト 10 に示します) で、ランナーがジャンプしている間のプラットフォームとの衝突を処理します。

リスト 10. processPlatformCollisionDuringJump()
processPlatformCollisionDuringJump: function (sprite, platform) {
      var isDescending = sprite.descendAnimationTimer.isRunning();
   
      sprite.stopJumping();
   
      if (isDescending) { // Collided with platform while descending
         // land on platform

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

ランナーがジャンプ中にプラットフォームとぶつかる場合、ランナーがジャンプの下降中であれば、ジャンプをやめてプラットフォームに着地します。ランナーが上昇中であれば、プラットフォームの下側からプラットフォームと衝突し、ランナーは落下します。今のところ、ランナーの fall() メソッドはリスト 11 のように実装してあります。

リスト 11. 落下用の暫定メソッド
var SnailBait =  function () {

   ...


   this.runner.fall = function () {
      snailBait.runner.track = 1;
      snailBait.runner.top = snailBait.calculatePlatformTop(snailBait.runner.track) -
                             snailBait.runner.height;
   };
   ...
};

ランナーの fall() メソッドは即座に一番下のトラック上に (つまり最も下にあるプラットフォームの上に) ランナーを配置します。連載の次回の記事では、このメソッドに時間の経過による重力の影響を組み込むことで、リアルな落下をするメソッドに実装しなおします。


衝突検出のパフォーマンスをモニターする

90 パーセントはアイドル状態?

図 10 の表の一番上に表示されているエントリーを見ると、90 パーセントの時間、Snail Bait は単に何かするために待機しているのみであることがわかります。このように Snail Bait が素晴らしいパフォーマンスを示している理由は、図 10 のプロファイルを取得するために使用したブラウザーが Chrome (バージョン 26) であるためです。最近の他のすべてのブラウザーと同様、Chrome は canvas 要素のアクセラレーションを行います。ブラウザー・ベンダーは通常、Canvas API への呼び出しを WebGL に変換することで canvas 要素のアクセラレーションを実装しています。そのため、ユーザーは手軽な Canvas API を使用して WebGL のパフォーマンスを得ることができます。

衝突検出はパフォーマンスのボトルネックになりがちであり、分割軸原理などの計算負荷の重い衝突検出アルゴリズムを実装する場合はなおさらです。この記事ではここまでで、バウンディング・ボックスの改善や空間分割など、パフォーマンスを向上させるために使える簡単な手法を説明しましたが、ゲームのパフォーマンスを常にモニターし、パフォーマンスの問題が発生したら即座にそれを検出して修正できるようにするのも賢明な考えです。

最近のブラウザーには、高度な開発環境が用意されています。例えば、Chrome、Safari、Firefox、Internet Explorer、Opera はいずれも、実行中にコードのプロファイルを取得することができます。図 10 は Chrome のプロファイラーです。このプロファイラーは個々のメソッドの実行時間を、全体としての実行時間に対する相対値として表示します。

図 10. 衝突検出のパフォーマンス
Snail Bait の各メソッドの実行時間のパーセンテージを表示する Chrome のプロファイラー

図 10 を見ると、Snail Bait の didCollide() メソッドの実行時間は、ゲーム全体の実行時間の 0.05 パーセントにすぎないことがわかります。(「Self」列を見ると didCollide() の値は 0.01 パーセントですが、「Self」列はそのメソッド自体の実行時間のみを示し、そのメソッドによって呼び出される他のメソッドの実行時間は含まれません。)

ランナーが敵にぶつかると、ランナーは爆発します。次は、爆発の実装方法について見ていきましょう。


スプライトのアニメーション

図11 は、Snail Bait でランナーが蜂などの敵にぶつかったときに表示される、爆発のアニメーションを上から下へと順に示しています。

図 11. 衝突後に爆発するランナー
Snail Bait のランナーが蜂とぶつかって爆発する様子を示す画面のスクリーン・ショット

Snail Bait では、スプライト・アニメーターを使用して (図 11 のような) スプライトのアニメーションを実装しています。スプライト・アニメーターは、スプライトのアーティストが描画するセルを、指定された期間だけ一時的に変更します。例えば、爆発のスプライト・アニメーターは、ランナーのアニメーション・セルを図 12 に示すようなセルに 500 ミリ秒間変更します。

図 12. Snail Bait のスプライトシートに含まれる爆発のセル
爆発のスプライト・アニメーターによって変更される、爆発のスプライトシートのセル

スプライト・アニメーター・オブジェクトのコンストラクターをリスト 12 に示します。

リスト 12. スプライト・アニメーターのコンストラクター
// Sprite Animators...........................................................

var SpriteAnimator = function (cells, duration, callback) {
   this.cells = cells;
   this.duration = duration || 1000;
   this.callback = callback;
};

SpriteAnimator コンストラクターは 3 つの引数を取ります。第 1 の引数は Snail Bait のスプライトシートに含まれるバウンディング・ボックスの配列です。これらのバウンディング・ボックスは、一時的なアニメーション・セルであり、この引数は必須です。第 2 と 第 3 の引数はオプションです。第 2 の引数はアニメーションの指定継続時間であり、第 3 の引数はアニメーションの指定継続時間が経過したときにスプライト・アニメーターによって呼び出されるコールバック関数です。

SpriteAnimator メソッドは、オブジェクトのプロトタイプの中で定義されます (リスト 13)。

リスト 13. スプライト・アニメーターのメソッド
SpriteAnimator.prototype = {
start: function (sprite, reappear) {
      var originalCells = sprite.artist.cells,
          originalIndex = sprite.artist.cellIndex,
          self = this;

      sprite.artist.cells = this.cells;
      sprite.artist.cellIndex = 0;
      
      setTimeout(function() {
         sprite.artist.cells = originalCells;
         sprite.artist.cellIndex = originalIndex;

         sprite.visible = reappear;

         if (self.callback) {
            self.callback(sprite, self);
         }
      }, self.duration); 
   },
};

SpriteAnimator オブジェクトの start() メソッドは、元のアニメーション・セルと、現在のセルを指すインデックスを保存し、それぞれを一時的なセル、およびゼロで置き換えることによってアニメーションを開始します。そして、アニメーションの指定継続時間が経過すると、start() メソッドはスプライトのアニメーション・セルと元のインデックスをアニメーション開始前の状態に戻します。

リスト 14 は、Snail Bait がどのようにスプライト・アニメーターを使用して、ランナーを爆発させているかを示しています。

リスト 14. 爆発アニメーターを作成する
var SnailBait =  function () {
   this.canvas = document.getElementById('game-canvas'),
   this.context = this.canvas.getContext('2d'),
   ...

   this.RUN_ANIMATION_RATE = 17,     // frames/second
   this.EXPLOSION_CELLS_HEIGHT = 62, // pixels
   this.EXPLOSION_DURATION = 500,    // milliseconds

   this.explosionCells = [
      { left: 1,   top: 48, width: 50, height: this.EXPLOSION_CELLS_HEIGHT },
      { left: 60,  top: 48, width: 68, height: this.EXPLOSION_CELLS_HEIGHT },
      { left: 143, top: 48, width: 68, height: this.EXPLOSION_CELLS_HEIGHT },
      { left: 230, top: 48, width: 68, height: this.EXPLOSION_CELLS_HEIGHT },
      { left: 305, top: 48, width: 68, height: this.EXPLOSION_CELLS_HEIGHT },
      { left: 389, top: 48, width: 68, height: this.EXPLOSION_CELLS_HEIGHT },
      { left: 470, top: 48, width: 68, height: this.EXPLOSION_CELLS_HEIGHT }
   ],
   ...

   this.explosionAnimator = new SpriteAnimator(
      this.explosionCells,          // Animation cells
      this.EXPLOSION_DURATION,      // Duration of the explosion

      function (sprite, animator) { // Callback after animation
         sprite.exploding = false; 

         if (sprite.jumping) {
            sprite.stopJumping();
         }
         else if (sprite.falling) {
            sprite.stopFalling();
         }

         sprite.visible = true;
         sprite.track = 1;
         sprite.top = snailBait.calculatePlatformTop(sprite.track) - sprite.height;
         sprite.artist.cellIndex = 0;
         sprite.runAnimationRate = snailBait.RUN_ANIMATION_RATE;
      }
   );
};

Snail Bait は、図 12 のアニメーション・セルを使用してスプライト・アニメーターを作成します。このアニメーションの指定継続時間は 500ms であり、このアニメーションが終了すると、スプライト・アニメーターは爆発アニメーターのコールバック関数を呼び出し、この関数がランナーを一番下のプラットフォーム・トラックに配置します。今後の記事で最終的には、自機を失って現在のステージの最初から再開するようにコールバック関数を実装しなおす予定です。

リスト 15 は、ランナーの (少し期待外れの) explode() メソッドを示しています。

リスト 15. Snail Bait の explode() メソッド
SnailBait.prototype = {
   ...

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

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

ランナーがジャンプしている間、ランナーのアニメーションのフレーム・レートはゼロなので、ランナーのアニメーションは動きません。explode() メソッドは、ランナーのアニメーションのフレーム・レートを通常の値に設定するので、ランナーは爆発するセルを走り抜けるようになります。そして explode() メソッドは、ランナーの exploding() 属性を true にセットし、爆発アニメーターを開始します。


次回は

この連載の次回の記事では、重力の影響を組み込むことによってリアルな落下を実装する方法と、サウンドおよび音楽を Snail Bait に追加する方法を説明します。


ダウンロード

内容ファイル名サイズ
Sample codewa-html5-game8-code.zip1.2MB

参考文献

学ぶために

  • Core HTML5 Canvas』(David Geary 著、Prentice Hall、2012年): Canvas API とゲーム開発について広範にわたって説明している David Geary の著書です。関連する Web サイトとブログにもアクセスしてください。
  • バイナリ空間分割」: ウィキペディアで空間分割についての説明を読んでください。
  • The Making of an HTML5 Platform Game」: David Geary が 2013年 2月 20日に Atlanta HTML 5 ユーザー・グループに対して行ったプレゼンテーションを見てください。
  • 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 でプラットフォーム・ゲームについての説明を読んでください。
  • 「横スクロール」テレビ・ゲーム: ウィキペディアで横スクロール・テレビ・ゲームについての説明を読んでください。
  • HTML5 の基礎」: developerWorks の Knowledge path で HTML5 の基本について学んでください。
  • developerWorks の Web development ゾーン: Web ベースのさまざまなソリューションを解説した記事が豊富に用意されています。Web development 技術文書一覧に用意された、さまざまな技術記事やヒント、チュートリアル、技術標準、IBM Redbooks をご覧ください。
  • developerWorks テクニカル・イベント: これらのセッションで最新情報を入手してください。
  • developerWorks 最新イベント情報: IBM の製品およびツールについての情報や IT 業界の動向についての情報を迅速に把握してください。
  • developerWorks オンデマンド・デモ: 初心者向けの製品のインストールおよびセットアップから熟練開発者向けの高度な機能に至るまで、さまざまに揃ったデモを見てください。
  • Twitter での developerWorks: 今すぐ登録して developerWorks のツイートをフォローしてください。

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

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

議論するために

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

コメント

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=934594
ArticleTitle=HTML5 による 2D ゲームの開発: 衝突検出とスプライトのアニメーション
publish-date=06202013