给游戏装一个AI大脑:灵引 LingGuide 具身交互智能策略伙伴开发全记录

AI5天前发布 beixibaobao
12 0 0

摘要:本文详细介绍如何基于魔珐星云 XmovAvatar SDK 的参数流架构,使用 Qoder AI 编程工具从零搭建一个面向游戏与电竞场景的具身交互智能应用——「灵引 LingGuide」。文章涵盖魔珐星云平台配置、SDK 接入、LLM 大模型对接(火山引擎豆包)、ASR 语音识别集成等完整开发流程,并深入解析流式对话、首句即播报、实时打断等核心交互机制的实现原理。通过本文,读者可以快速掌握游戏场景下具身交互智能的开发技巧,打造出响应延迟 < 500ms、支持双模交互(文字 + 语音)的智能游戏策略系统。

魔珐星云PC端官方链接:https://xingyun3d.com?utm_campaign=daily&utm_source=CSDNwanfen3&utm_medium=&utm_term=&utm_content=

一、项目概述

1.1 为什么游戏场景需要具身交互智能?

传统游戏助手或攻略 Agent 往往停留在文字问答:玩家输入问题,系统返回一段攻略。可在电竞和游戏陪伴场景里,用户真正需要的是一个能即时回应、能语音交流、能随时打断、能贴在游戏画面里的策略伙伴。

灵引 LingGuide 正是面向游戏页面与电竞平台的具身交互智能应用,以 AI 数字人为载体,提供实时语音 / 文字形式的游戏攻略、装备搭配、阵容推荐、战术分析等策略服务。所谓「具身交互智能」,是指数字人不仅具备视觉形象与肢体语言,还能实时感知用户语音与文字输入,通过大模型理解意图后以语音 + 动作同步响应,形成「看得见、听得到、能思考」的沉浸式交互体验。

核心能力

  • 英雄攻略:根据版本更新,推荐强势英雄和出装方案
  • 战术分析:分析阵容搭配、地图策略、团战时机
  • 上分指导:根据玩家段位提供针对性的提升建议
  • 电竞赛事:解读职业比赛战术、战队风格、选手数据

交互方式

  • 文字输入:在对话面板中直接输入游戏问题
  • 语音输入:按住说话,松开自动识别并发送
  • 快捷提问:一键询问热门攻略问题
  • 实时打断:数字人播报过程中可随时打断

1.2 技术栈

技术 版本 用途
Vue 3 3.5.18 前端框架
Vite 7.1.2 构建工具
TypeScript 5.8.3 类型安全
OpenAI SDK 5.12.2 LLM 流式对话
XmovAvatar SDK 0.1.0-alpha.15 数字人渲染
腾讯云 ASR 语音识别
火山引擎 LLM doubao-1-5-pro-32k 大语言模型

二、前提准备

2.1 环境搭建

运行环境要求

  • Node.js ≥ 16(推荐 18/20)
  • 包管理器:pnpm(推荐),亦支持 npm/yarn

下载并启动项目

# 安装依赖
pnpm i
# 启动开发服务器
pnpm dev
# 浏览器访问
http://localhost:5173

2.2 密钥准备

启动前需准备三类密钥:

  • XmovAvatar:APP ID、APP Secret(魔珐星云数字人 SDK)
  • 腾讯云 ASR:ASR App ID、Secret ID、Secret Key(语音识别)
  • 火山引擎 LLM:API Key(豆包大模型,兼容 OpenAI 格式)

三、灵引 LingGuide 数字人搭建

3.1 登录魔珐星云平台

访问魔珐星云官网,登录账号后点击「控制台」进入应用管理界面。

给游戏装一个AI大脑:灵引 LingGuide 具身交互智能策略伙伴开发全记录

3.2 创建数字人应用

进入「应用管理」页面,点击「开始创建」按钮,发起一个新的数字人应用。

给游戏装一个AI大脑:灵引 LingGuide 具身交互智能策略伙伴开发全记录

3.3 应用基本信息配置

创建驱动应用,配置应用名称为「灵引 LingGuide」,预览模式选择横屏模式。

给游戏装一个AI大脑:灵引 LingGuide 具身交互智能策略伙伴开发全记录

