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


(欢迎转载。本文源地址:http://blog.csdn.net/speeds3/article/details/76209152)


现在的游戏越来越精细,但操作却在向简化的方向发展。另一方面,人的手指头是有限的,太复杂的操作上手也会很困难,所以在游戏中引入语音控制会是一个不错的选择。本文中会尝试在unity加入中文语音控制的功能。

unity官方教程中的几个项目很精简,但看起来很不错,里面有全套的资源。最后我选择了tanks-tutorial来做这个实验。

下载和修改项目

首先按照教程下好项目,把坦克移动和射击的代码加上。这时就已经可以称的上是一个“游戏”了,可以控制坦克在地图上环游,也可以开炮。虽然缺少了挨揍的敌人,但是对设想的用语音控制坦克移动和射击已经足够了。这里我把地图扩大了一些,把坦克的速度降了一些,这样不至于几下就开到了地图的边缘。

修改速度

准备语义理解服务

接下来就可以开始加入语音功能了。OLAMI官网有c#的示例,示例中分别有cloud-speech-recognition和natural-language-understanding两个部分,前者字面意思似乎是语音识别,后者看起来是自然语义理解,里面又分为speech-input和text-input两部分,只是speech-input是空的。看看readme,原来已经包含在cloud-speech-recognition了。由于在这里不关心语音识别,所以就把他俩当作一样使用了,一个对应语音理解,是我们需要的部分,一个对应文字理解,可以用来测试,正好。

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

在移动和射击脚本中添加语音控制接口

因为打算实现的方案是语音和键盘混合输入,键盘输入能打断语音控制的输入,所以这里要保存一些状态,记录是否是通过语音在控制行动或转向,以及语音转向的角度和当前已经转过的角度。代码如下:

TankMovement.cs
  // 语音控制中已经转过的角度
  private float turnAmount = 0f;
  // 语音控制中希望转到的角度
  private float turnTarget = 0f;
  // 记录是否是语音控制移动的状态
  private bool voiceMove;
  // 记录是否是语音转向的状态
  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;
        }

    }

转向和移动稍有些不同,移动时只要模拟按键值一直是1就可以,转向就有一个转到多少度的问题。所以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之前就写好了的,不过先规划好语义再去OLAMI添加也没什么问题。

添加语义

加完之后别忘了发布,再在应用管理页面配置上刚加的NLI模块。

用文本来测试语义解析

现在可以来测试一下语义能不能起作用了。这里是场景增加一个InputField,on end edit的回调函数中调用NluApiSample的GetRecognitionResult方法的。当然这其中少不了一些封装。

on end edit的回调函数
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请求的方式拿结果,但在我的机器上测试时感觉不到延时,坦克的转向、移动都很顺畅。

增加录音功能

unity中提供了一个Microphone类来实现麦克风的功能,可以直接得到AudioClip对象。这里采用按下F1开始录音,松开结束录音的方式。录音长度暂定为5秒。由于olami接口支持的是wav格式的PCM录音,所以在github上找到一个WavUtility来做转换。

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中有方法必须在主线程中执行,所以只能放在这里转换
                    byte[] audiodata = WavUtility.FromAudioClip (audioclip);
          // 将发送录音的过程放到新线程里,减少主线程卡顿
                    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数据。
[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;
}

测试

现在可以启动游戏,试试语音的控制了。在我的机器上,从录音结束到坦克开始行动大概要一两秒的时间。不过说前进,后退之后不用一直按着按键,感觉还是不错的。还可以说“左转1800度”来看坦克傻傻的转圈。😄

总结

总的来说,虽然是在线语义理解,但OLAMI还是可以用在游戏中实时性要求不是特别高的场景,比如自动向前跑动。OLAMI在文本语义理解上的速度表现更是出乎意料的好。如果能提高语音识别的速度,例如提供离线包,相信语音控制应用的范围会更大一些。这个游戏后续我还会继续完善,敬请期待。

附录

游戏试玩下载连接:
链接: http://pan.baidu.com/s/1pLDgq9t 密码: dmxx

源码下载:
链接: http://pan.baidu.com/s/1qYWcuYC 密码: gh3n

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 207,248评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,681评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,443评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,475评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,458评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,185评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,451评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,112评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,609评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,083评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,163评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,803评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,357评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,357评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,590评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,636评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,925评论 2 344

推荐阅读更多精彩内容