「kenmo」の編集履歴(バックアップ)一覧に戻る

kenmo - (2006/05/26 (金) 12:54:10) の編集履歴(バックアップ)


もくじー


自己紹介

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クラスに格納します。

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

そして、リプレイ情報を保存する場合、
KeyBufferクラスのリストをファイルにダンプし、
リプレイを再生する場合は、そのファイルを読み込み、
KeyBufferクラスのリストを再構築します。

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

C++

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

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

Java

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

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

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

C#

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

Python

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

その他

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

参考リンク

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



ゲームプログラムTips

これは何?

ゲームプログラムしていて、気が付いたことを書き留めておくものです。

当たり判定

当たり判定は「円」で充分

「円」による判定は、汎用的で、直感的に分りやすいためバグも出にくく、コストパフォーマンスが高いです。
なにか特別な理由がある(地形との当たり判定とかニードル(縦長の弾)など)以外は、「円」で判定を行ったほうが良いみたいです。

衝突応答は、複数回に分けて判定を行うとうまくいく

例えば、地形にぶつかった場合押し戻す、という処理を実装する場合、
XY方向の判定をまとめて行うのではなく、
  1. X方向に移動、ぶつかってたら押し戻し
  2. Y方向に移動、ぶつかってたら押し戻し
というように、2回に分けて判定を行うと、うまくいきます。

移動速度が速くて、通り抜けが起こる場合

以下のどちらかの方法を取ります。
  • 通り抜けが起きない移動量になるよう、制限をかける
  • 通り抜けが起きない移動量になるように、移動量を分割して判定を行う

例えば、シューティングの自機のショットはたいてい高速なので、2回以上に分けて当たり判定を行います。

敵のバランス

プレイヤーに不利益となる判定(ex.プレイヤーへの衝突)の場合、敵のサイズを小さくし、
プレイヤーに利益となる判定(ex.プレイヤーの弾の衝突)の場合、敵のサイズを大きくします。

つまり、敵は2つのサイズを持つことになります。

数学

一定のフレームでAからBに移動させる

  1. 一定のフレームをTとする。
  2. Bまでの方向ベクトル(v)を「B-A」で求める。
  3. |v|でその距離(L)を求める。
  4. 「L/T」で1フレームの移動距離(l)が求められる。
  5. vの単位ベクトルにlをかけると1フレームの移動ベクトル(v1)が求められる。
  6. 「A+v1」により、そのフレームの移動後の座標が求められる。

これを使うと、
  • テキトーに動かした敵をある位置まで自動的に戻す
  • 誘導弾
  • 任意の位置にいるプレイヤー座標から開始したエフェクトを特定の位置への線形移動(例えば、メッセージウィンドウなど)
といった処理を行うことができます。

物理

物理は複雑

物理計算をして移動量を決定すると、複雑な軌道をシミュレートすることが可能になります。

しかし、それはしばしばゲームの不安定要素となるため、
テーブルと移動開始時間からのタイマーにより移動量を決定すると、移動量が安定します。

AI

AIの使い道

AIは複雑なルールを持つゲームにおいて、
敵やNPCの思考ルーチン構築の負担を減らすものであり、
通常は必要ないです。

確実に使えるものとしては、用途の明確な、
  • LOS追跡アルゴリズム(追いかける)
  • LOS迎撃アルゴリズム(先読み弾)
  • 有限状態機械
  • A*による経路探索
ぐらいです。

逆に、
  • ニューラルネットワーク
  • 遺伝的アルゴリズム
は、とっても使いどころが難しいです。

リファクタリング

リファクタリングとは、
  • 外部から要求される動きを変えずに、ソースを読みやすくする
手法です。

リファクタリングの手法については、例えば、
  • 重複する処理の共通関数化
  • 長すぎるメソッドのサブルーチン化
  • 巨大なクラスの分割
などがあります。

これらの手法をカタログ・パターン化したのがリファクタリングです。



「まあ、動けばいいじゃん」
という思想は、とても正しく、健全です。

リファクタリングをしても、進捗率は1%も増加しません。

ただ、リファクタリングには、
機能拡張・修正を容易にし、未知のバグを検出するなどの
メリットがあることを忘れてはいけません。

トークンの選択・追加

ゲームを面白くするのは、トークンの増加です。

ただ、トークンを単純に増やすだけではダメで、
  • メリット(追加することにより、どんな効果が得られるのか?)
  • デメリット(追加することにより、どんなゲーム性が失われるのか?)
