メッセージベースによるゲーム駆動


みなさま、こんにちは!

Cygames Research 所属、エンジニアの和泉澤と申します。
ゲーム業界歴20数年。メガドライブの時代よりゲームプログラミング一筋です。
時折、幾つかの知見をご紹介させて頂けましたらと思います。


ゲーム制作における内部実装には、古今東西、様々な方法論が存在しています。
アクション、RPG、カードゲームやアドベンチャーゲーム等、そのジャンルにより採択される方法は多種多様でありますが、制作するゲームの持つ特徴を良く理解し、そのゲームに適した設計を行うことは、ある種、ゲームプログラミングにおける醍醐味とも言えましょう。
アクションならばこの実装方法、RPGならばこれ、等といったような一意の方法論は勿論在りません。しかしながら、ゲーム業界黎明期より無数に生み出されたゲーム制作の歴史の上に、効率的な方法論というものは、ある程度通例として積み上げられています。

極シンプルな例としまして、アクションゲームならば、コリジョンやAI処理はマルチスレッド化やファイバー化により並列化することは通例です。
こうした並列化を実現するための実装方法としましては、ダブルバッファ化された前フレームの結果を活用したり、latchやbarrier等(*1)による同期ポイントや、はたまたstd::atomicやstd::condition_variable等といった標準機能を利用するという選択等が挙げられるでしょう。

RPGならば、大量のパラメーターリソース群を必要としますが、代表的な表計算ソフトを用いた運用でも構いません。
それだけでは心許なければ、専用のパラメーターステーションとして機能するサーバーを立て、世代管理を前提としたDBを構築し、チームでアクセス可能なサイトを用意し運用するという形も良いでしょう。
これもまた、エンジニアの興す「実装」です。

また、ソーシャルゲーム制作において、ファイルを一つロードする機構をとってみても、それがローカルに存在すればローカルから、ローカルに無くまたそれが古い場合にはサーバーからダウンロードするといった実装は自然です。
その場合、それを利用するユーザーモジュールから見れば、単なるロード待ちにしか見えないような設計とする事、これもまた、本稿にて扱う大義の「実装」です。

これらのように、何かを実現するために、何かを設計、実装し運用する事がエンジニアの通常業務であります。
この時、そうして出来上がる成果物は、汎用性や再利用性に富んでいることが望ましいと言えましょう。

本稿では、大凡どのようなタイプのゲームにおいても運用可能な、基本的な実装テクニックの一つをご紹介します。


ゲームの基本?

ゲームに実装する様々の処理、これらはどのように呼び出されているのでしょう?
業界初期の頃には、以下のようなコーディングも珍しものではありませんでした。

メインループ()
{
    プレイヤー処理();
    敵キャラ達の処理();
    コリジョン関連();
    ゲーム進行判定();
    HUD更新();
}

現代ゲームプログラミングにおいて、行うべき処理は大きく増えきています。
しかしながら時を隔て現代に至るまで、その工夫はつまるところ、こうした処理群を如何に効率的に回していくのかという部分に集約されるかと思います。

その実装として、タスクやジョブ化、マルチスレッド化やファイバー化など様々な選択肢は勿論ありますが、もう少しだけ上位のレイヤー実装として、メッセージベースのゲーム駆動というものを挙げてみたいと思います。


さて、その前に。

メッセージ化されていない、所謂、シンプルな処理形式とはどのようなものでしょう?
幾つか、例を挙げて振り返ってみましょう。

設定はよくわかりませんが、何だか兎に角「敵を倒した」としましょう。
この時、スコアが10点加算されるゲームだとします。
ゲームの進行状況等を司るインスタンス「GameInformation」があるとして、このクラスにはAddScoreメソッドが在るかもしれません。この時、「敵を倒した」事実に関連する何かの処理からは、以下のようなコードが実行されるでしょう。

GameInformation.AddScore( 10 );

次のような例も、良い題材です。
プレイヤーの操作に関連する何らかの処理において、プレイヤーが攻撃アクションを行うことが決定されたとします。(パッド入力処理かもしれませんし、AIかもしれません)
プレイヤーを司るインスタンスに対し、以下のようなコードが実行されるでしょう。

player.ChangeAction( Attack );

重要な例をもう一つ。
フィールドに浮かぶ、「敵全滅アイテム」をプレイヤーが取得しました。存在する敵キャラを全滅させる必要がありそうです。
以下のようなコードになるでしょうか。

foreach( enemy: アクティブな敵キャラ全て )
{
    enemy.Kill();
}

