渴望在游戏中与小队无缝沟通

与好友共享开黑乐趣?

Hello,你的实时互动神器,让游戏沟通从此无界限!

在激烈的对战和广阔的虚拟世界中,实时语音交流是提升体验的关键。本教程将通过 BossRoomHelloDemo 的示例项目工程,展示如何使用 Unity Online Services(UOS)提供的Hello 服务,来实现游戏内的低延时、高品质的多人实时语音互动

教程中涉及 UOS 服务包括:

  • 游戏内语音服务 Hello:助力全球游戏实现超低延时、高品质实时语音互动

游戏内语音服务 Hello

UOS Hello(后续简称Hello)是 Unity 官方推出,声网®提供运营和技术支持的,低延时、高品质全球游戏内置实时语音服务,可以让玩家在游戏中实现实时、清晰、流畅的语音交流。

全面支持社交、竞技、低耗、世界、指挥等五大模式,提供酣畅淋漓的游戏体验。支持手游、端游、页游,同时支持这些游戏的多平台互通。适配各大洲主流硬件机型,保障全球优质网络覆盖,游戏语音超低延时、全球同服触手可及!适用于 FPS、Moba、MMORPG、社交元宇宙等游戏场景的语音功能实现。

为什么选择 Hello

1. 更全机型适配:各大洲核心地区主力机型深度适配,覆盖了30+平台开发框架,30000+移动终端,满足海外区域复杂的平台及机型适配要求。

2. 性能开销低:支持中低端显卡设备流畅运行,避免发热发烫、卡顿闪退等不良现象,体验更具性价比。

3. 全球覆盖:无论 APP 在欧美、东南亚、中东还是非洲,都能享受卓越的娱乐互动体验;声网®是RTC 领域唯一一个全球化服务团队,在全球超过 10 个区域设有办公室,可提供就近的本地化专业服务。

教程视频

教程学习大纲

2. 启用 Sync Relay & Hello 服务,配置房间 ID 号

3. 初始化 Hello SDK 和 SDK 鉴权

4. 初始化 RTC 语音引擎

5. 加入频道和检查设备权限

6. 开启麦克风和听筒,开始音频互动测试

7. 测试开启和关闭空间音频的效果

8. 调整空间音频参数(语音接收范围、衰减系数、人声模糊)和通话音量

9. 用户离开频道

教程示例程序

教程内学习用到的项目工程,可以通过下面提供的链接进行下载:

BossRoomHelloDemo 项目工程下载地址:

https://unitychina.coding.net/p/uos/d/BossRoomHelloDemo/git

教程操作步骤

接下来让我们跟着教程步骤开始学习吧!

1.1 下载项目工程

我们首先从提供的链接下载 BossRoomHelloDemo 的示例项目工程,你可以自行通过 git 的方式将代码仓库克隆到本地目录,也可以直接点击 “下载 ZIP” 的按钮来下载项目的压缩包文件。


教程中我先通过下载项目的压缩包文件的方式来讲解,等下载好压缩包文件以后,解压缩项目。

然后通过 Unity hub 打开该项目,推荐大家使用 Unity 2022LTS 以及以上的更高版本。在这里,我先用Unity 2022.3.41版本来打开项目了。


打开项目工程以后,我们会首先看到一个 UOS 服务弹出的欢迎窗口页面,大家后续可以根据自己制作的游戏的类型,进行选择对应的游戏模板后,进一步来查看符合自己游戏功能的可使用的 UOS 服务。

1.2 配置 UOS App 信息然后可以直接点击欢迎页面的「打开 UOS Launcher」按钮,或者点击菜单栏「UOS -> Open Launcher」,就可以打开 UOS 的 App 信息配置窗口了。

在最新版本的 UOS Launcher 中,大家会看到 UI 界面和之前不太一样了,因为我们更新升级了 Launcher 的功能。我们先点击 “Link App” 的按钮:


会弹出下面的窗口,给大家提供了 2 种方式来为你的项目绑定 UOS App信息。


UOS App 信息绑定方式 1

我们先来看第一种信息绑定的方式,“By App ID/Secret”,这种方式和之前是一样的,需要大家把创建好的 UOS 应用的 App 信息复制后填写在这里。


点击「UOS Developer Portal」按钮,接着会进入 UOS 官网,新建一个 UOS 应用。我们选择一个创建好的组织,输入项目的名字后,然后点击「创建并启用」。接着在 UOS 网页的「设置」页面,复制一下 UOS App 的信息。


然后在 Link App 面板中填写复制的 App 信息,并点击「Link App」按钮,就可以在 UOS Launcher 面板中看到自动关联的 App Name 和 App ID 信息了。


UOS App 信息绑定方式 2

我们再来看看第二种信息绑定的方式,“By Unity project”。由于「UOS Launcher」功能升级,给我们提供了一种新的方式,不需要手动在 UOS 网页端复制 App 信息,可以直接选择创建好的 UOS 应用即可。

在窗口页面中:

  • 第1步,“Select organization” 这里,先选择你的一个项目组织;

  • 第2步,“Select project” 这里,有 2 个选项,先点击按钮 “Select an exsting project”,然后在 “Project” 选项这里,从项目的下拉菜单中找到刚才已经创建过的项目;

  • 最后点击【Link App】按钮,就可以看到 Unity 项目工程自动和 UOS App 进行绑定好了。


第 2 步的时候,如果我们选择 "Create a new project" 的话,UOS Launcher 会自动帮我们创建一个 UOS 应用的,"Project name" 这里会自动填充为:项目名字 + 当前日期时间的。然后点击【Link App】按钮后,也是可以自动关联创建的 UOS 应用了。


大家可以进入菜单栏 Edit → Project Settings → Player → Product Name 这里,看到刚才使用的默认的项目名 BossRoomHello。


在这里的话,我还是先切回到我准备绑定的 UOS 应用:UOSHello_Demo。

2.启用 Sync Relay & Hello 服务,配置房间 ID 号

2.1 开启 Sync Relay 服务

接着在 UOS 的下拉服务列表中,找到 Sync - Relay,点击 Enable 开启服务。

2.2 开启 Hello 服务

继续在 UOS 的下拉服务列表中,找到 Hello 服务,点击 Enable 按钮后会自动链接到 UOS 网页端。

在【概览】页面,找到 Hello 服务,点击【免费试用】的按钮。

在弹出的窗口中,需要先填写联系人信息。

填写完信息并保存后,可以看到 Hello 服务已经开通了。

2.3 设置 SyncRelay 的房间 ID 号

在 UOS 网页端找到创建的房间配置 ID 号,先进行复制。

然后再找到 Startup 场景中的【NetworkManager】对象, 将刚才复制的房间配置 ID 号,填入 RelayTransport 组件的RoomProfileUUID 参数这里。为了在 Web 端正常通信,传输协议 Transport Type 这里,我们就使用帮我们设置好的 Websocket 协议。

这里参数设置完以后,大家就可以运行游戏开始体验语音互动的效果了。接下来,就详细展开来讲讲如何在项目中接入 UOS 的 Hello 语音功能!

