1

当你制作游戏的时候,你会想把图形、代码和所有插件的性能都发挥地淋漓尽致。Agora 的 Unity SDK 具有占用空间低和性能成本低的优点,这使得其成为众多手机游戏和VR游戏平台的绝佳工具!

在本教程中,我将展示如何使用 Agora 在 Unity MMO 演示文件中创建实时群视频聊天功能,此演示文件使用了 Agora SDK 和 Photon Unity Networking(简称PUN)。

在演示结束时,你会了解到如何下载 Agora Unity 插件,加入和退出另一个玩家的频道,并以可扩展的方式展示自己的玩家群。

在本教程中,我使用的引擎是 Unity 2018.4.18。

在开始之前,我们先来了解一下两个重要因素:

因为这是一个联网演示,所以我们有以下两个测试策略:

需要有2台设备,每台设备都需要有摄像头(我有一台个人计算机和Mac手提电脑)

在同一台机器上,使用 Unity 构建的客户端和Unity编辑器的客户端进行测试(此方案不太理想,因为这两个版本会争夺网络摄像头的访问权限,这令人很头疼)。

接下来我们开始吧!

创建 Agora.io developer 账号来获取你的应用ID

为应用ID创建相应的 Photon developer 账号

从 Unity Asset Store 打开将 Agora SDK 导入到你的项目

将 Photon 多人海盗游戏导入到你的项目

在导入多人海盗游戏资源后,PUN 向导就会自动显示。若没有自动显示,请根据以下路径Window > Photon Unity Networking > PUN Wizard > “Setup Project” >粘贴你用于申请Photon账号的应用ID或者邮箱。
3

创建Agora引擎

我们使用的是默认的“Charprefab”,它位于 Assets > DemoVikings > Resources。

它已经通过 Photon 设置加入网络大厅/房间并通过网络发送消息。

创建一个新文本 AgoraVideoChat,并将它加到 CharPrefab。

在 AgoraVideoChat 中,我们需要添加以下代码:

using agora_gaming_rtc;

// *NOTE* Add your own appID from console.agora.io

[SerializeField]

private string appID = "";

[SerializeField]

private string channel = "unity3d";

private string originalChannel;

private IRtcEngine mRtcEngine;

private uint myUID = 0;

void Start()

{

   if (!photonView.isMine)

     return;

 // Setup Agora Engine and Callbacks.

 if(mRtcEngine != null)

 {

     IRtcEngine.Destroy();

 }

 originalChannel = channel;

 mRtcEngine = IRtcEngine.GetEngine(appID);

 mRtcEngine.OnJoinChannelSuccess = OnJoinChannelSuccessHandler;

 mRtcEngine.OnUserJoined = OnUserJoinedHandler;

 mRtcEngine.OnLeaveChannel = OnLeaveChannelHandler;

 mRtcEngine.OnUserOffline = OnUserOfflineHandler;

 mRtcEngine.EnableVideo();

 mRtcEngine.EnableVideoObserver();

 mRtcEngine.JoinChannel(channel, null, 0);

}

private void TerminateAgoraEngine()

{

    if (mRtcEngine != null)

    {

        mRtcEngine.LeaveChannel();

        mRtcEngine = null;

        IRtcEngine.Destroy();

    }

}

private IEnumerator OnLeftRoom()

{

        //Wait untill Photon is properly disconnected (empty room, and connected back to main server)

        while (PhotonNetwork.room != null || PhotonNetwork.connected == false)

            yield return 0;

TerminateAgoraEngine();

}

// Cleaning up the Agora engine during OnApplicationQuit() is an essential part of the Agora process with Unity.

private void OnApplicationQuit()

{

    TerminateAgoraEngine();

}

这是一个基本的 Agora 设置协议,与 Unity SDK 下载中提供的AgoraDemo非常相似。我们要了解它并迈出掌握 Agora 平台的第一步。

你会发现 photon.isMine 现在对我们感到非常恼怒,下一步,我们需要增加一些 Agora 的回调方法来解决这个问题。

我们可以把 public class AgoraVideoChat : MonoBehaviour 变更为 public class AgoraVideoChat : Photon.MonoBehaviour,以此来涵盖 Photon 行为。

Agora有许多的回调方法可以解决这个问题,但是我们目前只需要添加以下代码:

private void OnJoinChannelSuccessHandler(string channelName, uint uid, int elapsed)
{
    if (!photonView.isMine)
        return;

    myUID = uid;
    Debug.LogFormat("I: {0} joined channel: {1}.", uid.ToString(), channelName);

//CreateUserVideoSurface(uid, true);
}

