JavaScript ゲームのための単純な 2D 物理エンジンを作成する

JavaScript ゲームに Box2D などのサード・パーティーの物理演算ライブラリーを使用するのは、その複雑さやオーバーヘッドを考えるとやりすぎであることがあります。この記事では、単純な 2D 物理エンジンを実装する方法を紹介します。重力およびゲーム・オブジェクト同士の衝突検出によって、単純な物理学の世界を作り出す方法を学んでください。記事では、物理エンジンの構造、衝突を検出および解決するアルゴリズム、そして「自作の」物理エンジンを作成する理由を説明します。

Adam Ranfelt, Software Developer, The Nerdery

Adam RanfeltAdam Ranfelt は、The Nerdery の特定の技術にとらわれない姿勢を証明する生き証人です。2009年にコンピューター・サイエンスの学位を取得してミネソタ大学を卒業した後、技術の最前線で働くことを楽しんでいます。彼は、HTML、ActionScript、Java、および Objective-C のエキスパートです。2010年に The Nerdery に入社する前は、Curb-Crowser Design で数々のデジタル・メディア・プロジェクトを構築、保守していました。それぞれのジョブに適した正しい技術を使用することを信念とする彼は、10 を超えるプログラミング言語に精通し、現在もその数を増やしています。彼の技術スキルならびにリーダーシップとしての手腕が認められ、2012年には主任ソフトウェア・エンジニアに昇進しました。



2012年 12月 20日

はじめに

2D ゲームには、形状や大きさがさまざまなものが登場します。ゲームによっては衝突検出などの、システムの近似シミュレーションを提供する独自の 2D 物理エンジンを作成することが適切な選択となる場合があります。これは特に、JavaScript を使用する場合に言えることです。対象がどのプラットフォームでも、堅牢な物理エンジンを開発するのは困難ですが、JavaScript ゲームには単純で簡潔なエンジンのほうがより好ましいことは珍しくありません。よく使われている物理エンジンを簡潔にしたものを必要としている場合には、余計な機能をすべて省いて一から物理エンジンを作成すると作業がはかどります。

この記事では、プラットフォーム・ゲームの基本フレームワークを構築する物理エンジンの実装例を探ります。この実装例を、Box2D などの既存の物理エンジンと比較してください。コード・スニペットで、コンポーネントがどのように相互作用するのかを説明します。

ダウンロード」セクションから、記事で使用するサンプルのソース・コードはダウンロードすることもできます。


単純化する理由

ゲーム開発において、単純化という言葉が意味する内容は、多岐にわたる可能性があります。物理エンジンの場合で言うと、単純化という言葉は計算の複雑さに関して使われるのが一般的です。複雑さがより深く関係してくるのは、ゲームのなかで共通する基本部分が明らかにされた後からです。計算に関して複雑であるということは、処理に費やされる時間が長くなる可能性があること、あるいは物理演算が難しくなる可能性があることを意味します。この記事の内容を理解するのに、微積分学に関する実用的な知識は必要ありません。

JavaScript は HTML5 の canvas との相性が良いことから、これからは物理演算が HTML5 の canvas をベースにしたゲームに適用されるのを目にすることが多くなるでしょう。iOS や Android をはじめとするモバイル・プラットフォームなどでは、ゲームを構成する要素として、HTML5 の canvas によるグラフィックが支配的になるケースもあります。より小規模なリソースのプラットフォームを対象にゲームを作成するということは、限られたリソースに可能な限り多くの処理を押し込んで実行することで、コストのかかるグラフィック計算を実行するだけの十分な余力を CPU に残すことを意味します。

CPU の使用状況

十分にテストされた堅牢なライブラリーから自作の簡潔なソリューションに移行する主な理由は、CPU による処理です。ここで言う「CPU による処理」とは、CPU の使用状況のことです。CPU の使用状況は、プログラムつまりゲームの実行中に、ある処理で使用可能、あるいは使用されている CPU による処理量を表します。物理エンジンでは、ゲームの物理エンジン以外の部分に匹敵するだけの CPU による処理を使用することもあります。単純化をするということは、すなわち CPU の使用状況をより低い状態にすることを意味します。

ゲームを実行する際に一般に目指す 1 秒あたりのフレーム数は、30 から 60 fps (frames per second) です。これはつまり、ゲーム・ループの処理を 33 ミリ秒から 16 ミリ秒の時間内に収めなければならないことを意味します。図 1 に一例を示します。複雑なソリューションに従うとなると、ゲームの CPU 使用状況において一部を占める可能性のある他の機能に支障をきたすことになります。ゲームを構成するあらゆるコンポーネントから CPU の使用量を可能な限り削減することが、長期的に見て功を奏することになります。

