はじめに
こんにちは。プログラマーのTです。
株式会社ジーンでは、社内でいろいろな勉強会が行われています。これまでも、いくつかの紹介を行ってきました。今回からは、設計勉強会で行った内容について紹介していきます。
大規模なプログラム
ゲームというのは、いろいろな要素を取り扱う、大規模なプログラムです。画像やモデル、音声やエフェクト、時にはネットワークを使うこともあります。大規模なプログラムを書くときにはどうしたらよいでしょうか?
現代のプログラムはとても複雑
たとえば、Visual Studioで Windowsアプリ―ケーションを作成し、以下のプログラムを書いたとします。
これは、実行するとHello!と表示されるメッセージボックスを出すだけのプログラムです。MessageBox()は、メッセージボックスを表示する関数です。
Visual Studioでこのプログラムを書いて実行すると、実際にはこのようなことが起こります。
まずVisual Studioがこのプログラムをコンパイルして、Windowsで実行可能な形式にします。それを実行します。エントリポイントからプログラムは実行され、MessageBox()関数は、新たにメッセージが表示されるウィンドウを作成します。ウィンドウにラベルやボタンを配置し、内容を設定する、設定されたデータを元に、表示領域を確保し、矩形やフォントが描画され、といった感じでいろいろなことが行われ、デバイスドライバなどを経て、最終的にディスプレイにウィンドウが表示されます。
実際には、気の遠くなるような様々な工程を経てプログラムが実行されるわけですが、このプログラムを書くとき、それらの背後にある莫大なシステムについては、まったく考える必要がありません。
これは大変重要なことです。良いプログラムはほとんどの場合、このように背後の詳細について知らなくても良いように作られています。これをプログラムのレイヤー化といいます。
プログラムのレイヤー化
これと同じように、ゲームをプログラミングする際も、プログラムをレイヤー化することを考えてみましょう。
ゲームでは、画像やモデル、音声などを使ってゲームシーンを組み立てることになります。しかし、それらは「ゲーム」の要素(例えば、プレイヤーや敵など)とは抽象度が異なります。ここでは次のように簡単に3つのレイヤーでゲームプログラムをとらえてみます。
ここでは簡単に3層としましたが、大規模なゲームではもっと細かく分ける場合もあるでしょう。
では、下位から順番に内容を見ていきましょう。
下位のプログラム
ゲームを作るには、画像や音声、3Dモデルのファイルを使用します。具体的にはPNG、WAV、FBXといったものです。これらは、中身を展開し読み込むと得られるのが、ピクセルデータ、音声データ、メッシュやマテリアルといったものになります。
ファイルを読み込んで得られたデータを、抽象化して提供するのが下位のプログラムの役割になります。これらのプログラムは、上位の実装を、「ファイル」という具体的なものから切り離す効果があります。例えば次のようなプログラムがそうです。
また、同じファイルを何度も読み込まないようにする機能や、非同期やダウンロードでデータを取得するようなアセット管理なども、このレイヤーに含まれる機能になります。
このようなアセット以外にも、コリジョンやレンダラー、ゲームデータの管理もこのレイヤーとして扱うことが多いです。
このレベルのプログラムは、UnityやUnreal Engineなどのゲームエンジンでは標準で用意されており、最近では書く機会は少ないかもしれません。ゲームエンジンでは、これらをコンポーネントと呼ばれる部品として提供していることが多いです。
中位のプログラム
下位のプログラムで読み込んだ画像やモデルデータから、ゲームの要素となるものを実装します。ここで実装するのは、プレイヤー、敵、フィールド、ギミックといった粒度で呼ばれるものです。
下位のプログラムで書いた、モデルデータやレンダラー等を組み合わせてプログラムを作成します。例えば次のような形です。
ゲームエンジンを使用する場合、Unityではゲームオブジェクトやプレハブ、Unreal Engineではアクタといったものがこのレイヤーに相当します。下位レイヤーに相当するコンポーネントをこれらに接続する形で作成していきます。
上位のプログラム
ゲームの要素を組み合わせてゲームを組み立てるレイヤーです。
Unityではシーン、Unreal Engineではレベルといった、一定のステージ上で各要素が影響しあってゲームのルールを進めていくレイヤーになります。
例えば、RPGであれば、プレイヤーとNPCが接触してイベントが発生したり、敵にエンカウントしたりといったプログラムがこのレイヤーになります。ゲームエンジンでは、シーンやレベルといったもので表現される場合が多いです。
レイヤーを考慮していないプログラム
ここまで、レイヤー化について説明してきましたが、もしレイヤーを使わずにプログラムを書くとするとどうなるでしょうか。例えば、下位レイヤーを作らずに、プレイヤーを直接実装することを考えると、次のようなイメージになります。
このようなプログラムにはどんな問題があるでしょうか。いくつか考えてみましょう。
まず、変更に弱いという問題点があります。
抽象化されていない"ファイル名"や"ファイルフォーマット"に依存しているので、それらの変更にコストがかかります。
また、下位レイヤーのプログラムをプレイヤーが内包することになり、ソースコードが肥大化しやすくなります。下位レイヤーで共通化できるはずのものが含まれてしまっているため、再利用性が低いのも問題です。例えば、Playerと似たEnemyやNPCを作ろうと思ったときに、ファイルの読み込み部分のコードが独立して利用できないため、コードをコピーペーストするといったことが起きやすくなります。
もう1つは見通しの悪さです。ファイルの操作と、プレイヤーの移動、頂点の操作やレンダリングなど、関係ないものが1つのクラス(ファイル)に混在してしまうことになります。このようなソースコードを編集するのはかなり難しくなります。過去の記事でコードの見やすさについて説明しましたが、これでは該当の関数や処理を見つけるだけでもかなり苦労することになるでしょう。
レイヤー化の利点
レイヤーを作成して、問題の領域を分けることのメリットはいくつかあります。
一つは作業を分担しやすいということです。それぞれの問題が独立しており、レイヤー単位、ゲーム要素単位、コンポーネント単位、と、様々な単位で分割することができます。この例では、プレイヤー担当、イベント担当、レンダリング担当、といったようにです。
また、各プログラムを比較的小さいサイズで完結させることができます。これは見やすさを向上するのにも役立ちます。
他にも、動作テストやデバッグを行うことが簡単であるという点も挙げられます。
もし、プレイヤーの表示が崩れる場合がある、といった問題が出たとしましょう。その時、レイヤーを使用していない場合のデバッグはかなり困難です。すべてが一体となっているため、手当たり次第にコードをコメントアウトしたり変更してみたりして、バグを発見するといったことになりかねません。
レイヤー化されていれば、ファイルのロードがおかしいのか、モデルの計算がおかしいのか、レンダリングがおかしいのか、それぞれ個別にテストすることが可能です。
おわりに
今回は、設計の勉強会の紹介で、ゲームにおけるプログラムのレイヤー化についてお話ししました。
大きなプログラムもレイヤー化し、さらにレイヤーに含まれる要素単位に、と、問題領域を細かくすることで、開発を進めやすくなります。ぜひ試してみてください。
すこしでも参考になれば幸いです。
今回は以上になります。ありがとうございました。