// Remote Client Joins Channel.

private void OnUserJoinedHandler(uint uid, int elapsed)

{

    if (!photonView.isMine)

        return;

//CreateUserVideoSurface(uid, false);

}

// Local user leaves channel.

private void OnLeaveChannelHandler(RtcStats stats)

{

    if (!photonView.isMine)

        return;

}

// Remote User Leaves the Channel.

private void OnUserOfflineHandler(uint uid, USER_OFFLINE_REASON reason)

{

    if (!photonView.isMine)

        return;

}

现在让我们一起来玩玩 VikingScene 关卡!让我们查看一下此时的日志。

万岁!

4

我们现在正在Agora频道,并且有可能和其他16个玩家进行视频聊天,或将直播给近一百万的观众!到底怎么回事?我们到底在哪里?

创建Agora视频界面

Agora 的 Unity SDK 使用 RawImage 对象来渲染网络摄像头和移动摄像头的视频提要,以及立方体和其他原始形状(具体请参见AgoraEngine>Demo>SceneHome以获取此操作的示例)。

创建原始图像(右击层次窗口>UI>原始图像 )并将它命名为“用户视频”

添加 Videosurface 脚本(Component>Scripts>agora_gaming\u rtc>VedioSurface)

将对象拖动 Assets>Prefabs Folder

从层次结构中删除 UserVideo 对象(此时你可以离开画布),我们需要的只是 prefab。

添加以下代码到 AgoraVideoChat

// add this to your other variables

    [Header("Player Video Panel Properties")]

    [SerializeField]

    private GameObject userVideoPrefab;

private int Offset = 100;

    private void CreateUserVideoSurface(uint uid, bool isLocalUser)

    {

        // Create Gameobject holding video surface and update properties

        GameObject newUserVideo = Instantiate(userVideoPrefab);

        if (newUserVideo == null)

        {

            Debug.LogError("CreateUserVideoSurface() - newUserVideoIsNull");

            return;

        }

        newUserVideo.name = uid.ToString();

GameObject canvas = GameObject.Find("Canvas");

        if (canvas != null)

        {

            newUserVideo.transform.parent = canvas.transform;

        }

        // set up transform for new VideoSurface

        newUserVideo.transform.Rotate(0f, 0.0f, 180.0f);

        float xPos = Random.Range(Offset - Screen.width / 2f, Screen.width / 2f - Offset);

        float yPos = Random.Range(Offset, Screen.height / 2f - Offset);

        newUserVideo.transform.localPosition = new Vector3(xPos, yPos, 0f);

        newUserVideo.transform.localScale = new Vector3(3f, 4f, 1f);

        newUserVideo.transform.rotation = Quaternion.Euler(Vector3.right * -180);

// Update our VideoSurface to reflect new users

        VideoSurface newVideoSurface = newUserVideo.GetComponent<VideoSurface>();

        if (newVideoSurface == null)

        {

            Debug.LogError("CreateUserVideoSurface() - VideoSurface component is null on newly joined user");

        }

if (isLocalUser == false)

        {

            newVideoSurface.SetForUser(uid);

        }

        newVideoSurface.SetGameFps(30);

}

将新创建的prefab添加到 Charprefab 的 UserPrefab 中,并从回调方法中取消 CreateUserVideoSurface()的注释。

再次运行它!现在我们可以看到我们的本地视频流渲染到我们的游戏。如果我们从另一个 Agora 频道进行呼叫,我们将看到更多的视频帧填充到屏幕。我在手机上使用“AgoraDemo”应用来进行测试,但是你也可以用使用1对1呼叫web演示来测试,或者从另一台机器上运行同样的Demo。

现在我们拥有了我们的 Agora 模块并成功运行,接下来是时候在 Photon 连接两个在线玩家来创建功能了。

Photon 网络—加入聊天群

为了加入/邀请/离开聊天群体,我们将要创建一个简单的UI。

在 CharPrefab,创建一个画布和3个按钮,分别命名为邀请按钮,加入按钮和离开按钮。

接下来,我们要在基本 CharPrefab 对象上创建一个叫 PartyJoiner 的新脚本。在脚本中添加以下代码:

using UnityEngine.UI;

[Header("Local Player Stats")]

    [SerializeField]

    private Button inviteButton;

    [SerializeField]

    private GameObject joinButton;

    [SerializeField]

    private GameObject leaveButton;