3.4 形象配置

为数字人选择合适的外观形象,匹配游戏策略伙伴的定位风格。

给游戏装一个AI大脑:灵引 LingGuide 具身交互智能策略伙伴开发全记录

3.5 场景配置

场景配置选择「透明背景」,确保数字人可以无缝叠加到游戏页面之上。

给游戏装一个AI大脑:灵引 LingGuide 具身交互智能策略伙伴开发全记录

3.6 音色配置

挑选与游戏策略伙伴人设相匹配的音色,让灵引的语音更具辨识度。

给游戏装一个AI大脑:灵引 LingGuide 具身交互智能策略伙伴开发全记录

3.7 表演配置

配置数字人的动作与表演风格,使其在待机、说话等状态下呈现自然的肢体语言。

给游戏装一个AI大脑:灵引 LingGuide 具身交互智能策略伙伴开发全记录

3.8 保存并获取 SDK 密钥

完成所有配置后,点击保存。进入「接入SDK」页面,复制 App ID 和 App Secret 密钥对。这对密钥将用于项目中连接数字人 SDK,是后续项目搭建的核心凭证。同时可参考官方文档将数字人应用接入到你的网页、App 或任意终端中。

给游戏装一个AI大脑:灵引 LingGuide 具身交互智能策略伙伴开发全记录

3.9 配置腾讯云 ASR 语音识别

登录腾讯云 ASR 控制台:https://console.cloud.tencent.com/asr

创建访问密钥,获取三项关键参数:

  • ASR App ID(数字)
  • ASR Secret ID(AKID 开头)
  • ASR Secret Key

3.10 配置火山引擎大语言模型

登录火山引擎控制台,进入 API 接入页面创建 API KEY。选择并开通豆包大模型(推荐 doubao-1-5-pro-32k-250115)。

四、核心代码讲解

给游戏装一个AI大脑:灵引 LingGuide 具身交互智能策略伙伴开发全记录

4.1 技术架构

┌─────────────────┐         ┌──────────────────┐         ┌─────────────────┐
│   浏览器端       │         │   魔珐星云SDK     │         │   第三方服务     │
│  Vue 3.5 + TS   │◄───────►│  XmovAvatar SDK  │◄───────►│  TTSA接口        │
│  电竞主题UI      │         │  端侧解算<500ms  │         │  数字人渲染      │
└────────┬────────┘         └──────────────────┘         └─────────────────┘
         │
         ├─────────────────┐         ┌─────────────────┐
         │                 │         │  火山引擎LLM     │
         │  OpenAI SDK     │◄───────►│  doubao-1-5     │
         │  流式对话       │         │  pro-32k        │
         └────────┬────────┘         └─────────────────┘
                  │
                  ├─────────────────┐         ┌─────────────────┐
                  │                 │         │  腾讯云ASR       │
                  │  实时语音识别    │◄───────►│  WebSocket流式  │
                  │  VAD检测        │         │  语音转文本      │
                  └─────────────────┘         └─────────────────┘

架构优势

  • 具身交互智能:数字人具备视觉形象 + 肢体语言 + 实时感知 + 智能决策,形成完整交互闭环
  • 参数流架构:SDK 端侧解算,响应延迟 < 500ms
  • 流式对话:LLM 流式输出,首句即播报,降低首字延迟
  • 双模交互:文字输入 + 语音录入,支持实时打断
  • 透明叠加:数字人以透明背景叠加在游戏页面之上,不遮挡主内容

4.2 全局常量配置

项目将所有配置集中在 constants/index.ts 中管理,包括应用参数、游戏数据、LLM 系统提示词、ASR 参数、SDK 配置和密钥。

// 应用常量
export const APP_CONFIG = {
  CONTAINER_PREFIX: 'CONTAINER_',
  DEFAULT_VAD_SILENCE_TIME: 300,
  AVATAR_INIT_TIMEOUT: 3000,
  SPEAK_INTERRUPT_DELAY: 2000
} as const
// SDK配置
export const SDK_CONFIG = {
  GATEWAY_URL: 'https://nebula-agent.xingyun3d.com/user/v1/ttsa/session',
  DATA_SOURCE: '2',
  CUSTOM_ID: 'demo'
} as const

