游戏开发实战Unity从零开发多人视频聊天功能,无聊了就和自己视频聊天(附源码 | Mirror | 多人视频 | 详细教程)
Posted 林新发
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了游戏开发实战Unity从零开发多人视频聊天功能,无聊了就和自己视频聊天(附源码 | Mirror | 多人视频 | 详细教程)相关的知识,希望对你有一定的参考价值。
一、前言
嗨,大家好,我是新发。
事情是这样的,我前几天写了一篇《【游戏开发实战】Unity使用Socket通信实现简单的多人聊天室(万字详解 | 网络 | TCP | 通信 | Mirror | Networking)》
有同学留言问我多人在线视频聊天切换清晰度怎么做,
嗯,作为一位热心的技术博主,我一般都是能帮则帮。
嘛,今天就来写个多人视频聊天的功能吧(并且可以切换清晰度)。
二、思考问题与解决方案
1、思考问题
多人视频聊天大家应该都不陌生,像腾讯视频会议那样,多个人的视频画面同时显示在界面中。
我的实现思路是每个客户端对本地摄像头画面进行采样,得到帧图像,然后对图像进行适当的压缩,转为字节流上传给服务端,接着服务端根据每个客户端设置的清晰度对帧图像进行压缩,然后转发帧图像给其他客户端,其他客户端接收到帧图像字节流后进行解码,最后显示到界面中。
画成图是这样子:
要实现上面的功能,我们需要先思考并解决以下几个必要问题:
1 Unity
中如何开启摄像头并对图像进行采样?
2 图像如何中转给其他客户端?
3 如何实现清晰度切换?
4 客户端如何对图像进行解码并显示?
2、解决方案
2.1、Unity中如何开启摄像头并对图像进行采样
Unity
提供了WebCamTexture
这个类,通过它我们可以很方便的访问摄像头图像。
具体用法我下文会讲。
2.2、图像如何中转给其他客户端
正常情况我们需要搭建一个中转的服务端,要实现数据的序列化、网络通信、数据的反序列化等。这里我想使用Mirror
网络库来实现。在之前那篇文章中我也有介绍过Mirror
:《【游戏开发实战】Unity使用Socket通信实现简单的多人聊天室(万字详解 | 网络 | TCP | 通信 | Mirror | Networking)》
2.3、如何实现清晰度切换
客户端把清晰度设置告诉服务端,服务端根据清晰度对图像进行压缩,把压缩后的图像下发给客户端。
由于我们是使用Mirror
,服务端也是Unity
客户端,所以我们可以直接使用Texture2D
的EncodeToJPG
接口对图像进行压缩,第二个参数就是压缩率,值从1~100
(默认是75
),
public static byte[] EncodeToJPG(this Texture2D tex, int quality);
2.4、客户端如何对图像进行解码并显示
通过网络传输过来的图像是字节流,我们需要把它反序列化为Unity
可现实的图像Texture2D
,我们直接使用Texture2D
的LoadImage
接口,
public static bool LoadImage(this Texture2D tex, byte[] data);
三、实际操作
下面,撸起袖子开始动手实际操作吧~
0、思维导图
养成好习惯,动手前先画思维图,如下:
1、界面设计与制作
使用axure
快速原型设计工具先简单设计一下界面,
登录界面,Host
就是作为房主,Client
就是作为路人,
视频聊天界面,排列显示多个视频画面,可切换视频清晰度,可随时退出房间,
2、UI素材获取
简单的UI
素材资源我是在阿里巴巴矢量图库上找,地址:https://www.iconfont.cn/
比如搜索按钮
,
找一个形状合适的,可以进行调色,我一般是调成白色,
因为Unity
中可以设置Color
,这样我们只需要一个白色按钮就可以在Unity
中创建不同颜色的按钮了。
弄点基础的美术资源,
注:那个头像是我自己用PhotoShop
画的哦,我之前用PhotoShop
画过一幅原创连环画,如下:
3、创建Unity工程
我使用的Unity
版本为2021.1.7f1c1 个人版
,我们要做的是一个多人视频聊天的功能,不需要使用到3D
相关的内容,所以我们创建工程时使用2D
模板,工程名就叫UnityVideoChat
吧~
创建成功,
4、制作UI界面
根据我们的原型设计,使用UGUI
制作界面:MainPanel.prefab
,
如下,
其中用于渲染视频图像的UI
独立做成一个预设:VideoImage.prefab
,方便进行克隆(每连接一客户端就克隆一个VideoImage
)
如下,使用RawImage
组件来显示图像,
5、下载Mirror网络插件
Mirror
的Unity Asset Store
地址:
https://assetstore.unity.com/packages/tools/network/mirror-129321
将Mirror
插件添加到自己的账号中,然后回到Unity
,在Package Manager
中就可以下载了,
下载下来导入Unity
中,
6、写C#代码
6.1、网络管理器:VideoChatNetwork.cs
先画个图,方便大家直观地知道VideoChatNetwork
做什么:
注:
VideoChatNetwork.cs
脚本完整代码见我的文末的工程源码,下面只讲一些重点的地方。
创建VideoChatNetwork.cs
脚本,它需要继承Mirror.NetworkManager
,
// VideoChatNetwork.cs
using Mirror;
public class VideoChatNetwork : NetworkManager
{
// ...
}
启动服务端:
StartHost();
启动客户端:
StartClient();
关闭服务端:
StopHost();
关闭客户端:
StopClient();
定义消息CreatePlayerMessage
(用于传递用户名):
public struct CreatePlayerMessage : NetworkMessage
{
public string name;
}
服务器启动成功回调,注册CreatePlayerMessage
消息响应函数,在响应函数中实例化Player
并添加到NetworkServer
中:
public override void OnStartServer()
{
base.OnStartServer();
// 注册事件
NetworkServer.RegisterHandler<CreatePlayerMessage>(OnCreatePlayer);
// ...
}
void OnCreatePlayer(NetworkConnection connection, CreatePlayerMessage createPlayerMessage)
{
// 实例化Player
GameObject playergo = Instantiate(playerPrefab);
playergo.GetComponent<Player>().accountName = createPlayerMessage.name;
// 添加Player
NetworkServer.AddPlayerForConnection(connection, playergo);
}
客户端连接成功回调:
public override void OnClientConnect(NetworkConnection conn)
{
base.OnClientConnect(conn);
// 转发通知
conn.Send(new CreatePlayerMessage { name = MainLogic.instance.accountName });
}
连接断开回调:
public override void OnClientDisconnect(NetworkConnection conn)
{
// TODO 重新登录
}
6.2、摄像头画面:Player.cs
Player
思维导图:
注:
Player.cs
脚本完整代码见我的文末的工程源码,下面只讲一些重点的地方。
创建Player.cs
脚本,它需要继承Mirror.NetworkBehaviour
,
using Mirror;
public class Player : NetworkBehaviour
{
// ...
}
先定义一些必要的UI
对象,其中用户名
使用[SyncVar]
注解进行自动同步,
public RawImage videoImage;
[SyncVar]
public string accountName;
public Text accountNameText;
在Start
函数中判断是否是本地用户isLocalPlayer
,如果是,则开启摄像头:
// Player.cs
private WebCamTexture webCam;
private void Start()
{
if (isLocalPlayer)
{
// 开启摄像头
WebCamDevice[] devices = WebCamTexture.devices;
webCam = new WebCamTexture(devices[0].name, 128, 128, 5); //设置宽、高和帧率
webCam.Play();
}
// ...
}
在Update
函数中对摄像头图像进行采样,0.3秒
采集一帧,可适当进行调整,同时把图像转为字节流并发送给服务端,
// Player.cs
private float timer;
public void Update()
{
if (isLocalPlayer && null != webCam)
{
timer += Time.deltaTime;
if (timer > 0.3f)
{
timer = 0;
// 采样
videoImage.texture = webCam;
// 图像转字节流
var bytes = MainLogic.instance.WebCamTextureToBytes(webCam);
// 发送字节流给服务端
CmdSendTextureBytes(bytes);
}
}
}
发送图像字节流给服务端,注意Command
为客户端远程调用服务端,
// 发送图像字节流给服务端
// Command为客户端远程调用服务端
[Command]
public void CmdSendTextureBytes(byte[] texture)
{
RpcReceiveTexture(texture);
}
客户端接收服务端的图像字节流数据,并显示到RawImage
上,注意ClientRpc
为服务端远程调用客户端,
// 客户端接收服务端的图像字节流数据,并显示到RawImage上
// ClientRpc为服务端远程调用客户端
[ClientRpc]
public void RpcReceiveTexture(byte[] textureBytes)
{
if(!isLocalPlayer)
{
// 压缩
var compressedTex = MainLogic.instance.CompressTexture(textureBytes);
// 显示
videoImage.texture = MainLogic.instance.BytesToTexture2D(compressedTex);
}
}
上面我们出现了Mirror
三个注解:[SyncVar]
、[Command]
、[ClientRpc]
,想要理解它们最好的办法是反编译我们的C#
的dll
,看它生成的代码(使用ILSpy.exe
对dll
进行反编译)。
[SyncVar]
[SyncVar]
会对我们的变量做自动同步(自动序列化、网络传递、反序列化),例:
[SyncVar]
public string accountName;
编译后生成的代码:
[SyncVar]
public string accountName;
public string NetworkaccountName
{
get
{
return accountName;
}
[param: In]
set
{
if (!SyncVarEqual(value, ref accountName))
{
string text = accountName;
SetSyncVar(value, ref accountName, 1uL);
}
}
}
// 序列化
public override bool SerializeSyncVars(NetworkWriter writer, bool forceAll)
{
bool result = base.SerializeSyncVars(writer, forceAll);
if (forceAll)
{
writer.WriteString(accountName);
return true;
}
writer.WriteULong(base.syncVarDirtyBits);
if ((base.syncVarDirtyBits & 1L) != 0L)
{
writer.WriteString(accountName);
result = true;
}
return result;
}
// 反序列化
public override void DeserializeSyncVars(NetworkReader reader, bool initialState)
{
base.DeserializeSyncVars(reader, initialState);
if (initialState)
{
string text = accountName;
NetworkaccountName = reader.ReadString();
return;
}
long num = (long)reader.ReadULong();
if ((num & 1L) != 0L)
{
string text2 = accountName;
NetworkaccountName = reader.ReadString();
}
}
我们代码中对accountName
的操作,都被替代为对NetworkaccountName
的操作,比如:
playergo.GetComponent<Player>().accountName = createPlayerMessage.name;
变成了:
playergo.GetComponent<Player>().NetworkaccountName = createPlayerMessage.name;
[Command]
[Command]
表示客户端远程调用服务端。
例:
[Command]
public void CmdSendTextureBytes(byte[] texture)
{
RpcReceiveTexture(texture);
}
编译后生成的代码:
[Command]
public void CmdSendTextureBytes(byte[] texture)
{
PooledNetworkWriter writer = NetworkWriterPool.GetWriter();
writer.WriteBytesAndSize(texture);
SendCommandInternal(typeof(Player), "CmdSendTextureBytes", writer, 0);
NetworkWriterPool.Recycle(writer);
}
protected void UserCode_CmdSendTextureBytes(byte[] texture)
{
RpcReceiveTexture(texture);
}
protected static void InvokeUserCode_CmdSendTextureBytes(NetworkBehaviour obj, NetworkReader reader, NetworkConnectionToClient senderConnection)
{
if (!NetworkServer.active)
{
Debug.LogError((object)"Command CmdSendTextureBytes called on client.");
}
else
{
((Player)obj).UserCode_CmdSendTextureBytes(reader.ReadBytesAndSize());
}
}
static Player()
{
RemoteCallHelper.RegisterCommandDelegate(typeof(Player),
"CmdSendTextureBytes", InvokeUserCode_CmdSendTextureBytes,
requiresAuthority: true);
}
我们可以看到,它把我们的调用转成了网络消息,变量做了序列化、传递和反序列化。
[ClientRpc]
[ClientRpc]
表示服务端远程调用客户端。
例:
[ClientRpc]
public void RpcReceiveTexture(byte[] textureBytes)
{
if(!isLocalPlayer)
{
// 压缩
var compressedTex = MainLogic.instance.CompressTexture(textureBytes);
// 显示
videoImage.texture = MainLogic.instance.BytesToTexture2D(compressedTex);
}
}
编译后生成的代码:
[ClientRpc]
public void RpcReceiveTexture(byte[] textureBytes)
{
PooledNetworkWriter writer = NetworkWriterPool.GetWriter();
writer.WriteBytesAndSize(textureBytes);
SendRPCInternal(typeof(Player), "RpcReceiveTexture", writer, 0, includeOwner: true);
NetworkWriterPool.Recycle(writer);
}
protected void UserCode_RpcReceiveTexture(byte[] textureBytes)
{
if (!base.isLocalPlayer)
{
byte[] compressedTex = MainLogic.instance.CompressTexture(textureBytes);
videoImage.texture = (Texture)(object)MainLogic.instance.BytesToTexture2D(compressedTex);
}
}
protected static void InvokeUserCode_RpcReceiveTexture(NetworkBehaviour obj, NetworkReader reader, NetworkConnectionToClient senderConnection)
{
if (!NetworkClient.active)
{
Debug.LogError((object)"RPC RpcReceiveTexture called on server.");
}
else
{
((Player)obj).UserCode_RpcReceiveTexture(reader.ReadBytesAndSize());
}
}
static Player()
{
RemoteCallHelper.RegisterRpcDelegate(typeof(Player),
"RpcReceiveTexture", InvokeUserCode_RpcReceiveTexture);
}
我们可以看到,它把我们的调用转成了网络消息,变量做了序列化、传递和反序列化。
6.3、业务逻辑:MainLogic.cs
MainLogic
思维导图:
注:
MainLogic.cs
脚本完整代码见我的文末的工程源码,下面只讲一些重点的地方。
创建MainLogic.cs
脚本,全局唯一一个实例对象,我们使用单例模式:
// MainLogic.cs
public class MainLogic
{
// 单例模式
private static MainLogic s_instance;
public static MainLogic instance
{
get
{
if (null == s_instance)
s_instance = new MainLogic();
return s_instance;
}
}
}
定义成员变量:
/// <summary>
/// 用户名
/// </summary>
public string accountName;
/// <summary>
/// 清晰度,0:高清,1:标清,2:普通
/// </summary>
public 以上是关于游戏开发实战Unity从零开发多人视频聊天功能,无聊了就和自己视频聊天(附源码 | Mirror | 多人视频 | 详细教程)的主要内容,如果未能解决你的问题,请参考以下文章
游戏开发实战Unity使用Socket通信实现简单的多人聊天室(万字详解 | 网络 | TCP | 通信 | Mirror | Networking)
游戏开发实战Unity使用Socket通信实现简单的多人聊天室(万字详解 | 网络 | TCP | 通信 | Mirror | Networking)
游戏开发实战Unity从零做一个任务系统,人生如梦,毕业大学生走上人生巅峰(含源码工程 | 链式任务 | 主线支线)
游戏开发实战Unity从零做一个任务系统,人生如梦,毕业大学生走上人生巅峰(含源码工程 | 链式任务 | 主线支线)