JavaScript で作成するゲームでのオブジェクト指向設計

OOP とデザイン・パターンを適用して、さらに賢いコードにする

ほとんどの JavaScript は、手続き型のループと巨大な if/else 文で構成されていますが、この記事ではそれよりも賢明な手法として、JavaScript で作成するゲームにオブジェクト指向設計を適用する方法を説明します。記事ではまず、JavaScript でのプロトタイプ継承と基本的なオブジェクト指向プログラミング (OOP) の概要を紹介します。従来の継承ベースのライブラリーを使用することにより、JavaScript で OOP によるもっと多くのメリットを得る方法を学んでください。さらに、より簡潔なコードを作成する方法を明らかにするアーキテクチャー・デザイン・パターンを、ゲーム・ループ、状態マシン、イベント・バブリングのサンプル・コードで検討します。

Dan Zdrazil, Software Developer, The Nerdery

Photo of Dan ZdrazilDan Zdrazil は 2008年に PHP に取り組み始め、その後、彼のポートフォリオには Flex/AS3、Zend、JavaScript、そして HTML が追加されていきました。The Nerdery でフルタイムのソフトウェア・エンジニアとして働く彼は、この会社の JavaScript 委員会に度々参加し、その専門知識を JavaScript ファンのために共有しています。Zdrazil はアジア言語および文学の文学士号を取得してミネソタ大学を卒業しました。



2012年 11月 15日

はじめに

この記事を読んで、JavaScript におけるオブジェクト指向プログラミング (OOP) について学び、プロトタイプ継承モデルと従来の継承モデルについて探ってください。記事ではサンプル・コードを用いて、ゲームに OOP 設計の構造と保守性による大きなメリットをもたらす共通パターンを説明します。その最終的な目標は、コードの各構成部分を人間が読んで理解できるようにし、それぞれに固有の概念と目的を表現する構成部分を 1 つにまとめることで、一連の命令とアルゴリズムを超越してきめ細かく調整された作品にすることです。


JavaScript における OOP の概要

頻繁に使用される略語

  • DOM: Document Object Model
  • DRY: Don't Repeat Yourself
  • OOP: Object-oriented programming

OOP が目指しているのは、データの抽象化、モジュール化、カプセル化、ポリモーフィズム、継承を提供することです。OOP により、コードのオーサリングからコードの概念を抽象化し、それによって簡潔さ、再利用性、読みやすさを実現することができます。けれどもその一方、ファイル数と行数が増え、(上手く管理されていない場合は) パフォーマンスが犠牲になります。

従来、ゲームの開発者たちは、純粋な OOP 手法を避けてきました。その意図は、CPU サイクルから可能な限り高いパフォーマンスを引き出すことにあります。JavaScript によるゲームのチュートリアルの大多数は、堅実な基礎を提供することはなく、簡単なデモ用に OOP 以外の手法を使用しています。JavaScript 開発者は、JavaScript 以外のゲーム環境の開発者にはない問題を抱えています。具体的に言うと、メモリーは開発者の手で管理されません。また、JavaScript ファイルはグローバル・コンテキストで実行されることから、スパゲッティー・コード、名前空間の衝突、迷路のような if/else 文などの保守に悩まされます。JavaScript でのゲーム開発を最大限有効に活用するには、OOP のベスト・プラクティスに従って、将来的な保守性、開発のスピード、そしてゲームの表現力を大幅に強化する必要があります。

プロトタイプ継承

従来の継承を使用する言語とは異なり、JavaScript にはクラスという構成体が組み込まれていません。JavaScript の世界では、関数は「第一級市民」です。そして関数には、あらゆるユーザー定義オブジェクトと同じくプロトタイプがあります。new キーワードを使って関数を呼び出すと、実際にはその関数のプロトタイプ・オブジェクトのコピーが作成され、関数に含まれる this キーワードのコンテキストとして、そのオブジェクトが使用されます。リスト 1 に一例を記載します。