关键配置说明

  • AVATAR_INIT_TIMEOUT:数字人 SDK 初始化等待时间,设置为 3 秒,确保 SDK 实例完成内部初始化后再调用 init()
  • GATEWAY_URL:魔珐星云 TTSA 网关地址,参数流架构的核心接口
  • DATA_SOURCE:数据源标识,‘2’ 表示使用云端渲染模式

LLM 系统提示词是定义灵引人设的关键:

export const LLM_CONFIG = {
  BASE_URL: 'https://ark.cn-beijing.volces.com/api/v3',
  DEFAULT_MODEL: 'doubao-1-5-pro-32k-250115',
  SYSTEM_PROMPT: `你是灵引,一位专业而热情的游戏策略伙伴。
## 你的身份
- 名称:灵引(LingGuide)
- 身份:游戏策略伙伴,电竞战术分析师
- 性格:冷静理性、热情耐心、分析精准、善于鼓励
- 语言风格:专业精准、简洁有力、善用数据和案例、偶尔用游戏术语
## 你的专业能力
1. **英雄攻略**:根据版本更新,推荐强势英雄和出装方案
2. **战术分析**:分析阵容搭配、地图策略、团战时机
3. **上分指导**:根据玩家段位提供针对性的提升建议
4. **电竞赛事**:解读职业比赛战术、战队风格、选手数据
## 工作原则
- 用数据和实战案例支撑每一个建议
- 尊重不同段位玩家的水平差异,给出阶梯式建议
- 鼓励团队合作,不鼓励solo carry心态
- 关注版本更新,提供时效性强的攻略信息
- 保持积极正面的态度,帮助玩家享受游戏乐趣`,
} as const

这段 System Prompt 定义了灵引的完整人设——从身份定位、性格特征到专业能力边界,确保 LLM 的每一次回复都符合「游戏策略伙伴」的角色设定。

4.3 avatar.ts – 数字人 SDK 服务

核心负责魔珐星云 XmovAvatar SDK 的初始化、连接和生命周期管理。采用 Promise 管理模式处理异步连接流程,支持超时控制和状态监控。

import type { AvatarConfig } from '../types'
import { generateContainerId, getPromiseState } from '../utils'
import { SDK_CONFIG, APP_CONFIG } from '../constants'
interface AvatarCallbacks {
  onSubtitleOn: (text: string) => void
  onSubtitleOff: () => void
  onStateChange: (state: string) => void
  onVoiceStateChange?: (status: string) => void
}
class AvatarService {
  private containerId: string
  constructor() {
    this.containerId = generateContainerId()
  }
  getContainerId(): string {
    return this.containerId
  }
  async connect(config: AvatarConfig, callbacks: AvatarCallbacks): Promise<any> {
    const { appId, appSecret } = config
    const { onSubtitleOn, onSubtitleOff, onStateChange, onVoiceStateChange } = callbacks
    // 检查容器是否存在
    const containerEl = document.getElementById(this.containerId)
    if (!containerEl) {
      console.error(`[AvatarService] 容器 #${this.containerId} 不存在!`)
      throw new Error(`容器 #${this.containerId} 不存在,请确保 AvatarRender 组件已渲染`)
    }
    // 构建网关URL
    const url = new URL(SDK_CONFIG.GATEWAY_URL)
    url.searchParams.append('data_source', SDK_CONFIG.DATA_SOURCE)
    url.searchParams.append('custom_id', SDK_CONFIG.CUSTOM_ID)
    // Promise管理连接状态
    let resolve: (value: boolean) => void
    let reject: (reason?: any) => void
    const connectPromise = new Promise<boolean>((res, rej) => {
      resolve = res
      reject = rej
    })
    // SDK构造选项
    const constructorOptions = {
      containerId: `#${this.containerId}`,
      appId,
      appSecret,
      enableDebugger: false,
      gatewayServer: url.toString(),
      onProxyWidgetEvent: (event: any) => {
        console.log('SDK事件:', event)
      },
      onStateChange,
      onMessage: async (error: any) => {
        const state = await getPromiseState(connectPromise)
        const plainError = new Error(error.message)
        if (state === 'pending') {
          reject(plainError)
        }
      },
      onVoiceStateChange: (status: string) => {
        // 当状态为 'end' 时,表示数字人停止说话
        if (status.includes('end')) {
          onVoiceStateChange?.(status)
        }
      },
    }
    // 创建SDK实例
    const avatar = new window.XmovAvatar(constructorOptions)
    // 等待初始化
    await new Promise(resolve => {
      setTimeout(resolve, APP_CONFIG.AVATAR_INIT_TIMEOUT)
    })
    // 初始化SDK
    await avatar.init({
      onDownloadProgress: (progress: number) => {
        console.log(`初始化进度: ${progress}%`)
        if (progress >= 100) {
          resolve(true)
        }
      },
      onClose: () => {
        onStateChange('')
        console.log('SDK连接关闭')
      }
    })
    // 等待连接完成(设置超时避免永久挂起)
    const connectTimeout = new Promise<boolean>((_, rej) => {
      setTimeout(() => rej(new Error('SDK连接超时')), 15000)
    })
    try {
      await Promise.race([connectPromise, connectTimeout])
      console.log('[AvatarService] 连接成功')
    } catch (error) {
      console.warn('SDK连接等待结束:', error)
      // 超时不抛错,可能已经初始化完成但没触发100%回调
    }
    // 连接成功后,注入CSS隐藏SDK字幕
    this.injectSubtitleKiller()
    return avatar
  }
  private injectSubtitleKiller(): void {
    // 注入全局CSS,隐藏SDK自带的字幕元素
    console.log('[AvatarService] 字幕隐藏已禁用,排查数字人渲染问题')
    return
  }
  disconnect(avatar: any): void {
    if (!avatar) return
    // 移除注入的CSS
    const killerStyle = document.getElementById('xmov-subtitle-killer')
    if (killerStyle) killerStyle.remove()
    try {
      avatar.stop()
      avatar.destroy()
    } catch (error) {
      console.error('断开连接时出错:', error)
    }
  }
}
export const avatarService = new AvatarService()