3.初始化 Hello SDK 和 SDK 鉴权3.1 游戏语音基本流程

下图展示了利用 Hello SDK 实现音频互动的工作流程。

相关的基本概念:

频道:由开发者调用 Hello 提供的 API 创建的、用于传输实时数据的通道。

发布:指频道中的用户将音视频数据发送到频道的操作。

订阅:指频道中的用户接收频道内已发布的音视频流的操作。

用户角色:用于定义频道内用户是否有发流权限,分别有主播和观众两种用户角色。

主播:频道内有发流权限的用户。

观众:频道内没有发流权限的用户。观众只能订阅远端音视频流,不能发布音视频流。

SD-RTN™:是 Software-Defined Real-Time Network 的缩写,即软件定义实时网,这是声网自建的底层实时传输网络。

3.2 初始化 HelloSDK

游戏运行后,可以在 MainMenu 场景中先找到动态生成的的 RTCAudioProxy 游戏对象,它身上挂载了项目中夫的 UOS Hello 语音功能的代码控制的核心脚本 RTCAudioProxy.cs。

在脚本中,首先封装了一个 InitHelloSDK 的方法,来实现对 Hello SDK 的实例化。由于项目中已经安装了 UOS Launcher,所以在 HelloSDK.InitializeAsync 的方法中,会使用 UOS Launcher 关联的 UOS App 信息来初始化 Hello SDK 的。

private async Task Init()
{
if (RtcEngine == null)
{
await InitHelloSDK();
        //此处省略其它代码行......
}
}

private async Task InitHelloSDK()
{
try
{
await HelloSDK.InitializeAsync();
}
catch (HelloClientException e)
{
Debug.Log($"failed to initialize sdk, clientEx: {e.Message}");
throw;
}
catch (HelloServerException e)
{
Debug.Log($"failed to initialize sdk, serverEx: {e.Message}");
throw;
}
}

3.3 SDK鉴权

由于当前项目中未使用 UOS Passport,所以在这里我们通过调用外部登录的方法 AuthTokenManager.ExternalLogin 来获取 AccessToken,方法内将根据系统分配的客户端的用户的 Id 信息来生成用户的身份凭证。

private uint userId;
private async Task Init()
{
if (RtcEngine == null)
{
await InitHelloSDK();
await AuthTokenManager.ExternalLogin(userId.ToString());
//此处省略其它代码行......
}
}

可以查看下 AuthTokenManager.cs 脚本中的进行鉴权的 ExternalLogin 方法,会根据当前用户的 UOS App 信息进行身份验证的。如果当前项目关联的 UOS App 信息不为空,则会调用异步方法 GenerateAccessTokenWithUosAppId 来生成访问的令牌。

public static async Task ExternalLogin(string userID, string personaID = null, string displayName = null)
{
if (string.IsNullOrEmpty(Instance.CurrentUosAppID))
{
throw new AuthException(ErrorCode.NotInitialized, "AuthTokenManager未经初始化");
}
await ExternalLoginWithAppId(Instance.CurrentUosAppID, userID, personaID, displayName);
}

4.初始化 RTC 语音引擎4.1 使用 HelloSDK 初始化 RTC 语音引擎

调用 HelloSDK 封装的 InitRtcEngine 方法来初始化语音引擎 IRtcEngine 实例。初始化实例时,需要传入2个参数,分别是 context 和 handler。

在 context 参数中需要同时设置好频道场景、音频场景、访问区域。

CHANNEL_PROFILE_TYPE:指的是频道场景,默认类型为直播场景(CHANNEL_PROFILE_LIVE_BROADCASTING)。推荐使用默认的直播场景以获取更好的音频体验。

AUDIO_SCENARIO_TYPE:指的是音频场景,默认类型是自动场景(AUDIO_SCENARIO_DEFAULT),会根据用户的角色和音频路由自动匹配合适的音质。

AREA_CODE:指的是 SDK 连接的服务器所在的区域。这里使用的是 AREA_CODE_GLOB,表示全球都可以访问服务器。

private IRtcEngine RtcEngine;

private async Task Init()
{
if (RtcEngine == null)
{
await InitHelloSDK();
await AuthTokenManager.ExternalLogin(userId.ToString());
await CreateRTCEngine();
}
}

private async Task CreateRTCEngine()
{
var handler = new UserEventHandler();
handler.OnUserJoinedEvent += OnUserJoined;
handler.OnUserOfflineEvent += OnUserOffline;

var context = new RtcEngineContext
{
channelProfile = CHANNEL_PROFILE_TYPE.CHANNEL_PROFILE_LIVE_BROADCASTING,
audioScenario = AUDIO_SCENARIO_TYPE.AUDIO_SCENARIO_DEFAULT,
areaCode = AREA_CODE.AREA_CODE_GLOB
};

RtcEngine = await HelloSDK.InitRtcEngine(context, handler);
     Debug.Log($"successfully init agora rtc engine");
//此处省略其它代码行......
}

AUDIO_SCENARIO_TYPE 分为以下几种类型:

  • AUDIO_SCENARIO_DEFAULT

0: (默认)自动场景,根据用户角色和音频路由自动匹配合适的音质。

  • AUDIO_SCENARIO_GAME_STREAMING

3: 高音质场景,适用于音乐为主的场景。例如:乐器陪练。

  • AUDIO_SCENARIO_CHATROOM

5: 聊天室场景,适用于用户需要频繁上下麦的场景。例如:教育场景。

  • AUDIO_SCENARIO_CHORUS

7: 合唱场景。适用于网络条件良好,要求极低延时的实时合唱场景。

  • AUDIO_SCENARIO_MEETING

8: 会议场景,适用于人声为主的多人会议。

  • AUDIO_SCENARIO_NUM

枚举的数量。

温馨提醒:游戏场景推荐大家使用 AUDIO_SCENARIO_DEFAULT、AUDIO_SCENARIO_GAME_STREAMING、AUDIO_SCENARIO_CHATROOM 这三种模式,不推荐使用其他模式。

初始化语音引擎 RtcEngine 实例时,还需要传入一个 Handle 类型的参数,并提前注册好了相关事件。这里自定义了一个 UserEventHandler.cs 脚本类,脚本中封装了几个方法,后续会使用来处理用户加入/离开频道、远端用户加入/离线时等等的行为。

internal class UserEventHandler : IRtcEngineEventHandler
{
public event System.Action

 OnJoinChannelSuccessEvent; public event System.Action

 OnUserJoinedEvent; public event System.Action

