土屋つかさの技術ブログは今か無しか

土屋つかさが主にプログラミングについて語るブログです。

そろそろゲームプログラミングの用語を共有していこうじゃないか(あるいは既存ゲームエンジンアーキテクチャに対する司エンジンの優位性について)

「Ruby Game Developing Advent Calendar 2016(http://www.adventar.org/calendars/1647)」の2日目です。1日目はあおいたくさんの「よくわかる Ruby ゲーム開発のいま(http://blog.aotak.me/post/153865094051)」でした。土屋もRubyでゲームを作っているのは、Rubyが書いていて楽なのと、DXRubyが使っていて手に馴染むからです。

■はじめに

 ゲームプログラミング/ゲームエンジンアーキテクチャの議論をしたい時に困るのが、概念のほとんどに名前がついてない事です。OSから毎フレーム呼びだされることをなんと良い表せばいいのか? 「スクリプト」が示している領域はどこからどこまでなのか? ゲームの構成要素の呼称は「ゲームオブジェクト」「コントロール」「ステートマシン」のいずれなのか?(あるいはそのいずれでもないのか?)、などなど。上げればきりがありません。

 自作のゲームエンジンである「司エンジン」と、既存のゲームエンジンを比較する文章を書いている時このことで非常に悩まされました。ゲームのジャンルや規模によってアーキテクチャは変わるだろうから、統一された用法がないのは仕方ないとも思うのですが、UE/Unity全盛期の今、一般的なモデルくらいは認識が共有されていても良いと思うのです。

 というわけでその「比較する文章」を掲載します。

※本記事は、10月に開催されたデジゲー博で頒布した「ポスト・ゲームエンジン〜tsukasa言語が示す次世代ゲームエンジンアーキテクチャ〜」の第1部抜粋になります。詳細については同人誌を参照してください(近々電子書籍配信も始める予定です)。

【第1部 メッセージ指向ゲーム開発言語「tsukasa」】

 tsukasa言語は「近年のゲーム開発が高コスト化しているのは、既存のゲームアーキテクチャがゲーム開発の大規模化にスケールできいないからで、解決の為には新しいアプローチの言語が必要」というコンセプトの基に設計されました。

 第1部では、これまで使われてきた一般的なゲームアーキテクチャについて解説し、そのアーキテクチャが内包している課題と、その課題に対してtukasa言語が採用したアプローチを説明します。

■1 一般的なゲームアーキテクチャ

 ここではまず、ゲームプログラミングの一般的なスタイル、その中でも根幹となるコア部分のゲームアーキテクチャについて、概念レベルで説明します。作られるゲームの規模や動作環境にかかわらず、近代のゲームプログラミングではほぼ同じ方法で実装されています。

 実行中のゲームプログラム(以下プログラム)の概念図を示し、以下、メインループ/ステートマシン/スクリプトを中心に、一般的なゲームアーキテクチャについて解説します。

(1)メインループ

 プログラムは動作している間、OSから60分の1秒ごとに繰り返し呼び出されます。呼び出されたプログラムは内部状態を更新し、更新された状態に応じて画面を描画し、OSに処理を返します。この定期的な呼び出しのことを「メインループ」と言います。また、秒間に60回画面が更新されることを60FPS(frame per second)と呼称します。

 60分の1秒を単位としているのは、過去の一般的なディスプレイが60Hz(秒間60回更新)で駆動していたことに由来しています(※1)。1回の呼び出しにかかる時間が60分の1秒を超えた場合、描画が間に合わないので処理落ちが発生してしまいす。その為、プログラムは可能な限り60分の1秒以内に更新を完了しOSに処理を戻すことが求められます。

 Windowsなどイベント駆動式のOSでは、このようなメインループ処理を構築しにくい為、通常はゲームエンジンがOSの挙動をラップして代わりにメインループ処理を制御します。ゲーム専用機の場合は、OSとゲームエンジンの機能は融合していて、OSが直接メインループ処理を提供します。

 次に、メインループの呼び出し時に、プログラムが行う作業を説明します。

 プログラムはファイルシステムにアクセスし、ゲームに必要なリソースデータを取得します。リソースデータには画像やBGM、ポリゴンモデルの頂点情報やキャラステータスのような数値データなどがあります。また、ゲームの進行制御に必要な「スクリプト(後述)」と呼ばれる特殊な性質を持つデータもリソースデータに含まれます。

 プログラムはゲームパッドやマウスなどの入力デバイスから値を取得し、リソースデータの情報を加味して、ゲームの状態を更新します。そしてその結果を出力デバイスであるディスプレイやスピーカーに出力します。これを秒間60回行うことによって、ユーザーに連続したゲーム体験を提供しているのです。

(2)ステートマシン

 次に、ファイルシステムから読み込んだリソースデータが、どのようにメモリ上に格納されているかを説明します。

 一般的なゲームプログラムでは、ゲームを構成する各要素をオブジェクトとして生成しています。オブジェクトには画像やBGMなど、リソースデータを直接表す物の他、主人公キャラのように複数のリソースデータの集合として作られるものや、「タイトル画面」のような論理単位をさすものもあります。

 これらのオブジェクトはステートマシン(state transition machine. 状態遷移機械)と呼ばれる構造になっています。個々のオブジェクトは内部でステート(状態)を持ち、自身が今どのステートにあるかに応じて、そのフレームでの処理が決定されます。

 生成されたオブジェクトは、単一のオブジェクトツリーに登録されます(※2)。プログラムは1回の呼び出しごとにこのオブジェクトツリーを全探査し、全てのオブジェクトに対してステートの更新処理を命令します。

 アクションゲームの主人公キャラのオブジェクトについて考えてみましょう。このオブジェクトは、ステートとして「通常」と「ジャンプ中」の2種類を持っています。

 現在は「通常」が自身のステートになっています。このステートの時は、ユーザーからのジャンプボタンの入力を受け付けています。

 ジャンプボタンが押されると、オブジェクトのステートが「ジャンプ中」に切り替わります。同時にジャンプがスタートし、キャラが上方向へ放物線移動し始めます。この間は、ユーザーからのジャンプボタンの入力は(既にジャンプしている最中なので)受け付けていません。

 ジャンプが終わり、キャラが元の高さに戻ると、ステートは「通常」に戻り、再びジャンプボタンの入力を受け付けるようになります。このように、複数のステートごとに処理を変えることで、ゲームの複雑な挙動を制御しているのです。

(3)スクリプト(制御構造を持つフローデータ)

 ゲームは他のコンテンツメディアに対して決定的に異なる、インタラクティブであるという特徴を持っています。コンテンツがユーザーの行動に応じて、動的に変化するのです。

 コンテンツを動的に変化させるためには、コンテンツを構成するデータが制御構造を持つ必要があります。例えば、ノベルゲームにおいてはシナリオデータが制御構造を持っています。ユーザーの選択によってストーリーが分岐する場合、その分岐処理はデータで表現されていなければなりません(※3)。

 このデータは、コンテンツ全体で莫大な容量になることが多く、その時点で使う部分だけをメモリに載せ、使い終わった物から破棄するロジックが必要です。このように使われていくデータのことをフローデータと呼びます。ノベルゲームでは、プレイヤーが読み終わったシナリオデータは不要なので、メモリ上から順次破棄して、新たなシナリオデータを読み込んでいきます。

 ゲームプログラミングでは、このような制御構造を持つフローデータとして「スクリプト」と呼ばれる簡易な言語を使用します。ノベルゲームに限らず、ほとんどのゲームはこのようなスクリプトを使って作られています。

■2 既存ゲームアーキテクチャの課題と司エンジンのアプローチ

 ここまで、一般的なゲームアーキテクチャについて解説してきました。この一般的なアーキテクチャは、商業ゲームの最初期に採用されて以来、今日まで抜本的な変化を経ず使われてきました。

 しかし近年、このアーキテクチャはひとつの限界に達しつつあります。現在のゲームプログラミングは、その初期とは比較にならないほど複雑化しつつあり、また、当時とは比較にならないほど大量の情報を処理しなければならなくなっています。そのため、このアーキテクチャではその複雑性と情報量を吸収しきれなくなりつつあるのです。

 またこの傾向は、今後も継続することは確実で、ゲーム開発のうえでボトルネックとなりつつあります。

 司エンジンは、この課題を解決する方法を示すために開発されたゲームエンジンです。

 司エンジンが採用している、ゲームプログラミングに最適化されたtsukasa言語によって、複雑化、増量化していくゲームのリソースに対してより良くスケールする開発環境を提供します。

 ここからは、一般的なゲームアーキテクチャが抱える課題と、それに対して司エンジン/tsukasa言語が採用したアプローチを説明します。

(1)メインループ

課題:複数フレームにまたがる処理の記述

 ゲームでは画面の再描画のために、秒間60回のメインループ処理が実行されます。プログラムは処理落ちを起こさせないために、60分の1秒以内に処理をOSに返さなければなりません。しかし、オブジェクトが取るほとんどの挙動は複数フレームにまたがる処理であり、このメインループ方式と相性がよくありません。

 メインループ方式で複数フレームにまたがる処理を行う場合、あるフレームで処理が完了しなかった多くの情報を保存しておいて、次フレームの再呼び出し時にその情報を取得させなければなりません。この処理はゲームのロジックとは無関係にも関わらず、ソースコード上に頻出し、コードのメンテナンス性を低下させます。

tsukasa言語のアプローチ:隠蔽されたメインループの採用

 メインループの処理を、tsukasa言語は処理系の中に隠蔽します。そのため、プログラマーはメインループ処理を意識する必要がありません。そして複数フレームにまたがる処理についても、継続して必要となる情報を、処理系が自動的に保存し、取得します。

(2)ステートマシン

課題:ステートの組み合わせ爆発

 ステートマシンは、オブジェクトが持つ多様なステートを管理するのに適切なものですが、近年のゲームでは1つのオブジェクトが取り得るステートの数が膨大になっています。場合によっては、次にどのステートに遷移するのかを判断するロジックが数千行にわたってしまい、バグの温床と化すことがあります。

 例えばスーパーマリオブラザーズの場合、主人公には見た目に応じて「通常」「キノコで大型化」「ファイアマリオ」「スター」という4種類のステートを取り得ます。また、移動時のステートとして「歩く」「走る」「ジャンプ」「泳ぐ」があります。「蔦を昇る」「ポールを下りる」などの特殊な移動ステートもあります。

 また、大型化時には「しゃがむ」ことができて、「しゃがみながらジャンプする」ことも可能です。つまり、1つのステートマシンが同時に複数のステートを持つことになります。また、あるステートを遷移する際に、現在のステートだけでなく、過去に取っていたステート情報に依存する場合もあります。くわえて、他オブジェクトとのインタラクションにも依存する場合があります。

 このように、ゲームのオブジェクトは非常に多数のステートを持つうえ、遷移判定が相互に依存するため、ステートの遷移を判定するロジックはとても複雑かつ巨大な物になる傾向があります。

tsukasa言語のアプローチ:プッシュダウンオートマトン方式ステートマシンの採用

 tsukasa言語で生成されるオブジェクトは、すべて、プッシュダウンオートマトンという方式のステートマシンになっています。プッシュダウンオートマトンとは、内部にプログラムを格納するスタックを持っているステートマシンです。プログラムスタックの中にあらかじめ、そのステートマシンに実行させたいプログラムを格納しておくことで、メインループ処理の際に、ステートマシンはスタックからプログラムを取得して、プログラムに対応するステートを実行し、その後そのプログラムを破棄します。

 プログラムのスタックには、必要に応じて複数のプログラムを動的にスタックすることができます。また複数のステートにまたがる処理の場合は、ステートマシンが自律的にそのプログラムをスタックに再格納します。これによって、ステートの遷移を判定するロジックを巨大にすることなく、小さなサイズに保てるようになります。

(3)スクリプトの課題とアプローチ

課題:汎用的なゲームスクリプト言語の不在

 商業ゲーム開発において、制御構造を持つフローデータ、すなわちスクリプト言語とその処理系は、これまでスタンダードとなりうる実装、仕様が発展しませんでした。そのため、開発タイトル毎にプログラマがゼロから実装し、作業コストを費やすことが少なくありませんでした。

 最近ではLuaのようなオープンソース系スクリプト言語が採用される例もありますが、ゲームのスクリプト処理系では特別な処理が必要となる場合があります。たとえばノベルゲームでは、「画面に表示されている所でスクリプトの評価を休止する」という処理が必要になり、それを設計レベルで採用しているのは吉里吉里やNScripterなど、一部のノベルゲーム用スクリプト言語に限定されています。

tsukasa言語のアプローチ:メッセージ指向言語の採用

 tsukasa言語は、ゲーム開発用のネイティブ言語であり、同時にスクリプト言語しても機能するように設計されました。その際、言語アーキテクチャにはメッセージ指向を採用しました。メッセージ指向はSmalltalk言語などで採用されている、プログラムをオブジェクト間でやりとりされるメッセージと捉えるアプローチです。

 tsukasa言語では、記述されている全てのプログラムはコマンドの列として解釈され、コマンドの列はそれぞれが対象とするオブジェクトに対して動的に送信されます。送信されたコマンド列は、それらのオブジェクトのプログラムスタックに格納され、各オブジェクトが処理される際に実行されます。

 全てのオブジェクトは疑似マルチスレッド的な挙動をとりますが、このコマンド送信方式によって、オブジェクト間の通信がフラットに行え、また、使用済みのコマンドが破棄されることで自動的にフローが維持されます。

■3 まとめ

 既存のゲームエンジンが採用しているゲームアーキテクチャを確認し、そのアーキテクチャが内包している課題と、その課題に対してtsukasa言語がどのようなアプローチを採用しているかについて説明しました。

 第2部からは、tsukasa言語の実装系である「司エンジン」のサンプルコードを見ながら、実際にtsukasa言語でゲームを作る方法を説明していきます。

 司エンジンはスターターキットがインターネット上で配布されており、ダウンロード後すぐに起動できるようになっています。是非ご自身で司エンジンを起動してサンプルコードを確認し、tsukasa言語の持つポテンシャルを確認してください。

■おわりに

 個人的には重要なテーマだと思っているのですが、結果的には過去記事の再録という手抜きになってしまいました(同日にAC2記事というのは今考えると無理筋だった)。
 よければこちらもどうぞ。

オリジナルのメッセージ指向ゲーム開発言語「司エンジン」でジャンプアクションゲームを作ってみる
http://d.hatena.ne.jp/t_tutiya/20161201/1480604440


 明日はあおいたくさんの「はじめての司プログラミング」です。期待age。