Triangle Factory は急成長中のベルギーのゲーム会社で、Unity を使用して『Hyper Dash』や最新ゲーム『Breachers』のような高品質のマルチプレイヤー VR タイトルを制作しています。Triangle Factory は、プレイヤーに没入感のある体験を提供するため、Cinemachine、Unity Profiler、Game Server Hosting、Matchmaker、Voice Chat(Vivox)、Friends などのツールを活用しています。
このブログでは、リードステージデザイナー兼リードテックアーティストの Jel Sadones 氏とリードデベロッパーの Pieter Vantorre 氏が、Blender から Unity へのパイプラインと、自社開発の VR タクティカル FPS タイトル『Breachers』に命を吹き込んだ方法を紹介します。
Unity は 10 年以上にわたって、当社が頼りにしているエンジンであり開発環境です。当社は長い間、多くの環境モデリングとデザインワークフローを使用してきました。今まで試してきた事として、ProBuilder のようなエンジン内モデリングツール(今でもラピッドプロトタイピングの際には活躍しています)の使用や、他のモデリングパッケージで作成したプレハブからのシーンの組み立てなどがあります。しかし、当社が現在取り組んでいるプロジェクトでは、Blender でステージのモデリングと編成を行い、Unity の AssetPostprocessor を用いて Unity プロジェクトに統合するというワークフローに帰着しました。
この記事では、当社が現在のワークフローに行き着いた経緯と、このワークフローが当社のゲームに必要な迅速なデザインイテレーションをどのようにサポートしているかを紹介します。
2021 年、当社は初の大型 VR タイトルで、5 対 5 のアリーナシューティングゲーム『Hyper Dash』をリリースしました。2019 年に本作の開発を開始した時点では、恐らく多くの開発者に馴染みがあるであろう Blender から Unity への基本的なワークフローに従っていました。つまり、単純に Blender でジオメトリをモデリングし、アセットを FBX ファイルとしてエクスポートし、Unity に手動で統合していたのです。手動での統合には以下のようにいくつかのステップが必要でした。
上記のプロセスは小規模なプロジェクトでは上手く機能しますが、プロジェクトが拡大し進化すると、たちまち面倒なものになってしまいます。次作の開発を計画し始めたとき、ワークフローの大幅な改善が必要になることは分かっていました。
『Breachers』は、複雑なステージレイアウト、より繊細なゲームプレイメカニクス、より技術的なシステム、そして最新世代のスタンドアロン VR ハードウェアを対象にした、より高度で洗練されたグラフィックスを備えた対戦型シューティングゲームです。複雑さという点では、『Hyper Dash』よりも数段上であり、すぐにワークフローに影響が出ることを感じました。
プロトタイピングの段階では、窓のバリケードなどの動的オブジェクトについては、まだプレハブに大きく依存していました。これは、ゲームの初期段階において、チーム間で相手の姿が見えないよう室内外の視線を遮るために窓枠の内側に設置するオブジェクトです。
プロトタイプの検証中、常に窓の位置を動かしてゲームプレイを向上させていました。そのためには、Blender でジオメトリを変更して Unity に再エクスポートし、変更に合わせてバリケードオブジェクトを手動で動かさなければなりませんでした。Unity のシーンビュー内を動き回って、手動で確認や修正を行う作業に何時間も費やしました。多くの時間をかけたにも関わらず、プレイテストでのゲームプレイ中に初めて見落としが判明したことは何回もありました。
このワークフローでは、社内のプレイテストとオープンアルファ(無料マップを 1 つ公開してコミュニティからのフィードバックを収集するという計画)の一環としてのプレイテストの両方を実施しながら、迅速なマップデザインのイテレーションを行うことが不可能なのは明らかでした。当社は、コミュニティからのフィードバックを心待ちにする一方、それらをすべて手動でマップに適用していくという作業には全く気が進みませんでした。
プレハブベースのデザインワークフローのもう 1 つの潜在的な欠点は、パフォーマンスです。当社のゲームは、主にモバイル型のスタンドアロン VR ヘッドセットを対象にしています。できる限りビジュアルを目立たせたいと考えていたため、ワークフローから最大限のパフォーマンスを確保する必要がありました。
プレハブを元にステージを組み立てる方法は、モデリングプログラムで水を通さないメッシュを作成する方法よりも効率が悪い場合があります。2 つのモジュラーの壁パーツをはめ合わせた際に、常に結合されていないジオメトリのループができてしまいます。また、プレハブを使用する場合、(オブジェクトの下側や壁側に面しているため)目には見えないものの、貴重なライトマップスペースを占めるジオメトリをシーンに多く配置してしまいがちです。このような小さな非効率でも、ステージ全体で積み重なると、パフォーマンスの無駄遣いやビジュアルの低下に繋がる可能性があります。
プレハブの問題として最後に触れておきたいのが、オブジェクトの名前変更といった一見何の問題もなさそうな変更を Blender のソースモデルに加えることで、コードが動かなくなってしまうことがあるということです。ゲームやステージの開発が進むにつれ、アセットを再編成したり、より的確な、または一貫性のある名前を使用したくなることがあると思います。しかし、Blender でオブジェクトの名前を変更して再エクスポートすると、Unity でオブジェクトに対して行なったオーバーライドや追加が、容易に(そして警告表示なく)機能しなくなることがあり、リグレッションにつながる可能性があります。
以下の単純化した例では、換気口の格子のプレハブがあり、そこから煙を出そうとしています。当社のアーティストは、メッシュを Unity にインポートした後、煙のパーティクルシステムを子オブジェクトとして追加し、メタルオブジェクトであることをマークするためにプレハブに Surface Type コンポーネントを追加しました。
ここでは、Blender でメッシュの名前を変更するとどうなるかを示しています。
変更後の名前でメッシュを再インポートすると、Unity は以前のメッシュを名前によって特定できなくなるため、モデルとなるプレハブからオブジェクトを削除します。削除されたオブジェクトの子オブジェクトはプレハブのルートに移動し、既存のスクリプトは削除されます。ここでもまた、回避したい手動での修正作業が発生してしまいます。
2022 年初頭に『Breachers』のプロトタイピング段階が終了し、本制作に移行する準備が整った際、当社のアートチームと開発チームは話し合いの場を設け、これらの問題を改善するためにできることを調査し始めました。当社は、『Breachers』の開発に求められる、迅速かつ柔軟なイテレーションをサポートする、理想的なアセットパイプラインの実現に向けた明確な目標を定義しました。
前述したように、当社の主な目標は、Blender でゲームの正確なビジュアライゼーションを可能にすることでした。これには、Unity での最終的な出力結果だけでなく、ゲームプレイのメカニクス設定を適切に反映することも含まれます。『Breachers』のゲームプレイは、ステージのレイアウトだけでなく、動的オブジェクト(突破可能な壁など)や目に見えない要素(サウンドボリュームやコライダーなど)にも依存します。当社は、デザイン段階でこれらの情報をすべて可視化し、Unity に正確に引き継ぎたいと考えました。
カスタムプロパティは当社のワークフローには欠かせず、これらは Blender でオブジェクトに割り当てられます。その後、FBX フォーマットで Unity に引き継がれ、アセットが Unity にインポートされたときにそれらを読み込んでカスタムロジックを実行することができます。
これにより、大きな柔軟性と安定性を確保できます。これらのプロパティは、パイプライン全体を通してオブジェクトとの紐付けが維持されるため、コードが動かなくなったり、整合性を損なったりする心配なく、ステージ内のオブジェクトの再編成や名前の変更を好きなように行うことができます。
Unity には AssetPostprocessor という強力なクラスがあり、インポート時にアセットの修正を行ってくれます。当社は、カスタムプロパティを解析して処理するために、インポート時にこのクラスを使用しています。
当社は、PrefabLink というカスタムプロパティを作成しました。これは、Blender からインポートされたオブジェクトを、インポートされたモデルのトランスフォームを維持したまま、Unity プロジェクト内に既に存在するプレハブに置き換えるよう Unity に伝える機能を持ちます。これにより、Unity にインポートされた後のプレハブの利点を残しつつ、Blender にこれらの動的オブジェクトを配置することができます。先程の Blender シーン内の窓のバリケードはその良い例です。
サーフェスの定義は『Breachers』では非常に重要です。金属の階段を登り降りする音は、コンクリートの床を歩く音とは違います。弾丸が木材を貫通するのと、鋼鉄を貫通するのとでは全く異なります。そして、各サーフェスタイプには固有の衝撃エフェクトがあります。Unity で各プロップを調べ、正しいサーフェスタイプをタグ付けする作業は非常に時間がかかるため、Blender のデザイン段階でも、ジオメトリコライダーにカスタムプロパティを設定することで、この問題に取り組んでいます。
最適化のもう 1 つの重要な設定は、Unity の静的フラグです。正しく設定することで、可視性カリング、ライトベイキング、バッチ処理などに大きな影響を与えます。Blender でカスタムプロパティを使用すると、再利用可能なプロップを含む、ステージのすべてのパーツへの設定が可能になり、その情報を Unity ですべてのステージに引き継ぐことができます。
最後に、コライダーの設定方法を共有します。Unity には、モデルアセット名の末尾に _LOD0 や _LOD1 などを追加すると、モデルの詳細レベルのバリアントを自動的に検出する、シンプルかつ効果的なシステムがあります。当社はこの仕組みにヒントを得て、コライダーにも同様のシステムを実装しました。ジオメトリの名前に _BoxCollider や _NoCollision を含めるだけで、Blender のメッシュを Unity のコライダーに置き換えられるというものです。
具体例として、カスタムプロパティを読み込み、インポートされた各オブジェクトに適切な静的フラグを割り当てる LevelSetupPostprocessor のスニペットを掲載します。
public class LevelSetupPostprocessor : AssetPostprocessor
{
// カスタムプロパティを使用している各オブジェクトの Dictionary
private readonly Dictionary<string, (string[], object[])> _userPropertyMap = new ();
// サポートされているすべてのカスタムプロパティ
private static readonly string[] SupportedPropNames = new []
{
"Surface",
"Layer",
"PrefabLink",
"Collision",
"StaticFlags",
"LightmapScale",
"LightMeshPreset"
};
// AssetPostprocessor の Unity イベント
// モデルの各オブジェクトに対して呼び出される
private void OnPostprocessGameObjectWithUserProperties(GameObject go, string[] propNames, object[] values)
{
// 対象のカスタムプロパティが含まれているか確認し、Dictionary に追加する
if (SupportedPropNames.Select(x => x.ToLowerInvariant()).Intersect(propNames.Select(x => x.ToLowerInvariant())).Any())
{
_userPropertyMap.Add(go.name, (propNames, values));
}
}
// AssetPostprocessor の Unity イベント
private void OnPostprocessModel(GameObject model)
{
// 見つかった各カスタムプロパティに対して
// Model Prefab Variant 内の対応する Gameobject を取得し
// 適切なロジックを適用する
for(int i = _userPropertyMap.Count -1; i >= 0; i--)
{
var kvp = _userPropertyMap.ElementAt(i);
GameObject go = FindGameObjectInHierarchy(model, kvp.Key); // モデルの子要素を名前で検索
string[] propNames = kvp.Value.Item1;
object[] values = kvp.Value.Item2;
for(int j = 0; j < propNames.Length; j++)
{
object value = values[j];
switch (propNames[j])
{
case "staticflags":
HandleStaticFlags(go, value);
break;
// ...
}
}
}
}
// Blender のカスタムプロパティに基づいてオブジェクトに StaticFlags を適用
private void HandleStaticFlags(GameObject go, object value)
{
string[] staticFlags = value.ToString().Split(',');
StaticEditorFlags activeFlags = 0;
for(int i = 0; i < staticFlags.Length; ++i)
{
string flag = staticFlags[i].ToLower().Trim();
switch (flag)
{
case "batching static":
activeFlags |= StaticEditorFlags.BatchingStatic;
break;
// ...
default:
LogWarning($"Unknown static flag {flag} detected when importing {go.name}", go);
break;
}
}
GameObjectUtility.SetStaticEditorFlags(go, activeFlags);
}
}
プロセス全体をスムーズにするには、Blender 側でもいくつかの調整を行う必要がありました。
カスタムプロパティは Blender の UI では少し見えにくく、アーティストが毎回カスタムプロパティを手入力する必要があり、ユーザーエクスペリエンスとしてはあまり良くありません。また、手動でのテキスト入力に頼ると、エラーが発生しやすくなり、そもそも Blender で設定を行う利点の多くが失われてしまいます。プレハブベースのワークフローから Blender に移行することでまた、オブジェクトを閲覧して選択できるオブジェクトライブラリなど、プレハブが持つ利点の一部を利用できなくなりました。幸運なことに、Blender は Unity と同様、非常に柔軟で、簡単に拡張可能です。
プレハブの編成の問題は、Blender 3.2 のアセットライブラリで解決しました。このシステムは Unity のプレハブシステムに少し似ています。別のファイルでアセットを作成し、それを Blender シーンにインポートでき、アセットファイルへの変更は自動的に Blender シーンに反映されます。さらに、カスタムプロパティやコライダーが、Blender のこのアセットの各インスタンスに正しく適用されるようにします。
Blender については、より分かりやすいユーザーインターフェースからカスタムプロパティを設定できるようにする社内用のアドオンを作成しました。このアドオンは、各プロパティを手動入力する代わりに、関連する Blender オブジェクトを選択してボタンを押すことで、カスタムプロパティの設定を簡略化します。
The Bundle Exporter アドオンは、すべての FBX ファイルをワンクリックでエクスポートするために使用しているオープンソースのアドオンです。当社は、これをカスタムプロパティでも機能するように修正を加え、特定のニーズに応じたエクスポートの迅速化のため、UI を変更しました。
『Breachers』のステージデザインのワークフローをセットアップするのは、初期段階での大きな時間投資が必要でしたが、このプロジェクトにとって正しい選択だったと考えています。また、その作業は楽しくもありました。
初期の設計からアルファテスト、そして最終リリース前の数か月間に至るまで、今回のゲーム制作において、ステージのイテレーションは迅速かつ負担の軽いものでした。当社は、デザイナーやアーティストの不要な仕事や多忙なスケジュールを無くし、さらに以前なら開発者を必要としていた業務を彼らに任せることができました。
当社は、スムーズな統合を可能にする、Unity と Blender の能力に感銘を受けました。この統合は、『Breachers』を満足の行く、世界に誇れるゲームにするために不可欠だったと強く信じています。
ここまでお読みいただきありがとうございます。ゲームをお楽しみください!
Is this article helpful for you?
Thank you for your feedback!