音推箱子:AR眼鏡上的語(yǔ)音交互游戲開發(fā)實(shí)戰(zhàn))
1. 從童年記憶到語(yǔ)音交互這個(gè)推箱子項(xiàng)目到底在做什么推箱子這個(gè)游戲估計(jì)沒幾個(gè)人沒玩過。規(guī)則簡(jiǎn)單到一句話就能說清把箱子推到目標(biāo)點(diǎn)上不能拉只能推箱子進(jìn)了死角就重來。小時(shí)候在文曲星、諾基亞或者網(wǎng)頁(yè)Flash上玩得不亦樂乎現(xiàn)在回頭看它其實(shí)是最經(jīng)典的路徑規(guī)劃和狀態(tài)空間搜索問題——每一步都要考慮“推完之后人還能不能繞過去”腦子稍微轉(zhuǎn)慢一點(diǎn)就卡關(guān)。但這次我做的不是普通的推箱子。我用 Rokid AIUI 把它改成了一個(gè)能“聽懂人話”的版本。什么意思就是玩家不用按鍵、不用觸屏直接對(duì)著眼鏡說“往上推”“左邊那個(gè)箱子往右”“撤銷一步”游戲就會(huì)執(zhí)行對(duì)應(yīng)操作。聽起來好像只是加了個(gè)語(yǔ)音輸入但實(shí)際做下來涉及的東西比想象中多得多語(yǔ)音指令的語(yǔ)義解析、游戲狀態(tài)的實(shí)時(shí)同步、多步操作的意圖理解、還有在AR眼鏡這種算力和交互都受限的設(shè)備上怎么保證響應(yīng)速度。這個(gè)項(xiàng)目適合誰(shuí)看如果你對(duì)語(yǔ)音交互開發(fā)感興趣或者手里正好有Rokid的AR眼鏡想找點(diǎn)好玩的東西練手再或者你單純想重溫一下推箱子、順便看看現(xiàn)在的AIUI能玩出什么花樣那這篇內(nèi)容應(yīng)該能給你不少參考。我會(huì)把整個(gè)項(xiàng)目的設(shè)計(jì)思路、AIUI的接入方式、推箱子核心邏輯的實(shí)現(xiàn)、以及實(shí)際調(diào)試中踩過的坑全部攤開來講。代碼層面我會(huì)給出關(guān)鍵片段和配置參數(shù)你照著抄基本能跑起來。先說清楚一件事Rokid AIUI 是Rokid提供的一套語(yǔ)音交互開發(fā)框架它把語(yǔ)音喚醒、語(yǔ)音識(shí)別、自然語(yǔ)言理解、語(yǔ)音合成這些能力打包成了可調(diào)用的接口。你不需要自己去訓(xùn)練聲學(xué)模型也不用搭一套NLU管線直接調(diào)用它的API就能讓設(shè)備聽懂指令。這對(duì)獨(dú)立開發(fā)者來說省了大力氣但前提是你得理解它的意圖配置邏輯不然識(shí)別率會(huì)讓你懷疑人生。推箱子這邊核心其實(shí)就三塊地圖數(shù)據(jù)結(jié)構(gòu)、移動(dòng)合法性判斷、勝利條件檢測(cè)。聽起來簡(jiǎn)單但加上語(yǔ)音交互之后每一塊都要重新考慮。比如玩家說“把左邊那個(gè)箱子推到右邊”這句話里包含了目標(biāo)箱子、推動(dòng)方向、甚至可能隱含了多步操作你得把它拆成游戲能理解的原子指令。再比如玩家說“ undo”你得知道撤銷的是哪一步還要保證撤銷之后游戲狀態(tài)完全回滾。我整個(gè)項(xiàng)目的開發(fā)周期大概兩周其中一半時(shí)間花在AIUI的意圖配置和調(diào)試上另一半花在推箱子邏輯和語(yǔ)音指令的映射上。硬件用的是Rokid Glass 2開發(fā)環(huán)境是Unity Rokid SDK。下面我按模塊拆開講盡量把每個(gè)決策背后的原因說清楚。2. 為什么選Rokid AIUI來做語(yǔ)音推箱子2.1 語(yǔ)音交互和推箱子的天然契合點(diǎn)推箱子這個(gè)游戲有個(gè)特點(diǎn)操作是離散的、方向性的、步驟明確的。玩家每做一個(gè)決策本質(zhì)上就是在說“把某個(gè)箱子往某個(gè)方向推一格”。這跟語(yǔ)音指令的結(jié)構(gòu)高度吻合。你不需要像動(dòng)作游戲那樣要求低延遲的連續(xù)輸入也不需要像RPG那樣處理復(fù)雜的對(duì)話樹。推箱子的語(yǔ)音交互可以設(shè)計(jì)得非常干凈指令短、意圖明確、反饋直接。另一個(gè)契合點(diǎn)是推箱子天然適合“ hands-free”場(chǎng)景。你戴著AR眼鏡雙手可能在做別的事情或者單純不想舉著手機(jī)。語(yǔ)音操作讓游戲體驗(yàn)更自然。我試過用觸控板玩推箱子玩到后面手指酸得不行語(yǔ)音就完全沒這個(gè)問題。但這里有個(gè)坑推箱子的操作頻率其實(shí)不低。一關(guān)可能要走幾十步如果每步都要說“往上推”嘴巴也會(huì)累。所以我在設(shè)計(jì)指令集的時(shí)候特意加入了一些“批量指令”比如“連續(xù)往上推三格”“把那個(gè)箱子推到最右邊”讓玩家可以用一句話完成多步操作。這就對(duì)AIUI的語(yǔ)義理解提出了更高要求。2.2 AIUI相比自建語(yǔ)音管線的優(yōu)勢(shì)我一開始考慮過自己搭一套語(yǔ)音識(shí)別NLU的方案。用開源的ASR引擎再寫一套規(guī)則匹配或者接一個(gè)輕量級(jí)意圖分類模型。但算了一下工作量發(fā)現(xiàn)不劃算。自建方案的問題在于第一ASR的準(zhǔn)確率在嘈雜環(huán)境下很難保證尤其是AR眼鏡的麥克風(fēng)陣列和手機(jī)不一樣遠(yuǎn)場(chǎng)識(shí)別需要專門的降噪和波束成形處理第二NLU部分如果要支持靈活的指令表達(dá)規(guī)則會(huì)越寫越多最后變成一座屎山第三語(yǔ)音合成還得單獨(dú)接一個(gè)TTS引擎延遲和音質(zhì)都是問題。Rokid AIUI把這些都封裝好了。它的語(yǔ)音喚醒詞可以自定義識(shí)別引擎針對(duì)Rokid設(shè)備做了優(yōu)化NLU支持通過配置文件定義意圖和槽位TTS也有多種音色可選。我只需要關(guān)注“玩家說了什么”和“游戲該怎么響應(yīng)”這兩件事中間的語(yǔ)音處理鏈路AIUI全包了。具體來說AIUI的意圖配置是這樣的你定義一個(gè)意圖intent比如“move_box”然后給它配幾個(gè)槽位slot比如“direction”方向和“steps”步數(shù)。再寫一些訓(xùn)練語(yǔ)料比如“往上推”“向上移動(dòng)一格”“把箱子往左推兩格”。AIUI會(huì)根據(jù)這些語(yǔ)料自動(dòng)學(xué)習(xí)之后玩家說類似的話就能匹配到這個(gè)意圖。這個(gè)過程不需要寫代碼在配置文件里改就行。2.3 硬件選型和開發(fā)環(huán)境搭建我用的設(shè)備是Rokid Glass 2操作系統(tǒng)是基于Android的所以開發(fā)環(huán)境就是Unity Android SDK Rokid AIUI SDK。Unity版本用的是2021 LTS這個(gè)版本對(duì)AR眼鏡的兼容性比較好社區(qū)里遇到的問題也少。環(huán)境搭建的步驟大致如下安裝Unity Hub和Unity 2021 LTS勾選Android Build Support。從Rokid開發(fā)者官網(wǎng)下載AIUI SDK和Glass 2的Unity插件包。在Unity里新建3D項(xiàng)目導(dǎo)入Rokid SDK的unitypackage。配置Player Settings包名、最低API級(jí)別Android 8.0、目標(biāo)架構(gòu)ARM64。在場(chǎng)景里添加RokidAIUI預(yù)制體填入從開發(fā)者后臺(tái)申請(qǐng)到的AppKey和AppSecret。連接Glass 2通過adb調(diào)試。這里有個(gè)細(xì)節(jié)Rokid AIUI的AppKey是和包名綁定的如果你改了包名需要去后臺(tái)重新申請(qǐng)。我一開始沒注意改了一次包名之后語(yǔ)音功能直接失效排查了半天才發(fā)現(xiàn)是這個(gè)問題。注意Rokid AIUI的免費(fèi)版有調(diào)用次數(shù)限制個(gè)人開發(fā)夠用但如果要做商業(yè)化項(xiàng)目需要提前評(píng)估調(diào)用量。3. 推箱子核心邏輯的實(shí)現(xiàn)細(xì)節(jié)3.1 地圖數(shù)據(jù)結(jié)構(gòu)的設(shè)計(jì)選擇推箱子的地圖可以用多種方式表示。最簡(jiǎn)單的是二維字符數(shù)組用不同的字符代表墻、地板、箱子、目標(biāo)點(diǎn)、玩家。這種方式的優(yōu)點(diǎn)是直觀、易調(diào)試缺點(diǎn)是每次判斷移動(dòng)都要遍歷數(shù)組效率不高。我最終用的是位圖實(shí)體列表的混合結(jié)構(gòu)。地圖的靜態(tài)部分墻和地板用一個(gè)二維布爾數(shù)組表示true是墻false是地板。動(dòng)態(tài)部分箱子、目標(biāo)點(diǎn)、玩家用單獨(dú)的列表存儲(chǔ)每個(gè)實(shí)體記錄自己的坐標(biāo)。這樣判斷移動(dòng)的時(shí)候只需要檢查目標(biāo)坐標(biāo)是不是墻、有沒有箱子不需要遍歷整個(gè)地圖。具體的數(shù)據(jù)結(jié)構(gòu)是這樣的public class LevelData { public int width; public int height; public bool[,] walls; // true表示墻 public ListVector2Int targets; // 目標(biāo)點(diǎn)坐標(biāo) public ListVector2Int boxes; // 箱子坐標(biāo) public Vector2Int playerPos; // 玩家坐標(biāo) }箱子和目標(biāo)點(diǎn)都用List而不是數(shù)組是因?yàn)樗鼈兊臄?shù)量會(huì)變化雖然推箱子里面箱子數(shù)量不變但撤銷操作需要回滾狀態(tài)。用List方便做快照和恢復(fù)。3.2 移動(dòng)合法性判斷的完整邏輯推箱子的移動(dòng)判斷比看起來要復(fù)雜。玩家往一個(gè)方向移動(dòng)可能遇到以下幾種情況目標(biāo)位置是墻不能移動(dòng)。目標(biāo)位置是空地直接移動(dòng)。目標(biāo)位置是箱子箱子的下一格是空地或目標(biāo)點(diǎn)推動(dòng)箱子玩家移動(dòng)。目標(biāo)位置是箱子箱子的下一格是墻或另一個(gè)箱子不能移動(dòng)。這四種情況要按順序判斷不能漏。我一開始寫的時(shí)候漏了“箱子后面是另一個(gè)箱子”的情況導(dǎo)致兩個(gè)箱子疊在一起游戲直接卡死。代碼邏輯大概是這樣bool TryMove(Vector2Int direction) { Vector2Int targetPos playerPos direction; // 檢查邊界和墻 if (IsWall(targetPos)) return false; // 檢查是否有箱子 int boxIndex GetBoxIndexAt(targetPos); if (boxIndex 0) { Vector2Int boxTargetPos targetPos direction; // 箱子后面不能是墻或另一個(gè)箱子 if (IsWall(boxTargetPos) || GetBoxIndexAt(boxTargetPos) 0) { return false; } // 推動(dòng)箱子 boxes[boxIndex] boxTargetPos; } // 移動(dòng)玩家 playerPos targetPos; return true; }這段代碼看起來簡(jiǎn)單但有幾個(gè)容易出錯(cuò)的地方。第一IsWall函數(shù)要同時(shí)檢查邊界和墻邊界外默認(rèn)是墻。第二箱子的坐標(biāo)更新要在玩家移動(dòng)之前不然會(huì)覆蓋。第三每次移動(dòng)之后要記錄歷史狀態(tài)用于撤銷。3.3 勝利條件檢測(cè)和關(guān)卡數(shù)據(jù)格式勝利條件很簡(jiǎn)單所有箱子都在目標(biāo)點(diǎn)上。但實(shí)現(xiàn)的時(shí)候要注意箱子和目標(biāo)點(diǎn)都是List判斷的時(shí)候不能假設(shè)順序一致。我的做法是遍歷每個(gè)箱子檢查它的坐標(biāo)是否在目標(biāo)點(diǎn)列表中。bool IsLevelComplete() { foreach (var box in boxes) { if (!targets.Contains(box)) return false; } return true; }關(guān)卡數(shù)據(jù)我用的是文本文件每行一個(gè)字符串用不同的字符表示不同的元素#表示墻表示地板$表示箱子.表示目標(biāo)點(diǎn)*表示箱子在目標(biāo)點(diǎn)上表示玩家表示玩家在目標(biāo)點(diǎn)上這種格式是推箱子社區(qū)的標(biāo)準(zhǔn)格式網(wǎng)上有大量現(xiàn)成的關(guān)卡可以下載。我內(nèi)置了20關(guān)難度從易到難最后一關(guān)是經(jīng)典的“ Microban”系列里的一個(gè)中等難度關(guān)卡。解析的時(shí)候逐行逐字符讀取根據(jù)字符類型往對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu)里填。這里有個(gè)細(xì)節(jié)*和這兩種字符需要同時(shí)更新箱子和目標(biāo)點(diǎn)、玩家和目標(biāo)點(diǎn)的狀態(tài)。我一開始只處理了$和.導(dǎo)致帶*的關(guān)卡加載出來箱子不在目標(biāo)點(diǎn)上排查了好一會(huì)兒。4. 語(yǔ)音指令與游戲邏輯的映射實(shí)現(xiàn)4.1 AIUI意圖配置的完整流程AIUI的意圖配置是在Rokid開發(fā)者后臺(tái)完成的。你需要?jiǎng)?chuàng)建一個(gè)技能skill然后在技能里定義意圖和槽位。我定義了以下幾個(gè)意圖意圖名稱觸發(fā)示例槽位說明move往上推、向下移動(dòng)、往左走一格direction, steps移動(dòng)玩家/推箱子undo撤銷、退一步、回退無(wú)撤銷上一步操作restart重來、重新開始、重置無(wú)重置當(dāng)前關(guān)卡next_level下一關(guān)、繼續(xù)無(wú)進(jìn)入下一關(guān)query還有幾個(gè)箱子、現(xiàn)在第幾關(guān)無(wú)查詢游戲狀態(tài)每個(gè)意圖都需要配置訓(xùn)練語(yǔ)料。語(yǔ)料要盡量覆蓋不同的表達(dá)方式但也不能太多否則會(huì)影響識(shí)別準(zhǔn)確率。我的經(jīng)驗(yàn)是每個(gè)意圖配15-20條語(yǔ)料比較合適太少識(shí)別率低太多容易混淆。槽位的定義也很關(guān)鍵。direction槽位我定義了四個(gè)值up、down、left、right。steps槽位是數(shù)字范圍1-10。AIUI支持自定義槽位和系統(tǒng)預(yù)置槽位數(shù)字用系統(tǒng)預(yù)置的sys.number就行。配置完成之后AIUI會(huì)生成一個(gè)模型需要等待幾分鐘到十幾分鐘不等。模型訓(xùn)練好之后就可以在Unity里調(diào)用了。4.2 語(yǔ)音指令的解析和映射邏輯AIUI返回的結(jié)果是一個(gè)JSON包含意圖名稱和槽位值。比如玩家說“往上推兩格”返回的結(jié)果大概是{ intent: move, slots: { direction: up, steps: 2 } }我需要把這個(gè)結(jié)果轉(zhuǎn)換成游戲里的操作。轉(zhuǎn)換邏輯分兩步第一步是把方向字符串轉(zhuǎn)成Vector2Int第二步是根據(jù)步數(shù)循環(huán)執(zhí)行移動(dòng)。void OnAIUIResult(AIUIResult result) { switch (result.intent) { case move: Vector2Int dir DirectionFromString(result.slots[direction]); int steps result.slots.ContainsKey(steps) ? result.slots[steps] : 1; for (int i 0; i steps; i) { if (!TryMove(dir)) break; } break; case undo: UndoLastMove(); break; case restart: RestartLevel(); break; // ...其他意圖 } }這里有個(gè)細(xì)節(jié)如果玩家說“往上推三格”但推到第二格就推不動(dòng)了應(yīng)該怎么辦我的處理是推到不能推為止然后給一個(gè)語(yǔ)音反饋“只能推到這里了”。這樣比直接報(bào)錯(cuò)要友好。4.3 多步指令和模糊指令的處理策略多步指令是語(yǔ)音推箱子的一個(gè)亮點(diǎn)但也是最容易出問題的地方。玩家說“把左邊那個(gè)箱子推到右邊”這句話里包含了目標(biāo)箱子、推動(dòng)方向、以及隱含的“推到底”意圖。AIUI的NLU不一定能準(zhǔn)確解析這種復(fù)雜指令所以我做了兩層處理。第一層是AIUI的意圖匹配。如果匹配到了move意圖就按上面的邏輯執(zhí)行。第二層是自定義的模糊指令處理。如果AIUI返回的是“不理解”或者匹配到了錯(cuò)誤的意圖我會(huì)用一個(gè)簡(jiǎn)單的關(guān)鍵詞匹配來兜底。比如檢測(cè)到“左邊”“箱子”“右邊”這幾個(gè)詞就嘗試找到最左邊的箱子然后往右推。這個(gè)兜底邏輯當(dāng)然不完美但實(shí)際測(cè)試下來能覆蓋大部分常見表達(dá)。我試過說“把那個(gè)箱子往右推”“右邊那個(gè)箱子往左”“最上面的箱子往下”基本都能正確響應(yīng)。實(shí)操心得AIUI的NLU對(duì)語(yǔ)序比較敏感?!巴贤啤焙汀巴仆稀钡淖R(shí)別結(jié)果可能不一樣。配置語(yǔ)料的時(shí)候盡量把常見的語(yǔ)序都覆蓋到。4.4 語(yǔ)音反饋和TTS的集成方式語(yǔ)音反饋是提升體驗(yàn)的關(guān)鍵。玩家每做一個(gè)操作如果沒有任何反饋會(huì)不確定指令有沒有被識(shí)別。我在以下幾個(gè)時(shí)機(jī)觸發(fā)TTS移動(dòng)成功播報(bào)“已向上移動(dòng)”或“推了一步”。移動(dòng)失敗播報(bào)“前面是墻”或“推不動(dòng)了”。撤銷播報(bào)“已撤銷”。關(guān)卡完成播報(bào)“恭喜過關(guān)進(jìn)入下一關(guān)”。查詢播報(bào)當(dāng)前關(guān)卡號(hào)和剩余箱子數(shù)。TTS的調(diào)用很簡(jiǎn)單AIUI SDK提供了Speak接口傳入文本就行。但要注意TTS播報(bào)會(huì)占用音頻通道如果玩家在播報(bào)過程中又說了一條指令可能會(huì)被截?cái)唷N业奶幚硎窃赥TS播報(bào)期間暫停語(yǔ)音識(shí)別播報(bào)結(jié)束后再恢復(fù)。這樣雖然會(huì)稍微增加響應(yīng)延遲但避免了指令丟失。5. 實(shí)際調(diào)試中遇到的坑和解決方案5.1 語(yǔ)音識(shí)別率低的常見原因和優(yōu)化方法調(diào)試初期語(yǔ)音識(shí)別率低得讓人崩潰。我說“往上推”它識(shí)別成“晚上推”說“撤銷”它識(shí)別成“車銷”。排查之后發(fā)現(xiàn)幾個(gè)原因第一環(huán)境噪音。AR眼鏡的麥克風(fēng)對(duì)低頻噪音比較敏感空調(diào)聲、風(fēng)扇聲都會(huì)影響識(shí)別。解決辦法是在AIUI后臺(tái)開啟降噪模式同時(shí)盡量在安靜環(huán)境下使用。第二語(yǔ)速和發(fā)音。AIUI對(duì)語(yǔ)速有一定的適應(yīng)范圍太快太慢都不行。我在語(yǔ)料里加入了一些不同語(yǔ)速的樣本識(shí)別率有所提升。第三喚醒詞和指令之間的間隔。如果喚醒之后馬上說指令識(shí)別率會(huì)下降。我的做法是在喚醒詞之后加一個(gè)短暫的提示音提醒玩家可以說話了這樣識(shí)別率明顯提高。第四意圖混淆。“撤銷”和“重來”這兩個(gè)意圖的語(yǔ)料有重疊導(dǎo)致經(jīng)常識(shí)別錯(cuò)。我把它們的語(yǔ)料重新整理了一遍確保沒有交叉問題就解決了。5.2 游戲狀態(tài)同步和延遲問題的處理語(yǔ)音交互的延遲是不可避免的。從玩家說完到游戲響應(yīng)中間要經(jīng)過語(yǔ)音識(shí)別、NLU、網(wǎng)絡(luò)傳輸、游戲邏輯處理這幾個(gè)環(huán)節(jié)總延遲大概在300-500毫秒。這個(gè)延遲在推箱子里是可以接受的因?yàn)橥葡渥硬皇菍?shí)時(shí)動(dòng)作游戲。但有一個(gè)問題如果玩家連續(xù)說兩條指令第二條可能會(huì)在第一條還沒處理完的時(shí)候就到達(dá)。我的處理是加了一個(gè)指令隊(duì)列所有指令按順序執(zhí)行執(zhí)行完一條再執(zhí)行下一條。同時(shí)給玩家一個(gè)視覺反饋比如屏幕上的指令列表讓玩家知道哪些指令還在排隊(duì)。另一個(gè)問題是狀態(tài)同步。語(yǔ)音指令執(zhí)行之后游戲狀態(tài)變了但AIUI那邊不知道。如果玩家緊接著說“撤銷”AIUI需要知道當(dāng)前是哪一步。我的做法是在每次操作之后把當(dāng)前的操作歷史同步給AIUI的會(huì)話上下文這樣后續(xù)指令可以基于最新的狀態(tài)來解析。5.3 常見問題速查表問題現(xiàn)象可能原因排查方法解決方案語(yǔ)音無(wú)響應(yīng)AppKey錯(cuò)誤或過期檢查開發(fā)者后臺(tái)的AppKey狀態(tài)重新申請(qǐng)并更新配置識(shí)別結(jié)果亂碼麥克風(fēng)權(quán)限未開啟檢查AndroidManifest權(quán)限添加RECORD_AUDIO權(quán)限意圖匹配錯(cuò)誤語(yǔ)料配置不合理在后臺(tái)查看識(shí)別日志調(diào)整語(yǔ)料增加區(qū)分度TTS無(wú)聲音音頻通道被占用檢查是否有其他音頻在播放釋放音頻通道或調(diào)整優(yōu)先級(jí)游戲卡死移動(dòng)邏輯死循環(huán)檢查TryMove的邊界條件增加邊界檢查和日志撤銷失效歷史狀態(tài)未記錄檢查UndoLastMove的調(diào)用時(shí)機(jī)每次移動(dòng)前保存快照5.4 性能優(yōu)化和資源管理AR眼鏡的算力有限推箱子雖然不復(fù)雜但如果不注意優(yōu)化也會(huì)出現(xiàn)卡頓。我做了以下幾件事第一地圖渲染用對(duì)象池。箱子和目標(biāo)點(diǎn)的預(yù)制體不要每次動(dòng)態(tài)創(chuàng)建銷毀而是預(yù)先創(chuàng)建好用的時(shí)候激活不用的時(shí)候隱藏。這樣避免了頻繁的GC。第二語(yǔ)音識(shí)別和游戲邏輯分線程處理。AIUI的回調(diào)是在主線程執(zhí)行的如果游戲邏輯太重會(huì)阻塞語(yǔ)音響應(yīng)。我把游戲邏輯放在一個(gè)單獨(dú)的協(xié)程里AIUI回調(diào)只負(fù)責(zé)把指令放入隊(duì)列不直接執(zhí)行。第三減少TTS的調(diào)用頻率。不是每次操作都需要語(yǔ)音反饋比如連續(xù)移動(dòng)的時(shí)候可以只在最后一步播報(bào)。這樣既減少了音頻通道的占用也降低了延遲。6. 這個(gè)項(xiàng)目還能怎么擴(kuò)展6.1 加入更多語(yǔ)音交互玩法現(xiàn)在的語(yǔ)音指令還比較基礎(chǔ)可以擴(kuò)展的方向很多。比如加入“提示”功能玩家說“提示一下”游戲就給出下一步的建議。這個(gè)需要實(shí)現(xiàn)一個(gè)推箱子的求解器用BFS或者A*算法找到解然后取第一步作為提示。還可以加入“教學(xué)模式”玩家說“教我玩”游戲就一步步引導(dǎo)玩家完成關(guān)卡。這個(gè)需要把求解器的完整解拆成步驟逐步播報(bào)。另一個(gè)方向是多人協(xié)作。兩個(gè)人輪流用語(yǔ)音操作一個(gè)人說“往上推”另一個(gè)人說“往左推”游戲按順序執(zhí)行。這個(gè)在聚會(huì)場(chǎng)景下應(yīng)該挺有意思。6.2 關(guān)卡編輯器和自定義關(guān)卡現(xiàn)在的關(guān)卡是硬編碼在文本文件里的如果能讓玩家自己設(shè)計(jì)關(guān)卡可玩性會(huì)高很多??梢宰鲆粋€(gè)簡(jiǎn)單的關(guān)卡編輯器玩家用語(yǔ)音指令放置墻、箱子、目標(biāo)點(diǎn)然后保存成文本格式。這個(gè)功能的技術(shù)難點(diǎn)在于語(yǔ)音指令的粒度要足夠細(xì)比如“在第三行第四列放一個(gè)箱子”這種指令的NLU配置會(huì)比較復(fù)雜。6.3 適配更多Rokid設(shè)備Rokid的產(chǎn)品線不止Glass 2還有Air、Max等不同形態(tài)的設(shè)備。它們的麥克風(fēng)陣列、算力、屏幕分辨率都不一樣。如果要適配更多設(shè)備需要做一定的兼容性處理。比如Air沒有屏幕那視覺反饋就要改成純語(yǔ)音反饋Max的算力更強(qiáng)可以支持更復(fù)雜的求解器。6.4 接入大模型做更自然的對(duì)話現(xiàn)在的語(yǔ)音交互還是基于意圖匹配的玩家必須說“往上推”這種結(jié)構(gòu)化指令。如果接入大模型就可以支持更自然的對(duì)話。比如玩家說“我覺得這個(gè)箱子應(yīng)該往右推”大模型可以理解這個(gè)意圖然后轉(zhuǎn)換成游戲操作。這個(gè)方向的技術(shù)挑戰(zhàn)在于延遲和成本大模型的推理時(shí)間比意圖匹配要長(zhǎng)得多在AR眼鏡上可能不太現(xiàn)實(shí)但可以把推理放在云端。我在實(shí)際開發(fā)中的體會(huì)是語(yǔ)音交互游戲的核心難點(diǎn)不在語(yǔ)音本身而在“如何把人類的模糊表達(dá)映射到精確的游戲操作”。AIUI解決了語(yǔ)音識(shí)別和意圖理解的問題但意圖到操作的映射邏輯還是需要開發(fā)者自己精心設(shè)計(jì)。這個(gè)項(xiàng)目我前后改了五版指令集才達(dá)到比較滿意的識(shí)別率和操作流暢度。如果你也在做類似的東西建議先把指令集設(shè)計(jì)好再動(dòng)手寫代碼不然返工的成本會(huì)很高。