关键配置项说明

  • enableDebugger: false:生产环境关闭调试模式,避免控制台输出过多 SDK 内部日志
  • gatewayServer:魔珐星云网关服务器地址,支持自定义数据源,是参数流架构的核心入口
  • onDownloadProgress:初始化进度回调,100% 时表示资源加载完成,连接就绪
  • onVoiceStateChange:语音状态回调,当数字人说话结束时触发,用于流式播报的段落控制
  • getPromiseState:检查连接 Promise 的状态,避免在已 resolve 的情况下重复 reject

4.4 llm.ts – 大语言模型服务

基于 OpenAI SDK 封装,支持火山引擎豆包大模型。提供普通对话和流式对话两种模式,流式模式支持实时逐字输出。

import OpenAI from 'openai'
import type { LlmConfig, ChatMessage } from '../types'
import { LLM_CONFIG } from '../constants'
class LlmService {
  private openai: OpenAI | null = null
  private currentApiKey: string = ''
  private initClient(config: LlmConfig): void {
    if (this.currentApiKey === config.apiKey && this.openai) {
      return // 避免重复初始化
    }
    const baseURL = config.baseURL || LLM_CONFIG.BASE_URL
    this.openai = new OpenAI({
      apiKey: config.apiKey,
      dangerouslyAllowBrowser: true, // 允许浏览器端调用
      baseURL: baseURL,
      // 确保使用 fetch API 支持流式
      fetch: (url, init) => {
        console.log('LLM请求URL:', url)
        console.log('LLM请求配置:', { 
          method: init?.method, 
          headers: init?.headers,
          body: init?.body 
        })
        return fetch(url, init)
      }
    })
    this.currentApiKey = config.apiKey
  }
  // 普通对话模式
  async sendMessage(config: LlmConfig, userMessage: string): Promise<string | null> {
    this.initClient(config)
    if (!this.openai) {
      throw new Error('LLM客户端未初始化')
    }
    const messages: ChatMessage[] = [
      { role: 'system', content: LLM_CONFIG.SYSTEM_PROMPT },
      { role: 'user', content: userMessage }
    ]
    try {
      const completion = await this.openai.chat.completions.create({
        messages,
        model: config.model
      })
      const response = completion.choices[0]?.message?.content
      return response || null
    } catch (error) {
      console.error('LLM请求失败:', error)
      throw error
    }
  }
  // 流式对话模式(核心)
  async sendMessageWithStream(config: LlmConfig, userMessage: string): Promise<AsyncIterable<string>> {
    this.initClient(config)
    if (!this.openai) {
      throw new Error('LLM客户端未初始化')
    }
    const messages: ChatMessage[] = [
      { role: 'system', content: LLM_CONFIG.SYSTEM_PROMPT },
      { role: 'user', content: userMessage }
    ]
    try {
      const stream = await this.openai.chat.completions.create({
        messages,
        model: config.model,
        stream: true // 开启流式输出
      })
      // 返回异步迭代器,逐字输出
      return (async function* () {
        let chunkCount = 0
        for await (const part of stream) {
          chunkCount++
          const content = part.choices[0]?.delta?.content
          if (content) {
            yield content
          }
        }
      })()
    } catch (error) {
      console.error('流式请求失败:', error)
      throw error
    }
  }
}
export const llmService = new LlmService()