 OnUserOfflineEvent; public UserEventHandler() { } public override void OnJoinChannelSuccess(RtcConnection connection, int elapsed) { Debug.Log("Join Channel Success " + connection.channelId + " " + connection.localUid); OnJoinChannelSuccessEvent?.Invoke(connection.localUid); } public override void OnLeaveChannel(RtcConnection connection, RtcStats stats) { Debug.Log("Leave Channel Success" + connection.channelId + " " + connection.localUid); } public override void OnUserJoined(RtcConnection connection, uint remoteUid, int elapsed) { Debug.Log("OnUserJoined " + connection.channelId + " " + remoteUid); OnUserJoinedEvent?.Invoke(remoteUid); base.OnUserJoined(connection, remoteUid, elapsed); } public override void OnUserOffline(RtcConnection connection, uint remoteUid, USER_OFFLINE_REASON_TYPE reason) { Debug.Log("OnUserOffline " + connection.channelId + " " + remoteUid); OnUserOfflineEvent?.Invoke(remoteUid); base.OnUserOffline(connection, remoteUid, reason); } public override void OnError(int err, string msg) { Debug.LogWarning("######### OnError ###########" + err + " " + msg); } } 



4.2 配置语音引擎

初始化语音引擎示例后,我们调用 EnableAudio 方法来启用音频模块。用户的角色类型分为主播和观众,SDK 默认设置的用户角色为观众。在这里,我们调用 SetClientRole 方法将角色类型修改为主播

private async Task CreateRTCEngine()
{
//此处省略其它代码行......

RtcEngine = await HelloSDK.InitRtcEngine(context, handler);
Debug.Log($"successfully init agora rtc engine");

RtcEngine.EnableAudio();
    RtcEngine.SetClientRole(CLIENT_ROLE_TYPE.CLIENT_ROLE_BROADCASTER);
}

通过下面的图示,我们来看看主播和观众两种角色的区别:

  • 主播:可以在频道内 发布 音视频,同时也可以 订阅 其他主播发布的音视频。

  • 观众:可以在频道内 订阅 音视频,不具备发布 音视频权限。

4.3 获取用户在频道内的权限凭证 AccessToken

在加入频道之前,我们还需要先进行鉴权,来获取用户在指定频道内的权限凭证。

脚本中封装了方法 GenerateAccessToken 来实现这个功能。在方法中根据传入的频道名字、语音通话的角色类型是发布者还是订阅者,来获取到用户在指定频道内的权限凭证后,保存在变量 accessToken 中。这个方法,在后面的加入频道的方法调用之前,会被执行调用的。

private string accessToken;

private async Task GenerateAccessToken(string channel, HelloOptions options = null)
{
try
{
var tokenInfo = await HelloSDK.Instance.GenerateAccessToken(channel, options);
accessToken = tokenInfo.AccessToken;
}
catch (HelloClientException e)
{
Debug.LogErrorFormat("failed to generate token, clientEx: {0}", e.Message);
throw;
}
catch (HelloServerException e)
{
Debug.LogErrorFormat("failed to generate token, serverEx: {0}", e.Message);
throw;
}
}

5.加入频道和检查设备权限

5.1 创建加入频道的方法

当用户加入房间后,会调用JoinChannel 方法来加入当前的语音频道。

  • 在用户加入房间的时候,将当前客户端的 id 号赋值给变量 userId。然后调用 Init 方法来初始化 Hello SDK 和语音引擎对象。

  • 如果远端用户离线再上线的话,参数 uid 是会发生变化的,所以会再调用一次AuthTokenManager.ExternalLogin 方法进行验证用户的身份。

  • 然后再调用一下刚才封装好的方法 GenerateAccessToken 来获取用户在指定频道内的权限凭证。

  • 接着可以调用 JoinChannel 的方法加入语音频道了。

  • 同时也会通过一个bool 值类型的标志变量 isInChannel 记录下当前用户已经在频道内了。

  • 加入频道时,也可以传入一个 bool 值类型的变量 enableSpatialAudio ,来供用户选择是否要开启空间音频的效果。

private bool isInChannel;

public async void JoinChannel(uint uid, bool enableSpatialAudio)
{
userId = uid;
if (RtcEngine == null)
{
await Init();
}
if (!isInChannel && RtcEngine != null)
{
await AuthTokenManager.ExternalLogin(userId.ToString());
await GenerateAccessToken(channelName, new HelloOptions { role = Role.Publisher });
int ret = RtcEngine.JoinChannel(accessToken, channelName, "", userId);
isInChannel = true;
Debug.Log($"JoinChannel ## ret:{ret}, userId: {userId}");
}
if (enableSpatialAudio)
{
Instance.EnableSpatialAudio();
}
else
{
Instance.DisableSpatialAudio();
}
}

5.2 设置频道的名字

加入某一个频道的时候,需要传入频道的名字。而当我们在使用 Sync Relay 的服务创建房间时,就已经将创建的房间 ID 号设置为频道的名字了。

找到RTCAudioProxy.cs脚本,可以看到封装的设置频道名字的方法 SetChannelName:

private string channelName;

public void SetChannelName(string name)
{
this.channelName = name;
 }

SetChannelName 方法在 LobbyUIMediator.cs 脚本的下列 2 个位置有被引用到:

调用位置1:当创建房间的时候,调用 SetChannelName 的方法来设置一下频道名字。

private void OnRoomCreated(CreateRoomResponse response)
{
if (response.Code == (uint)RelayCode.OK)
{
// 需要在连接到Relay服务器之前,设置好房间信息
m_ConnectionManager.NetworkManager.GetComponent ().SetRoomData(response);
if (response.Visibility == LobbyRoomVisibility.Private && !string.IsNullOrEmpty(response.JoinCode))
{
m_ConnectionManager.NetworkManager.GetComponent ().SetJoinCode(response.JoinCode);
}
m_LocalUser.IsHost = true;
m_LocalRoom.RoomUuid = response.RoomUuid;
m_LocalRoom.RoomCode = response.RoomUuid;// TODO RoomCode not supported yet, use RoomUuid instead for now
m_LocalRoom.JoinCode = response.JoinCode;
m_LocalRoom.RoomName = response.Name;

Debug.Log($"Room created by user: {response.OwnerId} and room uuid is {response.RoomUuid}");
m_ConnectionManager.StartHost(m_LocalUser.DisplayName);
HelloService.RTCAudioProxy.Instance.SetChannelName(response.RoomUuid);
}
else
{
Debug.Log("Create Room Fail By Lobby Service, code: " + response.Code);
UnblockUIAfterLoadingIsComplete();
}
}

调用位置2:当用户加入房间的时候,调用 SetChannelName 的方法来选择要加入的频道的名字。

private void OnRoomInfoGot(QueryRoomResponse response, string joinCode)
{
if (response.Code == (uint)RelayCode.OK)
{
// TODO
if (response.Visibility == LobbyRoomVisibility.Private)
{
response.JoinCode = joinCode;
m_ConnectionManager.NetworkManager.GetComponent ().SetJoinCode(joinCode);
}

// 需要在连接到Relay服务器之前,设置好房间信息
NetworkManager.Singleton.GetComponent ().SetRoomData(response);

m_LocalRoom.RoomUuid = response.RoomUuid;
m_LocalRoom.RoomCode = response.RoomUuid;// TODO RoomCode not supported yet, use RoomUuid instead
m_LocalRoom.JoinCode = response.JoinCode;
m_LocalRoom.RoomName = response.Name;

Debug.Log("Joining Room: " + response.RoomUuid);
m_ConnectionManager.StartClient(m_LocalUser.DisplayName);
HelloService.RTCAudioProxy.Instance.SetChannelName(response.RoomUuid);
}
else
{
Debug.Log("Query Room Fail By Lobby Service, code: " + response.Code);
UnblockUIAfterLoadingIsComplete();
}
}