図 1. ゲーム・ループの 1 ループ・ステップにおける CPU 使用状況の例
ゲーム・ループの 16ms の 1 ループ・ステップを占める、ゲーム・ロジック、物理エンジンのステップ、レンダリング・エンジンのステップの CPU 使用状況

コンポーネントの決定

2D を使用すると、さまざまに異なる手の込んだエフェクトをエミュレートつまり模倣することができますが、それはエンジンが適切に処理されている場合に限られます。ゲームを作成するときには、必要となるコンポーネントを検討してください。必ず作成しなければならないコンポーネント、そしてエンジンに計算させる必要のないコンポーネントを決定します。例えば、図 2 に示す重心のようなものを模倣するのは難しいですが、小さなヒット領域については簡単に模倣することができます。

図 2. 重心
重力が中心に向かっている様子を示す重心の図

物理エンジンの作成

このセクションでは、物理エンジンの構成要素と、どの機能を組み込むかを決定する方法について説明します。

物理エンジンを作成する際の最初の主要なステップは、エンジンに実装する機能を選択し、処理の順序を決めることです。実装する機能を決定するのは大したことではないように思えるかもしれませんが、選択した機能によって、物理エンジンを構成するコンポーネントが決まってくるだけでなく、実装が困難な可能性がある領域が明らかになる場合もあります。サンプル・アプリケーションでは、図 3 に示すようなゲームのためのエンジンを作成します。

図 3. プラットフォーム・ゲーム
ゲーム・シーンの図

図 3 に示されているボックスの意味は以下のとおりです。

  • プレイヤー: 内部に斜線が引かれているボックス
  • クリア条件 (ゴール): 黒で塗りつぶされているボックス
  • 通常のプラットフォーム: 実線のボックス
  • 上下に弾むプラットフォーム: 点線のボックス

プログラマーが作成するグラフィックとしては単純であることは置いといて、この図からゲームの機能を思い描くことはできるはずです。プレイヤーは、クリア条件 (ゴール) に到達するとゲーム・クリアです。このゲームはプラットフォーム・ゲームなので、以下の物理エンジンの最も基本的なビルディング・ブロックが必要となります。

  • 速度、加速度、重力
  • 衝突検出
  • 弾性衝突
  • 通常の衝突

プレイヤーを動かすには、位置関連の属性を使用します。また、プレイヤーがゴールに到達したり、ゲームの中で動き回ったりするなかで衝突検出が行われます。異なるタイプの衝突を使用することで、ゲームの地面の種類を変えることができます。また、プレイヤーを 1 人のみにすることで、基本的にゲームの動的オブジェクトは 1 つになるため、コード内の衝突処理の量を減らすことができます。

これで、ゲームの機能と、機能の物理的側面は明らかになったので、次は物理エンジンの構造設計に取り掛かります。

適切なエンジン・ランタイムを選択する

物理エンジンには、大きく分けて高精度エンジンと、リアルタイム・エンジンという 2 つの形があります。高精度エンジンは、難解な物理計算や、重要な物理計算をシミュレートするために使用されます。リアルタイム・エンジンは、テレビ・ゲームで使用されるタイプのエンジンです。このサンプル・アプリケーションでの物理エンジンには、リアルタイム・エンジンを使用します。このエンジンは、停止を要求されるまで、計算を無限に実行し続けます。

リアルタイム・エンジンには、物理エンジンのタイミングを制御するための選択肢として、以下の 2 つがあります。

静的
常に、すべてのフレームが通過することが見込まれる一定の期間をエンジンに提供します。静的リアルタイム・エンジンは、コンピューターによって実行速度が変わるため、その動作が異なることはよくあります。
動的
経過時間をエンジンにフィードします。

この記事のサンプル物理エンジンは実行を継続する必要があるため、エンジンに対して実行する無限ループをセットアップしなければなりません。この処理パターンは、「ゲーム・ループ」と呼ばれます。この記事では、ゲーム・ループで実行される処理のそれぞれを「ステップ」と呼びます。動的エンジンにする場合は、requestAnimationFrame API を使用してください。リスト 1 に、requestAnimationFrame を実行するために必要なコードを記載します。この API は、Khronos Group の CVS リポジトリーに収容されているポリフィルを使用します。

リスト 1. ポリフィルを使用した requestAnimFrame
// call requestAnimFrame with a parameter of the
// game loop function to call
requestAnimFrame(loopStep);

物理エンジンのループ

ループ内での処理の順序を決めるのは簡単そうに思えるかもしれませんが、これは簡単な決定ではありません。このステップにはいくつか異なる選択肢があるものの、ここではこれまでに明らかにした機能をベースにサンプル・エンジンを設計します (図 4 を参照)。