设计要点

  • dangerouslyAllowBrowser: true:允许在浏览器端直接调用 LLM API,适用于 Demo 场景
  • 自定义 fetch:拦截请求用于日志追踪,便于调试流式数据
  • sendMessageWithStream 返回 AsyncIterable:调用方可通过 for await 逐字消费,实现流式播报
  • API Key 缓存:currentApiKey 避免重复初始化 OpenAI 客户端

4.5 action-manager.ts – 流式播报动作队列

流式播报是灵引的核心交互机制。当 LLM 流式返回文本时,需要将文本分段发送给数字人 SDK 进行语音合成和动作驱动。ActionManager 通过队列模式管理这些播报段落,确保按顺序播放。

import type { Ref } from 'vue'
import type { ActionQueueItem } from '../types'
import { generateSSML } from '../utils'
interface SpeakOptions {
  isStart?: boolean  // 是否为流式对话起始
  isEnd?: boolean    // 是否为流式对话结束
}
interface ActionManagerOptions {
  instanceRef: Ref<any | null>
  onVoiceReady?: () => void
  onVoiceEnd?: () => void
}
export class ActionManager {
  private queue: ActionQueueItem[] = []
  private isSpeaking = false
  private instanceRef: Ref<any | null>
  private onVoiceReady?: () => void
  private onVoiceEnd?: () => void
  constructor(options: ActionManagerOptions) {
    this.instanceRef = options.instanceRef
    this.onVoiceReady = options.onVoiceReady
    this.onVoiceEnd = options.onVoiceEnd
  }
  speak(text: string, options: SpeakOptions = {}) {
    const ssml = generateSSML(text.replace(/n+/g, 'n'))
    this.queue.push({
      ssml,
      isStart: options.isStart ?? false,
      isEnd: options.isEnd ?? false
    })
    this.processQueue()
  }
  reset() {
    this.queue = []
    this.isSpeaking = false
  }
  private async processQueue() {
    if (this.isSpeaking) return
    if (!this.queue.length) return
    const instance = this.instanceRef.value
    if (!instance) return
    this.isSpeaking = true
    while (this.queue.length) {
      const item = this.queue.shift()
      if (!item) break
      this.onVoiceReady?.()
      instance.speak(item.ssml, item.isStart, item.isEnd)
      // 如果是流式中间段,不等待直接继续
      if (!item.isEnd) {
        continue
      }
      // 等待 speak 完成
      await new Promise(resolve => setTimeout(resolve, 50))
    }
    this.isSpeaking = false
    this.onVoiceEnd?.()
  }
}

核心设计

  • SSML 封装:每段文本通过 generateSSML() 转换为 SSML 格式,支持音调、语速、音量参数
  • 队列管理:即使多段文本同时到达,也按 FIFO 顺序逐段播报
  • isStart / isEnd 标记:告知 SDK 当前段落是流式对话的开始、中间还是结束,SDK 据此控制动作连贯性
  • 流式中间段不等待:if (!item.isEnd) continue 确保中间段落快速入队,降低播报延迟

4.6 流式播报分段策略

