On more than one occasion, we’ve shared real-time audio and video in the game, such as how to achieve the sound detection in the game, and the voice chat challenge in the werewolf killing game. Basically, it is shared from the technical principles and Agora SDK. This time, let’s take a different Angle. From a Unity developer’s perspective, we’ll share how you can add real-time voice calling to your multiplayer online games in Unity.

Here we use Unity’s popular “Tanks!! As the foundation of multiplayer online games, tank games are familiar to many people. You can find it in the Unity Asset Store. We will then add multiplayer Voice chat using the Agora Voice SDK in the Unity Asset Store.

Here’s what you need to do before you start:

  • Install Unity and register for a Unity account
  • Learn how to create iOS and Android projects in Unity
  • A multi-player Unity game across mobile platforms (for this article we chose Tanks)
  • Knowledge of C# and Unity scripts
  • Sign up for an Agora developer account
  • At least two mobile devices (if you have an iOS device, an Android device is ideal)
  • Install Xcode

Creating a New Unity project

We assume that everyone is a developer who has used Unity, but to cater for the larger population. Let’s start at the beginning. Of course, the initial steps are simple, so we’ll try to illustrate them with pictures.

First, after opening Unity, let’s create a new project.

If you’ve downloaded Tanks before!! Click the “Add Asset Package” button next to the page and select Add it.

If you haven’t downloaded Tanks yet!! You can download it in the Unity Store.

The Tanks!!!!!! There are a few more steps that need to be done before the reference project can be deployed to mobile phones. First, we need to enable Unity Live Mode for this project in Unity Dashboard. The path for this setup is project → Multiplayer → Unet Config. Although Tanks!!!!!! It only supports up to four players 4, but we are setting “Max Player per room” to 6.

Building for iOS

Now we are ready to create the iOS version. Open Build Setting, switch the system platform to iOS, and Build. Remember to update the Bundle Identifier (as shown in the figure below) after switching system platforms.

Let’s open Unity-iphone.xcodeProj, sign and run it on the test device.

We have now completed the creation of the iOS project. Now we’re going to create the Android project.

Building for Android

Android projects are much simpler than iOS projects. Because Unity can be created, signed, and deployed directly, there is no need for Android Studio. I assume you’ve already associated Unity with the Android SDK folder. Now we’ll open Build Setting and switch the platform to Android.

Before we can get it up and running, we need to make some simple adjustments to the code. All we need to do is comment out a few lines of code, add a simple return declaration, and replace a file.

Background: Tanks!! Android includes the Everyplay plugin for on-screen recording and sharing of games. The problem was that Everyplay went out of service in October 2018, and there were still some unresolved issues with the plugin that would have failed to compile if we hadn’t addressed them.

First, we need to correct a syntax error in the Everyplay plugin build.gradle file. The path to this file is: Plugins → Android → Everyplay → build.gradle.

Now we open the Gradle file, select all the code, and replace the code below. Tanks!!! The team updated the code on Github, but somehow it didn’t make it into the plugin.

// 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}}Copy the code

The last change we need to make is to turn Everyplay off. You may be wondering: Why are we shutting down Everyplay? Because the Android app crashes when the plug-in is initialized. I’ve found that the fastest way to do this is to update a few lines of code in the Everyplaysettings. cs file (where: Assets → Plugins → EveryPlay → Scripts) so that EveryPlay returns “false” every time it checks to see if it is enabled.

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; }}}Copy the code

Now we are ready to Build. Open Build Settings in Unity, select the Android Platform, and press the “Switch Platform” button. Next, change the bundle ID for the Android App in Player Settings. Here, I use a com. Agora. Tanks. Voicedemo.

Integrated voice chat

Next, we’ll use the Agora Voice SDK for Unity to add voice chat to cross-platform projects. We open the Unity Asset Store and search for Agora Voice SDK for Unity.

After the plug-in page is loaded, click “Download” to Download it. Once the download is complete, select “Import” and integrate it into your project.

We need to create a script for the game to interact with the Agora Voice SDK. Create a new C# file (agorainterface.cs) in your project and open it in Visual Studio.

There are two important variables in this script:

static IRtcEngine mRtcEngine;
public static string appId = "Your Agora AppId Here";
Copy the code

First, replace “Your Agora AppId Here” with App ID, which can be obtained by logging in to Agora. IO and entering Agora Dashboard. MRtcEngine is static so that it is not lost when OnUpdate is called. Since other scripts in the game may reference the App ID, it is public static.

To save time, I have written the code for agorainterface.cs (as shown below). You can use it directly to avoid repeating the wheel.

Here’s a quick explanation of the code. First, we have some logic at the beginning for check/requset Android Permission. Then we initialize the Agora RTC Engine with the App ID, and then we attach some event callbacks, which is pretty straightforward.

MRtcEngine. OnJoinChannelSuccess said user has successfully joined the specified channel.

The last important feature is update, which we want to call when Agora RTC Engine is enabled. The Pull() method, which is critical to making the plug-in work.

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 (); }}}Copy the code

Note that the code above can be replicated for all Unity projects.

Leave the channel

If you’ve ever used the Agora SDK, you may have noticed that there are no join and leave channels. Let’s start with leavehandler. cs, a new C# script that calls theleaveHandler when the user returns to the main menu. The easiest way to do this is to open the method for a specific game object after a LobbyScene has been opened.

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 } } }Copy the code

The game object we are looking for here is LeftSubPanel (MainPanel → MenuUI → LeftSubPanel).

Tanks!!! There are two ways to join a multiplayer game, one is to create a new game, the other is to join the game. So there are two places where we need to add the “Join channel” command.

Let’s go to the UI Script Asset folder (Assets → Scripts → UI) and open the CreateGame. In line 61, you’ll find the way the game matches players, and here we can add some logic for joining channels. The first thing we need to do is apply the Agora SDK library.

using agora_gaming_rtc;
Copy the code

In line 78 of StartMatchmakingGame(), we need to add some logic to get the Agora RTC Engine running, and then use “what the user entered” as the channel name (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); }}); }Copy the code

StartMatchmakingGame() contains join channels

Now we need to open lobbyServerEntry. cs (Assets → Scripts → UI) and add some logic to enable users to “Find a Game” to join someone else’s room.

Open lobbyServerEntry.cs in Visual Studio and find line 63, where there is a JoinMatch(). Let’s add a few lines at line 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); }}); }Copy the code

Done!

We have now completed the Agora SDK integration and are ready to Build and test on both iOS and Android. We can follow the methods in the above section for Building and deploying.

For your convenience, I have uploaded a copy of this Tutorial script to Github: github.com/digitallysa…

If you encounter problems with Agora SDK API calls, please refer to our official documentation (docs.agora.io) and share with our engineers and more colleagues in the Agora section of the RTC Developer community.

If you have any development problems, please visit the QUESTION and answer section of Agora and post to communicate with our engineers.