リスト 1. プロトタイプを持つオブジェクトを作成する
// constructor function
function MyExample() {
  // property of an instance when used with the 'new' keyword
  this.isTrue = true;
};

MyExample.prototype.getTrue = function() {
  return this.isTrue;
}

MyExample();
// here, MyExample was called in the global context, 
// so the window object now has an isTrue property—this is NOT a good practice

MyExample.getTrue;
// this is undefined—the getTrue method is a part of the MyExample prototype, 
// not the function itself

var example = new MyExample();
// example is now an object whose prototype is MyExample.prototype

example.getTrue; // evaluates to a function
example.getTrue(); // evaluates to true because isTrue is a property of the 
                   // example instance

慣例として、クラスを表す関数の名前は大文字で始めることで、その関数がコンストラクターとして意図されていることを示す必要があります。関数の名前は、その関数が作成するデータを表します。

クラスのインスタンスを作成するには、new キーワードとプロトタイプ・オブジェクトを組み合わせるという魔法を使います。プロトタイプ・オブジェクトは、メソッドとプロパティーの両方を持つことができます (リスト 2 を参照)。

リスト 2. プロトタイプを使用することによる単純な継承
// Base class
function Character() {};

Character.prototype.health = 100;

Character.prototype.getHealth = function() {
  return this.health;
}

// Inherited classes

function Player() {
  this.health = 200;
}

Player.prototype = new Character;

function Monster() {}

Monster.prototype = new Character;

var player1 = new Player();

var monster1 = new Monster();

player1.getHealth(); // 200- assigned in constructor

monster1.getHealth(); // 100- inherited from the prototype object

親クラスを子クラスに割り当てるには、new を呼び出して、その結果を子クラスの prototype プロパティーに割り当てなければなりません (リスト 3 を参照)。したがって、デフォルト値をクラス定義で渡したいというのでなければ、コンストラクターはできるだけ簡潔にして、副次効果がないようにしてください。