図 4. 物理演算のループ・ステップ
実行ループ・ステップのプロセス例: ゲーム・ロジックに対するユーザー操作から始まり、位置ロジック、衝突検出、衝突解決、そして最後にレンダリングが行われるという順序です。

リスト 2 に記載する計算の実行ステップでは、それぞれのタイプの計算を 1 つのパスで行います。各オブジェクトの計算を個別に実行するという手法もありますが、その場合には、他の計算との依存性により、おかしな結果になるのが通常です。

リスト 2. 物理演算のループ・ステップの擬似コード
1 User Interaction
2 Positional Logic
3 Detect Collisions
4 Resolve Collisions

ワークフロー、機能、そして作成するエンジンのタイプが決まったので、各構成部分の作成を開始します。

剛体力学

科学としての物理学は非常に広範な分野に及び、各種の計算が含まれます。電磁気学は、磁気または電気の計算で構成される一方、ニュートン物理学は、一般的な位置、速度、加速度の計算で構成され、物理システムで重力をエミュレートするために使用することができます。これらの物理学の各分野はそれぞれが単独でも素晴らしいものですが、これらの計算の複雑さについては、この記事では取り上げません。

物理システムを決定する際にエンジンをどのように構成するかは、目的とする計算のタイプによって変わってきます。サンプル・エンジンでは剛体力学、つまり変形することのない物理学を実装します。剛体力学を使用する場合、軟体力学で見られるような力による変形の計算や、あらゆる重力多体系で見られるような力が加わることによる変化の計算などを回避することができます。


エンジンの構成要素

物理エンジンは計算に関してはかなり複雑ですが、パターンを覚えてしまえば、構築するのは比較的簡単です。リスト 2 に、ループ・ステップの擬似コードの概要を記載しました。このステップで実行する各計算は、それぞれに独自のオブジェクトまたは API で構成することができます。エンジン・オブジェクト・グラフは、以下の主要コンポーネントで構成されます。

  • 物理エンティティー
  • 衝突ディテクター (衝突検出プログラム)
  • 衝突ソルバー (衝突解決プログラム)
  • 物理エンジン

エンティティー (エンジンが働きかけるオブジェクト、物体、またはモデル) は、最もアクティブでないコンポーネントです。Box2D でこれに相当するのは b2Body クラスです。「ディテクター」と「ソルバー」は連携して動作します。始めにディテクターがエンティティー間の衝突を検出すると、その衝突をソルバーに渡します。するとソルバーは衝突を変化に変換して、衝突が作用しているエンティティーに適用するという仕組みです。

物理エンジンは自身にエンジンを包含すると同時に、基本的にシステムの各フェーズでそれぞれのコンポーネントを管理および準備し、コンポーネントと通信します。図 5 に、物理エンジンに含まれる要素間の関係を示します。

図 5. 物理エンジン
物理エンジン・ステップのプロセス例: 新しい位置が計算された後、衝突検出、衝突解決と続きます。

以上の 4 つのコンポーネントがエンジンを支える主力となります。最初に実装するコンポーネントは、画面上のすべてのオブジェクトを表現する物理エンティティーです。


物理エンティティー

物理エンティティーは、エンジンの中で最も小さくて最も単純なコンポーネントですが、それと同時に最も重要なコンポーネントでもあります。前述のとおり、物理エンティティーは画面上の各要素を表現します。エンティティーは、ゲームにおいても物理においても、そのエンティティーに関係するすべてのメタデータを保持することで、オブジェクトの状態を表現します (リスト 3 を参照)。

リスト 3. エンティティーの JavaScript オブジェクト
// Collision Decorator Pattern Abstraction

// These methods describe the attributes necessary for
// the resulting collision calculations

var Collision = {

    // Elastic collisions refer to the simple cast where
    // two entities collide and a transfer of energy is
    // performed to calculate the resulting speed
    // We will follow Box2D's example of using
    // restitution to represent "bounciness"

    elastic: function(restitution) {
        this.restitution = restitution || .2;
    },

    displace: function() {
        // While not supported in this engine
	       // the displacement collisions could include
        // friction to slow down entities as they slide
        // across the colliding entity
    }
};

// The physics entity will take on a shape, collision
// and type based on its parameters. These entities are
// built as functional objects so that they can be
// instantiated by using the 'new' keyword.