[Header("Remote Player Stats")]

    [SerializeField]

    private int remotePlayerViewID;

    [SerializeField]

    private string remoteInviteChannelName = null;

private AgoraVideoChat agoraVideo;

private void Awake()

    {

        agoraVideo = GetComponent<AgoraVideoChat>();

    }

private void Start()

    {

        if(!photonView.isMine)

        {

            transform.GetChild(0).gameObject.SetActive(false);

        }

inviteButton.interactable = false;

        joinButton.SetActive(false);

        leaveButton.SetActive(false);

    }

private void OnTriggerEnter(Collider other)

    {

        if (!photonView.isMine || !other.CompareTag("Player"))

        {

            return;

        }

// Used for calling RPC events on other players.

        PhotonView otherPlayerPhotonView = other.GetComponent<PhotonView>();

        if (otherPlayerPhotonView != null)

        {

            remotePlayerViewID = otherPlayerPhotonView.viewID;

            inviteButton.interactable = true;

        }

    }

private void OnTriggerExit(Collider other)

    {

        if(!photonView.isMine || !other.CompareTag("Player"))

        {

            return;

        }

remoteInviteChannelName = null;    

        inviteButton.interactable = false;

        joinButton.SetActive(false);

    }

public void OnInviteButtonPress()

    {

        //PhotonView.Find(remotePlayerViewID).RPC("InvitePlayerToPartyChannel", PhotonTargets.All, remotePlayerViewID, agoraVideo.GetCurrentChannel());

    }

public void OnJoinButtonPress()

    {

        if (photonView.isMine && remoteInviteChannelName != null)

        {

            //agoraVideo.JoinRemoteChannel(remoteInviteChannelName);

            joinButton.SetActive(false);

            leaveButton.SetActive(true);

        }

    }

public void OnLeaveButtonPress()

    {

        if (!photonView.isMine)

            return;

    }

[PunRPC]

    public void InvitePlayerToPartyChannel(int invitedID, string channelName)

    {

        if (photonView.isMine && invitedID == photonView.viewID)

        {

            joinButton.SetActive(true);

            remoteInviteChannelName = channelName;

        }

    }

将相应的“OnButtonPress”函数添加到创建的Unity UI按钮中。 [示例:InviteButton -> “OnInviteButtonPress()”]

将 CharPrefab 标签设置为“Player”

在 CharPrefab 增加一个 SphereCollider 组件(Component bar > Physics >SphereCollider),检查“Is Trigger”框是否被选中,并将其半径设置为1.5

快速 Photon 提示—本地功能

如你所见,我们需要在 AgoraVideoChat 类中实现另外两种方法。在此之前,让我们一起来看看刚刚复制的代码。

private void Start()
{
    if (!photonView.isMine)
    {
        transform.GetChild(0).gameObject.SetActive(false);
    }
    inviteButton.interactable = false;
    joinButton.SetActive(false);
    leaveButton.SetActive(false);
}

“如果这个 Photon 视图不是我的,请将我的第一个子对象设置为False”——重要的是,要记住,尽管这个脚本是在 CharPrefab 上启动的,且 CharPrefab 由我们的设备/键盘输入进行本地控制的,但是这个脚本也会在场景中的其他 CharPrefab 上运行。他们的画布会呈现出来,print 语句也会显示出来。

通过在其他所有 CharPrefab 上将第一个子对象(我的“画布”对象)设置为 false,我的屏幕上将仅仅显示本地画布,而不是 Photom“房间”的每一个玩家。

让我们与两个不同的客户端一起建立和运行程序,看看会发生什么。。。

。。。等等,我们已经在同一个聊天群里了?

你应该还记得,我们设置了 private string channel,并且在Start()方法中调用了 mrtcEngine.JoinChannel(channel,null,0)。在每一个客户端开始运行的时候,我们都创建了并且(或者)加入了一个叫做“unity3d”的 Agora 频道。

为了避免这种情况,我们必须在每一个客户端设置一个新的默认频道名称,这样他们就可以在单独的 Agora 频道中开始运行,并且邀请彼此来到属于他们的独一无二的频道中。

现在让我们在 AgoraVideoChat 中实现另外两种方法:JoinRemoteChannel(stringremotechannelName)和GetCurrentChannel().