5.3 调用加入频道的方法

加入频道的方法 JoinChannel,脚本中有 2 个位置会引用该方法。

调用位置1:

在 OnAssignedPlayerNumber 方法中会调用 JoinChannel 的方法,同时传入的 bool 值为 false。原理是:在创建队伍和加入队伍以后,选择角色的时候,我们默认这里是关闭空间音频效果的,这样用户可以自由通话交流对方都能听到声音。

void OnAssignedPlayerNumber(int playerNum)
{
m_ClassInfoBox.OnSetPlayerNumber(playerNum);
HelloService.RTCAudioProxy.Instance.JoinChannel((uint)playerNum + 1, false);
}

调用位置2:

会在 ClientPlayerAvatar.cs 脚本的 OnNetworkSpawn 方法中被引用到,而 OnNetworkSpawn 方法通常用于对动态生成的网络对象的初始化操作。

这里代码的含义是:如果是自己客户端通过网络生成的角色对象,会调用加入频道的方法 JoinChannel。同时会根据 ClientPrefs.cs 脚本中封装的 GetSpaceAudio() 方法,获取到设置的 UI 界面存储的是否打开空间音频的值,作为参数传入。

public override void OnNetworkSpawn()
{
name = "PlayerAvatar" + OwnerClientId;

if (IsClient && IsOwner)
{
LocalClientSpawned?.Invoke(this);
}

if (m_PlayerAvatars)
{
m_PlayerAvatars.Add(this);
}
if (IsOwner)
{
HelloService.RTCAudioProxy.Instance.JoinChannel((uint)OwnerClientId + 1, ClientPrefs.GetSpaceAudio() > 0);
}
}

这里的参数设置完以后,大家已经可以点击 Play 开始体验项目了,我们先在 Unity 编辑器中运行项目:


然后会在 Console 控制台看到相关的日志输出:

  • 输出 “Create Room Successfully”,说明房间创建成功了;

  • 输出 “Room Created by user”这里,能看到当前的房间的 room uuid ,和 UOS 网页端的 Sync Relay 页面显示的当前房间 ID 号是一致的;

  • 而且看到当前房间的状态是 “已就绪”,对应输出的日志信息显示也是 “Ready” 的状态;

  • 输出 “successfully init agora rtc engine”,说明语音引擎已经成功实现了初始化的操作;

  • 输出 “JoinChannel”,说明用户已经成功加入语音频道了;

  • 输出 “DisableSpatialAudio”,说明在这个界面的时候,用户进行语音通话是关闭空间音频效果的;

  • 输出 “Join Channel Success”,说明本地用户调用 JoinChannel 方法成功加入频道后,自动触发了回调方法 OnJoinChannelSuccess。


public override void OnJoinChannelSuccess(RtcConnection connection, int elapsed)
{
Debug.Log("Join Channel Success " + connection.channelId + " " + connection.localUid);
OnJoinChannelSuccessEvent?.Invoke(connection.localUid);
}

5.4 设备检测

在我们开始进行实时音频互动前,首先需要检查是否已经获取到设备的权限。

处理权限请求

先来给大家介绍下如何获取设备的摄像头、麦克风等权限!不同平台下的处理方式是不一样的。

温馨提醒:在 Unity 2018.3 或以上版本中,Unity 不会主动向用户获取麦克风和相机权限,因此你需要请求获取权限。

  • Windows 系统会在应用程序首次尝试使用摄像头或麦克风等功能时,自动向用户弹出权限请求提示,因此你无需额外处理权限请求。

  • 如果你开发的目标平台是iOS 或 macOS,声网 RTC Unity SDK 在 Editor 文件夹下提供一个构建后处理脚本 BL_BuildPostProcess.cs。在你将项目从 Unity Editor 中构建并导出为 iOS 项目后,该脚本会自动向 Info.plist 文件添加摄像头和麦克风权限,无需你手动处理。

  • 如果你开发的目标平台是Android,Unity 提供了权限请求的 API CheckPermissions。你可以调用该方法来获取相关权限。

获取安卓权限

接下来参考下列步骤来获取安卓权限:

  • 导入 Unity 的 Android 命名空间

#if (UNITY_2018_3_OR_NEWER && UNITY_ANDROID)
using UnityEngine.Android;
#endif

  • 创建需要获取的权限列表,包括摄像头和麦克风。

#if (UNITY_2018_3_OR_NEWER && UNITY_ANDROID)
private string[] permissionList = new string[]
{
//Permission.Camera,
Permission.Microphone
};
#endif

  • 检查是否已获取权限。

private void CheckPermissions()
{
#if (UNITY_2018_3_OR_NEWER && UNITY_ANDROID)
foreach (string permission in permissionList)
{
if (!Permission.HasUserAuthorizedPermission(permission))
{
Permission.RequestUserPermission(permission);
}
}
#endif
}

调用获取权限的方法

调用 CheckPermissions 方法检查是否已获取实现语音互动所需的权限。

private void Update()
{
CheckPermissions();
}

6.开启麦克风和听筒,开始音频互动测试

此时,我们已经完全可以运行项目,来进行语音通话测试了。为了便于体验多人实时语音互动的效果,你可以将你的项目提前 Build 一个想要的平台的可执行文件。

6.1 构建项目可执行文件(Windows 平台)

我们先来 Build 一个 Windows 平台的可执行文件测试下。点击 File → BuildSettings,这里的 Development Build 默认是先不打勾的,Resolution 这里先选择窗口模式(Windowed),预先设置个 1024 * 768 的窗口大小,大家可以自己修改设置。然后点击 “Build”,文件放在 BossRoomHelloDemo_exe 文件夹下面。

Build 完成后,运行项目,然后点击 “创建队伍”。同时打开可执行文件 BossRoomHello.exe,点击 “加入队伍”,此时界面会提示 “加入失败,请打开或关闭 Debug 模式后重试”。


这是因为编辑器默认是属于开发者模式的,我们构建的 exe 文件并没有选择开发者模式。如果你想一个在 Unity 编辑器中运行,另一个在 exe 文件中运行的话,那就需要勾选 Development Build 选项,然后重新 Build 即可。


Build 完成后可以再次运行测试下效果,已经可以成功加入队伍了。


6.2 当有远端用户加入

此时在 Console 控制台可以看到打印输出的信息 “OnUserJoined”,显示远端用户已经加入了。


当远端的用户上线时,会自动触发脚本中 override 重写的回调方法 OnUserJoined。

public override void OnUserJoined(RtcConnection connection, uint remoteUid, int elapsed)
{
Debug.Log("OnUserJoined " + connection.channelId + " " + remoteUid);
OnUserJoinedEvent?.Invoke(remoteUid);
base.OnUserJoined(connection, remoteUid, elapsed);
}