var PhysicsEntity = function(collisionName, type) {

    // Setup the defaults if no parameters are given
    // Type represents the collision detector's handling
    this.type = type || PhysicsEntity.DYNAMIC;

    // Collision represents the type of collision
    // another object will receive upon colliding
    this.collision = collisionName || PhysicsEntity.ELASTIC;

    // Take in a width and height
    this.width  = 20;
    this.height = 20;

    // Store a half size for quicker calculations
    this.halfWidth = this.width * .5;
    this.halfHeight = this.height * .5;

    var collision = Collision[this.collision];
    collision.call(this);

    // Setup the positional data in 2D

    // Position
    this.x = 0;
    this.y = 0;

    // Velocity
    this.vx = 0;
    this.vy = 0;

    // Acceleration
    this.ax = 0;
    this.ay = 0;

    // Update the bounds of the object to recalculate
    // the half sizes and any other pieces
    this.updateBounds();
};

// Physics entity calculations
PhysicsEntity.prototype = {

    // Update bounds includes the rect's
    // boundary updates
    updateBounds: function() {
        this.halfWidth = this.width * .5;
        this.halfHeight = this.height * .5;
    },

    // Getters for the mid point of the rect
    getMidX: function() {
        return this.halfWidth + this.x;
    },

    getMidY: function() {
        return this.halfHeight + this.y;
    },

    // Getters for the top, left, right, and bottom
    // of the rectangle
    getTop: function() {
        return this.y;
    },
    getLeft: function() {
        return this.x;
    },
    getRight: function() {
        return this.x + this.width;
    },
    getBottom: function() {
        return this.y + this.height;
    }
};

// Constants

// Engine Constants

// These constants represent the 3 different types of
// entities acting in this engine
// These types are derived from Box2D's engine that
// model the behaviors of its own entities/bodies

// Kinematic entities are not affected by gravity, and
// will not allow the solver to solve these elements
// These entities will be our platforms in the stage
PhysicsEntity.KINEMATIC = 'kinematic';

// Dynamic entities will be completely changing and are
// affected by all aspects of the physics system
PhysicsEntity.DYNAMIC   = 'dynamic';

// Solver Constants

// These constants represent the different methods our
// solver will take to resolve collisions

// The displace resolution will only move an entity
// outside of the space of the other and zero the
// velocity in that direction
PhysicsEntity.DISPLACE = 'displace';

// The elastic resolution will displace and also bounce
// the colliding entity off by reducing the velocity by
// its restituion coefficient
PhysicsEntity.ELASTIC = 'elastic';

モデルとしてのエンティティー

リスト 3 のロジックに示されているように、ロー・データだけが格納された物理エンティティーは、データ・セットのさまざまなバリエーションを生成します。MVC や MVP などの MV* パターンのいずれかを理解している読者の方は、このエンティティーは、これらのパターンを構成する Model コンポーネントを表すことはご存じのはずです (MVC については「参考文献」を参照)。エンティティーは、さまざまなデータを格納します。これらのデータはすべて、該当するオブジェクトの状態を表現するためのものです。物理エンジンで実行されるステップごとに、これらのデータが変更され、最終的にエンジン全体の状態が変更されます。さまざまに分類されるエンティティーの状態データについては、後で詳しく説明します。

お気付きかもしれませんが、リスト 3 のエンティティーには、このエンティティーを画面上に表示するためのレンダリング関数も描画関数も含まれていません。それは、物理ロジックと表示をグラフィカルな表現から切り離し、任意のグラフィカルな表現をレンダリングできるようにすることで、ゲーム・エンティティーのスキンを設定可能にするためです。図 6 ではオブジェクトを長方形の画像で表現していますが、その形は長方形に限られるわけではありません。エンティティーには、あらゆる形の画像を適用することができます。

図 6. 境界ボックスとスプライト
中央にヒット領域があるスプライト

位置、速度、加速度: 位置データ

エンティティーには、位置データが組み込まれます。位置データとは、スペース内でのエンティティーの移動方法を表現する情報です。位置データには、ニュートンの運動方程式 (「参考文献」を参照) で見られるような基本ロジックが含まれます。これらのデータ点のうち、このサンプルのエンティティーに関係するのは加速度、速度、位置です。

速度は、一定の期間にわたる位置の変化です。同様に、加速度も一定の期間での速度の変化です。位置の変化を計算するのは、極めて単純明快な計算になります (リスト 4 を参照)。

リスト 4. 位置の方程式
p(n + 1) = v * t + p(n)

ここで:

  • p はエンティティーの位置です。位置は多くの場合、x と y で表現されます。
  • v は速さ、または速度 (進行方向を含む速さ) です。これは、一定の期間で位置が変化した量です。
  • t は経過した時間の長さです。JavaScript では、時間の長さはミリ秒で測定されます。

リスト 4 の方程式から、エンティティーの位置は必然的に、適用される時間に影響されることがわかります。同様に、速度は加速度によって更新されます (リスト 5 を参照)。

リスト 5. 速度の方程式
v(n + 1) = a * t + v(n)