public void JoinRemoteChannel(string remoteChannelName)

    {

        if (!photonView.isMine)

            return;

mRtcEngine.LeaveChannel();

mRtcEngine.JoinChannel(remoteChannelName, null, myUID);

        mRtcEngine.EnableVideo();

        mRtcEngine.EnableVideoObserver();

channel = remoteChannelName;

}

public string GetCurrentChannel() => channel;

当被邀请的 Photon ID 和本地玩家ID匹配的时候,这段代码允许我们接收在 Photon 网络中被调用的事件,并对每个玩家透露并粘贴这些事件。

当 Photon 事件击中准确的玩家,他们可以选择加入另一个玩家的远程频道,并使用Agora 网络通过视频聊天和他们建立连接。

现在来测试我们构建的功能,见见我们聊天群中的其他玩家。

收尾工作—构建UI,并离开群聊

现在你已经成功地使用 Agora 加入了频道,并且在你的频道中看到了其他玩家的视频。当其他用户加入频道时,你的屏幕上将弹出视频容器。

然而,它看起来并不太理想,从技术上讲,如果你不退出游戏并重新登录,你就无法离开这个频道。

让我们修复它!

UI框架

我们将要创建一个 ScrollView 对象,用于保存和组织按钮。

在 Charprefab>Canvas:内部,确保画布缩放模式已经设置为“屏幕缩放”(默认情况下是“恒定像素大小”,根据我的经验,这对于绝大多数 Unity UI 情况来说都不是理想的选择)。

在 CharPrefab 对象内部,右击画布,选择 UI>Scroll View(滚动视图)
8

设置“Scroll View(滚动视图)”Rect Transform(矩形变换)方式为“拉伸/拉伸”(选项在右下角),并确保锚点、枢轴和 Rect Transform (矩形变换)与上图红色框中的值匹配。

取消选中“水平”并删除水平滚动条子对象

将“Content”子对象设置为“Top/Stretch”(选项在最右列,从上往下第二项)

创建一个名为“spawmpoint”的空游戏对象作为内容的子级 —设置Rect Transform(矩形变换)“Top/Center”(选项在中间列,从上往下第二项)—设置“Pos Y”的值为-20

确保锚点:最小值/最大值和轴值等于显示的值

9

在 AgoraVideoChat 添加以下代码:

[SerializeField]
private RectTransform content;
[SerializeField]
private Transform spawnPoint;
[SerializeField]
private float spaceBetweenUserVideos = 150f;
private List<GameObject> playerVideoList;

在 Start()添加 playerVideoList = new List<GameObject>();

我们将要把 CreateUserVideoSurface 方法完全替换为以下代码:

// Create new image plane to display users in party
    private void CreateUserVideoSurface(uint uid, bool isLocalUser)

    {

        // Avoid duplicating Local player video screen

        for (int i = 0; i < playerVideoList.Count; i++)

        {

            if (playerVideoList[i].name == uid.ToString())

            {

                return;

            }

        }

// Get the next position for newly created VideoSurface

        float spawnY = playerVideoList.Count * spaceBetweenUserVideos;

        Vector3 spawnPosition = new Vector3(0, -spawnY, 0);

// Create Gameobject holding video surface and update properties

        GameObject newUserVideo = Instantiate(userVideoPrefab, spawnPosition, spawnPoint.rotation);

        if (newUserVideo == null)

        {

            Debug.LogError("CreateUserVideoSurface() - newUserVideoIsNull");

            return;

        }

        newUserVideo.name = uid.ToString();

        newUserVideo.transform.SetParent(spawnPoint, false);

        newUserVideo.transform.rotation = Quaternion.Euler(Vector3.right * -180);

playerVideoList.Add(newUserVideo);

// Update our VideoSurface to reflect new users

        VideoSurface newVideoSurface = newUserVideo.GetComponent<VideoSurface>();

        if(newVideoSurface == null)

        {

            Debug.LogError("CreateUserVideoSurface() - VideoSurface component is null on newly joined user");

        }

if (isLocalUser == false)

        {

            newVideoSurface.SetForUser(uid);

        }

        newVideoSurface.SetGameFps(30);

// Update our "Content" container that holds all the image planes

        content.sizeDelta = new Vector2(0, playerVideoList.Count * spaceBetweenUserVideos + 140);

UpdatePlayerVideoPostions();

        UpdateLeavePartyButtonState();

}

同时添加以下两种新方法:

