咱們曾經不止一次爲你們分享過遊戲中的實時音視頻,例如怎麼實現遊戲中的聽聲辨位、狼人殺遊戲中的語音聊天挑戰等。基本上,都是從技術原理和 Agora SDK 出發來分享的。此次咱們換一個角度。咱們將從 Unity 開發者的角度分享一下,在 Unity 中如何給本身的多人在線遊戲增長實時語音通話功能。java
咱們在這裏利用了 Unity 上流行的 「Tanks!!! asset reference」 坦克遊戲做爲多人在線遊戲做爲基礎,相信不少人都不會陌生。你們能夠在 Unity Asset Store 中搜到它。而後,咱們會利用 Unity Asset Store 中的 Agora Voice SDK 爲它增長多人語音聊天功能。android
在開始前,你須要作如下準備:ios
咱們默認你們都是用過 Unity 的開發者,可是爲了照顧更多的人。咱們仍是要從頭講起。固然,開始的操做步驟很簡單,因此咱們會盡可能以圖片來講明。git
首先,打開 Unity 後,讓咱們先建立一個新的項目。github
若是你以前已經下載過 Tanks!!! ,那麼咱們點擊頁面旁邊的「Add Asset Package」按鈕,選擇添加它便可。xcode
若是你還未下載過 Tanks!!! 那麼能夠在 Unity Store 中下載它。bash
在將 Tanks!!! 參考項目部署到手機以前,還有幾步須要作。首先,咱們須要在 Unity Dashboard 中,爲這個項目開啓 Unity Live Mode。該設置的路徑是:project → Multiplayer → Unet Config。儘管 Tanks!!! 只支持最多四個玩家4,但咱們在將「Max Player per room」設置爲6。app
圖:這個界面說明 Unity Live Mode 已經開啓如今咱們已經準備好來建立 iOS 版本了。打開 Build Setting,將系統平臺切換到 iOS,而後 Build。在切換系統平臺後,請記得更新 Bundle Identifier(以下圖所示)。maven
圖:建立了一個「Build」文件夾用於儲存 iOS 項目 圖:Build 完成讓咱們打開 Unity-iPhone.xcodeproj
,sign 並讓它在測試設備上運行。ide
如今咱們已經完成了 iOS 項目的建立。接下來咱們要建立 Android 項目了。
Android 項目相比 iOS 來說要更簡單一些。由於 Unity 能夠直接建立、sign 和部署運行,無需藉助 Android Studio。我默認你們已經將 Unity 與 Android SDK 文件夾關聯起來了。如今咱們要打開 Build Setting,而後將系統平臺切換到 Android。
在咱們建立並運行以前,咱們還須要對代碼作出一些簡單的調整。咱們只須要註釋掉幾行代碼,加一個簡單的返回聲明,再替換一個文件。
背景信息:Tanks!!! Android 包含了 Everyplay 插件,用以實現遊戲屏幕錄製和分享。問題是,Everyplay 在2018年十月中止了服務,而插件仍然存在一些未解決的問題,若是咱們不對其進行處理會致使編譯失敗。
首先,咱們要糾正一下 Everyplay 插件 build.gradle 文件中的語法錯誤。該文件的路徑是:Plugins → Android → everyplay → build.gradle。
如今,咱們打開了 gradle 文件,全選全部代碼,而後將下方的代碼替換上去。Tanks!!! 團隊在 Github 上更新了代碼,可是不知道爲何並沒能更新到插件中。
// UNITY EXPORT COMPATIBLE
apply plugin: 'com.android.library'
repositories {
mavenCentral()
}
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.0.0'
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
}
android {
compileSdkVersion 23
buildToolsVersion "25.0.3"
defaultPublishConfig "release"
defaultConfig {
versionCode 1600
versionName "1.6.0"
minSdkVersion 16
}
buildTypes {
debug {
debuggable true
minifyEnabled false
}
release {
debuggable false
minifyEnabled true
proguardFile getDefaultProguardFile('proguard-android.txt')
proguardFile 'proguard-project.txt'
}
}
sourceSets {
main {
manifest.srcFile 'AndroidManifest.xml'
java.srcDirs = ['src']
aidl.srcDirs = ['src']
renderscript.srcDirs = ['src']
res.srcDirs = ['res']
jniLibs.srcDirs = ['libs']
}
}
lintOptions {
abortOnError false
}
}
複製代碼
最後咱們要作的修改就是關閉 Everyplay。你可能想問:爲何咱們要關閉 Everyplay 呢?由於當插件初始化時會致使 Android 應用崩潰。我發現最快速的方法就是在 EveryPlaySettings.cs 文件中更新幾行代碼(該文件的路徑:Assets → Plugins → EveryPlay → Scripts),如此一來,每當 Everyplay 視圖檢測自身是否處於開啓狀態時,咱們都會給它返回「false」。
public class EveryplaySettings : ScriptableObject
{
public string clientId;
public string clientSecret;
public string redirectURI = "https://m.everyplay.com/auth";
public bool iosSupportEnabled;
public bool tvosSupportEnabled;
public bool androidSupportEnabled;
public bool standaloneSupportEnabled;
public bool testButtonsEnabled;
public bool earlyInitializerEnabled = true;
public bool IsEnabled
{
get
{
return false;
}
}
#if UNITY_EDITOR
public bool IsBuildTargetEnabled
{
get
{
return false;
}
}
#endif
public bool IsValid
{
get
{
return false;
}
}
}
複製代碼
如今咱們已經準備好 Build 了。在 Unity 中打開 Build Settings,選擇 Android 平臺,而後按下「Switch Platform」按鈕。隨後,在 Player Settings 中爲 Android App 修改 bundle id。在這裏,我使用的是 com.agora.tanks.voicedemo。
接下來,咱們要利用 Unity 中的 Agora voice SDK for Unity 來給跨平臺項目增長語音聊天功能了。咱們打開 Unity Asset Store ,搜索 Agora Voice SDK for Unity。
當插件頁面完成加載後,點擊「Download」開始下載。下載完成後,選擇「Import」,將它集成到你的項目中。
咱們須要建立一個腳原本讓遊戲與 Agora Voice SDK 進行交互。咱們在項目中新建一個 C# 文件(AgoraInterface.cs),而後在 Visual Studio 中打開它。
在這個腳本中有兩個很重要的變量:
static IRtcEngine mRtcEngine;
public static string appId = "Your Agora AppId Here";
複製代碼
先要將「Your Agora AppId Here」 替換成 App ID,咱們可在登陸 Agora.io ,進入 Agora Dashboard 獲取。mRtcEngine
是靜態的,這樣在OnUpdate
調用的時候,纔不會丟失。因爲遊戲中的其它腳本可能會引用 App ID,因此它是public static
。
考慮到節省時間,我已經將AgoraInterface.cs
的代碼寫好了(以下所示)。你們能夠直接使用,避免重複造車輪。
在這裏簡單解釋一下代碼。首先,咱們在開頭有一些邏輯,用於 check/requset Android Permission。而後咱們用 App ID 初始化 Agora RTC Engine,而後咱們附加了一些事件回調,這部分很簡單易懂。
mRtcEngine.OnJoinChannelSuccess
表示用戶已經成功加入指定頻道。
最後一個重要功能就是update
,當啓用了 Agora RTC Engine 時,咱們想要調用引擎的.Pull()
方法,它對於插件是否能運行起來很關鍵。
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
using agora_gaming_rtc;
#if(UNITY_2018_3_OR_NEWER)
using UnityEngine.Android;
#endif
public class AgoraInterface : MonoBehaviour
{
static IRtcEngine mRtcEngine;
// PLEASE KEEP THIS App ID IN SAFE PLACE
// Get your own App ID at https://dashboard.agora.io/
// After you entered the App ID, remove ## outside of Your App ID
public static string appId = "Your Agora AppId Here";
void Awake()
{
QualitySettings.vSyncCount = 0;
Application.targetFrameRate = 30;
}
// Start is called before the first frame update
void Start()
{
#if (UNITY_2018_3_OR_NEWER)
if (Permission.HasUserAuthorizedPermission(Permission.Microphone))
{
}
else
{
Permission.RequestUserPermission(Permission.Microphone);
}
#endif
mRtcEngine = IRtcEngine.GetEngine(appId);
Debug.Log("Version : " + IRtcEngine.GetSdkVersion());
mRtcEngine.OnJoinChannelSuccess += (string channelName, uint uid, int elapsed) => {
string joinSuccessMessage = string.Format("joinChannel callback uid: {0}, channel: {1}, version: {2}", uid, channelName, IRtcEngine.GetSdkVersion());
Debug.Log(joinSuccessMessage);
};
mRtcEngine.OnLeaveChannel += (RtcStats stats) => {
string leaveChannelMessage = string.Format("onLeaveChannel callback duration {0}, tx: {1}, rx: {2}, tx kbps: {3}, rx kbps: {4}", stats.duration, stats.txBytes, stats.rxBytes, stats.txKBitRate, stats.rxKBitRate);
Debug.Log(leaveChannelMessage);
};
mRtcEngine.OnUserJoined += (uint uid, int elapsed) => {
string userJoinedMessage = string.Format("onUserJoined callback uid {0} {1}", uid, elapsed);
Debug.Log(userJoinedMessage);
};
mRtcEngine.OnUserOffline += (uint uid, USER_OFFLINE_REASON reason) => {
string userOfflineMessage = string.Format("onUserOffline callback uid {0} {1}", uid, reason);
Debug.Log(userOfflineMessage);
};
mRtcEngine.OnVolumeIndication += (AudioVolumeInfo[] speakers, int speakerNumber, int totalVolume) => {
if (speakerNumber == 0 || speakers == null)
{
Debug.Log(string.Format("onVolumeIndication only local {0}", totalVolume));
}
for (int idx = 0; idx < speakerNumber; idx++)
{
string volumeIndicationMessage = string.Format("{0} onVolumeIndication {1} {2}", speakerNumber, speakers[idx].uid, speakers[idx].volume);
Debug.Log(volumeIndicationMessage);
}
};
mRtcEngine.OnUserMuted += (uint uid, bool muted) => {
string userMutedMessage = string.Format("onUserMuted callback uid {0} {1}", uid, muted);
Debug.Log(userMutedMessage);
};
mRtcEngine.OnWarning += (int warn, string msg) => {
string description = IRtcEngine.GetErrorDescription(warn);
string warningMessage = string.Format("onWarning callback {0} {1} {2}", warn, msg, description);
Debug.Log(warningMessage);
};
mRtcEngine.OnError += (int error, string msg) => {
string description = IRtcEngine.GetErrorDescription(error);
string errorMessage = string.Format("onError callback {0} {1} {2}", error, msg, description);
Debug.Log(errorMessage);
};
mRtcEngine.OnRtcStats += (RtcStats stats) => {
string rtcStatsMessage = string.Format("onRtcStats callback duration {0}, tx: {1}, rx: {2}, tx kbps: {3}, rx kbps: {4}, tx(a) kbps: {5}, rx(a) kbps: {6} users {7}",
stats.duration, stats.txBytes, stats.rxBytes, stats.txKBitRate, stats.rxKBitRate, stats.txAudioKBitRate, stats.rxAudioKBitRate, stats.users);
Debug.Log(rtcStatsMessage);
int lengthOfMixingFile = mRtcEngine.GetAudioMixingDuration();
int currentTs = mRtcEngine.GetAudioMixingCurrentPosition();
string mixingMessage = string.Format("Mixing File Meta {0}, {1}", lengthOfMixingFile, currentTs);
Debug.Log(mixingMessage);
};
mRtcEngine.OnAudioRouteChanged += (AUDIO_ROUTE route) => {
string routeMessage = string.Format("onAudioRouteChanged {0}", route);
Debug.Log(routeMessage);
};
mRtcEngine.OnRequestToken += () => {
string requestKeyMessage = string.Format("OnRequestToken");
Debug.Log(requestKeyMessage);
};
mRtcEngine.OnConnectionInterrupted += () => {
string interruptedMessage = string.Format("OnConnectionInterrupted");
Debug.Log(interruptedMessage);
};
mRtcEngine.OnConnectionLost += () => {
string lostMessage = string.Format("OnConnectionLost");
Debug.Log(lostMessage);
};
mRtcEngine.SetLogFilter(LOG_FILTER.INFO);
// mRtcEngine.setLogFile("path_to_file_unity.log");
mRtcEngine.SetChannelProfile(CHANNEL_PROFILE.GAME_FREE_MODE);
// mRtcEngine.SetChannelProfile (CHANNEL_PROFILE.GAME_COMMAND_MODE);
// mRtcEngine.SetClientRole (CLIENT_ROLE.BROADCASTER);
}
// Update is called once per frame
void Update ()
{
if (mRtcEngine != null) {
mRtcEngine.Poll ();
}
}
}
複製代碼
注意,以上代碼可複用於全部 Unity 項目。
若是你曾經使用過 Agora SDK,你可能注意到了,這裏沒有加入頻道和離開頻道。讓咱們先從「離開頻道」開始動手,建立一個新的 C# 腳本LeaveHandler.cs
,咱們須要在用戶返回到主菜單的時候調用 theleaveHandler
。最簡單的方法就是在 LobbyScene 打開後,爲特定遊戲對象開啓該方法。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using agora_gaming_rtc;
public class LeaveHandler : MonoBehaviour
{
// Start is called before the first frame update
void OnEnable()
{
// Agora.io Implimentation
IRtcEngine mRtcEngine = IRtcEngine.GetEngine(AgoraInterfaceScript.appId); // Get a reference to the Engine
if (mRtcEngine != null)
{
Debug.Log("Leaving Channel");
mRtcEngine.LeaveChannel();// leave the channel
}
}
}
複製代碼
在這裏,咱們要找的遊戲對象是 LeftSubPanel
(以下圖,MainPanel → MenuUI → LeftSubPanel )。
Tanks!!! 中有兩種方法加入多人遊戲,一種是建立新遊戲,另外一種是加入遊戲。因此有兩個地方,咱們須要增長「加入頻道」的命令。
讓咱們先找到 UI Script Asset 文件夾(該文件夾路徑:Assets → Scripts → UI),而後打開CreateGame.cs
文件。在第61行,你會找到遊戲用於匹配玩家的方法,在這裏咱們能夠加入一些邏輯用於加入頻道。首先咱們要作的就是應用 Agora SDK 庫。
using agora_gaming_rtc;
複製代碼
在StartMatchmakingGame()
的第78行,咱們須要加入一些邏輯來獲取正在運行中的Agora RTC Engine,而後將「用戶輸入的內容」做爲頻道名稱(m_MatchNameInput.text)。
private void StartMatchmakingGame()
{
GameSettings settings = GameSettings.s_Instance;
settings.SetMapIndex(m_MapSelect.currentIndex);
settings.SetModeIndex(m_ModeSelect.currentIndex);
m_MenuUi.ShowConnectingModal(false);
Debug.Log(GetGameName());
m_NetManager.StartMatchmakingGame(GetGameName(), (success, matchInfo) =>
{
if (!success)
{
m_MenuUi.ShowInfoPopup("Failed to create game.", null);
}
else
{
m_MenuUi.HideInfoPopup();
m_MenuUi.ShowLobbyPanel();
// Agora.io Implimentation
var channelName = m_MatchNameInput.text; // testing --> prod use: m_MatchNameInput.text
IRtcEngine mRtcEngine = IRtcEngine.GetEngine(AgoraInterfaceScript.appId); // Get a reference to the Engine
mRtcEngine.JoinChannel(channelName, "extra", 0); // join the channel with given match name
Debug.Log("joining channel:" + channelName);
}
});
}
複製代碼
StartMatchmakingGame()包含了加入頻道
如今咱們須要打開LobbyServerEntry.cs
(Assets → Scripts → UI),而後加入一些邏輯,以實現讓用戶能夠經過「Find a Game」來加入其餘人的房間。
在 Visual Studio 打開 LobbyServerEntry.cs
,而後找到第63行,這裏有一個 JoinMatch()
。咱們在第80行增長几行代碼。
private void JoinMatch(NetworkID networkId, String matchName)
{
MainMenuUI menuUi = MainMenuUI.s_Instance;
menuUi.ShowConnectingModal(true);
m_NetManager.JoinMatchmakingGame(networkId, (success, matchInfo) =>
{
//Failure flow
if (!success)
{
menuUi.ShowInfoPopup("Failed to join game.", null);
}
//Success flow
else
{
menuUi.HideInfoPopup();
menuUi.ShowInfoPopup("Entering lobby...");
m_NetManager.gameModeUpdated += menuUi.ShowLobbyPanelForConnection;
// Agora.io Implimentation
var channelName = matchName; // testing --> prod use: matchName
IRtcEngine mRtcEngine = IRtcEngine.GetEngine(AgoraInterfaceScript.appId); // Get a reference to the Engine
mRtcEngine.JoinChannel(channelName, "extra", 0); // join the channel with given match name
// testing
string joinChannelMessage = string.Format("joining channel: {0}", channelName);
Debug.Log(joinChannelMessage);
}
}
);
}
複製代碼
如今咱們已經完成了Agora SDK 的集成,而且已經準備好進行 iOS 端和 Android 端的 Build 與測試。咱們能夠參照上述內容中的方法來進行 Building 與部署。
爲了便於你們參考,我已經將這份 Tutorial 中的腳本上傳了一份到 Github: github.com/digitallysa…
若是你遇到 Agora SDK API 調用問題,能夠參考咱們的官方文檔(docs.agora.io),也歡迎在 RTC 開發者社區 的 Agora 版塊與咱們的工程師和更多同行交流、分享。
如遇到開發問題,歡迎訪問聲網 Agora問答版塊,發帖與咱們的工程師交流。