ゲームプログラミング初級教科書のような内容になってしまっておりますが、これらは本稿における良い題材達です。
これら極シンプルな実装群には一つ大きな問題点が内在しており、この回避が、メッセージ式を採択する理由の一つでもあります。
ここで挙げる「問題点」とは、いったいなんでしょう?

それは、「依存度の高さ」です。


メッセージベースとは?

正確には、message-driven processing と呼ばれる、サーバー・クライアントモデル指向のプログラミング手法です。

ある処理をコールする際に、例えばあるクラスのインスタンスのメソッドを生でコールする形とは根本的に異なり、そのコールを抽象化した上でキューイングしておき、それらを司るシステムが統一的にコールを代替するような仕組みです。

{// スコア加算
    using Message;
    Manager::Send(
        Ordinary,    // 普通配達 (次のフレームで配信される)
        Message( "AddScore", 10 )
    );
}

{// プレイヤーのアクション変更
    using Message;
    Manager::Send(
        Dispatch,    // 即配信
        Message( "ChangeAction", "Attack" ),
        SearchReceiver( "Player" )    // 宛先指定
    );
}

{// 敵全滅
    using Message;
    Manager::Send(
        Ordinary,
        Message( "KillEnemies" ),
        Broadband    // ブロードバンド配信
    );
}

これらは例ですが、基本的に、自身以外のモジュールへ対するゲームロジカルなコールは、メッセージを配信する事で行っていきます。
この手法によるメリットを挙げていきましょう。

モジュール間依存度の低さ

処理をコールする側は、コール先のクラス構成やその内容に依存していません。コール先クラスが定義されたヘッダを参照する必要もありません。
定めるべきは、メッセージ内容に関する書式のみです。
投げたメッセージを誰が受け取るか、そのインスタンスが存在しているか、その処理がどういう順番で行われるか、そうした一切を考慮する必要がありません。
投げたメッセージが結果を要求するタイプのものならば、それもメッセージで受け取ります。

昨今、ますます肥大化の一途を辿るゲームコードにおいて、そこへ内在する大量のモジュール達が、それぞれ複雑に依存し合っているという状況は決して健全ではありません。
その解決を図る手段が、メッセージベースだけであるわけではありませんが、一つの解決策として良く機能します。

受け取り意思表明

メッセージは、特定のインスタンスを指定して投げることも可能ですが、多くの場面では宛先を指定しません。
そのメッセージを受け取りたいモジュールは、「こういうメッセージが来たら自分に配信してくれ」と宣言します。「手を挙げて待っている」ような姿をイメージするとわかりやすいでしょう。
この事も、送り手と受け手の依存度を下げることに貢献しています。

ブロードバンド配信

受け取り意思表明の仕組みを利用し、広域配信が可能です。
上記のように、「敵を倒す」というメッセージは、宛先に「Broadband」が指定されています。
そのメッセージが必要なモジュール達は、それぞれに手を挙げて待っているだけで構いません。どんなモジュールが自分をコールするかといった、呼び出し元の事情を気にする必要はありません。

スレッド間配信

スレッドを超えたメッセージは、メッセージマネージャーにより適切にディスパッチされ、適時配信されます。
無駄の無い理想的なロックレスマルチスレッディングを、容易に実現できます。

ネットワーク越え配信

最も大きなメリットです。
一般的に、リアルタイムマルチプレイアクションゲーム等を制作する上において、各種処理をメッセージ化により扱う事は通例となっているかと思います。
メッセージを受け取るモジュールは、それが同一端末内から発信されたものか、はたまたネットワークの向こうから送られてきているものかを、意識する必要はありません。

上記の、「プレイヤーに攻撃アクションを起こさせるメッセージ」は、一人プレイならばパッド情報等に基づいて同一端末内より配信されるでしょう。
いわゆる「狩りゲー」等に代表されるような、ピアツーピアマルチプレイゲームならば、そのメッセージは、エリアサーバーとなっている誰かの端末から配達されるでしょう。
受け取ったモジュールは(ここではプレイヤー処理は)、マルチプレイ中であるかどうかを考慮する必要はありません。ただ、そのメッセージを処理すれば良いのです。


モジュール間依存度を下げ、チーム規模の大きい開発場面でのイテレーション速度を維持し、ネットワーク対応やロックレスマルチスレッディングにも寄与することの出来る、シンプルですが強力な手法です。

みなさんのプロジェクトでも、是非採用を検討されてみては如何でしょうか?

参考資料


Cygamesではゲーム開発を支える様々な技術研究にも取り組んでいます。

技術研究に興味があるかたは、是非Cygamesで一緒に働きませんか?
採用ページはこちらです。