// organizes the position of the player video frames as they join/leave
private void UpdatePlayerVideoPostions()
{
for (int i = 0; i < playerVideoList.Count; i++)
{
playerVideoList[i].GetComponent<RectTransform>().anchoredPosition = Vector2.down * 150 * i;
}
}// resets local players channel
public void JoinOriginalChannel()
{
if (!photonView.isMine)
return;if(channel != originalChannel || channel == myUID.ToString())
{
channel = originalChannel;
}
else if(channel == originalChannel)
{
channel = myUID.ToString();
}JoinRemoteChannel(channel);
}

现在注释 UpdateLeavePartyButtonState()。

接下来,将新创建的 ScrollView UI 对象拖动到适当的槽中,然后设置它们的 rect transform(矩形变换)值,如下所示:

马上就要完成了!

现在我们必须要在 AgoraVideoChat 添加“Leave Party(离开群聊)”的功能:

public delegate void AgoraCustomEvent();
public static event AgoraCustomEvent PlayerChatIsEmpty;
public static event AgoraCustomEvent PlayerChatIsPopulated;private void RemoveUserVideoSurface(uint deletedUID)
{
foreach (GameObject player in playerVideoList)
{
if (player.name == deletedUID.ToString())
{
// remove videoview from list
playerVideoList.Remove(player);
// delete it
Destroy(player.gameObject);
break;
}
}// update positions of new players
UpdatePlayerVideoPostions();Vector2 oldContent = content.sizeDelta;
content.sizeDelta = oldContent + Vector2.down * spaceBetweenUserVideos;
content.anchoredPosition = Vector2.zero;UpdateLeavePartyButtonState();
}private void UpdateLeavePartyButtonState()
{
if (playerVideoList.Count > 1)
{
PlayerChatIsPopulated();
}
else
{
PlayerChatIsEmpty();
}
}

更新 AgoraVideoChat 回调:

// Local Client Joins Channel.
private void OnJoinChannelSuccessHandler(string channelName, uint uid, int elapsed)
{
if (!photonView.isMine)
return;myUID = uid;CreateUserVideoSurface(uid, true);
}// Remote Client Joins Channel.
private void OnUserJoinedHandler(uint uid, int elapsed)
{
if (!photonView.isMine)
return;CreateUserVideoSurface(uid, false);
}// Local user leaves channel.
private void OnLeaveChannelHandler(RtcStats stats)
{
if (!photonView.isMine)
return;foreach (GameObject player in playerVideoList)
{
Destroy(player.gameObject);
}
playerVideoList.Clear();
}// Remote User Leaves the Channel.
private void OnUserOfflineHandler(uint uid, USER_OFFLINE_REASON reason)
{
if (!photonView.isMine)
return;if (playerVideoList.Count <= 1)
{
PlayerChatIsEmpty();
}RemoveUserVideoSurface(uid);
}

在 PartyJoiner 添加:

private void OnEnable()

    {

        AgoraVideoChat.PlayerChatIsEmpty += DisableLeaveButton;

        AgoraVideoChat.PlayerChatIsPopulated += EnableLeaveButton;

    }

private void OnDisable()

    {

        AgoraVideoChat.PlayerChatIsEmpty -= DisableLeaveButton;

        AgoraVideoChat.PlayerChatIsPopulated -= EnableLeaveButton;

    }

public void OnLeaveButtonPress()

    {

        if(photonView.isMine)

        {

            agoraVideo.JoinOriginalChannel();

            leaveButton.SetActive(false);

        }

    }

private void EnableLeaveButton()

    {

        if(photonView.isMine)

        {

            leaveButton.SetActive(true);

        }

    }

private void DisableLeaveButton()

    {

        if(photonView.isMine)

        {

            leaveButton.SetActive(false);

        }

    }

在两种不同的编辑器中运行这个Demo并加入一个群聊!我们首先通过Photon网络连接到同一个网络游戏大厅,然后通过Agora的SD-RTN网络连接我们的视频聊天聚会!

总结

连接 Agora 网络来显示我们的视频聊天频道

我们成功地让其他用户加入我们的群聊,看到了他们,并与他们实时交谈

我们更进一步地构建了一个可扩展的UI界面,这个界面足够容纳所有你想聊天的人!

如果你在建立自己的网络群视频聊天过程中遇到任何问题或障碍,请随时直接联系或通过 Agora Slack 频道联系我!

以下是完整项目的链接:

GitHub

AgoraIO-Community/agora-unity-partychat-demo

Unity video party chat using Agora SDK and Photon. Refer to the Medium post for project setup from scratch! - AgoraIO-Community/agora-unity-partychat-demo