ここで:

  • v は速さ、または速度 (進行方向を含む速さ) です。これは、一定の期間で位置が変化した量です。
  • a はエンティティーの加速度です。これは、一定の期間で速度が変化した量です。
  • t は経過した時間の長さです。JavaScript では、時間の長さはミリ秒で測定されます。

リスト 5 はエンティティーの加速度を表現しているという点を除き、リスト 4 と同じです。この 2 つの方程式をエンティティーのロジックの中でループ処理したとしても、それは単にパラメーターの値を格納するだけになるので、ここではループ処理は行いませんが、これらの方程式のパラメーターの意味を理解しておくことは重要です。また、エンティティーの空間表現についても検討する必要があります。

幅と高さ: 空間データ

「空間データ」とは、オブジェクトが占めるスペースとヒット領域を表現するために必要なパラメーターのことを指しています。空間データに影響するのは、形状やサイズなどの要素です。サンプル・プラットフォームでは、ヒット領域に「スモーク・アンド・ミラー」手法を適用します (スモーク・アンド・ミラーとは、錯覚させること、または騙すことの例えで、マジシャンのイリュージョンに由来しています)。

ヒット領域は、エンティティーを表現するグラフィックやスプライトより大きい場合もあれば、小さい場合もあるのが一般的です。大抵の場合、ヒット領域は境界ボックスや長方形で構成されます。単純さと計算削減の必要性を考え、この実装では長方形だけを使用してヒット領域をテストすることにします。

グラフィックおよび物理演算では、長方形を 4 つの数値で表現することができます。図 7 に示すように、位置には左上の地点を使用し、サイズには幅と高さを使用します。

図 7. ボックスの表現
空間測定値で表現されるボックス

境界ボックスに必要なのは位置とサイズだけです。他のすべてのパラメーターは、位置とサイズに依存します。役に立つ計算には、中点と辺の計算もあります。この 2 つはいずれも、衝突の計算によく使われます。

反発: 衝突データ

物理エンティティーに格納されるデータ・コンポーネントとして最後に説明するのは、衝突データです。これは、衝突中のオブジェクトの動作方法を決定する情報です。このサンプルでの衝突には変位と弾性衝突だけを含めるので、要件はかなり絞られてきます (図 8 を参照)。弾性衝突は、Box2D で使用されている命名パターンに従い、跳ね返り特性を「反発」として定義します。

図 8. 完全弾性衝突の例
衝突解決軌道経路の例

以上のデータ・コンポーネントがあれば、システムがこのサンプル実装で一定の期間に発生するすべての変化を計算するのに十分なデータが揃います。


高度な衝突の概念

このサンプル・システムには、このエンティティーで十分用は足りますが、大抵の場合は他にも各種の衝突解決に使用されるパラメーターがあります。このセクションでは、他のタイプのパラメーターについて簡単に説明します。

現実世界での値

ゲームのデザイナーは、現実の世界に基づいてゲームの世界をモデル化し、ゲームにおけるさまざまなものを現実世界の単位で測ることがよくあります。システムを現実世界の単位 (メートルなど) でモデル化する実装に必要なのは、データ・ポイント間の距離を現実世界の距離に、またはその逆に変換するためのスケール係数だけです。

この記事では、これらの変換の例を記載しませんが、Box2D (「参考文献」を参照) に記載されているサンプルに、変換率とその参考例が記載されています。

質量と力

大多数の物理エンジンでは質量と力が使用されるため、このサンプル・システムがそうなっていないのを不思議に思うかもしれません。このサンプル・システムでは質量を必要としないため、力は質量と加速度の積であるという事実を利用して、質量は常に 1 であるという前提のもとに、リスト 6 に示す計算をすることができます。

リスト 6. 質量を算出する力の方程式
// Our mass is always equal to one
var mass = 1;

// Force = mass * acceleration
var force = mass * acceleration;

// We can work the mass out of the equation
// and it won't change anything
force = acceleration;

上記の計算については、この記事では説明しません。このサンプル・システムでは 1 つのエンティティーしか使用していませんが、複数の移動するエンティティーに関するさまざまな衝突を計算するには、すべての力の和を用いた物理計算、またはエネルギー保存則を用いた物理計算を使用することができます。

形状と凹面

サンプル・システムでサポートするのは回転しない境界ボックスですが、より正確な衝突検出と衝突解決が必要になる場合もあります。そのような場合には、多角形 (ポリゴン) の手法を検討してください。つまり、複数の境界ボックスを組み込んで 1 つのエンティティーを表現するという手法です (図 9 を参照)。

図 9. 凹 (Concave) 面と凸 (Convex) 面
凹多角形と凸多角形

