利用OLAMI在unity游戲中加入中文語音控制(一)


(歡迎轉(zhuǎn)載界睁。本文源地址:http://blog.csdn.net/speeds3/article/details/76209152)


現(xiàn)在的游戲越來越精細,但操作卻在向簡化的方向發(fā)展。另一方面,人的手指頭是有限的略号,太復(fù)雜的操作上手也會很困難,所以在游戲中引入語音控制會是一個不錯的選擇洋闽。本文中會嘗試在unity加入中文語音控制的功能玄柠。

unity官方教程中的幾個項目很精簡,但看起來很不錯诫舅,里面有全套的資源羽利。最后我選擇了tanks-tutorial來做這個實驗。

下載和修改項目

首先按照教程下好項目刊懈,把坦克移動和射擊的代碼加上这弧。這時就已經(jīng)可以稱的上是一個“游戲”了,可以控制坦克在地圖上環(huán)游虚汛,也可以開炮匾浪。雖然缺少了挨揍的敵人,但是對設(shè)想的用語音控制坦克移動和射擊已經(jīng)足夠了卷哩。這里我把地圖擴大了一些蛋辈,把坦克的速度降了一些,這樣不至于幾下就開到了地圖的邊緣将谊。

修改速度

準備語義理解服務(wù)

接下來就可以開始加入語音功能了冷溶。OLAMI官網(wǎng)有c#的示例,示例中分別有cloud-speech-recognition和natural-language-understanding兩個部分尊浓,前者字面意思似乎是語音識別逞频,后者看起來是自然語義理解,里面又分為speech-input和text-input兩部分栋齿,只是speech-input是空的虏劲。看看readme褒颈,原來已經(jīng)包含在cloud-speech-recognition了柒巫。由于在這里不關(guān)心語音識別,所以就把他倆當(dāng)作一樣使用了谷丸,一個對應(yīng)語音理解堡掏,是我們需要的部分,一個對應(yīng)文字理解刨疼,可以用來測試泉唁,正好鹅龄。

把SpeechApiSample.cs和NluApiSample.cs拖入unity里,稍作修改就可以直接使用亭畜。

在移動和射擊腳本中添加語音控制接口

因為打算實現(xiàn)的方案是語音和鍵盤混合輸入扮休,鍵盤輸入能打斷語音控制的輸入,所以這里要保存一些狀態(tài)拴鸵,記錄是否是通過語音在控制行動或轉(zhuǎn)向玷坠,以及語音轉(zhuǎn)向的角度和當(dāng)前已經(jīng)轉(zhuǎn)過的角度。代碼如下:

TankMovement.cs
  // 語音控制中已經(jīng)轉(zhuǎn)過的角度
  private float turnAmount = 0f;
  // 語音控制中希望轉(zhuǎn)到的角度
  private float turnTarget = 0f;
  // 記錄是否是語音控制移動的狀態(tài)
  private bool voiceMove;
  // 記錄是否是語音轉(zhuǎn)向的狀態(tài)
  private bool voiceTurn;

  private void Update () {
        // Store the value of both input axes.
        float movement = Input.GetAxis (m_MovementAxisName);
        if (movement != 0) {
            voiceMove = false;
            m_MovementInputValue = movement;
        } else if (!voiceMove) {
            m_MovementInputValue = 0f;
        }

        float turn = Input.GetAxis (m_TurnAxisName);
        if (turn != 0) {
            voiceTurn = false;
            m_TurnInputValue = turn;
        } else if (!voiceTurn) {
            m_TurnInputValue = 0f;
        }
        EngineAudio ();
    }

  private void Turn () {
        // Determine the number of degrees to be turned based on the input, speed and time between frames.
        float turn = m_TurnInputValue * m_TurnSpeed * Time.deltaTime;

        if (turnTarget != 0) {
            turnAmount += turn;
            if (turnTarget > 0) {
                if (turnAmount > turnTarget) {
                    m_TurnInputValue = 0f;
                    turnTarget = 0f;
                    turnAmount = 0f;
                    voiceTurn = false;
                }
            } else {
                if (turnAmount < turnTarget) {
                    m_TurnInputValue = 0f;
                    turnTarget = 0f;
                    turnAmount = 0f;
                    voiceTurn = false;
                }
            }
        }

        // Make this into a rotation in the y axis.
        Quaternion turnRotation = Quaternion.Euler (0f, turn, 0f);

        // Apply this rotation to the rigidbody's rotation.
        m_Rigidbody.MoveRotation (m_Rigidbody.rotation * turnRotation);
    }

    public void VoiceMove(float movement) {
        if (movement != 0) {
            voiceMove = true;
            m_MovementInputValue = movement;
        } else {
            voiceMove = false;
            m_MovementInputValue = 0f;
        }
    }

    public void VoiceTurn(float turn) {
        if (turn == 0) {
            voiceTurn = false;
            return;
        }
        turnTarget = turn;
        voiceTurn = true;
        if (turn > 0) {
            m_TurnInputValue = 1.0f;
        } else {
            m_TurnInputValue = -1.0f;
        }

    }

轉(zhuǎn)向和移動稍有些不同劲藐,移動時只要模擬按鍵值一直是1就可以八堡,轉(zhuǎn)向就有一個轉(zhuǎn)到多少度的問題。所以Turn的代碼里加了一些處理聘芜。

TankShootin中就比較簡單兄渺,直接添加方法:

public void VoiceFire() {
    m_CurrentLaunchForce = m_MaxLaunchForce / 2;
    Fire ();
}

考慮到語音輸入本身需要時間,這里沒有加入冷卻的代碼汰现,而且蓄力直接定為滿格的1/2挂谍。

為了方便之后在錄音和輸入文本后使用,將語音控制包裝到TankVoiceControl中瞎饲,并將腳本附加到tank上口叙。

TankVoiceControl.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TankVoiceControl : MonoBehaviour {

    TankMovement move;

    TankShooting shooting;

    // Use this for initialization
    void Start () {
        move = GetComponent<TankMovement> ();
        shooting = GetComponent<TankShooting> ();
    }

    // Update is called once per frame
    void Update () {

    }

    public void VoiceMove(float movement) {
        move.VoiceMove (movement);
    }

    public void VoiceTurn(float turn) {
        move.VoiceTurn (turn);
    }

    public void VoiceFire() {
        shooting.VoiceFire ();
    }

  // 處理OLAMI解析出來的語義
    public void ProcessSemantic(Semantic sem) {
        if (sem.app == "game") {
            string modifier = sem.modifier [0];
            Slot[] slots = sem.slots;
            switch (modifier) {
            case "move":
                {
                    string move = "0f";
                    foreach (Slot slot in slots) {
                        if (slot.name == "movement") {
                            move = slot.value;
                        }
                    }
                    VoiceMove (float.Parse (move));
                }
                break;
            case "stop":
                {
                    VoiceMove (0f);
                }
                break;
            case "leftturn":
                {
                    string turn = "0f";
                    foreach (Slot slot in slots) {
                        if (slot.name == "turn") {
                            turn = slot.value;
                        }
                    }
                    VoiceTurn (0 - float.Parse (turn));
                }
                break;
            case "rightturn":
                {
                    string turn = "0f";
                    foreach (Slot slot in slots) {
                        if (slot.name == "turn") {
                            turn = slot.value;
                        }
                    }
                    VoiceTurn (float.Parse (turn));
                }
                break;
            case "fire":
                {
                    VoiceFire ();
                }
                break;
            }
            return;
        }
    }
}

ProcessSemantic方法用來處理OLAMI接口返回的語義。

在OLAMI平臺添加語義

其實我的語義是在ProcessSemantic之前就寫好了的企软,不過先規(guī)劃好語義再去OLAMI添加也沒什么問題庐扫。

添加語義

加完之后別忘了發(fā)布,再在應(yīng)用管理頁面配置上剛加的NLI模塊仗哨。

用文本來測試語義解析

現(xiàn)在可以來測試一下語義能不能起作用了形庭。這里是場景增加一個InputField,on end edit的回調(diào)函數(shù)中調(diào)用NluApiSample的GetRecognitionResult方法的厌漂。當(dāng)然這其中少不了一些封裝萨醒。

on end edit的回調(diào)函數(shù)
public void OnSubmitText(string text) {
        string result = VoiceService.GetInstance().sendText (text);
        VoiceResult voiceResult = JsonUtility.FromJson<VoiceResult> (result);
        if (voiceResult.status.Equals ("ok")) {
            Nli[] nlis = voiceResult.data.nli;
            if (nlis.Length != 0) {
                foreach (Nli nli in nlis) {
                    if (nli.type == "game") {
                        foreach (Semantic sem in nli.semantic) {
                            voiceControl.ProcessSemantic (sem);
                            return;
                        }
                    }
                }
            }
        }
    }
VoiceService的sendText方法
public string sendText(string text) {
        return nluApi.GetRecognitionResult ("nli", text);
    }

保存腳本,測試苇倡。文本的語義理解速度非掣恢剑快,雖然是通過http請求的方式拿結(jié)果旨椒,但在我的機器上測試時感覺不到延時晓褪,坦克的轉(zhuǎn)向、移動都很順暢综慎。

增加錄音功能

unity中提供了一個Microphone類來實現(xiàn)麥克風(fēng)的功能涣仿,可以直接得到AudioClip對象。這里采用按下F1開始錄音,松開結(jié)束錄音的方式好港。錄音長度暫定為5秒愉镰。由于olami接口支持的是wav格式的PCM錄音,所以在github上找到一個WavUtility來做轉(zhuǎn)換钧汹。

VoiceController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine.UI;
using UnityEngine;
using System;
using System.Threading;

public class VoiceController : MonoBehaviour {
    AudioClip audioclip;

    bool recording;

    [SerializeField]
    TankVoiceControl voiceControl;

    // Use this for initialization
    void Start () {
    }

    // Update is called once per frame
    void Update () {
        if (Input.GetKeyDown (KeyCode.F1)) {
            recording = true;
        } else if (Input.GetKeyUp(KeyCode.F1)) {
            recording = false;
        }
    }

    void LateUpdate() {
        if (recording) {
            if (!Microphone.IsRecording (null)) {
        // 開始錄音
                audioclip = Microphone.Start (null, false, 5, 16000);
            }
        } else {
            if (Microphone.IsRecording(null)) {
                Microphone.End (null);
                if (audioclip != null) {
          // WavUtility中有方法必須在主線程中執(zhí)行丈探,所以只能放在這里轉(zhuǎn)換
                    byte[] audiodata = WavUtility.FromAudioClip (audioclip);
          // 將發(fā)送錄音的過程放到新線程里,減少主線程卡頓
                    Thread thread = new Thread (new ParameterizedThreadStart(process));
                    thread.Start ((object) audiodata);
                }
            }

        }
    }

    void process(object obj) {
        byte[] audiodata = (byte[]) obj;
        string result = VoiceService.GetInstance ().sendSpeech (audiodata);
        audioclip = null;
        Debug.Log (result);
        VoiceResult voiceResult = JsonUtility.FromJson<VoiceResult> (result);
        if (voiceResult.status.Equals ("ok")) {
            Nli[] nlis = voiceResult.data.nli;
            if (nlis != null && nlis.Length != 0) {
                foreach (Nli nli in nlis) {
                    if (nli.type == "game") {
                        foreach (Semantic sem in nli.semantic) {
                            voiceControl.ProcessSemantic (sem);
                        }
                    }
                }
            }
        }
    }
}

// 下面的幾個class用于解析json數(shù)據(jù)拔莱。
[Serializable]
public class VoiceResult {
    public VoiceData data;
    public string status;
}

[Serializable]
public class VoiceData {
    public Nli[] nli;
}

[Serializable]
public class Nli {
    public DescObj desc;
    public Semantic[] semantic;
    public string type;
}

[Serializable]
public class DescObj {
    public string result;
    public int status;
}

[Serializable]
public class Semantic {
    public string app;
    public string input;
    public Slot[] slots;
    public string[] modifier;
    public string customer;
}

[Serializable]
public class Slot {
    public string name;
    public string value;
    public string[] modifier;
}

測試

現(xiàn)在可以啟動游戲碗降,試試語音的控制了。在我的機器上辨宠,從錄音結(jié)束到坦克開始行動大概要一兩秒的時間遗锣。不過說前進货裹,后退之后不用一直按著按鍵嗤形,感覺還是不錯的。還可以說“左轉(zhuǎn)1800度”來看坦克傻傻的轉(zhuǎn)圈弧圆。??

總結(jié)

總的來說赋兵,雖然是在線語義理解,但OLAMI還是可以用在游戲中實時性要求不是特別高的場景搔预,比如自動向前跑動霹期。OLAMI在文本語義理解上的速度表現(xiàn)更是出乎意料的好。如果能提高語音識別的速度拯田,例如提供離線包历造,相信語音控制應(yīng)用的范圍會更大一些。這個游戲后續(xù)我還會繼續(xù)完善船庇,敬請期待吭产。

附錄

游戲試玩下載連接:
鏈接: http://pan.baidu.com/s/1pLDgq9t 密碼: dmxx

源碼下載:
鏈接: http://pan.baidu.com/s/1qYWcuYC 密碼: gh3n

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市鸭轮,隨后出現(xiàn)的幾起案子臣淤,更是在濱河造成了極大的恐慌,老刑警劉巖窃爷,帶你破解...
    沈念sama閱讀 216,496評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件邑蒋,死亡現(xiàn)場離奇詭異,居然都是意外死亡按厘,警方通過查閱死者的電腦和手機医吊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來逮京,“玉大人卿堂,你說我怎么就攤上這事≡炻玻” “怎么了御吞?”我有些...
    開封第一講書人閱讀 162,632評論 0 353
  • 文/不壞的土叔 我叫張陵麦箍,是天一觀的道長。 經(jīng)常有香客問我陶珠,道長挟裂,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,180評論 1 292
  • 正文 為了忘掉前任揍诽,我火速辦了婚禮诀蓉,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘暑脆。我一直安慰自己渠啤,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,198評論 6 388
  • 文/花漫 我一把揭開白布添吗。 她就那樣靜靜地躺著沥曹,像睡著了一般。 火紅的嫁衣襯著肌膚如雪碟联。 梳的紋絲不亂的頭發(fā)上妓美,一...
    開封第一講書人閱讀 51,165評論 1 299
  • 那天,我揣著相機與錄音鲤孵,去河邊找鬼壶栋。 笑死,一個胖子當(dāng)著我的面吹牛普监,可吹牛的內(nèi)容都是我干的贵试。 我是一名探鬼主播,決...
    沈念sama閱讀 40,052評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼凯正,長吁一口氣:“原來是場噩夢啊……” “哼毙玻!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起漆际,我...
    開封第一講書人閱讀 38,910評論 0 274
  • 序言:老撾萬榮一對情侶失蹤淆珊,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后奸汇,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體施符,經(jīng)...
    沈念sama閱讀 45,324評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,542評論 2 332
  • 正文 我和宋清朗相戀三年擂找,在試婚紗的時候發(fā)現(xiàn)自己被綠了戳吝。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,711評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡贯涎,死狀恐怖听哭,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤陆盘,帶...
    沈念sama閱讀 35,424評論 5 343
  • 正文 年R本政府宣布普筹,位于F島的核電站,受9級特大地震影響隘马,放射性物質(zhì)發(fā)生泄漏太防。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,017評論 3 326
  • 文/蒙蒙 一酸员、第九天 我趴在偏房一處隱蔽的房頂上張望蜒车。 院中可真熱鬧,春花似錦幔嗦、人聲如沸酿愧。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽嬉挡。三九已至,卻和暖如春呼渣,著一層夾襖步出監(jiān)牢的瞬間棘伴,已是汗流浹背寞埠。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評論 1 269
  • 我被黑心中介騙來泰國打工屁置, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人仁连。 一個月前我還...
    沈念sama閱讀 47,722評論 2 368
  • 正文 我出身青樓蓝角,卻偏偏與公主長得像,于是被迫代替她去往敵國和親饭冬。 傳聞我的和親對象是個殘疾皇子使鹅,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,611評論 2 353

推薦閱讀更多精彩內(nèi)容