VGT 音声管理システム技術詳細#
前文#
現在の作業において、ゲーム音声に関しては、設定とテストが大きな痛点であることがわかります。私が働いている環境では、音声がリソースとして生成されてから最終的にゲームに組み込まれるまでには、三者の調整が必要です:企画が要件を提供 → 音声デザインがリソースを提供 → 音声デザインが Wwise を設定 → プログラムがゲームに組み込む → 企画が効果を検証する。
このプロセスは比較的冗長であり、調整のたびに複数の部門の参加が必要で、効率が低いことがわかります。また、Wwise は機能が強力ですが、複雑な再生ロジックの設定は依然として煩雑であり、特にシーケンシャル再生や条件フィルタリングなどの機能が関わるときには特にそうです。
そのため、私たちは UE エンジンの音声パイプライン「VGT」を独自に開発しました。基盤は Wwise に基づいてデコード再生を行います(Wwise の音声処理能力は依然として強力です)が、上層ではリソース管理や再生ロジック制御を完全に再設計し、いくつかの実用的なサポートシステム(ブループリントサポート、コールバックシステム、シーケンスシステム、音画同期システムなど)を追加しました。このパイプラインは、上記の協力問題を解決するだけでなく、複雑な Wwise プロジェクト設定から解放され、リソース管理と読み込み効率においても多くの最適化を行いました。
コアシステムモジュール#
UI モジュール#
VGT は、ユーザーがリソースを迅速にインポートし、再生をテストするための UI インターフェースを提供します。企画の方々が Excel を使うことに慣れていることを考慮し、Excel データ駆動の設定方法を作成しました。インポートされた wav 音声ファイルは、Wwise のトランスコードツールを利用して wem 形式に変換されます。
インターフェースから見ると、全体の操作プロセスは比較的直感的で、企画は直接インポート、設定、テストができ、プログラムや音声デザイナーのサポートを待つ必要はありません。
リソース管理モジュール#
大規模なゲームの音声数は確かに問題であり、数万件の音声がある場合、すべてをメモリにロードすることは現実的ではありません。私たちは Wwise の SoundBank の考え方を参考にして、「Module」アーキテクチャを設計しました。
各 Module は独立した音声パッケージのようなもので、ゲームのニーズに応じて動的にロードおよびアンロードできます。たとえば、特定のレベルの音声を 1 つの Module にパッケージ化し、レベルに入るときにロードし、離れるときにアンロードします。Module のデータ構造は以下のように定義されています:
struct VoiceModuleData{
FString moduleName;
FString modulePath;
int32 maxPlayedEventInstanceNum;
int32 maxSavedEventInfoNum;
// このモジュールで再生可能な音声
TMap<FString, TArray<TSharedPtr<EventData>>> eventMap;
// このモジュール内の各音声に対応するコンテナタイプ(ランダムコンテナ、順序コンテナ...)
TMap<FString, int32> eventTypeMap;
}
ローカルでトランスコードされた wem 音声リソースには、各ファイルに一意の ID を割り当て、過剰な文字列操作によるパフォーマンスの損失を回避します。また、ファイルパスを迅速に特定するために、AVL ツリーに基づくパス管理システムを設計しました:
この設計の利点は、音声 ID に基づいて対応するファイルを検索する必要があるときに、迅速に対応するディレクトリパスを特定できることです。検索効率は基本的に O (log n) レベルです。
Event 情報の管理については、次のようなデータ構造を定義しました:
struct EventInfo{
int32 ResourceID; // リソースに割り当てられた一意のID
int32 Order; // 順次再生に関連するパラメータ
int32 Probability; // ランダム再生の重み
int32 AdditionalFieldsNum; // 追加のフィルタ条件の数
}
Event 情報は対応する Module ディレクトリに保存され、必要に応じてロードされます。
リソース再生モジュール#
メモリの負担を軽減するために、ストリーミング再生方式を採用しました。これにより、すべての音声ファイルを事前にメモリにロードする必要がなくなります。
底層の再生実装は依然として Wwise の External Source メカニズムに基づいています:
// 音声パスに基づいてexternalSourceを構築
TArray<AkOSChar> FullPath;
FullPath.Reserve(fullPath.Len() + 1);
FullPath.Append(TCHAR_TO_AK(*fullPath), fullPath.Len() + 1);
AkExternalSourceInfo externalSourceInfo(FullPath.GetData(), externalSrcCookie, AKCODECID_VORBIS);
TArray<AkExternalSourceInfo> externalSources;
externalSources.Add(externalSourceInfo);
//再生
playingID = AkAudioDevice->PostEventOnGameObjectID(eventID, akGameObjectID, AK_EndOfEvent | AK_EnableGetSourcePlayPosition, &FVoicePlayer::OnEventCallback, Data.Get(), externalSources);
しかし、重要なのは上層で、私たちは完全に独自の再生ロジック制御システムを実装しました。このシステムは基本的に Wwise の Random Container と Sequence Container の機能を再現していますが、より柔軟で拡張が容易です。
私たちは RandomContainer(ランダム再生コンテナ)、SequenceContainer(順次再生コンテナ)、SingleContainer(単回再生コンテナ)を実装しました。システムは Module に保存されている EventType に基づいてどの再生戦略を使用するかを判断します。
RandomContainer の例として、完全な重みランダムアルゴリズムを実装し、重複再生防止メカニズムも追加しました:
float TotalPercentage = 0.0f;
for (const TSharedPtr<RandomEventConfig>& Config : EventConfigs)
{
TotalPercentage += Config->Probability;
}
// すべての重みの合計が100でない場合は正規化処理を行います
if (TotalPercentage != 100.0f)
{
if (fabs(TotalPercentage) < FLT_EPSILON)
{
// すべてが0の場合は平均分配
for (const TSharedPtr<RandomEventConfig>& Config : EventConfigs)
{
Config->Probability = (1.0 / EventConfigs.Num()) * 100.0f;
}
}
else
{
// それ以外は比率に基づいて正規化
for (const TSharedPtr<RandomEventConfig>& Config : EventConfigs)
{
Config->Probability = (Config->Probability / TotalPercentage) * 100.0f;
}
}
}
// 再生可能な音声をフィルタリング(すでに再生されたものを除外)
float AvailableTotalPercentage = 0.0f;
TArray<TSharedPtr<RandomEventConfig>> AvailableConfigs;
for (auto i = 0; i < EventConfigs.Num(); ++i)
{
if (EventConfigs[i]->bShouldReplaced)
{
continue;
}
AvailableTotalPercentage += EventConfigs[i]->Probability;
AvailableConfigs.Add(EventConfigs[i]);
}
// すべての音声が再生された場合はリセット
if (AvailableTotalPercentage <= 0)
{
ForceRefreshPlaySet();
return PlayVoice(akGameObjectID, trackID, percent, callback, pParam);
}
// 重みに基づいてランダムに選択
float RandomNumber = FMath::RandRange(0.0f, AvailableTotalPercentage);
float AccumulatedPercentage = 0.0f;
for (const TSharedPtr<RandomEventConfig>& Config : AvailableConfigs)
{
AccumulatedPercentage += Config->Probability;
if (RandomNumber < AccumulatedPercentage)
{
if (Config->bShouldReplaced == true)
{
continue;
}
PlayVoice();
break;
}
}
SequenceContainer の実装も同様の考え方で、再生インデックスを維持し、順次音声を再生し、ループ再生やジャンプなどの機能をサポートします。
プログラムの呼び出しを便利にするために、統一されたコールバックシステムも設計しました。このシステムは Wwise のコールバックイベントを集中管理し、異なるコンテキストに応じて対応するコールバック関数をトリガーし、C++ とブループリントの両方をサポートします:
パフォーマンス最適化とスレッド安全性#
パフォーマンス最適化に関しては、いくつかの重要な最適化を行いました:
まず、遅延ロードです。すべての Module の情報を一度にメモリにロードするのではなく、必要なときにのみロードします。
次に、データ形式の最適化です。各 Module の Event 情報が多いため、JSON の逆シリアル化は確かにパフォーマンスのボトルネックです。そのため、パッケージ化時に JSON をバイナリ形式に変換し、実行時に仮想メモリに直接マッピングし、各 Event のデータ内のオフセット位置を解析し、必要なときに直接アクセスします:
GetEvent(eventName) -> LoadEventOffsetMap() -> LoadBinaryEventConfigMap():
TUniquePtr<FArchive> DataFileReader(IFileManager::Get().CreateFileReader(*EventConfigPath));
DataFileReader->Seek(EventDataOffsetMap[eventName]);
ResultConfig->LoadBinary(DataFileReader);
return ResultConfig;
これにより、メモリ使用量と読み込み時間を大幅に削減できます。
スレッド安全性については、モジュール化設計を採用し、各スレッドに独自の VoicePlayerInstance を持たせ、ロック競合を減らしました:
音画同期とシーケンスサポート#
基本的な再生機能に加えて、音画同期システムも実装しました。この機能は、音声とアニメーション、UI などの正確な同期問題を解決するために主に設計されており、特にカットシーンアニメーションで非常に役立ちます。
私たちは UE の Sequencer システムを統合し、自動的に音声の長さを計算することで、ミリ秒単位の同期精度を実現しました。システムは音声ファイルを自動的に分析して正確な長さを取得し、Ticker メカニズムを通じて毎フレームの同期チェックを実施します。
さらに、TrackID の概念もサポートしており、異なる音軌の再生をより良く管理および制御できます。これは複雑な対話システムにとって非常に有用です。
まとめ#
全体の VGT システムの核心的な考え方は、Wwise を基盤にして上層の再生ロジックとリソース管理を再設計し、音声システム全体をより柔軟かつ効率的にすることです。まだいくつかのモジュール(ブループリントサポートや多言語管理など)がここでは詳細に紹介されていませんが、コアのアーキテクチャと設計思想は基本的にこのようなものです。
このシステムの最大の価値は、企画が音声設定作業を独立して完了できるようにすることであり、同時にプログラムにより柔軟な再生制御能力を提供することです。もちろん、パフォーマンスの最適化と安定性も私たちが特に重視している点であり、ゲームにおける音声システムの安定性は非常に重要です。