FMOD – 淺談多語系的應用 Dialogue and Localization (Multiple Languages)

如果沒預算請到各地專業的配音,請不要忘記Google小姐!

前言

相信讀者在專案過程中都遇過多語系的切換功能,多語系在遊戲中是很常見且必要的功能。但…這麽多語音檔整理下來肯定非常頭痛吧?尤其數量一多的時候Bank裏面包含的Event肯定又臭又長,讓人眼花繚亂。

檔案分類

我們可以在Fmod的Asset資料夾裏新增Languages資料夾(根據遊戲中需要用到的語言),把遊戲中所有用到的多語言音檔分別放在裡面,不同資料夾的檔案名稱需要一致相同,這樣未來在切換語系的時候才不會找不到索引。

把語言分門別類

把音檔放在資料夾內讓FMOD Studio讀取,一個資料夾代表一個語系。

提醒

可以把資料夾位置放在任何地方,不限於Asset底下(路徑可以在Studio內更改)。

blank

新增Locale Code

每個語系都有屬於自己的Locale Code,像中文就是ZHCN,英文是ENUS(基本上就是4碼組合)。名字會影響到路徑設定,Bank Name等,建議照技術規範來命名,方便未來在接入上避免不必要的麻煩。

新建遊戲中會用到的語系代碼

打開Preference裏面的Asset標籤,在下方空白處右鍵新增我們要的語系。

提醒

必須命名每個語系在遊戲中的Locale Code,這個代號將會在Build的時候為每一個語系生成獨立的bank。

blank

使用Audio Table

AudioTable,基本上所有的聲音都要透過這份表單來讀取。跟以前比較大的差別就是Table跟Bank下面存的資訊不同。Bank下面存放的是Event,Table裏面存放的是音效檔實際的路徑和Key Name,然而透過讀取Table表就可以知道現在播放的是哪些檔案,就可以減少把語音拉到Event裏面容易產生名稱混亂的狀況。

在Bank內新建Audio Table

找到BANK頁籤下面的對話bank,裏面新增一份Localized Table,透過Browser選取之前存放語音檔案的根目録。

提醒

每一個Bank裡面只能有一個Audio Table。

blank

使用Programmer Event

我們在前面已經把素材(Asset)和Audio Table都建立好了,接下來就要在FMOD Studio裡面把事件建立出來。

新建3D Action

可以視情況來新增不同類型的Event,看遊戲內要使用的聲音是否要位置 / 參數 / 時間控制等功能來決定要新增的類型。

blank

在Action事件裡面新增Programmer Instrument

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

blank

拖放AudioTable到Placeholder裡面

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

blank

切換不同語系看看

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

blank

新建Programmer Sound腳本

FMOD中要播放語音有兩種方式,不同模式下的腳本讀取會長的不太一樣。

種類讀取方式
StreamingAsset從Unity Asset下的StreamingAsset資料夾裡讀取
FMOD Bank從FMOD Build出來的Bank裡面讀取

那在Unity裡面播放我們前面在STUDIO做好的Event要怎麽做呢?回到Unity裡面我們新建一個FMOD_ProgrammerSound的Script。以下的腳本是從API使用手冊摳過來的官方寫法,具體可以理解成:

  1. 設定callback
  2. 初始化需要播放的Event路徑
  3. 選擇Unity尋找Audio播放的位置(從SteamingAsset / Banks)
  4. 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會導致聲音播放的位置不正確。