はじめに
こんにちは。プログラマーのTです。
株式会社ジーンでは、社内でいろいろな勉強会が行われています。今回は、前回に引き続き、設計勉強会で行った内容について紹介していきます。
概要
ここまでの勉強会の紹介で、クラスを1つの役割にする、レイヤーして実装することを紹介しました。
今度は、それらをどのようにつなげるかということが問題になります。
ゲームでは、一般的なプログラムに比べてオブジェクト同士の関係が密接です。ここでは、様々なオブジェクト同士が何かをやり取りするときのパターンについて、いくつか解説します。
オブジェクトの構成要素
例えば、プレイヤーを作成するとき、3Dモデルやコリジョンは、プレイヤーの要素としては不可分です。そのような構成要素を、Has-a関係といいます。
プログラムの実装としては、単純にクラス内に内包するという実装を行います。例えば次のような形になります。
逆に、構成要素であるものを外に出してしまうと、不要な連結コードが必要になってしまいます。
クラスを分けすぎないように、それが必須な構成要素なのか考えて実装しましょう。
直接の構成要素ではないが関係が深いもの
次に、関係はあるが不可分ではない要素について考えてみましょう。例えば、サウンドやエフェクトはプレイヤーの必須の構成要素とまでは言えませんが、プレイヤーのアクションから発生するものです。
サウンドやエフェクトは、発生の管理を別途行うほうが良いため、通常はゲームの開始から終了まで存在する管理クラスを経由して発生させることがほとんどです。
このようなクラスは、ゲームプログラミングでは一般的にシングルトンパターンを使って実装することが多いです。例えば、以下のような実装が考えられます。
この実装は、コーディングがシンプルです。また、ほぼ関数呼び出しと変わらないので、パフォーマンスも比較的良いです。反面、これらのクラスの呼び出しを直接行うことによる依存性も発生します。例えば、サウンドマネージャーに何らかの問題が発生して、エラーになった場合に影響を受けてしまいます。他にも、サウンドマネージャーの変更にも対応する必要があります。
そのような依存性を薄めるため、後述するインターフェイスと組み合わせて使用されることが多いです。
無関係ではないが遠い関係のもの
無関係ではないが遠い関係のものの典型的な例として、実績によるトロフィー獲得があります。
実績クラスは、敵を100体倒した、ステージ5までクリアした、など、プレイヤーだけでなくいろいろなクラスに横断的に関係することになります。
そのようなクラスを、直接実装すると、あちこちに実績クラスのコードが現れることになります。例えば次のようなものです。
このような実装を行うと、プログラムがややこしくなりすぎてしまいます。
また、いろんなクラスに実績クラスを呼び出すための処理を書くことになってしまいます。
これを解決するために、通知システムを作成します。
Playerをはじめとするゲーム要素が通知を送るのみとし、それを受け取るクラスがなんらかのタイミングで処理するという形をとります。
このようなプログラムはリアルタイム性が必要ないものに使用します。オーバーヘッドが多いため、毎フレーム呼び出しをするのには向いていません。
実装例をクラス図にしました。
どのような実装になるのか見ていきましょう。
通知を発生させるプレイヤー等のゲーム要素側では、ゲームイベントを通知するのみにします。GameEventのnotify()を呼び出します。
次はゲームイベントを購読して、必要な処理を行うAchievementProgressクラスの実装です。
notify()されたイベントが敵を倒したイベントならカウントアップし、条件を満たしたら、AchievementクラスのsetCompleted()を呼び出します。
これにより、ゲーム本体と実績部分の操作を分離することができました。コードの見通しがシンプルで、拡張性も高くなりました。
上記の例ではenum値しか渡していませんが、通知用の基底クラスを作って、種類ごとに個別のパラメータを保存することもできます。
もちろんこの方法は、関数呼び出しと比べて、オーバーヘッドがかなり大きいです。即時に反映しないといけない処理にこのような実装を使うことはあまり現実的ではありません。
インターフェイスの活用
インターフェイスは強力な武器です。C++では厳密にはインターフェイスというものはありませんが、純粋仮想関数を用いて、同様のことが可能です。
これをうまく活用することで、実装をシンプルにすることができます。ゲームプログラミングでよく行われる例についてみてみましょう。
集約
同じようなものを集約する場合に役に立ちます。最も一般的な使用例です。
純粋なインターフェイスではありませんが、UnityのMonoBehaviourや、Unreal EngineのActorは、ワールド上に配置できるオブジェクトの代表的な基底クラスです。これにより、シーンやレベルから共通的な処理を行うことができます。インターフェイスと聞いて最初に思い浮かべる使いかたではないでしょうか。
シングルトンのインターフェイス化
前述したシングルトンは便利ですが、直接クラスを返すとそのクラスへの依存性が高まってしまいます。そこで、インターフェイス化を行い、シングルトンへのアクセスを仮想化することで、柔軟性を持たせることができます。
例えば、以下の例ではログ呼び出しをインターフェイス化することで、画面出力やファイル出力、ログの読み捨てなど、様々な用途に対してインスタンス生成を切り替えることで実装できます。インターフェイスの恩恵により、呼び出し側は何も意識する必要がないところも良いところです。
入力のインターフェイス化
最近のゲームでは、リプレイ機能を備えているものがあります。これを実現するためにもインターフェイスが役に立ちます。プレイヤーの入力をインターフェイスとして仮想化することで、コントローラからの入力の他、再現を行うものや、自動プレイをするコントローラを定義し、簡単に切り替えることが可能になります。この実装の良いところは、プレイヤー内部の実装に影響が全くなく、動作を切り替えられる点にあります。
おわりに
今回は、オブジェクト間のやり取りの方法やインターフェイスの活用についてお話ししました。
ゲームプログラムは、オブジェクト同士の関係が多い分、すぐに複雑になりやすい傾向があります。解説した内容を参考に、よりよいプログラミングの参考にしていただければと思います。
今回は以上になります。ありがとうございました。