当制作游戏的时候,我们会想把图形、代码和所有插件的性能都发挥地淋漓尽致。Agora 的 Unity SDK 具有空间占用低和性能成本低的优点,这使得其成为众多手机游戏和VR游戏平台的绝佳工具!
在本教程中,我将展示如何使用 Agora 在 Unity MMO Demo中实现实时群视频聊天功能,此Demo使用了 Agora SDK 和 Photon Unity Networking(简称PUN)。
在演示结束时,你会了解到如何下载 Agora Unity 插件,加入和退出另一个玩家的频道,并以可扩展的方式展示自己的玩家群。
在本教程中,我使用的引擎是 Unity 2018.4.18。
在开始之前,我们先来了解一下两个重要因素:
因为这是一个联网演示,所以我们有以下两个测试策略:
需要有2台设备,每台设备都需要有摄像头(我有一台个人计算机和Mac手提电脑)
在同一台机器上,使用 Unity 构建的客户端和Unity编辑器的客户端进行测试(此方案不太理想,因为这两个版本会争夺网络摄像头的访问权限,这令人很头疼)。
接下来我们开始吧!
创建声网开发者账号来获取你的应用ID
为应用ID创建相应的 Photon developer 账号
从 Unity Asset Store 打开将 Agora SDK 导入到你的项目
将 Photon 多人海盗游戏导入到你的项目
在导入多人海盗游戏资源后,PUN 向导就会自动显示。若没有自动显示,请根据以下路径Window > Photon Unity Networking > PUN Wizard > “Setup Project” >粘贴你用于申请Photon账号的应用ID或者邮箱。
创建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有许多的回调方法可以解决这个问题,但是我们目前只需要添加以下代码:
// Local Client Joins Channel.
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 关卡!让我们查看一下此时的日志。
万岁!
我们现在就在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,我的屏幕上将仅仅显示本地画布,而不是 Photon“房间”的每一个玩家。
让我们与两个不同的客户端一起建立和运行程序,看看会发生什么
等等,我们已经在同一个聊天群里了?
你应该还记得,我们设置了 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(滚动视图)
设置“Scroll View(滚动视图)”Rect Transform(矩形变换)方式为“拉伸/拉伸”(选项在右下角),并确保锚点、枢轴和 Rect Transform (矩形变换)与上图红色框中的值匹配。
取消选中“水平”并删除水平滚动条子对象
将“Content”子对象设置为“Top/Stretch”(选项在最右列,从上往下第二项)
创建一个名为“spawmpoint”的空游戏对象作为内容的子级 —设置Rect Transform(矩形变换)“Top/Center”(选项在中间列,从上往下第二项)—设置“Pos Y”的值为-20
确保锚点:最小值/最大值和轴值等于显示的值
在 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界面,这个界面足够容纳所有你想聊天的人!
以下是完整项目的链接: