在 Unity 多人遊戲中實現語音對話

咱們曾經不止一次爲你們分享過遊戲中的實時音視頻,例如怎麼實現遊戲中的聽聲辨位、狼人殺遊戲中的語音聊天挑戰等。基本上,都是從技術原理和 Agora SDK 出發來分享的。此次咱們換一個角度。咱們將從 Unity 開發者的角度分享一下,在 Unity 中如何給本身的多人在線遊戲增長實時語音通話功能。java

咱們在這裏利用了 Unity 上流行的 「Tanks!!! asset reference」 坦克遊戲做爲多人在線遊戲做爲基礎,相信不少人都不會陌生。你們能夠在 Unity Asset Store 中搜到它。而後,咱們會利用 Unity Asset Store 中的 Agora Voice SDK 爲它增長多人語音聊天功能。android

在開始前,你須要作如下準備:ios

  • 安裝 Unity 並註冊 Unity 帳號
  • 瞭解若是在 Unity 中建立 iOS、Android 項目
  • 一款跨移動平臺多玩家的 Unity 遊戲(本文中咱們選擇的是 Tanks)
  • 瞭解 C# 和 Unity 腳本
  • 註冊一個 Agora 開發者帳戶
  • 至少兩個移動設備(若是有一個 iOS 設備,一個 Android 設備就再理想不過了)
  • 安裝 Xcode

新建 Unity 項目

咱們默認你們都是用過 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 已經開啓

Building for iOS

如今咱們已經準備好來建立 iOS 版本了。打開 Build Setting,將系統平臺切換到 iOS,而後 Build。在切換系統平臺後,請記得更新 Bundle Identifier(以下圖所示)。maven

圖:建立了一個「Build」文件夾用於儲存 iOS 項目

圖:Build 完成

讓咱們打開 Unity-iPhone.xcodeproj,sign 並讓它在測試設備上運行。ide

如今咱們已經完成了 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問答版塊,發帖與咱們的工程師交流。

相關文章
相關標籤/搜索