如果沒預算請到各地專業的配音,請不要忘記Google小姐!
前言
相信讀者在專案過程中都遇過多語系的切換功能,多語系在遊戲中是很常見且必要的功能。但…這麽多語音檔整理下來肯定非常頭痛吧?尤其數量一多的時候Bank裏面包含的Event肯定又臭又長,讓人眼花繚亂。
檔案分類
我們可以在Fmod的Asset資料夾裏新增Languages資料夾(根據遊戲中需要用到的語言),把遊戲中所有用到的多語言音檔分別放在裡面,不同資料夾的檔案名稱需要一致相同,這樣未來在切換語系的時候才不會找不到索引。
把語言分門別類
把音檔放在資料夾內讓FMOD Studio讀取,一個資料夾代表一個語系。
提醒
可以把資料夾位置放在任何地方,不限於Asset底下(路徑可以在Studio內更改)。

新增Locale Code
每個語系都有屬於自己的Locale Code,像中文就是ZHCN,英文是ENUS(基本上就是4碼組合)。名字會影響到路徑設定,Bank Name等,建議照技術規範來命名,方便未來在接入上避免不必要的麻煩。
新建遊戲中會用到的語系代碼
打開Preference裏面的Asset標籤,在下方空白處右鍵新增我們要的語系。
提醒
必須命名每個語系在遊戲中的Locale Code,這個代號將會在Build的時候為每一個語系生成獨立的bank。

使用Audio Table
AudioTable,基本上所有的聲音都要透過這份表單來讀取。跟以前比較大的差別就是Table跟Bank下面存的資訊不同。Bank下面存放的是Event,Table裏面存放的是音效檔實際的路徑和Key Name,然而透過讀取Table表就可以知道現在播放的是哪些檔案,就可以減少把語音拉到Event裏面容易產生名稱混亂的狀況。
在Bank內新建Audio Table
找到BANK頁籤下面的對話bank,裏面新增一份Localized Table,透過Browser選取之前存放語音檔案的根目録。
提醒
每一個Bank裡面只能有一個Audio Table。

使用Programmer Event
我們在前面已經把素材(Asset)和Audio Table都建立好了,接下來就要在FMOD Studio裡面把事件建立出來。
新建3D Action
可以視情況來新增不同類型的Event,看遊戲內要使用的聲音是否要位置 / 參數 / 時間控制等功能來決定要新增的類型。

在Action事件裡面新增Programmer Instrument
此時我們要在容器裡面新增一個Programmer Instrument Event。

拖放AudioTable到Placeholder裡面
新增好事件之後,我們把剛才製作的Audio Table拖放到下方的Placeholder中,這樣就可以讓我們Asset正確出現在事件中。

切換不同語系看看
完成以上步驟之後,我們回到右下角可以在Locale下拉選單這裏切換我們預先設定好的語系,並觀察Programmer Instrument裡面的素材發生了什麽變化,到目前位置多語系的製作已經完成了8成了!