灵引采用「智能分段 + 首句优先」策略,在 LLM 流式输出过程中实时将文本推送给数字人:

// 流式播报核心逻辑(摘自 app.ts)
// 中文标点符号正则
const cnSplitSign = /[。?!;… ,:]/
// 英文标点符号正则
const enSplitSign = /[.?!;:,]/
const minimum = 20  // 首句最少缓存20个可读字符
const context = {
  cache: '',           // 缓存文本
  chars: 0,           // 缓存的可读字符数
  firstSpeakSend: false, // 是否已发送首句
  spaceCount: 0       // 空格计数(英文分词辅助)
}
// 创建一个 Promise,在第一句发送后立即 resolve
let firstSentenceResolved = false
const firstSentencePromise = new Promise<void>((resolve) => {
  // 在后台继续处理流式数据
  ;(async () => {
    try {
      // 流式播报响应内容
      for await (const content of stream) {
        if (typeof content !== 'string') continue // 防御编程
        context.cache += content // 将该段文本加入缓存
        // 英文以空格开头加一个计数器做分割推送
        if (content.startsWith(' ')) {
          context.spaceCount += 1
        }
        const chars = content.match(/[u4e00-u9fa5a-zA-Z0-9]/g)?.length ?? 0 // 统计段内可读字符数
        let shouldSend = false
        if (!context.firstSpeakSend) {
          // 首句:需要达到最小字符数且遇到标点符号
          shouldSend = context.spaceCount
            ? context.spaceCount > minimum - 1 && enSplitSign.test(content)
            : context.chars > minimum && cnSplitSign.test(content)
        } else {
          // 后续句子:遇到标点符号即可发送
          shouldSend = context.spaceCount ? enSplitSign.test(content) : cnSplitSign.test(content)
        }
        if (!shouldSend) {
          context.chars += chars
          continue
        }
        // 发送缓存的文本
        actionManager.speak(context.cache, {
          isStart: !context.firstSpeakSend,
          isEnd: false
        })
        // 如果是第一句,立即 resolve Promise,让 sendMessage 返回
        if (!context.firstSpeakSend && !firstSentenceResolved) {
          firstSentenceResolved = true
          context.firstSpeakSend = true
          resolve() // 第一句发送后立即返回成功信号
        } else if (context.firstSpeakSend) {
          context.firstSpeakSend = true
        }
        context.cache = ''
        context.chars = 0
        context.spaceCount = 0
      }
      // 处理剩余的缓存文本
      if (context.cache.length > 0) {
        actionManager.speak(context.cache, {
          isStart: !context.firstSpeakSend,
          isEnd: true
        })
        // 如果首句还没发送就结束了(短回复),也要resolve
        if (!firstSentenceResolved) {
          firstSentenceResolved = true
          resolve()
        }
      } else if (context.firstSpeakSend) {
        // 如果已经发送过内容但没有剩余文本,发送结束标记
        actionManager.speak('', {
          isStart: false,
          isEnd: true
        })
      } else {
        // 流结束但没有任何内容,也要resolve避免卡死
        if (!firstSentenceResolved) {
          firstSentenceResolved = true
          resolve()
        }
      }
    } catch (error) {
      console.error('流式处理错误:', error)
      // 如果第一句还没发送就出错了,也要 resolve
      if (!firstSentenceResolved) {
        firstSentenceResolved = true
        resolve()
      }
    }
  })()
})
// 等待第一句发送完成,然后立即返回
await firstSentencePromise
return 'success'

策略解析

  • 首句延迟优化:首句需缓存至少 20 个可读字符(汉字/英文/数字)并遇到标点才发送,避免过短片段导致语音不自然
  • 中英文分治:中文按标点断句,英文额外统计空格数辅助分词
  • 首句即返回:sendMessage() 在第一句发送后立即 resolve,前端无需等待整个流结束就能获得响应
  • 尾部清理:流结束后检查剩余缓存,确保最后一段文本也被播报

4.7 useAsr.ts – 语音识别 Composable

基于腾讯云 ASR WebSocket 接口封装的语音识别 Composable,支持 VAD(语音活动检测)自动断句。

