说一下未来已来——如何在VR游戏中实现3D语音

我们实际使用GME SDK完成相关的开发,一起来看下代码是如何运行的。本篇是基于Google开源的CardBoard SDK进行的示例程序。

导入SDK

导入 GoogleVR SDK

GoogleVR SDK 官网点击下载SDK,我们演示使用的版本为GVR SDK for Unity v1.200.1。下载完成之后,新建一个 Unity 工程,我们演示使用的版本为 Unity 2018.4.23f1。双击 GoogleVRForUnity_1.200.1.unitypackage 导入SDK。

导入 GME SDK

点击下载指引,找到 【SDK v2.7.1 版本】,点击下载 Unity SDK。解压后将文件拷贝到Unity工程中,删除 Plugin 中的平台文件夹,只保留 Android、gmesdk.bundle以及x86_64。详细参考游戏多媒体引擎Unity工程配置

使用GoogleVR SDK

1、找到示例工程

找到 GoogleVR->Demos->Scenes 下的HelloVR场景,双击打开。

2、工程配置

在PlayerSettings中的XRSettings一栏,勾选Virtual Reality Supported,再添加 Cardboard 组件。

在 Prefernce 中的 External Tools下设置好 Android SDK 的路径及 Android NDK 的路径。

3、导出

将场景HelloVR添加到 Scene In Build,将 Platform 切换到 Android,设置好导出时候的 Package Name,便可以导出验证。

使用GME实时语音

游戏多媒体引擎Unity接入文档首先创建一个代码文件,名字为 GMEVoice,在工程中新建一个空物体,将代码挂载在空物体上。

双击代码文件 GMEVoice,在代码中引入GME。

using TencentMobileGaming;

1、初始化SDK

初始化GME SDK需要 Appid,请在腾讯云游戏多媒体引擎控制台申请,此处我们使用官方提供的测试 Appid。另外还需要自定义一个OpenId,数值大于10000,我们使用随机数来产生。

int int_OpenId = UnityEngine.Random.Range(12345, 22345);
string str_OpenId = int_OpenId.ToString();
int isInit = ITMGContext.GetInstance().Init("1400089356", str_OpenId);

2、调用 Poll 函数

在 Update 中我们调用 Poll 函数。

void Update()
{
     ITMGContext.GetInstance().Poll();
}

3、进入语音房间

进入语音房间需要鉴权,鉴权需要的AuthKey在腾讯云游戏多媒体引擎控制台上获取,与AppID对应。鉴权的具体参数及获取方法参考游戏多媒体引擎Unity接入文档鉴权部分。此处我们使用官方给的测试账号的AppID、AuthKey,进入的房间为 20200601,OpenId为随机出来的数字转string类型。

byte[] authBuffer = GetAuthBuffer("1400089356", "20200601", str_OpenId, "1cfbfd2a1a03a53e");
public static byte[] GetAuthBuffer(string AppID, string RoomID, string OpenId, string AuthKey)
{
    return QAVAuthBuffer.GenAuthBuffer(int.Parse(AppID), RoomID, OpenId, AuthKey);
}

接下来我们对进房事件进行监听,然后在 Unity 的Start 中调用进房函数。

ITMGContext.GetInstance().OnEnterRoomCompleteEvent += new QAVEnterRoomComplete(OnEnterRoomComplete);
ITMGContext.GetInstance().EnterRoom("20200601",ITMGRoomType.ITMG_ROOM_TYPE_FLUENCY, authBuffer);

4、进房回调

调用进房接口之后,需要监听回调并在回调中处理进房结果。如果进房成功便打开麦克风及扬声器。

void OnEnterRoomComplete(int err, string errInfo)
{
    if (err != 0)
        {
            Debug.Log("进房失败,错误码为:" + err);
            return;
        }
    else
        {
            //进房成功
            //打开麦克风
            ITMGContext.GetInstance().GetAudioCtrl().EnableMic(true);
            //打开扬声器
            ITMGContext.GetInstance().GetAudioCtrl().EnableSpeaker(true);
        }
}

5、反初始化

我们需要在销毁代码的时候反初始化整个SDK。

void OnDestroy()
{ 
    ITMGContext.GetInstance().Uninit();    
}

这时候我们可以先尝试一下导出两个windows可执行文件,验证是否进房成功,是否能在进入游戏后就打开麦克风及扬声器进行实时语音对话。

使用3D音效