形状に破断面がある場合、その形状を凹形状にすることができます。図 9 に示されているように、凹形状には、いずれかの辺が中央に向かってへこんでいる形状が含まれます。凹形状には多くの場合、より正確で複雑な衝突検出アルゴリズムが必要になります。もう一方の凸形状は、中央に向かってへこんでいる辺が 1 つもない形状です。形状のサポートを決定する際には、システムがどのような種類の複雑さをサポートするのかを検討することが重要です。


衝突ディテクター

物理エンジンで 2 番目に重要なステップは、衝突の検出と解決です。衝突ディテクターは、まさにその名のとおりの役目を果たします。この記事での衝突ディテクターは単純なもので、長方形が他の長方形と衝突するかどうかを判定するだけの計算を組み込みますが (リスト 7 を参照)、オブジェクトがさまざまなタイプのオブジェクトと衝突することはよくあります。

リスト 7. 衝突ディテクター collideRect のテスト
// Rect collision tests the edges of each rect to
// test whether the objects are overlapping the other
CollisionDetector.prototype.collideRect = 
    function(collider, collidee) {

    // Store the collider and collidee edges
    var l1 = collider.getLeft();
    var t1 = collider.getTop();
    var r1 = collider.getRight();
    var b1 = collider.getBottom();
    
    var l2 = collidee.getLeft();
    var t2 = collidee.getTop();
    var r2 = collidee.getRight();
    var b2 = collidee.getBottom();
    
    // If the any of the edges are beyond any of the
    // others, then we know that the box cannot be
    // colliding
    if (b1 < t2 || t1 > b2 || r1 < l2 || l1 > r2) {
        return false;
    }
    
    // If the algorithm made it here, it had to collide
    return true;
};

リスト 7 は、境界ボックスをテストするために使用する衝突アルゴリズムです。このアルゴリズムは、長方形のすべての辺が、他の長方形の境界の外側にあるかどうかを決定するロジックを使用して、衝突が成功したかどうかを判別します。この衝突検出では長方形以外の形状をサポートしていませんが (図 10 を参照)、説明をする上では十分です。

図 10. 衝突検出の例
衝突ターゲットを使用した衝突検出の例

動くエンティティーは 1 つしかないので、どのオブジェクトと衝突するかを判定するという課題は解決されます。衝突検出アルゴリズムを作成するときには、不要なエンティティーを取り除くことが役立ちます。


衝突ソルバー

衝突パイプラインの最後の構成要素は、ソルバーです。衝突が発生した場合、衝突しているエンティティーはそれぞれの衝突解決後の位置を計算する必要があります。それにより、衝突している状態のエンティティーはその状態から抜け出し、新しい方向へ移動する処理が行われます。このサンプルでのオブジェクトは、衝突時に加えられた力を完全に吸収するか、その力の一部を反射するかのいずれかです。ソルバーには、変位を計算するためのメソッドと、前述の弾性衝突の場合のメソッドを組み込みます。

図 11 に示すように、ソルバーが衝突を解決することで、プレイヤーの方向と位置が変化します。このサンプル・ソルバーは、まずプレイヤーを、エンティティーが移動してきた方向の、最初に衝突が発生した地点まで戻します。

図 11. 衝突解決による変位の図
衝突解決による変位プロセスの図: 最初に衝突を検出し、次に変位スペースを明らかにし、最後に移動して衝突を解決します。

次に、ソルバーの方程式によってエンティティーの速度が変更されます。変位アルゴリズムでは速度を使用せずに変位を計算し、弾性衝突アルゴリズムでは前述の反発を使用して、プレイヤーの速度の低下を計算して速度に反映します (リスト 8 を参照)。