由于我们还需要在此时做一些其它事件的处理,所以又自定义了一个 OnUserJoined 方法,该方法已经在最初进行初始化语音引擎的时候,注册给了 handle 对象 。

接着我们来分析一下代码:在 OnUserJoined 方法中,会将远端用户加入到 remoteUidList 集合中,同时还将远端用户的空间位置信息都存入了字典 remotePositionInfoDict 中,也会通过调用 UpdateRemotePosition 方法来更新远端用户的空间位置信息。

同时也会调用 SetSpatialAudioAttenuationByUid 方法来调整当前用户的空间音频的衰减系数,也可以调用 SetSpatialAudioBlurByUid 方法来调整人声模糊度的效果的。这两个方法后面会展开来详细讲解的。

private async Task CreateRTCEngine()
{
var handler = new UserEventHandler();
handler.OnUserJoinedEvent += OnUserJoined;
//此处省略其它代码行......
}

private void OnUserJoined(uint uid)
{
remoteUidList.Add(uid);
RemoteVoicePositionInfo positionInfo = new RemoteVoicePositionInfo() { position = new float[3], forward = new float[3] };
remotePositionInfoDict.Add(uid, positionInfo);
SetSpatialAudioBlurByUid(uid, ClientPrefs.GetVoiceBlur() > 0);
SetSpatialAudioAttenuationByUid(uid, ClientPrefs.GetAttenuation());
if (SpatialAudioEngine != null)
{
SpatialAudioEngine.UpdateRemotePosition(uid, positionInfo);
}
}

6.3 构建 APK 文件

如果你想在 Android 手机上运行的话,可以选择将你的平台选择 “Android”,然后点击 “Switch Platform”,等待平台的切换。构建 APK 的时候,你可以根据你的运行环境决定是否需要勾选 Development Build 参数即可。这里我先勾选,点击 "Build",设置 APK 的名字为 BossRoomHello.apk。

大家可以自行选择 Game 窗口的分辨率大小,然后点击运行项目,创建房间并加入后,进入游戏场景。我们发现在 Android 平台时,游戏场景中多了可操作的虚拟摇杆,这是为了适配移动端平台而专门添加的。你可以实验下,通过摇杆来控制角色的前后左右移动。


6.4 启用麦克风/听筒

在加入队伍、选择角色、全体准备就绪后,会加载进入 BossRoom 的游戏场景。

6.4.1 查看麦克风/听筒的UI控件

在场景中找到游戏对象 SettingsPanelCanvas,先来看看这里的 UI。

下拉菜单子对象 MicrophoneSettings 是用来控制开启或者关闭麦克风的。

麦克风的 UI:MicrophoneSettings有两个选项,闭麦或者关麦,默认的 Value 是 1,是开麦的状态。

下拉菜单子对象 ReceiverSettings 是用来控制开启或者关闭听筒的。

听筒的 UI:ReceiverSettings 有两个选项,打开听筒或者关闭听筒,默认的 Value 是 1,是打开听筒的状态。

再来看看 SettingsPanelCanvas 对象上挂载的 UISettingsVoice.cs 脚本中的事件的封装。

  • 脚本的 Start 方法中,已经提前注册了当选项的值发生变化时要监听的事件。

  • 当打开或者关闭麦克风时,会执行回调方法 OnMicrophoneChanged。在 OnMicrophoneChanged 方法中,首先会调用 ClientPrefs.cs 脚本中封装的 SetMicrophoneState 的方法来保存当前的麦克风的状态值。

  • 然后再调用 RTCAudioProxy.cs 脚本中的 EnableMicrophone 方法来控制是否开启麦克风。

  • 开启听筒的脚本控制的原理也是这样的,回调事件都封装在方法 OnReceiverChanged 中。

private void OnMicrophoneChanged(int index)
{
ClientPrefs.SetMicrophoneState(index);

// todo: handle microphone off/on
HelloService.RTCAudioProxy.Instance.EnableMicrophone(index == 1);
}

private void OnReceiverChanged(int index)
{
ClientPrefs.SetReceiverState(index);

// todo: handle receiver off/on
HelloService.RTCAudioProxy.Instance.EnableRemoteAudio(index == 1);
}

6.4.2 查看ClientPrefs类中数据存储和读取

在前面的代码中,可以看到选择开启或者关闭麦克风、听筒时,选择后的数值会在 ClientPrefs 类封装的方法中进行存储。

打开 ClientPrefs.cs 脚本后,可以看到脚本中定义了一些要保存的数据的键值 key,然后在自定义的方法中使用 Unity 默认的 PlayerPrefs 玩家偏好类,来存储和读取面板修改的麦克风、听筒、空间音频等相关的参数值。

public static class ClientPrefs
{
private const string k_MicrophoneKey = "MicrophoneState";
private const string k_ReceiverKey = "ReceiverState";

private const int k_DefaultMicrophoneState = 0;
    private const int k_DefaultReceiverState = 1;


public static int GetMicrophoneState()
{
return PlayerPrefs.GetInt(k_MicrophoneKey, k_DefaultMicrophoneState);
}

public static void SetMicrophoneState(int value)
{
PlayerPrefs.SetInt(k_MicrophoneKey, value);
}

public static int GetReceiverState()
{
return PlayerPrefs.GetInt(k_ReceiverKey, k_DefaultReceiverState);
}

public static void SetReceiverState(int value)
{
PlayerPrefs.SetInt(k_ReceiverKey, value);
    }
}

6.4.3 是否开启麦克风

我们来查看下是否开启麦克风的方法 EnableMicrophone:

如果打开了空间音频,或者没有打开空间音频但是在语音通话频道内时,我们会调用 MuteLocalAudioStream 方法来选择取消或恢复发布本地音频流。如果本地打开了麦克风,我们就发布本地音频流;如果关闭了麦克风,就取消发布本地音频流。

public void EnableMicrophone(bool enable)
{
if (isOpenSpatialAudio)
{
int r = SpatialAudioEngine.MuteLocalAudioStream(!enable);
Debug.Log($"SpatialAudioEngine EnableMicrophone:{enable} ## ret:{r}");
}
else if (RtcEngine != null)
{
int ret = RtcEngine.MuteLocalAudioStream(!enable);
Debug.Log($"EnableMicrophone:{enable} ## ret:{ret}");
}
}

6.4.4 是否开启听筒

再来查看下是否开启听筒的方法 EnableRemoteAudio,原理也是类似的。

在打开了空间音频,或者没有打开空间音频但是在语音通话频道内时,会调用 MuteAllRemoteAudioStreams 方法来选择取消或恢复订阅所有远端用户的音频流。如果本地打开了麦克风,我们就订阅远端用户的音频流;如果关闭了麦克风,则取消订阅远端用户的音频流。

