前文#
在當前工作中,我們可以發現對於遊戲語音來說,配置與測試是一個很大的痛點。在我工作的環境裡,一個語音從產出資源到最後配到遊戲裡需要三方人員的協調:策劃提供需求 → 音頻設計提供資源 → 音頻設計配置 Wwise → 程序配置到遊戲中 → 策劃檢驗效果。
可以發現這個流程是比較冗長的。
因此我們自研了一套 UE 引擎的語音管線 “VGT”,其底層基於 Wwise 解碼播放,上層則自定義了資源管理,播放邏輯與一些支持性系統(包括藍圖支持,回調系統,Sequence 系統,音畫同步系統等)。這套管線除了解決了上述痛點之外,也避免了繁瑣的 Wwise 配置流程,實現了高效的資源管理加載等特點。
核心系統模塊#
UI 模塊#
VGT 提供了 UI 界面來幫助用戶快速的導入資源與測試播放。同時通過 Excel 數據驅動來配置。導入的 wav 語音文件會借用 Wwise 的轉碼工具轉換為 wem 格式
資源管理模塊#
由於在大型遊戲中,語音數量可能會到一個很恐怖的級別,無論是資源儲存還是加載都會給內存帶來很大的壓力。
因此 VGT 中採用了 “Module” 的架構,類似於 Wwise 中的 SoundBank,每個 module 完全獨立,可以在遊戲中實現按需加載,卸載。同時運行時數據的加載,聲音實體的管理,事件播放的狀態也以 module 為單位。其中 module 的數據結構簡略版定義如下:
struct VoiceModuleData{
FString moduleName;
FString modulePath;
int32 maxPlayedEventInstanceNum;
int32 maxSavedEventInfoNum;
// 此模塊中可播放的語音
TMap<FString, TArray<TSharedPtr<EventData>>> eventMap;
// 此模塊中每條語音對應的容器類型(隨機容器,順序容器...)
TMap<FString, int32> eventTypeMap;
}
對於本地轉碼後的 wem 語音資源來說,分配 ID 作為唯一標識符,減少過多字符串帶來的性能消耗。同時也更方便根據 ID 區間來管理文件路徑:VGT 採用路徑與 ID 區間映射的方式來構建 AVL 樹,每個路徑下維護多個 ID 區間,這樣在 Streaming 播放時根據 ID 就可以快速查找到構建完整文件路徑。
上圖展示了一個簡單的目錄結構示例,每個目錄節點下維護著若干 ID 區間。當需要根據語音 ID 查找對應文件時,可以快速定位到對應的目錄路徑,實現 O (log n) 的查找效率。
對於 Event 信息管理,首先 VGT 系統中對於 Event 的數據結構定義大概如下:
struct EventInfo{
int32 ResourceID; // 為資源分配的唯一ID
int32 Order; // 是否為順序播放
int32 Probability; // 是否為隨機播放
int32 AdditionalFieldsNum; // 額外篩選條件
}
Event 信息表是儲存在對應的 Module 目錄下,根據需要進行加載或卸載。
資源播放模塊#
為了減輕內存壓力,VGT 中採用了 Streaming 播放的方式,減輕語音加載到內存中的壓力。
下面是簡略版的底層播放邏輯:
// 根據音頻路徑構建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);
在上一層我們構建了隨機播放和順序播放兩種邏輯,根據 Module 模塊裡儲存的 EventType 表來判斷調用哪種方法。以下是精簡版示例,主要展示隨機播放邏輯
隨機播放:
float TotalPercentage = 0.0f;
for (const TSharedPtr<RandomEventConfig>& Config : EventConfigs)
{
TotalPercentage += Config->Probability;
}
// If the sum of all probabilities is not 100, normalize them
if (TotalPercentage != 100.0f)
{
if (fabs(TotalPercentage) < FLT_EPSILON)
{
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;
}
}
在構建完隨機,順序播放邏輯之後,我們還需要設計一套回調系統來方便程序的調用。VGT 的回調系統主要是設計了一個集中處理 Wwise 回調事件的方法,根據上下文來觸發我們設定好的回調,在 C++ 和藍圖中都是這樣的設定。
性能優化與線程安全#
在 VGT 中為了實現更高效的性能,首先我們並不會把所有的 Module 信息表一次性加載到內存中,而是在有需要的時候再進行加載。其次由於每個 Module 的 Event 信息十分多,將其從 json 格式反序列化的時候對於性能也是一個痛點,因此我們在打包時會將 json 格式轉化為二進制格式,並且在加載模塊時並不會去解析反序列化,而是直接映射到虛擬內存中,僅解析每個 Event 在數據中的偏移位置來實現直接訪問的效果,減少了性能的消耗,以下為 sudo code:
GetEvent(eventName) -> LoadEventOffsetMap() -> LoadBinaryEventConfigMap():
TUniquePtr<FArchive> DataFileReader(IFileManager::Get().CreateFileReader(*EventConfigPath));
DataFileReader->Seek(EventDataOffsetMap[eventName]);
ResultConfig->LoadBinary(DataFileReader);
return ResultConfig;
關於線程安全,結構圖如下:
總結#
以上是對整個 VGT 項目的核心模塊介紹,實際上還有很多其他模塊沒有列出,比如音畫同步和 Sequence 模塊。如果後續有時間作者會進一步完善介紹。