如果以上步骤完成后,能够进入游戏后进行实时语音通话,那么我们接下来开始接入3D音效效果。游戏多媒体引擎3D音效文档

1、引入音效文件

点击下载地址下载音效文件,此文件为官方提供。我们把这个模型文件命名为3d_model,并放在Unity工程目录StreamingAssets下,确保能随可执行文件一同导出。我们写一个协程,用来将这个音效文件拷贝到Application.persistentDataPath下,方便引用。

StartCoroutine(copyFileFromAssetsToPersistent("3d_model"));

public IEnumerator copyFileFromAssetsToPersistent(string fileName)
{
    string fromPath = Application.streamingAssetsPath + "/" + fileName;
    string toPath = Application.persistentDataPath + "/" + fileName;
    if (!File.Exists(toPath))
    {
        Debug.Log("copying from " + fromPath + " to " + toPath);
#if UNITY_ANDROID && !UNITY_EDITOR
        WWW www1 = new WWW(fromPath);
        yield return www1;
        File.WriteAllBytes(toPath, www1.bytes);
        Debug.Log("file copy done");
        www1.Dispose();
        www1 = null;
#else
        File.WriteAllBytes(toPath, File.ReadAllBytes(fromPath));
#endif
    }
        yield return null;
}

2、初始化3D音效

我们在进房成功的回调中,初始化3D音效,路径为我们协程中写的路径。

string filePath = Application.persistentDataPath + "/3d_model";
int ret = QAVError.OK;
ret = ITMGContext.GetInstance().GetAudioCtrl().InitSpatializer(filePath);

3、开启3D音效

使用接口EnableSpatializer开启3D音效,在这里我们进房成功后,初始化3D音效成功后就启动3D音效。第二个参数与范围语音有关,此处不需关注。

ITMGContext.GetInstance().GetAudioCtrl().EnableSpatializer(true, false);

4、设置范围

设置语音的接收范围,此处的范围参数涉及到衰减,单位为Unity中的距离单位,为了突出效果,此处我们设置为100000。

ITMGContext.GetInstance().GetRoom().UpdateAudioRecvRange(100000);

5、更新自身坐标

通过更新坐标到服务器,游戏多媒体引擎服务器会根据房间内成员的坐标将声音进行3D处理,为了保证3D音效的实时性,需要在Update函数中每帧更新自身坐标。

在此Demo中,由于我们的代码挂载在另一个空物体上,所以我们需要将摄像机的位置实时传到接口中,我们声明一个GameObject,用于传递Demo中Player的坐标。

public GameObject currentPlayer;

在Unity编辑器中,我们将Player附给currentPlayer。

此处为了使3D效果明显,我们更新坐标时,将position都乘以一个100的系数进行放大。

void Update()
{
      ITMGContext.GetInstance().Poll();
      if (isRoomEntered)
      {
            Transform selftrans = currentPlayer.gameObject.transform;
            Matrix4x4 matrix = Matrix4x4.TRS(Vector3.zero, selftrans.rotation, Vector3.one);
            int[] position = new int[3] {
                     (int)selftrans.position.z*100, 
                     (int)selftrans.position.x*100, 
                     (int)selftrans.position.y*100 
                  };
           float[] axisForward = new float[3] { matrix.m22, matrix.m02, matrix.m12 };
           float[] axisRight = new float[3] { matrix.m20, matrix.m00, matrix.m10 };
           float[] axisUp = new float[3] { matrix.m21, matrix.m01, matrix.m11 };
           ITMGContext.GetInstance().GetRoom().UpdateSelfPosition(position,
             axisForward, 
             axisRight, 
             axisUp);
       }
}

最终代码:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO;
using TencentMobileGaming;
public class GMEVoice : MonoBehaviour
{
    // 用于传递摄像机位置
    public GameObject currentPlayer;
    // 用于判断进房状态
    private bool isRoomEntered = false;

