もくじー


自己紹介

http://d.hatena.ne.jp/kenmo/で日記書いてます。
プログラム歴は、
HSP→VB→Java→C++→Python
でトータル5年ぐらいです。
でも、一番長いのは、さんざん仕事でやらされたVBAだったりします。

ゲーム開発におけるオブジェクト指向

はじめに

この内容は、実際に色々とオブジェクト指向でゲームを開発し、試行錯誤した結果得られた情報です。
なので、単なる経験則であり絶対ではないことをご了承くださいませー。
あと、実際にオブジェクト指向で作成したのは、アクションとシューティングのみであるため、他のジャンルへの適用がどうなるかは分かりません。

この内容がオブジェクト指向でゲーム開発する人の手がかりとなればなによりです~(´ー`;

オブジェクト指向は不自由

従来の手続き型言語に慣れた人にとって、
オブジェクト指向は、あれもダメ、これもダメ、と手足を縛られるような記述を要求します。

なので、慣れない間は苦痛でしかありません。
今すぐゲームを作りたい人にとって、オブジェクト指向は苦痛でしかないのです。

その不自由さが「モジュールの再利用や拡張を容易にする」というメリットを生み出すのですが、
それを享受できるようになるには、それなりの「経験」と「知識」が必要になります。

ある一定のレベルに達するまでは、再利用どころか、廃棄物のようなコードが大量に作られることになります。


デザインパターン

「このパターンを使って設計しよう!」
と思って、ゲームを設計したことはありません。

作ったものを見直してみたら、
「そういえば、○○パターンだった」
ということがほとんどです。

ただ、そういった見直しをすることで、
作ったものに対する特徴やメリット・デメリットを再評価することができます。

デザインパターンは、オブジェクト指向開発のモノサシなのかもしれません。


UML

UMLはオブジェクト指向開発における設計書を記述するためのグラフィカルな言語です。
オブジェクト指向による設計をまとめたり、人に説明するのに必須のツールとなっています。
図の種類には色々ありますが、
とりあえず「クラス図」と「シーケンス図」が理解・記述できるようになれば問題ないでしょう。
クラス図とは、クラス同士の静的な構造を表現する図です。
シーケンス図とは、オブジェクト間のメッセージのシーケンスを表現する図です。

ちなみに、UMLを記述するためのフリーのツールとしては、
JUDE(http://jude.change-vision.com/jude-web/index.html)などがあります。


どの領域をオブジェクト指向にするか

C++やJavaなどたいていのオブジェクト指向言語は、従来の手続き型で記述することも可能となっています。

ならば、オブジェクト指向と手続き型を混在させるのが、
ある意味、言語思想に基づいた記述であるといえます。

どの領域をオブジェクト指向にするかを考える必要があります。

再利用

デバイス制御やファイルアクセスなど低レベルな処理は、どのゲームでも使用します。
この部分の再利用は容易です。

しかし、ゲームシステムは、たいてい何らかの特性を持ちます。
なので、ゲームシステムそのものを再利用することは困難です。

ただし、ゲームにはある程度のお約束事があるので、そういった部分を抽出し、枠組みとして再利用することは可能です。

カプセル化

カプセル化はゲームの拡張を容易にします。

例えば、シューティングのボスというトークンを作るとします。
ボスの状態変数が「無敵モード」の場合、他のトークン(自機の弾・ボムなど)と当たり判定をしない、というゲームルールを設定しました。

実装には、カプセル化を適用して、ボスに当たり判定を行うかどうかを判定するメソッドを用意しました。

さて、この時点では、いちいちメソッドを通さずに、状態変数に直接アクセスした方が楽なのでは?とても無駄なことをしているのでは?と思うかもしれません。

しかしその後、ゲームデザインを修正して、ボスが「出現アニメ中」状態の場合についても当たり判定をしない、という拡張が必要になりました。

が、ボスにはカプセル化が適用されているので、「当たり判定の判定メソッド」の修正だけで大丈夫でした。

ここで、その当たり判定を状態変数に直接アクセスして行っていた場合を考えてみてください。
さらに、それが複数の箇所で行われていた場合を想像してみてください。
待っているのは修正・コピペ・デグレ地獄です。

ただし、カプセル化は万能ではありません。

不用意なカプセル化は、クラスの肥大化、冗長な記述を招き、データにアクセスするコストを増大させます。
配慮の欠けたカプセル化は、思いついたアイデアを実装することを不自由にする恐れがあり、実装をためらわせます。

おそらく最良の手順は、最初は全て変数をpublicにしておくのが良いでしょう。
そして、その変数に起因して発生する処理が、今後拡張が必要な、もしくは予想される臭いを感じ取ったら、その変数をカプセル化してください。
どちらともいえない場合は、publicのままにしておき、直接変数にアクセスしないような手順を取るようにしてください。


共通オブジェクト

デバイス周りとして、
  • ウィンドウ表示
  • 画像描画
  • サウンド再生
  • キー入力
などのAPIは、どのゲームでも利用することになります。

ただ、あなたが利用するAPIは、たいていゲーム用には作られていない、
もしくはあなたの作りたいゲームには適していない、ものです。

まずはこういったAPIのラッパーオブジェクトを作成することからはじめます。

この下準備は、トークンオブジェクトの実装にとても有益なものとなります。


また、広い領域でリソースを使い回しする場合に、
  • 画像/サウンドなどのリソース管理クラス
もオブジェクト化しておくと便利です。

その他、
  • 数値演算(ベクトル・行列・クォータニオン)
  • 幾何学図形(円・矩形・直線・球・箱など)
  • 幾何学図形の当たり判定
  • ログ出力
  • データセーブ
  • 外部スクリプト読込
  • マップデータ読込
などもオブジェクト化しておくと再利用が容易になります。


トークンオブジェクト

トークンオブジェクトは、デバイスクラスのポインタをスタティックなメンバ変数で持つ基底クラスを継承します。

Flyweightっぽい感じです。

これにより、派生トークンは自由にデバイスへアクセスをすることができます。

また、派生トークンに要求されるメソッドの例としては、
  • 初期化
  • 更新(移動など)
  • 描画
  • 消滅(終了処理)
などがあります。

更新と描画を切り分けることにより、更新はしないけど描画する、といった制御を行うことが可能になります。

他にもゲームの仕様により、
  • 当たり判定を行うか?(ex. isCollide())
  • ダメージを与える(ex. damage())
などのメソッドを用意します。

トークン管理


トークンの生成は、newなどで直接インスタンスの生成を行わないようにします。
Factoryパターンを適用した「トークン管理クラス」を通して生成します。

これにより、C++であれば、メモリリークの回避が可能になります。

実装メソッドの例としては、
  • 初期化(ex. init())
  • 要素の生成(ex. create())
  • 管理している要素の配列orリストorイテレーターを返す(ex. getList())
  • 要素の削除(ex. remove())
  • 要素を全て削除(ex. removeAll())
などです。

これらのメソッドは、すべてスタティックメソッドとして実装してください。
そうすることで、どこからでもトークンの生成・取得・削除が可能となります。

FSM vs Stateパターン

FSMはStateパターンに置き換えることが可能です。
そして、StateパターンはFSMに置きかえることが可能です。

FSMによる記述は、状態変数による冗長なswitch文を要求します。
それに対して、Stateパターンでは、switch文を除去します。

また、FSMは状態に対応する処理に付随するデータをお互いに参照できますが、
Stateパターンではデータを各状態ごとに完全に切り離します。

この現象は、Stateパターンの絶対優位を保障するものではありません。

例えば、Stateパターンで、別の状態のデータを参照したい場合、
複雑な手順を踏むことになります。
つまり、「データ参照」に対する制限が存在していると言えます。

全てをStateパターンに置き換えるよりも、混在させるほうが柔軟な設計が可能になります。
例えば、シーンオブジェクト(タイトルシーン・ゲームメインシーン)はStateパターンで実装して、
そのシーンオブジェクト内の状態遷移(開始アニメ・メイン・終了アニメ)はFSMで行う、というように。

どこをFSM(自由)にして、どこをStateパターン(不自由)にするかの見極めが重要です。

まとめると、、、
メリット デメリット
FSM 柔軟・手軽 相互参照地獄。switch文地獄
Stateパターン データ独立性の確保 硬直的・オブジェクト地獄
となり、使い方を誤ると地獄にハマります。

シーンオブジェクト

シーンオブジェクトを使うと、上位の階層がこのようなシンプルなフローになります。
ゲーム開始

デバイスオブジェクト初期化

シーンオブジェクトをシーン管理クラスに登録

ゲームループ
↓↑
シーン更新→シーンチェンジ

ゲーム終了



シーンチェンジは、シーンが保持しているシーン管理クラスのchangeScene()で引数にシーンIDを渡してコールします。
管理クラスのシーン保持をmap(キーをシーン名、値をシーンオブジェクト)にすると、シーン名でシーンチェンジできるため、意味が明確になります。

シーンオブジェクトのデメリットは、シーン間で直接データの受け渡しができないことです。
そのため、受け渡し用のデータオブジェクトのポインタを共有する必要があります。

トークン同士の判定をどこで行うか

トークン同士はたいてい、お互いの存在を知らないようにできています。
もし、トークン同士が自由に通信できるようにしてしまったら、
あなたの作ったトークン同士の関連性(ゲームルール)を、
それぞれのトークンが持つことになります。

例えば、
弾トークンは勝手に自機トークンとの距離を調べて、一定距離の間にあれば自機トークンを破壊するのでしょうか。
否、弾トークンは「座標」と「速度」というパラメータを元に、「動く」という動作を持っているだけです。


判定そのものは、トークンを管理している上位の階層で行うべきなのです。

パーティクルクラス

パーティクルクラスを実装することにより、演出の再利用をすることができます。

ただし、ここでのパーティクルとは、
  • 他のトークンとの関連性がなくタイマーの経過によりのみ消滅するパーティクル
を指します。

そのため、厳密なパーティクルとは別の概念ですが、
「当たり判定」などを考える必要がないため、再利用が容易です。


パーティクルクラスは、
  • 更新(ex. update())
  • 描画(ex. draw())
の操作を行うだけのクラスです。

更新では、
  • 移動量の計算
  • 座標の更新
  • タイマーの減少
  • 消滅判定(タイマーが0のとき消滅フラグを立てる)
という処理を行い、
描画では、それに基づいたパーティクルの描画を行います。

パーティクルを管理するクラスは、
あらかじめstaticなパーティクル生成メソッドを実装しておき、
メソッドに対応した、パーティクルを生成します。

これにより、パーティクル生成がとても柔軟に行えるようになり、
再利用を容易にします。

マップレイヤークラス


マップエディターで作成したデータを読み込む場合、
  • 読み込みのクラス(ここではMapLoader)
  • 読み込んだデータを配列に格納するクラス(ここではLayer)
を利用します。

手順としては、
  1. 必要なLayerクラスをcreateする
  2. createしたLayerクラスのポインタをMapLoaderのloadメソッドに渡す
  3. MapLoaderはマップデータを解析し、Layerにデータを詰め込んで、Layerを返す
となります。

注意点としては、Layerはマップデータのプログラム上で利用できるように変換しただけのものなので、
基本的にReadOnlyの領域ということです。
これを直接いじることは、もとのマップデータを壊すことを意味します。

なので、通常は、Layerは地形トークンや敵トークンなど必要なデータを生成したあと、
deleteするのが、好ましい設計といえます。

外部スクリプト読み込みクラス

命令数が少ない場合は、1つのクラス内に命令をベタ書きします。
命令の呼び出し判定は、switch文でも問題ないでしょう。

命令数が多くなる場合、Commandパターンを利用し、命令をオブジェクト化します。

これにより、各命令の意味が明確化し、変更・拡張を容易にします。

補足としては、命令の呼び出し判定を、map(命令名称をキー、命令オブジェクトを値とするハッシュテーブル)を利用しテーブル呼び出しすると、switch文を除去できます。

リプレイクラス


ゲームにおいて、キーの入力判定として必要なのはたいてい、
  1. そのキーを押しているか?(Press)
  2. そのキーがそのフレーム内に押されたか?(Push)
  3. そのキーがそのフレーム内に離されたか?(Release)
の3つの情報です。
これらの情報は、KeyBufferクラスに格納します。

1についてはそのフレームだけで判定できるのですが、
2、3については、前フレームの入力情報が必要になります。

なので、キー情報の作成の際には、前フレームの入力情報を忘れずにー。


そして、KeyBufferManagerは、
毎フレームごとに、KeyBufferクラスをリストに追加していきます。

そして、リプレイ情報を保存する場合、
KeyBufferクラスのリストをファイルにダンプし、
リプレイを再生する場合は、そのファイルを読み込み、
KeyBufferクラスのリストを再構築します。
そして、リストを先頭から実行すれば、リプレイデータが再生されます。

クラス図には含めませんでしたが、
リストデータ(リプレイデータ)を、符号化・復号化するクラスがあるといいと思います。

GUIクラス

TODO:

オブジェクト指向が使える言語

C++

メモリリーク、バッファオーバーランなど、
オブジェクト指向と無関係なところで悩まされます。
クラスの記述にクセがあるので、慣れないうちは、そもそもコンパイルが通らないこともしばしば。

関数ポインタやTemlateクラスなど、どんな書き方もできるのが魅力でもあり欠点。

Java

C++を洗練した、事実上オブジェクト指向のスタンダード。
C++よりも簡潔に記述できる。

Web上や書籍での有用なオブジェクト指向開発の解説は、
Javaで書かれていることが多いので、覚えておいて損はない言語です。

あと、統合開発環境のEclipseを使えば、
リファクタリングなどで快適なオブジェクト指向開発ができてしまうのも魅力。
UMLとの連携も強力。

C#

Javaをより洗練させた言語。
VisualC#2005を使えば、Eclipseのようにリファクタリングが使える(=超快適コーディング生活)

Python

変なスクリプト言語。
  • インデントブロック
  • モジュール重視
  • えせクラス
  • すべてがオブジェクト
  • 関数型プログラミング
など変な特徴があります。
モジュール重視・えせクラスなので、あまりオブジェクト指向って感じじゃないです。
今はこれを使っています。

その他

使ったことがないけど、、興味あるオブジェクト指向言語。
  • D言語
Javaよりも洗練された言語、とのこと。爆速コンパイル。
  • Ruby
スクリプト言語。
Pythonよりもキレイな書き方ができる。
クラス重視。

参考リンク

オブジェクト指向の使い方がなんとなく分ってきたら、ここで理論武装。
ゲームへの適用方法が詳しく解説されています。
GOFとは異なる切り口によるデザパタの解説。
あのパターンってどうやるんだっけ?と思ったらここで確認。


コメント

おかしな内容・記述、不明な点がございましたら、コメントをどうぞ。
  • 左のメニューレイアウト位置が他のページと違うので、メニューの項目の並びの事ではないですよ。
    これ以上あっちこっちで日記もっても書くことないっすよー(苦笑 -- (D.K) 2006-05-28 02:02:45
  • >メニュー位置が他のページと違う
    おー、そういうことでしたかー。
    気づきませんでした、、。
    >これ以上あっちこっちで日記もっても
    そういえばそうですね。失礼しましたー。 -- (kenmo) 2006-05-28 08:32:57
  • >メニュー位置が他のページと違う
    編集した部分の幅がある値より大きくなると
    CSSの関係でメニューが段落ちしてしまいます
    多分画像とかの幅の影響ではないかと思います
    デザイン部分は丼さんが編集権限をもっていると
    思うのでもう少し広げてもらったら解決すると思います -- (わんきち) 2006-05-28 22:06:34
  • おおなるほど。それが原因だったのですね。
    Firefoxでは問題なかったので、気にしてなかったのですがー。
    丼さんにお願いしておこうかな、、。 -- (kenmo) 2006-05-29 11:49:09
  • おわー失礼しました。
    今日少し広げてみましたが、いかがでしょうか?
    Firefoxだと表示が大分違いますね。
    -- (丼) 2006-05-29 15:12:09
  • おおー、素早い対応ありがとうございます。
    Firefoxだと、タイトな感じですねー。 -- ((*´∀`)o旦 kenmo) 2006-05-29 20:16:55
  • ここの記事見て改良しましたが当たり判定はカプセル化しないときつかったです。修正が大変>< -- (nanasi) 2006-12-26 13:48:41
  • どこに書けばいいか分からなかったのでここで!
    dhellライブラリのdrawLineにバグがー。
    垂直線を引くときのy1とy2が逆になってました。 -- (meitei) 2007-08-05 09:40:27
  • おおっと、、、しょーもないバグですみません。
    指摘ありがとうございます~。
    ついでにCPU使用率が100%になるのが気になったので、それも直しておきました。 -- (kenmo) 2007-08-05 11:32:40
  • お小遣いあげるからメールしておいで(人・ω・)☆ http://gffz.biz/index.html -- (ぷぅにゃん) 2011-11-29 18:33:57
名前:
コメント:

すべてのコメントを見る