public void EnableRemoteAudio(bool enable)
{
if (isOpenSpatialAudio)
{
int r = SpatialAudioEngine.MuteAllRemoteAudioStreams(!enable);
Debug.Log($"SpatialAudioEngine EnableMicrophone:{enable} ## ret:{r}");
}
else if (RtcEngine != null)
{
int ret = RtcEngine.MuteAllRemoteAudioStreams(!enable);
Debug.Log($"EnableRemoteAudio:{enable} ## ret:{ret}");
}
}

然后运行游戏,打开/关闭麦克风、打开/关闭听筒,测试两个角色之间进行对话时,是否还能听到对方的声音。

7.测试开启和关闭空间音频的效果

接下来我们再来看看空间音频的开启和关闭的交互效果实现。

先找到场景中的 SettingsPanelCanvas 对象,查看子对象 SettingsButton 的 UI 按钮,已经注册了事件,会执行绑定的 UISettingsCanvas.cs 脚本中的 OnClickSettingsButton 方法。在脚本的该方法中,实现了点击该按钮会打开设置面板:

7.1 查看空间音频的UI控件

在游戏的【基础设置】界面,是控制游戏的背景音乐和各种音效的音量的,这里不再展开讲解,大家可以自行查看脚本的代码控制。

我们切换到【高级设置】界面,可以看到这个界面上的所有的 UI 控件都在场景中的 SettingsPanel/AdvancedSettings下面。

  • SpaceAudio 是开启空间音频效果的 UI;

  • RangeAudio 是设置语音接收范围的 UI;

  • Attenuation 是调节衰减系数的 UI;

  • VoiceBlur 是开启人声模糊的 UI;

7.2 查看空间音频的使用指南

  • 点击【高级设置】界面的【指南】按钮:


可以查看下空间音频 & 范围音频的使用原理的介绍:


7.3 开启空间音频

当前项目中默认是开启空间音频的。只有启用了空间音频后,我们才能来调整空间音频的语音接收范围、衰减系数、人声模糊的效果。

接下来看看脚本中启用空间音频的方法 EnableSpatialAudio:

public void EnableSpatialAudio()
{
if (RtcEngine != null)
{
RtcEngine.MuteLocalAudioStream(true);
RtcEngine.MuteAllRemoteAudioStreams(true);
if (SpatialAudioEngine == null)
{
SpatialAudioEngine = RtcEngine.GetLocalSpatialAudioEngine();
var ret = SpatialAudioEngine.Initialize();
Debug.Log("_spatialAudioEngine: Initialize " + ret);

//设置音频属性和场景 ####
RtcEngine.SetAudioProfile(AUDIO_PROFILE_TYPE.AUDIO_PROFILE_DEFAULT);
RtcEngine.SetAudioScenario(AUDIO_SCENARIO_TYPE.AUDIO_SCENARIO_GAME_STREAMING);
}
else
{
int ret = RtcEngine.EnableSpatialAudio(true);
Debug.Log($"EnableSpatialAudio ## ret:{ret}");
}
isOpenSpatialAudio = true;
SpatialAudioEngine.SetMaxAudioRecvCount(10);
SetSpatialAudioRecvRange(ClientPrefs.GetRangeAudioDistance());
SetSpatialAudioBlur(ClientPrefs.GetVoiceBlur() > 0);
SetSpatialAudioAttenuation(ClientPrefs.GetAttenuation());

SpatialAudioEngine.MuteLocalAudioStream(ClientPrefs.GetMicrophoneState() != 1);
SpatialAudioEngine.MuteAllRemoteAudioStreams(ClientPrefs.GetReceiverState() != 1);
}
}

如果语音引擎对象 RtcEngine 不为空时:先取消发布本地音频流,并取消订阅所有远端用户的音频流。

情况1:如果空间音频对象为空,获取到本地的空间音频对象并进行初始化,然后设置音频的编码属性和场景。

SetAudioProfile 指的是设置音频编码属性,包含采样率、码率、编码模式和声道数。

SetAudioScenario 指的是设置音频场景。不同的音频场景下,设备的音量类型是不同的,在这里选择的是高音质场景 AUDIO_SCENARIO_GAME_STREAMING 的类型。

情况2:如果空间音频对象不为空,直接启用空间音频。

接着使用 bool 记录下当前的状态:空间音频已打开。同时要设置音频接收范围内最多可接收的音频流数,设置空间音频的各种参数(包括语音接收距离、人声模糊、音频衰减系数)。还会根据本地设置的是否开启麦克风和听筒,来决定是否发布本地采集的音频流,是否取消订阅所有远端用户的音频流。

再来看看调用开启空间音频脚本的地方:

调用位置1:

当通过 UI 的下拉菜单选项选择开启或者关闭空间音频时,会触发 UI 注册的回调方法 OnSpaceAudioChanged。方法中根据 value 的值来调用刚才封装的方法 EnableSpatialAudio 来启用空间音频,同时关闭空间音频时会执行方法 DisableSpatialAudio 。

private void OnSpaceAudioChanged(int value)
{
ClientPrefs.SetSpaceAudio(value);

// todo: handle space audio changed;
if (value == 0)
{
rangeAudioUpdated?.Invoke(0);
}
else
{
rangeAudioUpdated?.Invoke(ClientPrefs.GetRangeAudioDistance());
}
Debug.Log("Space Audio Changed: " + (value > 0 ? "Open" : "Close"));
if (value > 0)
{
HelloService.RTCAudioProxy.Instance.EnableSpatialAudio();
}
else
{
HelloService.RTCAudioProxy.Instance.DisableSpatialAudio();
}
}

调用位置2:

在 JoinChannel 的异步方法中,用户加入频道时,会根据存储的是否开启空间音频的值,来决定是否启用空间音频的效果。

public async void JoinChannel(uint uid, bool enableSpatialAudio)
{
userId = uid;
if (RtcEngine == null)
{
await Init();
}
if (!isInChannel && RtcEngine != null)
{
await AuthTokenManager.ExternalLogin(userId.ToString());
await GenerateAccessToken(channelName, new HelloOptions { role = Role.Publisher });
int ret = RtcEngine.JoinChannel(accessToken, channelName, "", userId);
isInChannel = true;
Debug.Log($"JoinChannel ## ret:{ret}, userId: {userId}");
}
if (enableSpatialAudio)
{
Instance.EnableSpatialAudio();
}
else
{
Instance.DisableSpatialAudio();
}
}

7.4 关闭空间音频

如果在设置面板上,将空间音频选择【关】的选项:

会在 Console 控制台看到回调方法的打印输出信息,显示空间音频已关闭。

在 Game 窗口中发现人物脚底的表示范围的绿色圆圈就看不到了,同时我们也发现无论两个角色之间距离有多远,在任何地方都可以听到对方的声音,不再受空间距离的限制。


当空间音频禁用时,ClientPrefs.GetSpaceAudio() 方法的返回值为0,也就是代码的逻辑会执行 DisableSpatialAudio 禁用空间音频的方法。

