我們曾經不止一次為大家分享過遊戲中的實時音視訊,例如怎麼實現遊戲中的聽聲辨位、狼人殺遊戲中的語音聊天挑戰等。基本上,都是從技術原理和 Agora SDK 出發來分享的。這次我們換一個角度。我們將從 Unity 開發者的角度分享一下,在 Unity 中如何給自己的多人線上遊戲增加實時語音通話功能。
我們在這裡利用了 Unity 上流行的 “Tanks!!! asset reference” 坦克遊戲作為多人線上遊戲作為基礎,相信很多人都不會陌生。大家可以在 Unity Asset Store 中搜到它。然後,我們會利用 Unity Asset Store 中的 Agora Voice SDK 為它增加多人語音聊天功能。
在開始前,你需要做以下準備:
- 安裝 Unity 並註冊 Unity 賬號
- 瞭解如果在 Unity 中建立 iOS、Android 專案
- 一款跨移動平臺多玩家的 Unity 遊戲(本文中我們選擇的是 Tanks)
- 瞭解 C# 和 Unity 指令碼
- 註冊一個 Agora 開發者賬戶
- 至少兩個移動裝置(如果有一個 iOS 裝置,一個 Android 裝置就再理想不過了)
- 安裝 Xcode
新建 Unity 專案
我們預設大家都是用過 Unity 的開發者,但是為了照顧更多的人。我們還是要從頭講起。當然,開始的操作步驟很簡單,所以我們會盡量以圖片來說明。
首先,開啟 Unity 後,讓我們先建立一個新的專案。
如果你之前已經下載過 Tanks!!! ,那麼我們點選頁面旁邊的“Add Asset Package”按鈕,選擇新增它即可。
如果你還未下載過 Tanks!!! 那麼可以在 Unity Store 中下載它。
在將 Tanks!!! 參考專案部署到手機之前,還有幾步需要做。首先,我們需要在 Unity Dashboard 中,為這個專案開啟 Unity Live Mode。該設定的路徑是:project → Multiplayer → Unet Config。儘管 Tanks!!! 只支援最多四個玩家4,但我們在將“Max Player per room”設定為6。
圖:這個介面說明 Unity Live Mode 已經開啟Building for iOS
現在我們已經準備好來建立 iOS 版本了。開啟 Build Setting,將系統平臺切換到 iOS,然後 Build。在切換系統平臺後,請記得更新 Bundle Identifier(如下圖所示)。
圖:建立了一個“Build”資料夾用於儲存 iOS 專案 圖:Build 完成讓我們開啟 Unity-iPhone.xcodeproj
,sign 並讓它在測試裝置上執行。
現在我們已經完成了 iOS 專案的建立。接下來我們要建立 Android 專案了。
Building for 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問答版塊,發帖與我們的工程師交流。