リスト 8. 変位による衝突解決
resolveElastic: function(player, entity) {
    // Find the mid points of the entity and player
    var pMidX = player.getMidX();
    var pMidY = player.getMidY();
    var aMidX = entity.getMidX();
    var aMidY = entity.getMidY();
    
    // To find the side of entry calculate based on
    // the normalized sides
    var dx = (aMidX - pMidX) / entity.halfWidth;
    var dy = (aMidY - pMidY) / entity.halfHeight;
    
    // Calculate the absolute change in x and y
    var absDX = abs(dx);
    var absDY = abs(dy);
    
    // If the distance between the normalized x and y
    // position is less than a small threshold (.1 in this case)
    // then this object is approaching from a corner
    if (abs(absDX - absDY) < .1) {

        // If the player is approaching from positive X
        if (dx < 0) {

            // Set the player x to the right side
            player.x = entity.getRight();

        // If the player is approaching from negative X
        } else {

            // Set the player x to the left side
            player.x = entity.getLeft() - player.width;
        }

        // If the player is approaching from positive Y
        if (dy < 0) {

            // Set the player y to the bottom
            player.y = entity.getBottom();

        // If the player is approaching from negative Y
        } else {

            // Set the player y to the top
            player.y = entity.getTop() - player.height;
        }
        
        // Randomly select a x/y direction to reflect velocity on
        if (Math.random() < .5) {

            // Reflect the velocity at a reduced rate
            player.vx = -player.vx * entity.restitution;

            // If the object's velocity is nearing 0, set it to 0
            // STICKY_THRESHOLD is set to .0004
            if (abs(player.vx) < STICKY_THRESHOLD) {
                player.vx = 0;
            }
        } else {

            player.vy = -player.vy * entity.restitution;
            if (abs(player.vy) < STICKY_THRESHOLD) {
                player.vy = 0;
            }
        }

    // If the object is approaching from the sides
    } else if (absDX > absDY) {

        // If the player is approaching from positive X
        if (dx < 0) {
            player.x = entity.getRight();

        } else {
        // If the player is approaching from negative X
            player.x = entity.getLeft() - player.width;
        }
        
        // Velocity component
        player.vx = -player.vx * entity.restitution;

        if (abs(player.vx) < STICKY_THRESHOLD) {
            player.vx = 0;
        }

    // If this collision is coming from the top or bottom more
    } else {

        // If the player is approaching from positive Y
        if (dy < 0) {
            player.y = entity.getBottom();

        } else {
        // If the player is approaching from negative Y
            player.y = entity.getTop() - player.height;
        }
        
        // Velocity component
        player.vy = -player.vy * entity.restitution;
        if (abs(player.vy) < STICKY_THRESHOLD) {
            player.vy = 0;
        }
    }
};

リスト 8 の弾性衝突では、プレイヤーとエンティティーの方向の角度差を調べることにより、長方形にオーバーラップした配置を判別する計算を使用します。長方形は正規化されるため、すべての計算は、長方形が正方形であるかのように処理されます。図 11 の例では、この類の計算が表現されています。この例の衝突解決では直接速度を変更していますが、複数のオブジェクトの衝突を扱う場合には、欲張って速度を直接設定しないように注意してください。複数のソースによって生じる速度の変化には、エネルギー保存則による方程式を使用し、さまざまな角度と速度を計算する必要があります。

変位アルゴリズムは、変位の計算でも使用されます。ただし、変位の計算では、速度が反映および低下される代わりに、ゼロに設定されます。

以上で、コンポーネントについての概要の説明は完了です。次は最後のステップとして、このシステムをエンジン内で組み立てます。


エンジン

システムを完成させる最後のコンポーネントは、ワークフロー擬似コード (リスト 2 を参照) を扱うエンジン本体です。通常、エンジンが行う作業は、エンティティーの制御に相当し、各ループ・ステップで衝突コンポーネントにデータを挿入したり、衝突コンポーネントからデータを削除したりする処理が含まれます。サンプル・エンジンの実装では、衝突が発生する前の位置の時間変化および重力を扱います (リスト 9 を参照)。

リスト 9. 物理エンジン
Engine.prototype.step = function(elapsed) {
    var gx = GRAVITY_X * elapsed;
    var gy = GRAVITY_Y * elapsed;
    var entity;
    var entities = this.entities;
    
    for (var i = 0, length = entities.length; i < length; i++) {
        entity = entities[i];
        switch (entity.type) {
            case PhysicsEntity.DYNAMIC:
                entity.vx += entity.ax * elapsed + gx;
                entity.vy += entity.ay * elapsed + gy;
                entity.x  += entity.vx * elapsed;
                entity.y  += entity.vy * elapsed;
                break;
            case PhysicsEntity.KINEMATIC:
                entity.vx += entity.ax * elapsed;
                entity.vy += entity.ay * elapsed;
                entity.x  += entity.vx * elapsed;
                entity.y  += entity.vy * elapsed;
                break;
        }
    }
    
    var collisions = this.collider.detectCollisions(
        this.player, 
        this.collidables
    );

    if (collisions != null) {
        this.solver.resolve(this.player, collisions);
    }
};

物理演算のステップに従って説明すると、エンジンはまず重力を計算します。次に、その結果とエンティティーの加速度を速度に適用してから、エンティティーの新しい x 位置および y 位置を計算します。この例での衝突は、衝突ディテクターの detectCollisions メソッドによって生成されます。この検出メソッドは、該当するメソッド (この例では collideRect) を使用するためのルーターとして機能します。同様に、ソルバーは汎用解決メソッドを使用して、呼び出しを resolveElastic または独自の変位解決メソッドに転送します。