接着查看下DisableSpatialAudio 方法

  • 会更新下 bool 变量值的状态为 false:表示当前空间音频效果未开启;

  • 如果空间音频对象不为空的话,清除所有的远端的位置信息,并取消发布本地采集的音频流,同时取消订阅所有远端用户的音频流。

  • 然后调用 EnableSpatialAudio 方法,来语音通话时禁用空间音频的效果;

  • 也是同样的会根据本地设置的是否开启麦克风和听筒,来决定语音引擎对象是否发布本地采集的音频流,以及是否取消订阅所有远端用户的音频流。


public void DisableSpatialAudio()
{
isOpenSpatialAudio = false;
if (RtcEngine != null)
{
if (SpatialAudioEngine != null)
{
SpatialAudioEngine.ClearRemotePositions();
SpatialAudioEngine.MuteLocalAudioStream(true);
SpatialAudioEngine.MuteAllRemoteAudioStreams(true);
}
int ret = RtcEngine.EnableSpatialAudio(false);
Debug.Log($"DisableSpatialAudio ## ret:{ret}");

RtcEngine.MuteLocalAudioStream(ClientPrefs.GetMicrophoneState() != 1);
RtcEngine.MuteAllRemoteAudioStreams(ClientPrefs.GetReceiverState() != 1);
}
}

然后我们可以再次运行游戏,测试下空间音频由开着到关闭后的声音效果。

8. 调整空间音频参数

8.1 调整语音接收范围

打开下拉菜单,可以看到语音接收范围分为近、中、远,默认参数选择的是近的范围:


当我们修改这里的下拉菜单的选项值时,通过查看 Console 控制台的日志信息 “Range Audio Changed”,可以看到会自动回调 UI 注册好的方法 OnRangeAudioChanged。

  • 在该方法中,会存储下当前选择的参数值是近或者中或者远。

  • 也会自动回调方法 GetRangeAudioDistance 来处理当前语音接收的范围。在脚本中提前设定的分别是角色的周围 5米、10米、15米,放在了数组 s_RangeAudioDistances 中,大家可以自行修改。


private void OnRangeAudioChanged(int value)
{
if (value < 0 || value > 2)
{
Debug.Log("Invalid value of range audio: " + value);
return;
}

ClientPrefs.SetRangeAudio(value);

// todo: handle space audio changed;
rangeAudioUpdated?.Invoke(ClientPrefs.GetRangeAudioDistance());
Debug.Log("Range Audio Changed: " + value);
HelloService.RTCAudioProxy.Instance.SetSpatialAudioRecvRange(ClientPrefs.GetRangeAudioDistance());
}

同时也会调用脚本中封装的方法 SetSpatialAudioRecvRange,将当前语音接收范围参数的 range 值来应用到实时语音通话效果中。

public void SetSpatialAudioRecvRange(float range)
{
if (SpatialAudioEngine != null)
{
int ret = SpatialAudioEngine.SetAudioRecvRange(range);
Debug.Log($"SetSpatialAudioRecvRange ## ret:{ret}");
}
}

使用不同的语音接收范围

接着我们关闭设置的 UI 面板,回到 Game 场景中,看到角色 Player 的脚底会有一个绿圈。

我们来查看下 Player 脚底的表示语音接收范围的绿圈,可以看到它是使用 Unity 的 LineRenderer 组件绘制出来的,绘制圆圈的代码在角色身上的 PlayerVoiceRange.cs 脚本中,大家可以自行查看绘制的实现思路。

将空间音频的语音接收范围,由【近】改为【中】:

将范围音频的接收范围,由【近】改为【中】以后,Game窗口中的语音范围的绿圈的变化:


将范围音频的接收范围,由【中】改为【远】以后,Game窗口中的语音范围的绿圈的变化:


8.2 调整空间音频的衰减系数

设置音频衰减系数

衰减系数:用来控制声音随着距离的衰减的快慢程度,取值范围为0~1,数值越大的话,衰减越快。这里初始设置的衰减系数为 0.1。


根据日志信息可以看到,当衰减系数的 Slider 的 UI 发生变化时,会执行回调方法 OnAttenuationSliderChanged。

  • 在 OnAttenuationSliderChanged 方法中,通过调用 ClientPrefs.cs 脚本的 SetAttenuation 方法,会保存记录下当前的衰减系数值。

  • 然后再调用 RTCAudioProxy.cs 脚本中的 SetSpatialAudioAttenuation 方法来调整空间音频的衰减效果。