新建Programmer Sound腳本
FMOD中要播放語音有兩種方式,不同模式下的腳本讀取會長的不太一樣。
種類 | 讀取方式 |
---|---|
StreamingAsset | 從Unity Asset下的StreamingAsset資料夾裡讀取 |
FMOD Bank | 從FMOD Build出來的Bank裡面讀取 |
那在Unity裡面播放我們前面在STUDIO做好的Event要怎麽做呢?回到Unity裡面我們新建一個FMOD_ProgrammerSound的Script。以下的腳本是從API使用手冊摳過來的官方寫法,具體可以理解成:
- 設定callback
- 初始化需要播放的Event路徑
- 選擇Unity尋找Audio播放的位置(從SteamingAsset / Banks)
- Play();
public class FMOD_ProgrammerSound : MonoBehaviour
{
FMOD.Studio.EVENT_CALLBACK dialogueCallback;
public FMODUnity.EventReference eventName;
void Start()
{
dialogueCallback = new FMOD.Studio.EVENT_CALLBACK(DialogueEventCallback);
}
public void Play(string keyName)
{
GCHandle stringHandle = GCHandle.Alloc(keyName);
VoiceEvent.setUserData(GCHandle.ToIntPtr(stringHandle));
VoiceEvent.setCallback(dialogueCallback);
VoiceEvent.start();
VoiceEvent.release();
}
[AOT.MonoPInvokeCallback(typeof(FMOD.Studio.EVENT_CALLBACK))]
static FMOD.RESULT DialogueEventCallback(FMOD.Studio.EVENT_CALLBACK_TYPE type, IntPtr instancePtr, IntPtr parameterPtr)
{
FMOD.Studio.EventInstance instance = new FMOD.Studio.EventInstance(instancePtr);
IntPtr stringPtr;
instance.getUserData(out stringPtr);
GCHandle stringHandle = GCHandle.FromIntPtr(stringPtr);
String keyName = stringHandle.Target as string;
switch(type)
{
case FMOD.Studio.EVENT_CALLBACK_TYPE.CREATE_PROGRAMMER_SOUND:
{
FMOD.MODE soundMode = FMOD.MODE._3D;
var parameter = (FMOD.Studio.PROGRAMMER_SOUND_PROPERTIES)Marshal.PtrToStructure(parameterPtr,typeof(FMOD.Studio.PROGRAMMER_SOUND_PROPERTIES));
if(keyName.Contains(".")) //播放Unity StreamingAssets下面的音效
{
// FMOD.Sound fmodSound;
// var soundResult = FMODUnity.RuntimeManager.CoreSystem.createSound(Application.streamingAssetsPath+"/"+keyName,soundMode, out fmodSound);
// if(soundResult==RESULT.OK)
// {
// fmodSound.setMode(MODE._3D_LINEARROLLOFF);
// parameter.sound = fmodSound.handle;
// parameter.subsoundIndex = -1;
// Marshal.StructureToPtr(parameter,parameterPtr,false);
// }
}
else //播放bank裏面的事件
{
FMOD.Studio.SOUND_INFO soundInfo;
var keyResult = FMODUnity.RuntimeManager.StudioSystem.getSoundInfo(keyName, out soundInfo);
if(keyResult!=RESULT.OK)
{
break;
}
FMOD.Sound fmodSound;
var soundResult = FMODUnity.RuntimeManager.CoreSystem.createSound(soundInfo.name_or_data,soundMode | soundInfo.mode, ref soundInfo.exinfo, out fmodSound);
if(soundResult==RESULT.OK)
{
parameter.sound = fmodSound.handle;
parameter.subsoundIndex = soundInfo.subsoundindex;
Marshal.StructureToPtr(parameter,parameterPtr,false);
}
}
break;
}
case FMOD.Studio.EVENT_CALLBACK_TYPE.DESTROY_PROGRAMMER_SOUND:
{
var parameter = (FMOD.Studio.PROGRAMMER_SOUND_PROPERTIES)Marshal.PtrToStructure(parameterPtr,typeof(FMOD.Studio.PROGRAMMER_SOUND_PROPERTIES));
var sound = new FMOD.Sound(parameter.sound);
sound.release();
break;
}
case FMOD.Studio.EVENT_CALLBACK_TYPE.DESTROYED:
{
stringHandle.Free();
break;
}
}
return FMOD.RESULT.OK;
}
}
(註解的部分是在StreamingAsset下播放音效檔案,此種方式播放的音效在Keyname需要添加”副檔名”,EX:Play(“Hello.wav”);)。
public void Play(string keyName)
{
keyName = audioNum[audiotableNum];
var VoiceEvent = FMODUnity.RuntimeManager.CreateInstance(eventName); //Assign FMOD.sound to an container
var audioOBJ = GetComponent<Select_Object>().Selected.gameObject; //Get Current Audio From Script
GCHandle stringHandle = GCHandle.Alloc(keyName);
VoiceEvent.setUserData(GCHandle.ToIntPtr(stringHandle));
VoiceEvent.setCallback(dialogueCallback);
VoiceEvent.set3DAttributes(audioOBJ.To3DAttributes()); //Set Audio position
VoiceEvent.start();
VoiceEvent.release();
記得分配播放的音效位置到想要的Object上,如果沒有set3DAttributes會導致聲音播放的位置不正確。

Nik Cheng
在VARLVIE擔任音樂製作/音效設計師,喜歡享受在單機遊戲內尋找沉浸探索的樂趣。近年開始籌備音頻引擎相關的教學,希望和有熱情的開發者們一同在學習的路上成長。