ホーム < ゲームつくろー! < オブジェクト指向設計編 <CS2:自機から弾を発射する(1)

その5 CS2:自機から弾を発射する(1)


 STGで自機から弾を発射する。この短い時間で終わる単純な振る舞いの中には、実は細かなオブジェクトの制御が隠されています。今回はオブジェクト指向で自機から弾を発射する実装をしてみましょう。あ、今から宣言しておきます。これ、大変な仕事です。



@ 問題意識(問題領域)

 今回の解決すべき問題は、

ボタンが押されたら、自機から弾が発射される。

という単純なものです。しかし、これが実に奥深いんです。うん…本当に奥深いんです(-_-;;



A 仕様作成

 細かな仕様を固めていきましょう。まず、問題意識にある言葉の定義からです。

 ボタンというのは、仮想的なものに過ぎません。キーボードのキーかもしれませんし、マウスのクリックに化けるかもしれません。すべてに共通することは「押されたら」という部分にあります。よって、ここを詳しく言えば「ユーザインターフェイスの仮想ボタンが押されたら」と抽象化されることになります。

 自機は、言わずもがなSTGの自機です。ただ、STGと言っても自機がメカの時もあれば鉄砲を撃つ人の時もあります。あまり固定概念で考えない方が良いわけです。やっぱりこれも「仮想的な自機」と考えるのが妥当でしょう。

 も同様です。どんな弾が放たれるのかは皆目検討もつきませんので、仮想的な弾としか言えません。

 問題領域が漠然としていますので、作成するクラスも抽象概念で繋がる程度のものになります。これは見方を変えれば「仕様変更が相当にある」と考えられる部分です。
 固有名詞のイメージを固めたところで、テストとなる仕様を作成します。

仕様
・ 弾発射ボタンを押すと自機から弾が1発発射される。
・ オート連射はしない。
ボタンを押す度に弾を発射可能。
・ 自機は3次元の空間内に位置し、任意の方向を向く。
・ 弾は自機の向いている方向に飛ぶ。
・ 弾の初期発射位置は自機の位置。
・ 弾は1フレームで、速度vだけ直線移動する。
・ 弾の速度は常に一定。
・ 弾は20フレーム分飛んだら消える。


 こんな感じですかね。では、これを元にクラス図作成のための静的解析の開始です。



B 名詞抽出

 静的解析の基本、第1歩はクラス候補の選択のために仕様から名詞を抜き出すことです。上の仕様において、名詞を純粋に洗い出すと次のようになります。

「弾発射ボタン」「自機」「弾」「位置」「方向」「弾の初期位置」「自機の位置」「速度v」「20フレーム」

 この中からクラス候補を抽出します。
 「発射ボタン」はユーザーインターフェイスクラスの候補として十分です。
 「自機」「」はクラスの大候補です。わかりやすい対象ですね。
 「位置」「方向」「弾の初期位置」「自機の位置」「速度v」「20フレーム」というのはいずれもクラスというよりは属性(メンバ変数)と考えた方が良いでしょう。


以上から、クラス候補は3つです。名詞は沢山ありましたが、わかりやすくすっきりです。

・ CUserInterface (ユーザインターフェイスクラス)
・ COperationObjクラス (自機クラス)
・ CBulletクラス (弾クラス)



C クラス図作成

 これらクラスの関係について考えて見ます。

 あ、考える前に1つお断りを。初期段階のクラス図というのは、物凄い変化するものです。作成過程でクラスが付け加えられたり消されたり、矢印が出現したり消えたりと、静的解析と言いつつも激しく動きます。最初から完璧なクラス図を作成できる人はいません。何度も繰り返してクラス図を眺める事でひそかに隠れている重大な欠陥に気付き、少しずつ柔軟性のあるクラス図になっていきます。これを肝にしっかり命じまして、以下をご覧頂ければと思います。

 ユーザインターフェイスクラスのボタンが押されると、弾が発射される。ということは、ユーザインターフェイスと弾オブジェクトは何らかの関係を持っているのではないかと考えてもおかしくはありません。先入観を持つと、「プレイヤーが操作するのは自機じゃないかい」と思ってしまうのですが、今回の仕様にはまだ自機の操作は含まれていません。ユーザインターフェイスクラスの視点から見て、今は自機クラスと関連を持たないとしておきましょう。

 自機クラスは今回誰からも操作を受けません。ですから、自機を操作する人はいないわけです。自機は弾の存在を知っているのでしょうか?これは、一応知っていると今はしておきましょう。しかし、きっと物凄い希薄な関係な気がします。

 弾は自機の位置から発射されるという仕様です。ということは、弾クラスは自機クラスの存在を薄々感じているかもしれません。ユーザインターフェイスクラスは多分知らないでしょう。

 以上から、最初のクラス図は下のようになりました。


 さて、これは相当に適当で雑に考えていますから、どんどん改良していきます。仕様では、弾は自機の位置から発射されることになっていました。つまり、どうにかして弾に自機の位置を教えないといけないわけです。ただ、ちょっと将来のことを考えた時、弾は自機だけでなく敵も撃ちますし、鉄砲にも込められます。そういう使いまわしの激しそうなクラスが自機自体を知ってしまうと、途端に「自機専用弾」のような扱いになってしまいます。それはまずいわけです。関係はあるのだけれど非常に希薄。それを実現するにはその4で出てきました「関係連結クラス」を用いるとベストです。今弾が知りたいのは自機の位置ですから、CPosConnectionクラスを新設して、このクラスに自機の位置を知らせてもらうことにしましょう。CPosConnectionクラスは一時的に自機と弾を保持することになるでしょうから、それらのクラスとは「集約」の関係になります。


 撃てという命令がプレーヤーの脳から発せられて、ボタンを押す。それはCUserInterfaceの「ボタンを押す」関数に伝えられます。さて、その先はどうなるでしょう?弾はその段階でまだ世界にありません。もしくは元から用意してあるかもしれませんが、まだ存在として発射プロセスにデビューしていません。一番簡単なのは、CUserInterfaceクラスが「ボタンを押す」関数内で弾を作り、自機の位置を取得し、それを弾に伝えることです。しかし、これはCUserInterfaceクラスがする本来の仕事と合致しません。CUserInterfaceクラスの本業は、ユーザからの情報を受け取って適切な命令を誰かに発する事です。命令の中身までやるのは、仕事の分業とクラスの専門化と言う点で良く無いでしょう。とは言え、前述の仕事をしてくれそうなクラスはありません。そこで、「撃て」という命令を受けて弾の生成と初期化を行うCOperateMngクラスを新設します。
 COperateMngクラスは、自機及び弾の制御を総合的に管理します。今回の仕様では、このクラスは「撃つ」という仕事、そして「弾を生成して初期化する」という仕事を担うことになります。CUserInterfaceはこのクラスに直接命令を下します。よって、CUserInterfaceはこのクラスを直接知ることになり、上図のCBulletクラスへの矢印は無くなります。


 ここまでをクラス図にしてみたところ、CPosConnectionクラスの存在が希薄になってしまいました。このクラスは自機の位置を弾に教えるだけなのですが、その程度の軽い仕事はCOperateMngクラスのメソッドでも十分可能です(操作として適切でしょう)。もし自機の位置を敵に教えるその教え方に色々ある(機能変化が激しい)のであれば、上図のようにクラスとして分ける意義はありますが、そういう可能性も今はあまり見えないので、このクラスの機能をCOperateMngクラスのメソッドとして吸収することにします。


 さて、右往左往しながらも、だいぶすっきりしてきました。上のクラス図からも、「撃つ」までの動作は出来るような気がしてきました。次は、「弾が等速で移動する」という仕様部分です。
 撃つというのは一瞬の動作です。それはCUserInterface::PushButton関数が担ってくれそうです。しかし、まさかプレイヤーが「弾よ動け」と別のボタンを押し続ける事は無いでしょう(笑)。つまり、弾を動かす部分と言うのは、全く別のクラスからの命令になるわけです。上の図にはその命令者がいませんので、プレイヤーに変わって「弾よ動け」と命令し続けるCBulletUpdaterクラスを新設します。

 CBulletUpdaterクラスの役目は、弾の位置を更新します。ということは、このクラスは弾(CBullet)を知っていることになります。CBulletクラスは自分を動かす人を知るべきでは無いでしょうから、矢印は一方通行です。


 弾はCBulletUpdater::SetBullet関数で登録され、CBulletUpdater::Move関数をシステムが呼び出す度にその位置が更新されます。SetBullet関数を呼び出すのは、弾を生成した直後でかつ弾を知っている人です。該当するのはCOperateMngになります。つまり、COperateMngクラスはCBulletUpdaterクラスを知っていると言うことになります。その関係が依存か、集約か、はたまた合成(コンポジション)か、次にそれを考えます。
 今システムがCBulletUpdaterにアクセスする可能性を考えていますから、COperateMngクラスが勝手にこのクラスのオブジェクトを消すわけにはいきません。つまり合成という選択はありません。弾の発射されるタイミングはプレイヤー次第ですから、もし依存関係にしてしまうと弾が発射される度に誰かにCBulletUpdaterオブジェクトを渡してもらわないといけません。それが出来る人は現行でCUserInterfaceですが、ユーザインターフェイスがアップデートも兼ねると言うのはおかしな話です。ということは、依存はちょっと辛いということになります。よって、COperateMngクラスとCBulletUpdaterクラスは「集約(関連)」関係にすることにします。


 これで、弾を動かす仕組みは大分整いました。次は仕様書にある「弾は20フレームで消える」という部分です。

 弾を生成するのはCOperateMngクラスです。生成した弾はCBulletUpdaterに移されます。その後の動きはCBulletUpdaterクラスに一任しますから、削除権限はCBulletUpdaterクラスでも良いかもしれません。削除といっても、これはCBulletUpdater::BulletLstリストから弾をはずすだけです。使われなくなった弾は人知れず自動削除されるものとしておきます(スマートポインタやガーベージコレクタの利用が前提です)。

 そろそろCBulletクラスのメンバ関数も見えてくるころです。まず、「20フレーム」というライフタイムを設定する関数が必要です。また、削除のタイミングを知るために、弾自体に今自分がどういう状況にあるかを伝える関数が必要になります。今の仕様では状態は2つ、「まだ動いているよ」という状態と「もう動けなくなりました」という状態です。これはフラグで区別することにしましょう。さらに、弾の状態を更新する関数が無いと状態を更新できませんから、状態更新関数も追加します。

 ついでにもう1つ変更しておきます。COperateMngクラスは確かに弾を生成しますが、その後の動きはCBulletUpdaterに任せてしまいます。生成してすぐに渡してしまうので、COperateMngクラスはそのポインタを保持しません。よって、COperateMngクラスとCBulletクラスの関係は「依存」となります。
 以上の変更をクラス図に付け加えます。


 残りの仕様はそれほど難しいところはありません。諸々の設定関数を加えたクラス図がこちらです。


 図が大き過ぎまして、縮めたら字がつぶれてしまいました。図をクリックすると大きいクラス図が表示されます。誠にすいません(^-^;。



D クラス図のOCP

 Cでクラス図の荒出しが終わりました。今の段階でもそれなりに動く感じはします。しかし、クラスは動けば良いと言うものではありません。もっと大切なのが変化に対する耐性、すなわち「頑健性」の検査です。そこで、オブジェクト指向のゆるぎない原則であるOCP(Open-Cloased Principle:開放閉鎖原則)が満たされているかをこのクラス図に対してチェックしてみます。仕様とクラス図を眺めて、変化しそうなところを考えると、自ずと以下のような「ダメ」な点が出てくるはずです。


 ○ CUserInterfaceクラス名の変更と抽象化

 CUserInterfaceクラスは、自機と弾の関係を制御するCOperateMngオブジェクトポインタを保持しています。これは結構な限定品クラスです。にもかかわらず「CUserInterface」と銘打つのはちょっと違うかなという感じがします。ユーザインターフェイス自体は他の箇所でも絶対に使うでしょうから、ここはインターフェイスを抽象化しておきましょう。
 将来的にはユーザインターフェイスは自機も操作するでしょうが、その作業はCUserInterfaceクラスに方向指定関数を加えれば良く、今回の図とはまた別物になります。よって、そのOCPは満たしていそうです。

 ○ COperateMngクラスが作る弾は大変化する

 弾と一言に言っても実に様々です。能力も容姿も、移動できる次元すらも異なります。そういう変化の激しい弾生成に対して、COperateMngクラスの弾初期化・生成部分は残念ながらうまく対応していないと思われます。実際、今はCBulletオブジェクトしか作れません。実際に弾のバリエーション例を列挙して見ますと、
・ 移動速度の変化
・ 軌道関数の変化
・ テクスチャの変化
・ ポリゴンの変化
・ アクションの変化
・ ターゲット設定の有無(固定弾か否か)
とまぁ、何から何まで異なるわけです。その弾の仕様が異なる度にCOperateMngクラスを派生していたのでは、COperateMngクラスが完全に弾に支配されてしまっています。両方のクラスは「依存」関係のはずなのにです。これは、COperateMngクラスが弾を生成するという仕事も担ってしまっているからです。本来このクラスの仕事は、自機と弾の「操作」です。よって、生成部分を分離して、CBulletクラスとCOperateMngクラスの結合度をもっと下げる必要があります。これは、一筋縄ではいかなそうです。

 ○ COperationObjだって大変化する

 COperateMngクラスに対する上と同じ理屈は、自機についても当然当てはまります。今回は自機の移動を考えていないテスト的な仕様なので、自機は「いるだけ」感が強いのですが、将来的には確実に自機も変化します。ここも、やはりOCPを満たしていません。さらなる改良が必要になりそうです。

 ○ 出力の問題

 実は、これが大問題です。今回の使用は出力を決めていませんが、テストをするに当たってはどこかしこに出力しなければなりません。にもかかわらず、今のクラス図には出力部分が無いのです。しかし、いったい何をどうやって出力すると言うのでしょうか。そして、ユーザの任意のタイミングで発射される弾をどうやって捉えたら良いのでしょうか?これは、やっぱり簡単ではないのです。


 他にもありそうですが、とりあえずは上に挙げた問題を解決しないことには、このクラス図を基にした実装はまるで変化に弱いことが分かると思います。OCPのチェックはとても大切なのです。


 「ボタンが押されたら、自機から弾が発射される」。奥深いでしょ〜(^-^)。それは実は当然でして、赤文字で示したわずか21文字の要求に、プレイヤーの入力、オブジェクト生成、オブジェクト更新、出力というゲームに必要な要素が全部含まれているんです。大変じゃないはずがありません(笑)。しかしながら、さすがにちょっと小休止を入れないと紙面的にもつらいです。ということで、OCPを満たす改良は次章に回すことにします。