private void OnAttenuationSliderChanged(float newValue)
{
var value = Mathf.Round(newValue * 10.0f) / 10.0f;
m_AttenuationSlider.value = value;
ClientPrefs.SetAttenuation(value);

// todo: handle audio attenuation changed;
Debug.Log("Attenuation Changed: " + value);

调整:所有远端用户的音频衰减系数

我们来查看下 RTCAudioProxy.cs 方法,在该方法中,会遍历集合 remoteUidList 中的所有的远端用户,通过调用 SetRemoteAudioAttenuation 方法,来设置所有远端用户的声音衰减效果。

public void SetSpatialAudioAttenuation(double value)
{
if (RtcEngine != null && SpatialAudioEngine != null)
{
foreach (var uid in remoteUidList)
{
int ret = SpatialAudioEngine.SetRemoteAudioAttenuation(uid, value, false);
Debug.Log($"SetSpatialAudioAttenuation ## uid:{uid} ## ret:{ret}");
}
}
}

调整:指定远端用户的音频衰减系数

如果想要调整某一个远端用户的音频衰减系数的话,可以查看下 OnUserJoined 的方法。在该方法中,当某一个远端用户上线加入频道时,同时会调用方法 SetSpatialAudioAttenuationByUid,这样就可以单独来设置某一个用户的空间音频的衰减系数了。

private void SetSpatialAudioAttenuationByUid(uint uid, double value)
{
if (SpatialAudioEngine != null)
{
SpatialAudioEngine.SetRemoteAudioAttenuation(uid, value, false);
}
}

此时,运行游戏可以测试下,设置不同的音频衰减系数后,听到的对话音频的效果。

8.3 启用人声模糊

是否开启人声模糊

人声模糊:是指让说话人的声音进行人工模糊处理,让声音变得不清楚,无法理解内容,但是又没有完全关闭这个声音。这里设置的默认是关闭人声模糊效果的。


同理当人声模糊的下拉菜单的 UI 发生变化时,会自动回调方法 OnVoiceBlurChanged。


在 OnVoiceBlurChanged 方法中,通过调用 ClientPrefs.cs 脚本的 SetVoiceBlur 方法,会保存记录下当前选择的结果。然后再调用 RTCAudioProxy.cs 脚本中的 SetSpatialAudioBlur 方法来调整人声模糊的效果。

private void OnVoiceBlurChanged(int value)
{
ClientPrefs.SetVoiceBlur(value);

// todo: handle voice blur changed;
Debug.Log("Voice Blur Changed: " + (value > 0 ? "Open" : "Close"));
HelloService.RTCAudioProxy.Instance.SetSpatialAudioBlur(value > 0);
}

调整:所有远端用户的人声模糊的效果

我们来查看下 RTCAudioProxy.cs 脚本的 SetSpatialAudioBlur 方法,在该方法中,会遍历集合 remoteUidList 中的所有的远端用户,通过调用 SetRemoteUserSpatialAudioParams 方法,来设置所有远端用户是否开启人声模糊的效果。

其中空间音频的 enable_blur 参数表示是否开启声音模糊的处理,如果为 true,表示开启模糊处理。

public void SetSpatialAudioBlur(bool enable)
{
if (RtcEngine != null)
{
SpatialAudioParams audioParams = new SpatialAudioParams();
audioParams.enable_blur.SetValue(enable);
foreach (var uid in remoteUidList)
{
int ret = RtcEngine.SetRemoteUserSpatialAudioParams(uid, audioParams);
Debug.Log($"SetSpatialAudioBlur ## uid:{uid} ## ret:{ret}");
}
}
}

调整:指定远端用户的人声模糊的效果

如果想要调整某一个远端用户的人声模糊效果的话,可以查看下 OnUserJoined 的方法。在该方法中,当某一个远端用户上线加入频道时,同时会调用方法 SetSpatialAudioBlurByUid,这样就可以单独来设置某一个用户的人声模糊效果了。

private void SetSpatialAudioBlurByUid(uint uid, bool enable)
{
if (SpatialAudioEngine != null)
{
SpatialAudioParams audioParams = new SpatialAudioParams();
audioParams.enable_blur.SetValue(enable);
RtcEngine.SetRemoteUserSpatialAudioParams(uid, audioParams);
}
}

此时,运行游戏可以测试下,设置开启人声模糊的效果后,是否还能听清楚对方说话的声音。

8.4 调整通话音量

SDK 支持对采集和播放的音频音量进行调整,以满足用户实际应用场景。例如,进行双人通话时,需要静音远端用户,可以通过调整播放音量的方法将音量设置为 0。

我们在项目中已经实现了基本的实时音频功能,现在来试试调整通话音量的功能。找到脚本中的 CreateRTCEngine 方法:

  • AdjustPlaybackSignalVolume:用来调节本地播放的所有远端用户信号音量。

  • AdjustRecordingSignalVolume:用来调节麦克风采集的信号音量。

  • 它们的取值范围都是 [0,400],0 表示静音,100 表示是默认的原始音量,400 表示是原始音量的 4 倍,自带溢出保护。

RtcEngine.AdjustPlaybackSignalVolume(300);
RtcEngine.AdjustRecordingSignalVolume(200);

9. 用户离开频道时的事件

9.1 当远端用户离线时

当某一个远端用户自己主动离线时,在 Game 界面会有弹窗提示对方已离开队伍的。当然,用户后面还可以再次重新上线加入这个频道的。如果在语音通信过程中,如果因网络问题导致连接断开,Hello SDK 会自动开启断线重连机制的。

此时在 Console 控制台可以看到用户下线时的日志信息 “OnUserOffline”,这时会自动回调方法 OnUserOffline。


public override void OnUserOffline(RtcConnection connection, uint remoteUid, USER_OFFLINE_REASON_TYPE reason)
{
Debug.Log("OnUserOffline " + connection.channelId + " " + remoteUid);
OnUserOfflineEvent?.Invoke(remoteUid);
base.OnUserOffline(connection, remoteUid, reason);
}

我们还自定义了一个 OnUserOffline 方法,该方法已经在最初进行初始化语音引擎的时候注册给了 handle 对象。

在 OnUserOffline 方法中,会将远端用户从 remoteUidList 集合和 remotePositionInfoDict 字典中移除;离开频道后,为避免计算资源的浪费,还需要调用 RemoveRemotePosition 方法来删除指定远端用户的空间位置信息。否则,该用户的空间位置信息会一直被保存。

private void OnUserOffline(uint uid)
{
remoteUidList.Remove(uid);
remotePositionInfoDict.Remove(uid);
if (SpatialAudioEngine != null)
{
SpatialAudioEngine.RemoveRemotePosition(uid);
}
}

9.2 离开频道

当本地客户端用户离开语音频道时,查看下此时的 Console 控制台的输出日志信息。

看到此时输出信息有 “LeaveChannel ## ret”, 因为此时调用了自定义封装的LeaveChannel方法。

当调用 RtcEngine.LeaveChannel 方法后,SDK 会终止音频互动、离开当前频道,并同时会释放会话相关的所有资源的。同时这里也要处理离开频道后的行为,比如禁用空间音频、清空远端用户的集合列表、清空远端用户的位置信息集合、重置是否还在频道内的标志变量等。

public void LeaveChannel()
{
if (isInChannel && RtcEngine != null)
{
int ret = RtcEngine.LeaveChannel();
Debug.Log($"LeaveChannel ## ret:{ret}");
DisableSpatialAudio();
remoteUidList.Clear();
remotePositionInfoDict.Clear();
isInChannel = false;
}
}

我们还可以看到 Console 控制台打印输出的还有另外一个日志信息 “Leave Channel Success”,因为此时自动回调了方法 OnLeaveChannel。

public override void OnLeaveChannel(RtcConnection connection, RtcStats stats)
{
Debug.Log("Leave Channel Success" + connection.channelId + " " + connection.localUid);
}

9.3 销毁RTC引擎

考虑到优化的处理,可以在用户需要时才进行实时音频通信,不需要时则将资源释放出来用于其它操作。所以我们通过调用 Dispose 的方法来释放 SDK 使用的所有资源。

private void DestroyRTCEngine()
{
if (RtcEngine != null)
{
RtcEngine.DisableAudio();
RtcEngine.Dispose();
RtcEngine = null;
}
}
private void DestroySpatialAudioEngine()
{
if (SpatialAudioEngine != null)
{
SpatialAudioEngine.Dispose();
SpatialAudioEngine = null;
}
}
private void OnDestroy()
{
DestroySpatialAudioEngine();
DestroyRTCEngine();
}

寄语

UOS Hello 作为 Unity 官方与声网合作推出的游戏内置实时语音服务,广泛支持多类型游戏及多平台互通,未来还将集成 WWISE ——这一业界领先、功能强大的游戏音频中间件,它不仅提供高度的自定义性和强大的实时处理能力,还以其丰富的音频效果和高效的资源管理,为游戏语音功能提供更全面、高效的解决方案,进一步提升游戏的音频品质和玩家的沉浸感。

学习途径

UOS 配套的相关学习教程视频也已同步上传至 Unity 中文课堂和 B 站,搜索“使用Hello轻松畅享游戏内多人实时语音互动”即可找到,欢迎大家前往学习,UOS 更多学习教程持续更新中,敬请期待!

Unity Online Services (UOS) 是一个专为游戏开发者设计的一站式游戏云服务平台,提供覆盖游戏全生命周期的开发、运营和推广支持。

了解更多 UOS 相关信息:

官网:https://uos.unity.cn

技术交流 QQ 群:823878269

公众号:UOS 游戏云服务

Unity 官方微信

第一时间了解Unity引擎动向,学习进阶开发技能

每一个“点赞”、“在看”,都是我们前进的动力

ad1 webp
ad2 webp
ad1 webp
ad2 webp