この実装のエンジンに備わるのは、極めて簡単な制御一式ですが、それを使用することは不可欠です。他のエンジンのなかには、これよりも重要な役割を担うエンジンもありますが、物理エンジンの中で相互作用するエンティティーや、物理演算のステップを管理するには、このエンジンで十分な場合もあります。よくある落とし穴として、システムが衝突可能なエンティティーを通り過ぎることがないように注意してください。エンティティーが衝突可能なエンティティーが占めるスペースを超えて移動した場合、ディテクターはそのエンティティーを完全に無視します。


まとめ

独自の物理エンジンを作成するときには、ゲームに単純なソリューションを実装することがメリットになる場合があります。堅牢なソリューションのほうが望ましいことに変わりはありませんが、計算の複雑さが重要となる物理エンジンを作成する場合、あるいは iOS や Android などの処理能力に限りがある機器を対象とした物理エンジンを作成する場合には、スモーク・アンド・ミラー手法を検討してください。計算リソースが少ない場合には、この記事で概説したソリューションが理想的です。

物理エンジンを機能させるには、いくつかのパーツが必要となります。そのうち最も重要なのは、エンティティー、衝突ディテクター、衝突ソルバー、そしてエンジンのコアです。それぞれに固有のジョブを扱うこれらのコンポーネントを連携して機能させ、形状とアルゴリズムを慎重に選択することで、物理演算を単純化することができます。サポートする機能を適切に決定する限り、より堅牢な物理エンジンの実装を小さい規模で構築することは可能です。より堅牢な物理エンジンは常に、自分で作成した独自のゲームに最善の手法を自分自身で判断する際の手掛かりとなります。


ダウンロード

内容ファイル名サイズ
Article source codeHomebrewPhysicsEngineSource.zip5KB

参考文献

学ぶために

  • Box2d: ゲーム向け 2D 物理エンジンの詳細を学んでください。
  • Model–View–Controller (MVC): ウィキペディアで MVC の説明を読んでください。MVC は、情報の表示をユーザーの操作による処理とは切り離すタイプのコンピューター・ユーザー・インターフェースです。
  • 運動学: 点、物体 (オブジェクト)、および物体体系の移動を記述する古典力学の分野について、ウィキペディアで学んでください。
  • WebGL の公開ウィキ: WebGL について学ぶためのリソースを入手してください。このウィキには、現在の仕様、ドキュメント、実装状況、および WebGL を利用したデモと Web アプリケーションのリポジトリーが用意されています。
  • requestAnimationFrame の使用方法: レンダリング・ループを実装する際に推奨される方法について詳しく学んでください。
  • HTML5 の Canvas を使って素晴らしいグラフィックスを作成する」(developerWorks、2011年2月): Canvas を使用して Web ページを改善する方法を学んでください。Canvas は HTML5 の単純な要素ですが、強力な機能が満載されています。
  • HTML5 Canvas: Canvas API の使用法に焦点を当てたこのデモを見て、極めて単純なアニメーションに色を設定する方法を学んでください。
  • HTML5 の基礎: 第 4 回 最後の仕上げ: Canvas」(developerWorks、2011 年 7 月): HTML5 の canvas 要素を紹介するこの記事に、この要素の機能を説明する数々のサンプル・コードが記載されています。
  • Mozilla の開発者たちによる Canvas チュートリアルとデモを調べてください。
  • Canvas チュートリアル: Mozilla の開発者たちによるチュートリアルとデモを調べてください。
  • HTML5 Canvas リファレンス: W3Schools.com で提供している、Canvas の知識を深めるのに役立つ演習を試してみてください。
  • Khronos Group webgl-utils.js: webgl-utils.js を使ってコードを追跡してみてください。
  • developerWorks Web architecture ゾーン: さまざまな Web ベースのソリューションを話題にした記事を調べてください。広範な技術に関する記事とヒント、チュートリアル、標準、そして IBM Redbooks については、Web development の技術文書一覧を参照してください。
  • developerWorks のテクニカル・イベントと Webcast: これらのセッションで最新情報を入手してください。
  • developerWorks オンデマンド・デモ: 初心者向けの製品のインストールおよびセットアップから熟練開発者向けの高度な機能に至るまで、さまざまに揃ったデモを見てください。
  • Twitter での developerWorks: 今すぐ登録して developerWorks のツイートをフォローしてください。

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

  • Spring のサポートと SVG デモを揃えた JavaScript 固有の box2d-js を入手してください。
  • IBM 製品の評価版: DB2、Lotus、Rational、Tivoli、および WebSphere のアプリケーション開発ツールとミドルウェア製品を体験するには、評価版をダウンロードするか、IBM SOA Sandbox のオンライン試用版を試してみてください。

議論するために

  • 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
ArticleID=851798
ArticleTitle=JavaScript ゲームのための単純な 2D 物理エンジンを作成する
publish-date=12202012