import { ref } from 'vue'
import type { AsrConfig, AsrCallbacks } from '../types'
import { ASR_CONFIG } from '../constants'
import { signCallback } from '../lib/asr'
export function useAsr(config: AsrConfig) {
  const asrText = ref('')
  const isListening = ref(false)
  let webAudioSpeechRecognizer: any = null
  const buildAsrConfig = (vadSilenceTime?: number) => ({
    signCallback: signCallback.bind(null, config.secretKey),
    appid: config.appId,
    secretid: config.secretId,
    secretkey: config.secretKey,
    engine_model_type: ASR_CONFIG.ENGINE_MODEL_TYPE, // '16k_zh'
    voice_format: ASR_CONFIG.VOICE_FORMAT,           // 1 = PCM
    filter_dirty: ASR_CONFIG.FILTER_DIRTY,           // 过滤脏话
    filter_modal: ASR_CONFIG.FILTER_MODAL,           // 过滤语气词
    filter_punc: ASR_CONFIG.FILTER_PUNC,             // 过滤标点
    convert_num_mode: ASR_CONFIG.CONVERT_NUM_MODE,   // 数字转换
    word_info: ASR_CONFIG.WORD_INFO,                 // 词信息
    needvad: ASR_CONFIG.NEEDVAD,                     // 启用VAD
    vad_silence_time: vadSilenceTime || config.vadSilenceTime || 300
  })
  const start = (callbacks: AsrCallbacks, vadSilenceTime?: number) => {
    // ... 创建 SpeechRecognizer 实例并开始录音
  }
  const stop = () => {
    // ... 停止录音
  }
  return { asrText, isListening, start, stop }
}

配置说明

  • engine_model_type: ‘16k_zh’:16kHz 采样率中文语音识别
  • needvad: 1:启用 VAD 自动检测语音起止,vad_silence_time: 300 表示静音 300ms 后自动断句
  • signCallback:腾讯云 ASR 的签名回调,使用 HMAC-SHA256 算法对请求进行鉴权

4.8 游戏主题 UI 架构

灵引采用深色电竞主题设计,以 #0a0e1a 为底色,青蓝 #00d4ff 与紫色 #a855f7 作为点缀色,营造专业电竞氛围。

页面结构

┌─────────────────────────────────────────────┐
│  🎮 灵引    首页  攻略  赛事  商城  社区   🔍 │  ← 顶部导航
├─────────────────────────────────────────────┤
│                                             │
│  ┌─────────────────────────────────────┐    │
│  │  Hero Banner 轮播(赛季/赛事/活动)  │    │  ← 主内容滚动区
│  └─────────────────────────────────────┘    │
│                                             │
│  ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐      │
│  │角色扮演│ │MOBA  │ │FPS   │ │策略卡牌│      │  ← 游戏分类
│  └──────┘ └──────┘ └──────┘ └──────┘      │
│                                             │
│  ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐      │
│  │ 皮肤  │ │ 键盘 │ │ 鼠标 │ │ 耳机  │      │  ← 电竞装备
│  └──────┘ └──────┘ └──────┘ └──────┘      │
│                                             │
│  ┌──────────────────────────────────┐       │
│  │ 🎯 灵引 策略伙伴          [+/-]  │       │  ← 对话面板(右下角悬浮)
│  │  [快捷问题chips]                 │       │
│  │  [输入框] [发送]                 │       │
│  │  [🎤 按住说话] [⏸ 打断]         │       │
│  └──────────────────────────────────┘       │
└─────────────────────────────────────────────┘
         + 数字人以透明背景叠加在右下角

CSS 主题变量

:root {
  /* 灵引电竞主题 - 青蓝紫渐变点缀深色系 */
  --color-primary: #00d4ff;       /* 青蓝主色 */
  --color-secondary: #a855f7;     /* 紫色辅色 */
  --bg-primary: #0a0e1a;          /* 深色背景 */
  --bg-secondary: #0f172a;        /* 次级背景 */
  --text-primary: #f1f5f9;        /* 主文字色 */
  --text-secondary: #94a3b8;      /* 次级文字色 */
  --border-color: rgba(0, 212, 255, 0.15); /* 青蓝半透明边框 */
  --glow-primary: 0 0 15px rgba(0, 212, 255, 0.2); /* 青蓝光晕 */
}