    // Start is called before the first frame update
    void Start()
    {
        // 初始化
        int int_OpenId = UnityEngine.Random.Range(12345, 22345);
        string str_OpenId = int_OpenId.ToString();
        int isInit = ITMGContext.GetInstance().Init("1400089356", str_OpenId);

        // 拷贝3D音效文件
        StartCoroutine(copyFileFromAssetsToPersistent("3d_model"));

        // 进房
        byte[] authBuffer = GetAuthBuffer("1400089356", "20200601",
 str_OpenId, "1cfbfd2a1a03a53e");
    if (isInit == 0) {
       ITMGContext.GetInstance().OnEnterRoomCompleteEvent +=
 new QAVEnterRoomComplete(OnEnterRoomComplete);
       ITMGContext.GetInstance().EnterRoom("20200601", ITMGRoomType.ITMG_ROOM_TYPE_FLUENCY, authBuffer);
    }
}

// Update is called once per frame
void Update()
{
   ITMGContext.GetInstance().Poll();

   // 如果进房成功,开始更新自身位置
   if (isRoomEntered)
   {
       Transform selftrans = currentPlayer.gameObject.transform;
       Matrix4x4 matrix = Matrix4x4.TRS(Vector3.zero, selftrans.rotation, Vector3.one);
       int[] position = new int[3] {
 (int)selftrans.position.z*100,
 (int)selftrans.position.x*100, 
 (int)selftrans.position.y*100 };
       float[] axisForward = new float[3] { matrix.m22, matrix.m02, matrix.m12 };
       float[] axisRight = new float[3] { matrix.m20, matrix.m00, matrix.m10 };
       float[] axisUp = new float[3] { matrix.m21, matrix.m01, matrix.m11 };
      ITMGContext.GetInstance().GetRoom().UpdateSelfPosition(position, 
axisForward, 
axisRight, 
axisUp);
    }
}
    
/// <summary>
/// 鉴权
/// </summary>
/// <param name="AppID"></param>
/// <param name="RoomID"></param>
/// <param name="OpenId"></param>
/// <param name="AuthKey"></param>
/// <returns></returns>
public static byte[] GetAuthBuffer(string AppID, string RoomID, 
string OpenId, string AuthKey)
{
return QAVAuthBuffer.GenAuthBuffer(int.Parse(AppID), RoomID, OpenId, AuthKey);
}

/// <summary>
/// 进房成功回调处理
/// </summary>
/// <param name="err"></param>
/// <param name="errInfo"></param>
void OnEnterRoomComplete(int err, string errInfo)
{
if (err != 0)
{
Debug.Log("进房失败,错误码为:" + err);
       return;
}
else
{
      isRoomEntered = true;
      //进房成功
      //打开麦克风
      ITMGContext.GetInstance().GetAudioCtrl().EnableMic(true);
      //打开扬声器
      ITMGContext.GetInstance().GetAudioCtrl().EnableSpeaker(true);

      // 初始化3D音效
      string filePath = Application.persistentDataPath + "/3d_model";
      Debug.Log("3D音效文件路径:"+ filePath);
      int ret_Init = ITMGContext.GetInstance().GetAudioCtrl().InitSpatializer(filePath);
      Debug.Log("初始化3D音效是否成功:"+ ret_Init);

          int ret_Enable = ITMGContext.GetInstance().GetAudioCtrl().EnableSpatializer(true, 
false);
      Debug.Log("开启3D音效是否成功:" + ret_Enable);
      if (ret_Enable != QAVError.OK)
      {
          return;
      }

      ITMGContext.GetInstance().GetRoom().UpdateAudioRecvRange(100000);
}
}

/// <summary>
/// 反初始化
/// </summary>
void OnDestroy()
{ 
ITMGContext.GetInstance().Uninit();    
}

/// <summary>
/// 用于从streamingAssets拷贝文件
/// </summary>
/// <param name="fileName"></param>
/// <returns></returns>
public IEnumerator copyFileFromAssetsToPersistent(string fileName)
{
string fromPath = Application.streamingAssetsPath + "/" + fileName;
string toPath = Application.persistentDataPath + "/" + fileName;
if (!File.Exists(toPath))
{
         Debug.Log("copying from " + fromPath + " to " + toPath);
#if UNITY_ANDROID && !UNITY_EDITOR
         WWW www1 = new WWW(fromPath);
         yield return www1;
         File.WriteAllBytes(toPath, www1.bytes);
         Debug.Log("file copy done");
         www1.Dispose();
         www1 = null;
#else
          File.WriteAllBytes(toPath, File.ReadAllBytes(fromPath));
#endif
}
yield return null;
}
}

验证并导出

我们将导出Android可执行文件apk文件进行验证。我们将Player的坐标x设置为3,导出一个apk,之后调整x的数值为-3,再导出一个apk,这样两个player的位置便是不同的。

进入VR游戏后,我们可以听到3D效果的实时语音。

技术创作101训练营

正文完