をしっかり考察し、自分が重視する価値観を満たすトークンのみを選択・追加する必要があります。

しかしこの考察・選択は、作り手のスキルに依存する部分でもあります。
いざ、追加してみたら予測のできなかった問題が発生した、
というのは、良くあることです。

そこで、実際にゲームに使うかどうかを「保留」できる仕組みを、
前もって作っておくことで、このリスクを回避することができます。

その仕組みとは、
トークンの生成ロジックをプログラムから切り離してしまうことです。
具体的には、
  • 外部スクリプト化
  • 外部マップデータ化
により、トークン生成の定義を外部で行えるようにします。

トークン生成の定義の副産物

トークン生成の定義を外部で行えるようにするようにすると、
生成するための入り口が簡易化されます。

それにより、
「敵トークン」の破壊により、「爆発トークン」を生成する
というような、トークン間の「連鎖反応」を実装しやすくなります。

他にも、
「ボストークン」が「雑魚トークン」を生成する
という連携や、
「エフェクト1トークン」消滅すると「エフェクト2トークン」を生成する
という連携の実装も容易になります。

飽和点を認識する

ゲームを作っていると、時々、些細なことが非常に気になったりします。
例えば、
このエフェクトをあと1フレーム伸ばすべきか?
などと。

おそらく、その問題を解決したところで得られるものは少ないです。

その問題を解決したい気持ちをグッとこらえて次のステップに進むことが大切です。

このことは、素材作成などについてもいえます。

絵の下手な人が必死でドット絵を描いても、
絵のうまい人が鼻歌交じりで数分で描いた絵には勝てません。

作曲の知識のない人がメロディーを必死に考えても、
一定のレベル以上の曲は作れません。

飽和点を認識することで、無駄な作業を避けるようにしましょう。

ミニマップ

ミニマップはスクロールが存在するゲームでの指針となります。
しかし、ミニマップを透明化できない場合(情報がごちゃごちゃある場合など)、フィールドの視界を妨げます。

その場合は、思い切って、上から下まで伸びた枠にしてしまうと、
視界を妨げることがなくなります。

バージョン管理

大きな修正を加える場合、万が一にとバックアップ・コピーを続けると、
コピーファイルだらけになってしまい、なんだかよくわからなくなってしまいます。

個人開発では大げさかもしれないですが、
バージョン管理システムを使うことにより、
  • ソース管理の簡易化
  • コメントの付加
  • コピーを取る手間の削減
などの効果を得ることができます。

Subversion

バージョン管理システムの主流となりつつあるのが、Subversionです。
Subversionはここからインストールします。
http://subversion.tigris.org/servlets/ProjectDocumentList?folderID=91
「svn-X.X.X-setup.exe」(Xはバージョン)だとインストーラーつきで簡単にインストールできます。

ただ、素のSubversionはコマンドプロンプトから起動することになるので、
便利に使えるようにするGUIツール「TortoiseSVN」を使います。

TortoiseSVN

http://tortoisesvn.tigris.org/download.html
ここから、Download pageのリンクをぺちっと押すと、SourceForgeのページに飛びますので、
TortoiseSVN-X.X.X.XXXX-svn-X.X.X.msi」(Xはバージョン)を落とし、インストールします。

また、デフォルト英語なので、
http://tortoisesvn.tigris.org/download.html
のLanguage Packsのリンクから、SourceForgeのページに飛び、
「LanguagePack_X.X.X_ja.exe」(Xはバージョン)を落とし、インストールします。

あとは

http://www.saisse.jp/pukiwiki/pukiwiki.php?Subversion#zf8c1d62
ここを参考に、TortoiseSVNの日本語化、リポジトリ作成、コミットなどをためしてみます。
(手抜き?)

これで、一人でもチーム開発の気分が味わえます(w

ネットワーク経由でバージョン管理したい場合

http://www.strikeout.jp/technote/?Subversion%2FWindows%A4%D8%A4%CE%A5%A4%A5%F3%A5%B9%A5%C8%A1%BC%A5%EB
ここを参考に、Apacheをインストールして、必要な設定をします。


コメント

おかしな内容・記述、不明な点がございましたら、コメントをどうぞ。
  • 左のメニューレイアウト位置が他のページと違うので、メニューの項目の並びの事ではないですよ。
    これ以上あっちこっちで日記もっても書くことないっすよー(苦笑 -- (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
名前:
コメント:

すべてのコメントを見る
記事メニュー
目安箱バナー