4.9 数字人渲染组件

AvatarRender 组件负责承载 SDK 渲染容器,并以固定定位叠加在游戏页面右下角。

<template>
  <div class="avatar-render">
    <!-- SDK 渲染容器 - 透明背景 -->
    <div :id="containerId" class="sdk-container" />
    <!-- 字幕显示 - 数字人脚下 -->
    <div v-if="appState.ui.subTitleText" class="subtitle-area">
      <div class="subtitle-text">{{ appState.ui.subTitleText }}</div>
    </div>
    <!-- 语音输入动画 -->
    <div v-show="appState.asr.isListening" class="voice-animation">
      <img :src="siriIcon" alt="语音输入" />
    </div>
    <!-- 加载状态 -->
    <div v-if="!appState.avatar.connected" class="loading-placeholder">
      <div class="loading-spinner"></div>
      <div class="loading-text">灵引准备中...</div>
    </div>
  </div>
</template>
<style scoped>
.avatar-render {
  position: fixed;
  right: 20px;
  bottom: 230px;
  width: 400px;
  height: 70vh;
  max-height: 750px;
  z-index: 100;
  pointer-events: none;
  display: flex;
  justify-content: flex-start;
  align-items: flex-end;
  overflow: visible;
}
.sdk-container {
  position: relative;
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: flex-start;
  align-items: flex-end;
  overflow: visible;
  z-index: 1;
  background: transparent !important;
}
</style>

设计要点

  • pointer-events: none:数字人层不拦截鼠标事件,用户仍可点击底层游戏页面元素
  • background: transparent !important:强制透明背景,确保数字人叠加在游戏页面上不遮挡内容
  • z-index: 100:低于对话面板的 z-index: 200,确保交互面板始终在数字人之上

4.10 应用入口与自动连接

App.vue 作为根组件,在挂载时自动发起数字人连接,支持最多 3 次重试。

<script setup lang="ts">
import { provide, onMounted, nextTick } from 'vue'
import ExhibitionPanel from './components/ExhibitionPanel.vue'
import AvatarRender from './components/AvatarRender.vue'
import { appState, appStore } from './stores/app'
// 提供全局状态和方法
provide('appState', appState)
provide('appStore', appStore)
// 自动连接虚拟人(最多重试3次)
onMounted(async () => {
  await nextTick()
  for (let attempt = 1; attempt <= 3; attempt++) {
    try {
      await appStore.connectAvatar()
      console.log('[App] 虚拟人连接成功')
      break
    } catch (error) {
      console.error(`[App] 第${attempt}次连接失败:`, error)
      if (attempt < 3) {
        await new Promise(r => setTimeout(r, 2000))
      }
    }
  }
})
</script>
<template>
  <div class="app-container">
    <!-- 全屏主内容区域 -->
    <ExhibitionPanel />
    <!-- 右下角数字人叠加层 - 透明背景 -->
    <AvatarRender />
  </div>
</template>

连接流程

  • 应用挂载后,通过 appStore.connectAvatar() 发起连接
  • 连接失败时等待 2 秒后重试,最多 3 次
  • 使用 Vue 的 provide/inject 机制将全局状态注入所有子组件

五、总结

本文基于魔珐星云 XmovAvatar SDK 参数流架构,结合 Vue 3.5 + TypeScript + Vite 技术栈,从零搭建了「灵引 LingGuide」具身交互智能策略伙伴。整套方案打通了数字人渲染、LLM 流式对话、ASR 语音识别三大链路,通过首句即播报、智能分段、实时打断等机制将交互延迟压至 500ms 以内,并以透明叠加渲染将数字人无缝融入游戏页面,实现了文字 + 语音双模、看得见听得到能思考的沉浸式策略伙伴体验。这个项目也说明:当 AI Coding 工具负责快速搭建工程骨架,魔珐星云负责具身表达和终端交互,游戏 Agent 就能更快从 Demo 走向可用成品。

魔珐星云PC端官方链接:https://xingyun3d.com?utm_campaign=daily&utm_source=CSDNwanfen3&utm_medium=&utm_term=&utm_content=

© 版权声明

相关文章