JavaScript でクラスと継承を定義する作業を始めてみると、従来の OOP 言語との大きな相違点に気付くはずです。それは、親オブジェクトのメソッドをオーバーライドしている場合、これらのメソッドにアクセスするための super プロパティーも parent プロパティーもないことです。これらのプロパティーの欠如を補う単純なソリューションはありますが、そのソリューションを使用することは DRY (Don't Repeat Yourself) 原則に反しています。おそらくこのことが、多くのライブラリーで従来の継承を真似ようとしている最大の理由になっているものと思われます。

リスト 3. 子クラスから親メソッドを呼び出す
function ParentClass() {
  this.color = 'red';
  this.shape = 'square';
}

function ChildClass() {
  ParentClass.call(this);  // use 'call' or 'apply' and pass in the child 
                           // class's context
  this.shape = 'circle';
}

ChildClass.prototype = new ParentClass(); // ChildClass inherits from ParentClass

ChildClass.prototype.getColor = function() {
  return this.color; // returns "red" from the inherited property
};

リスト 3color 属性と shape 属性の値は、プロトタイプの中にはなく、ParentClass コンストラクター関数の中で設定されます。ChildClass の新しいインスタンスの shape プロパティーは 2 回設定されることになります。1 回は ParentClass コンストラクター内で ‘square’ として設定され、もう 1 回は ChildClass コンストラクター内で ‘circle’ として設定されます。このような設定ロジックをプロトタイプに移せば、副次効果が少なくなり、コードが保守しやすくなります。

プロトタイプ継承モデルでは、関数を異なるコンテキストで実行するために、JavaScript の call メソッドまたは apply メソッドを使用することができます。この方法は、他の言語での superparent を使用した方法を置き換えるには十分効果的ですが、別の問題をもたらします。例えばクラスをリファクタリングするために、クラスの名前、親、または親の名前を変更すると、テキスト・ファイル内のさらに多くの場所に ParentClass トークンが散在することになります。この問題は、クラスが複雑になるにつれ、大きな問題になります。それよりも有効な方法は、クラスに基底クラスを継承させることです。そうすれば、コードの繰り返しが抑えられ、通常は従来の継承を再現する形になります。

従来の継承

プロトタイプ継承は OOP に完全に対応可能ですが、優れたプログラミングで目標とすることのすべてを満たすわけではありません。例えば、以下の問題を考えてください。

  • DRY ではありません。クラス名とプロトタイプがあちこちで繰り返されるため、コードが読みにくくなり、リファクタリングするのが難しくなります。
  • コンストラクターはプロトタイプを作成する中で呼び出されます。いったんサブクラス化を始めると、コンストラクター内で特定のロジックを使用できなくなります。
  • 強力なカプセル化を真にサポートするわけではありません。
  • 静的クラス・メンバーを真にサポートするわけではありません。

以上の問題を克服するために、多くの JavaScript ライブラリーでは、より従来型の OOP 構文を適用しようとしています。そのようなライブラリーのうち、比較的使いやすいのは Dean Edward 氏による Base.js です (「参考文献」を参照)。このライブラリーには、以下の有用な特徴があります。

  • すべてのプロトタイプはオブジェクトをマージ (ミックスイン) して作成されます (クラスとサブクラスの両方を 1 つのステートメントで定義することができます)。
  • クラスの新しいインスタンスの作成時にロジックを安全に実行できる場所として、特殊なコンストラクター関数を使用します。
  • 静的クラス・メンバーをサポートします。
  • 強力なカプセル化に向けたサポートにより、クラス定義が 1 つのステートメントで行われるように維持します (コードをカプセル化するというよりも、考え方の面でのカプセル化に主眼が置かれます)。

パブリックおよびプライベートのメソッドとプロパティーのサポート (カプセル化) は、他のライブラリーでのほうが強化されているかもしれませんが、Base.js が提供する簡潔な構文は、使用するにも憶えるにも簡単です。

リスト 4 に、Base.js と従来の継承を簡単に紹介します。このサンプル・コードは、抽象クラス Enemy の機能を具象クラス RobotEnemy で継承するものです。

リスト 4. Base.js と従来の継承の簡単な紹介
// create an abstract, basic class for all enemies
// the object used in the .extend() method is the prototype
var Enemy = Base.extend({
    health: 0,
    damage: 0,
    isEnemy: true,

    constructor: function() {
        // this is called every time you use "new"
    },

    attack: function(player) {
        player.hit(this.damage); // "this" is your enemy!
    }
});

// create a robot class that uses Enemy as its parent
// 
var RobotEnemy = Enemy.extend({
    health: 100,
    damage: 10,

    // because a constructor isn't listed here, 
    // Base.js automatically uses the Enemy constructor for us

    attack: function(player) {
        // you can call methods from the parent class using this.base
        // by not having to refer to the parent class
        // or use call / apply, refactoring is easier
        // in this example, the player will be hit
        this.base(player); 
       
        // even though you used the parent class's "attack" 
        // method, you can still have logic specific to your robot class
        this.health += 10;
    }
});

ゲーム設計での OOP パターン

基本的なゲーム・エンジンには、必ず使用する 2 つの関数があります。それは、updaterender です。render メソッドは通常、setInterval を使用するか、Paul Irish 氏が作成したような requestAnimationFrame のポリフィル (「参考文献」を参照) を使用します。requestAnimationFrame を使用する利点は、この関数が必要以上に呼び出されない点にあります。この関数が実行される頻度は、クライアント・モニターのリフレッシュ・レート (デスクトップ・クライアントの場合、通常は 1 秒間に 60 回) と同じです。さらに、ほとんどのブラウザーでは、ゲームが表示されているタブがアクティブにならない限り実行されません。このことから、以下のメリットがもたらされます。

  • ユーザーがゲームを表示していないときのクライアント・コンピューターの処理量が減ります。
  • モバイル機器でのバッテリーの使用が節約されます。
  • update ループと render ループが関連付けられていると、ゲームが効果的に一時中断の状態になります。

以上の理由から、requestAnimationFrame は「クライアント・フレンドリー」であると言われており、setInterval との比較で「より良い市民」と呼ばれています。

update ループと render ループを関連付けると、別の問題が持ち上がります。それは、render ループの実行速度が 15 fps (frames per second) であろうと 60 fps であろうと、ゲームのアクションとアニメーションが同じ速度で実行されるという問題です。この問題に対処する方法としては、ゲーム内にチック (tick) という時間単位を設けて、最後の update 呼び出しからの経過時間を渡すようにします。この経過時間はチック数に変換できるので、そのチック数に応じてモデル、物理エンジン、そしてその他の時間に依存するゲーム・ロジックの調整が可能になります。例えば、毒を受けたプレイヤーが、10 チックの間、1 チックあたり 10 ダメージ受けるとします。render ループが高速で実行されている場合、ある特定の update 呼び出しではプレイヤーがダメージを受けない可能性があります。一方、最後の render ループでガーベッジ・コレクションが起動されて 1.5 チックが渡された場合、この処理ロジックではプレイヤーは 15 ダメージ受けることになります。

モデル更新のタイミングをビューの render ループから完全に切り離すという別の考え方もあります。アニメーションやオブジェクトを多数使用するゲームや、描画するのに極めて大量のリソースを必要とするゲームでは、update ループを render ループに関連付けると、ゲームの実行速度が完全に遅くなってしまいます。この場合、requestAnimationFrame ハンドラーがどういうタイミングで、どの程度の頻度で起動されるかに関わらず、update メソッドを (setInterval を使用して) 設定された間隔で実行することができます。これらのループで費やされる時間の大半は、実際には render のステップで費やされるため、画面に描画されるのが 25 フレームだけだとしても、ゲームは一定の速度で実行され続けます。いずれの場合にしても、update サイクル間の時間の差は計算しなければなりません。例えば、1 秒あたりに update が実行される回数が 60 回だとすると、update 関数を 16 ミリ秒以下で完了しなければならないことになります。update 関数の実行に要する時間が 16 ミリ秒を超える場合 (あるいはブラウザーのガーベッジ・コレクションが実行された場合)、ゲームの実行速度はやはり遅くなります。リスト 5 に一例を記載します。

リスト 5. render ループと update ループを使用する基本的なアプリケーション・クラス
// requestAnim shim layer by Paul Irish
    window.requestAnimFrame = (function(){
      return  window.requestAnimationFrame       || 
              window.webkitRequestAnimationFrame || 
              window.mozRequestAnimationFrame    || 
              window.oRequestAnimationFrame      || 
              window.msRequestAnimationFrame     || 
              function(/* function */ callback, /* DOMElement */ element){
                window.setTimeout(callback, 1000 / 60);
              };
    })();

var Engine = Base.extend({
    stateMachine: null,  // state machine that handles state transitions
    viewStack: null,     // array collection of view layers, 
                         // perhaps including sub-view classes
    entities: null,      // array collection of active entities within the system
                         // characters, 
    constructor: function() {
        this.viewStack = []; // don't forget that arrays shouldn't be prototype 
		                     // properties as they're copied by reference
        this.entities = [];

        // set up your state machine here, along with the current state
        // this will be expanded upon in the next section

        // start rendering your views
        this.render();
       // start updating any entities that may exist
       setInterval(this.update.bind(this), Engine.UPDATE_INTERVAL);
    },

    render: function() {
        requestAnimFrame(this.render.bind(this));
        for (var i = 0, len = this.viewStack.length; i < len; i++) {
            // delegate rendering logic to each view layer
            (this.viewStack[i]).render();
        }
    },

    update: function() {
        for (var i = 0, len = this.entities.length; i < len; i++) {
            // delegate update logic to each entity
            (this.entities[i]).update();
        }
    }
}, 

// Syntax for Class "Static" properties in Base.js. Pass in as an optional
// second argument to.extend()
{
    UPDATE_INTERVAL: 1000 / 16
});

JavaScript での this のコンテキストに馴染みがないとしたら、.bind(this) が 2 回使用されていることに注目してください。1 回は無名関数の setInterval 呼び出し内の this.render.bind() で、もう 1 回は requestAnimFrame 呼び出し内の this.render.bind(this) です。setIntervalrequestAnimFrame はメソッドではなく関数であるため、特定のクラスやアイデンティティーには属さず、グローバル window オブジェクトに属しています。したがって、エンジンの render メソッドや update メソッド内にある thisEngine クラスのインスタンスを参照させるために、.bind(object) を呼び出すことによって、関数内の this に通常とは異なる振る舞いをさせます。Internet Explorer 8 またはそれ以前のバージョンを使用している場合は、バインドのポリフィルを追加する必要があります。


状態マシン

状態マシン・パターンは広く実装されていますが、適切に認識されてはいません。状態マシンは OOP の背後にある原則 (コードの概念を実行から抽象化すること) の拡張です。例えば、ゲームは以下の状態になることが考えられます。

  • プリロード中
  • スタート画面
  • ゲームの実行中
  • オプション・メニュー
  • ゲーム・オーバー (勝ち、負け、または続行)

上記のどの状態の実行可能コードも、他の状態が関係するものであってはなりません。例えば「プリロード中」状態のコードは、オプション・メニューがいつ開かれるかについての情報を持つべきではありません。命令型 (手続き型) プログラミングの場合、アプリケーション・ロジックを適切に順序付ける巨大な if 条件文や switch 条件文を提示する場合がありますが、これらの条件文ではコードの根底にある考えを表すことができないため、コードを保守するのがさらに難しくなります。他の状態 (ゲーム中のメニュー、ステージ間の遷移、追加機能など) を追加すると、条件文の保守が困難になります。

代わりとなる方法として、リスト 6 のコードを見てください。

リスト 6. 単純化された状態マシン
// State Machine
var StateMachine = Base.extend({
    states: null, // this will be an array, but avoid arrays on prototypes.
                  // as they're shared across all instances!
    currentState: null, // may or may not be set in constructor
    constructor: function(options) {
        options = options || {}; // optionally include states or contextual awareness

        this.currentState = null;
        this.states = {};

        if (options.states) {
            this.states = options.states;
        }

        if (options.currentState) {
            this.transition(options.currentState);
        }
    },

    addState: function(name, stateInstance) {
        this.states[name] = stateInstance;
    },

    // This is the most important function—it allows programmatically driven
    // changes in state, such as calling myStateMachine.transition("gameOver")
    transition: function(nextState) {
        if (this.currentState) {
            // leave the current state—transition out, unload assets, views, so on
            this.currentState.onLeave();
        }
        // change the reference to the desired state
        this.currentState = this.states[nextState];
        // enter the new state, swap in views, 
        // setup event handlers, animated transitions
        this.currentState.onEnter();
    }
});

// Abstract single state
var State = Base.extend({
    name: '',       // unique identifier used for transitions
    context: null,  // state identity context- determining state transition logic

    constructor: function(context) {
        this.context = context;
    },

    onEnter: function() {
        // abstract

        // use for transition effects
    },

    onLeave: function() {
        // abstract

        // use for transition effects and/or
        // memory management- call a destructor method to clean up object
        // references that the garbage collector might not think are ready, 
        // such as cyclical references between objects and arrays that 
        // contain the objects
    }
});

アプリケーションに状態マシンの具象サブクラスを作成する必要がない場合もありますが、State のサブクラスは、アプリケーションの状態ごとに必ず必要です。遷移ロジックを異なる複数のオブジェクトに分けることで、以下のことが可能になります。

  • コンストラクターを使用して、アセットのプリロードを即時に開始することができます。
  • 新しい状態をゲームに追加することができます。例えば、ゲーム・オーバー画面の前に表示される続行確認画面を追加する場合でも、どのグローバル変数がどの条件で影響されるかを巨大な if/else 構造や switch 構造で解明する必要はありません。
  • サーバーからロードされたデータをベースに状態を作成すれば、遷移ロジックを動的に定義することができます。

メインのアプリケーション・クラスは、各状態の内部ロジックが関係するものであってはなりません。同時に各状態は、メインのアプリケーション・クラスの内部とあまり関係するものであってもなりません。例えば、「プリロード中」状態には、ページのマークアップに組み込まれたアセットに応じてビューをインスタンス化し、最小限のゲーム・アセット (ムービー・クリップ、画像、サウンド) をシングルトン・アセット・マネージャーのキューに入れる役割があるとします。「プリロード中」状態では、「プリロード中」ビュー・クラスをインスタンス化したとしても、ビューの振る舞いを考慮する必要はありません。この場合、この概念 (つまり、状態が表現するオブジェクト) の役割は、データのプリロード中の状態にいるということが、アプリケーションにとって何を意味するかを定義することだけです。

状態マシン・パターンは、ゲーム・ロジックの状態だけに限られないことに注意してください。個々のビューにも、その表示のロジックから状態のロジックを取り除くことによるメリットが得られます。これは特に、サブビューを管理する場合や、Chain of Responsibility パターンと組み合わせてユーザー操作イベントを処理する場合に言えることです。


Chain of Responsibility: キャンバス上でのイベント・バブリングのエミュレーション

HTML5 の canvas 要素は、個々のピクセルを操作できる画像要素だと思ってください。従って、戦利品が置かれた草原とその上を移動するキャラクターが描画されている領域がある場合、ユーザーが何をクリックしたかをキャンバスは認識しません。メニューを描画する場合でも、キャンバスは特定の領域がボタンを表現していることを認識しません。キャンバスそのものとなるのは、イベントのアタッチ先の DOM 要素のみです。ゲームをプレイ可能なものにするには、ユーザーがキャンバス上でクリックすると何が起こり得るかを、ゲーム・エンジンが解釈できなければなりません。

Chain of Responsibility デザイン・パターンの目的は、イベントの送信側 (DOM 要素) を受信側 (コード) から分離することで、複数のオブジェクト (ビューやモデル) がそのイベントを処理する役割を担える可能性を持てるようにすることです。Web ページなどの従来の実装では、ビューやモデルにハンドラー・インターフェースを実装させた上で、すべてのマウス・イベントの処理を 1 つのシーン・グラフに委任します。このシーン・グラフが、クリックされた対象として関連する「もの」を見つけて、そのそれぞれにインターセプトするチャンスを与えます。けれども、これよりもさらに単純な手法は、実行時に定義されたハンドラーのチェーンをキャンバス自体に持たせることです (リスト 7 を参照)。

リスト 7. Chain of Responsibility パターンを使用してイベント・バブリングの処理をする
var ChainOfResponsibility = Base.extend({
        context: null,      // relevant context- view, application state, so on
        handlers: null,     // array of responsibility handlers
        canPropagate: true, // whether or not 

        constructor: function(context, arrHandlers) {
            this.context = context;
            if (arrHandlers) {
                this.handlers = arrHandlers;
            } else {
                this.handlers = [];
            }
        },

        execute: function(data) {
            for (var i = 0, len = this.handlers.length; i < len; i++) {
                if (this.canPropagate) {
                    // give a handler a chance to claim responsibility
                    (this.handlers[i]).execute(this, data);
                } else {
                    // an event has claimed responsibility, no need to continue
                    break;
                } 
            }
            // reset state after event has been handled
            this.canPropagate = true;
        },

        // this is the method a handler can call to claim responsibility
        // and prevent other handlers from acting on the event
        stopPropagation: function() {
            this.canPropagate = false;
        },

        addHandler: function(handler) {
            this.handlers.push(handler);
        }
});

var ResponsibilityHandler = Base.extend({
    execute: function(chain, data) {

        // use chain to call chain.stopPropegation() if this handler claims
        // responsibility, or to get access to the chain's context member property
        // if this event handler doesn't need to claim responsibility, simply
        // return; and the next handler will execute
    }
});

アプリケーションに固有のロジックはすべて ResponsibilityHandler のサブクラス内に収容されるため、サブクラス化を行わなくても ChainOfResponsibility クラスは機能します。実装によって変わるものだけが、該当するコンテキスト (例えば、表示されているビューなど) に渡されることになります。一例として、オプション・メニューを考えてみてください。オプション・メニューが開かれている間でも、そのメニューの周囲には一時中断されたゲームが表示されています (リスト 8 を参照)。ユーザーがメニューのいずれかのボタンをクリックしても、その背後にいるキャラクターがそのクリックに反応しないようにしなければなりません。

リスト 8. オプション・メニュー・クローズ・ハンドラー
var OptionsMenuCloseHandler = ResponsibilityHandler.extend({
    execute: function(chain, eventData) {
        if (chain.context.isPointInBackground(eventData)) {
            // the user clicked the transparent background of our menu
            chain.context.close(); // delegate changing state to the view
            chain.stopPropegation(); // the view has closed, the event has been handled
        }
    }
});

// OptionMenuState
// Our main view class has its own states, each of which handles
// which chains of responsibility are active at any time as well
// as visual transitions

// Class definition...
constructor: function() {
    // ...
    this.chain = new ChainOfResponsibility(
        this.optionsMenuView, // the chain's context for handling responsibility
        [
            new OptionsMenuCloseHandler(), // concrete implementation of 
			                               // a ResponsibilityHandler
            // ...other responsibility handlers...
        ]
    );
}

// ...
onEnter: function() {
    // change the view's chain of responsibility
    // guarantees only the relevant code can execute
    // other states will have different chains to handle clicks on the same view
    this.context.setClickHandlerChain(this.chain);
}
// ...

リスト 8 では、view クラスに一連の状態への参照があり、それぞれの状態が、クリック・イベントの処理方法に責任を持つオブジェクトを決定します。このようにすれば、ビューのロジックはそのビューが持つ独自性によって表現されるもの、つまりオプション・メニューの表示だけに制限されます。ゲームを更新して追加のボタン、さらに巧妙なエフェクト、あるいは新しいビューへの遷移を組み込むとしても、新しい機能のそれぞれを処理できる個別のオブジェクトがあれば、既存のロジックを変更、分割、あるいは作成し直す必要はありません。mousedown、mousemove、mouseup、そしてクリック・イベントのチェーンを賢く組み合わせることで、コードを複雑にすることなく、メニューからキャラクター、そしてドラッグ・アンド・ドロップ・インベントリー画面に至るまでのすべてのものを高度に構造化された体系的な方法で処理できるようになります。


まとめ

デザイン・パターンと OOP は本質的に長短併せ持つ概念であるため、むやみに使用すると、問題を解決するどころか問題を引き起こす場合があります。この記事では、JavaScript における OOP の概要を説明し、プロトタイプ継承モデルと従来の継承モデルについて詳しく検討しました。続いて、OOP 設計の構造と保守性から大きなメリットを引き出せるゲームの共通パターン (基本的なゲーム・ループ、状態マシン、そしてイベント・バブリング) について学びました。この記事は、よくある問題に対する一般的なソリューションの表面をかじっただけです。実践を重ねることで、表現力豊かなコードの作成能力が身に付くようになり、それによってコーディングに割く時間が短縮され、創造的な作業により多くの時間を費やせるようになるはずです。


ダウンロード

内容ファイル名サイズ
Article source codeObject-OrientedDesignSource.zip5KB

参考文献

学ぶために

  • A Base Class for JavaScript Inheritance」(Dean Edwards 著、2006年3月): このブログで、JavaScript OO の面倒を軽減する Dean 氏の JavaScript クラスについて詳しく学んでください。
  • requestAnimationFrame for smart animating」: Paul Irish 氏の Web サイトで、アニメーションで使用する基本的な API、requestAnimationFrame についての説明を読んでください。
  • requestAnimationFrame for smart(er) animating」: Erik Moller 氏のブログで、requestAnimationFrame に対する別のアプローチについて読んでください。
  • デザインパターン―オブジェクト指向における再利用のための』(ソフトバンククリエイティブ、1995年): よく起こりがちな設計問題と表現豊かなコードに対する単純で簡潔なソリューションを集めたこのカタログに目を通してください。
  • HTML5 の Canvas を使って素晴らしいグラフィックスを作成する」(developerWorks、2011年2月): Canvas を使用して Web ページを改善する方法を学んでください。Canvas は HTML5 の単純な要素ですが、強力な機能が満載されています。
  • HTML5 Canvas: Canvas API の使用法に焦点を当てたこのデモを見て、極めて単純なアニメーションに色を設定する方法を学んでください。
  • HTML5 の基礎: 第 4 回 最後の仕上げ: Canvas」(developerWorks、2011年7月): HTML5 の canvas 要素を紹介するこの記事に、この要素の機能を説明する数々のサンプル・コードが記載されています。
  • Canvas Pixel Manipulation: Safari Dev Center に用意されているこのデモは、Canvas を使って効果的なビジュアル・アセットを開発する好例です。
  • jQuery Events API: ユーザーがブラウザーと対話するときに適用する振る舞いを登録するためのメソッドについて学んでください。
  • WHATWG: W3C と協力して HTML5 の微調整に取り組んでいるこの開発者コミュニティーについて調べてください。
  • Canvas チュートリアル: Mozilla の開発者たちによるチュートリアルとデモを調べてください。
  • HTML5 Canvas リファレンス: W3Schools.com で提供している、Canvas の知識を深めるのに役立つ演習を試してみてください。
  • developerWorks Web architecture ゾーン: さまざまな Web ベースのソリューションを話題にした記事を調べてください。広範な技術に関する記事とヒント、チュートリアル、標準、そして IBM Redbooks については、Web 開発の技術文書一覧を参照してください。
  • developerWorks の Technical events and webcasts: これらのセッションで最新情報を入手してください。
  • developerWorks Live! briefings: 無料の developerWorks Live! briefing に参加して、IBM の製品およびツールについての情報や IT 業界の動向についての情報を迅速に把握してください。
  • developerWorks オンデマンド・デモ: 初心者向けの製品のインストールおよびセットアップから熟練開発者向けの高度な機能に至るまで、さまざまに揃ったデモを見てください。
  • Twitter での developerWorks: 今すぐ登録して developerWorks のツイートをフォローしてください。

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

  • OpenGL: 最新のドライバーを入手してください。
  • jQuery: このよく使われている JavaScript ライブラリーをダウンロードしてください。HTML 文書のトラバース、イベント処理、アニメーション化、そして Ajax のやりとりを単純化するこのライブラリーは、迅速な Web 開発に役立ちます。
  • Modernizr: HTML5 および CSS3 で駆動する次世代の Web サイトの作成に役立つ、オープンソースの JavaScript ライブラリーを入手してください。
  • Kibo: 特定のブラウザーに依存しない高速キーボード・イベント処理を目的に作成された、この評判の高いライブラリーをダウンロードしてください。
  • IBM 製品の評価版: DB2、Lotus、Rational、Tivoli、および WebSphere のアプリケーション開発ツールとミドルウェア製品を体験するには、評価版をダウンロードするか、IBM SOA Sandbox のオンライン試用版を試してみてください。

議論するために

コメント

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=845565
ArticleTitle=JavaScript で作成するゲームでのオブジェクト指向設計
publish-date=11152012