Unity小功能开发实战教程在UI画布上画网格线

Posted 周周的Unity小屋

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Unity小功能开发实战教程在UI画布上画网格线相关的知识,希望对你有一定的参考价值。

文章目录

👉一、前言

在大多软件中,网格线常常起到辅助线条的作用,像word中的网格线主要用来帮助用户将word文档中的图形、图像、文本框、艺术字等对象沿网格线对齐,并且在打印时网格线不被打印出来。接下来,我们就学习一下Unity中如何实现在UI画布上绘制网格线。

👉二、Unity上画网格线的效果


2.

3.

👉二、画网格线的原理

同样是使用了using UnityEngine.UI这个命名空间下的MaskableGraphic类来绘制网格线。
根据面向对象的思想,我们先抽象出画网格线的一些成员:线宽、线的颜色和线的间隔。
根据这些成员我们就可以计算得到一个个矩形面片的顶点信息,然后利用MaskableGraphic来绘制出一定数量的矩形面片,效果上形成我们想要的网格线样式。如下:
1.在水平方向上绘制出一条条垂直线

2.在垂直方向上绘制出一条条水平线

👉三、完整代码

using UnityEngine;
using UnityEngine.UI;

public class Grid : MaskableGraphic

    /// <summary>
    /// 网格线间隔
    /// </summary>
    public int gridSpace = 50;
    /// <summary>
    /// 网格线的像素宽度
    /// </summary>
    public float gridLineWidth = 1.0f;
    //可以自定义网格线颜色、如渐变色等,这里我是直接使用基类的颜色

    protected override void OnPopulateMesh(VertexHelper vh)
    
        vh.Clear();

        //取整数
        float width = Mathf.RoundToInt(rectTransform.rect.width);
        float height = Mathf.RoundToInt(rectTransform.rect.height);
        gridSpace = (int)Mathf.Clamp(gridSpace, 0, width);

        //先画水平方向上的线,从左到右绘制垂直线段
        for (int i = 0; i < width; i += gridSpace)
        
            //四个点可以绘制一个矩形面片
            var horizontal_A = new Vector2(i, 0);
            var horizontal_B = new Vector2(i, height);
            var horizontal_C = new Vector2(i + gridLineWidth, height);
            var horizontal_D = new Vector2(i + gridLineWidth, 0);
            vh.AddUIVertexQuad(GetRectangleQuad(color, horizontal_A, horizontal_B, horizontal_C, horizontal_D));
        
        //最后画垂直方向上的线,从下到上绘制水平线段
        for (int i = 0; i < height; i += gridSpace)
        
            var vertical_A = new Vector2(0, i);
            var vertical_B = new Vector2(0, i + gridLineWidth);
            var vertical_C = new Vector2(width, i + gridLineWidth);
            var vertical_D = new Vector2(width, i);
            vh.AddUIVertexQuad(GetRectangleQuad(color, vertical_A, vertical_B, vertical_C, vertical_D));
        
    

     //得到一个矩形面片
    private UIVertex[] GetRectangleQuad(Color color, params Vector2[] points)
    
        UIVertex[] vertexs = new UIVertex[points.Length];
        for (int i = 0; i < vertexs.Length; i++)
        
            vertexs[i] = GetUIVertex(points[i], color);
        
        return vertexs;
    
    
    //得到一个顶点信息
    private UIVertex GetUIVertex(Vector2 point, Color color)
    
        UIVertex vertex = new UIVertex
        
            position = point,
            color = color,
            uv0 = Vector2.zero
        ;
        return vertex;
    

👉三、注意事项

  1. 以上代码挂载到任意空物体即可使用。
  2. 调整网格线参数时需手动输入数值看效果,如用鼠标拖拽改变数值过快可能导致Unity编辑器卡崩掉(目前还没定位到具体原因)。
  3. 根据代码规律绘制网格线,需将空物体的锚点设置为(0,0);布局设置为Left。(当然你可根据你的需要自定义,具体看你需要那种效果的网格线)

游戏开发实战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客户端,所以我们可以直接使用Texture2DEncodeToJPG接口对图像进行压缩,第二个参数就是压缩率,值从1~100(默认是75),

public static byte[] EncodeToJPG(this Texture2D tex, int quality);
2.4、客户端如何对图像进行解码并显示

通过网络传输过来的图像是字节流,我们需要把它反序列化为Unity可现实的图像Texture2D,我们直接使用Texture2DLoadImage接口,

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网络插件

MirrorUnity 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.exedll进行反编译)。

[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小功能开发实战教程在UI画布上画网格线的主要内容,如果未能解决你的问题,请参考以下文章

游戏开发实战Unity手游第一人称视角,双摇杆控制,FPS射击游戏Demo(教程 | 含Demo工程源码)

游戏开发实战Unity从零开发多人视频聊天功能,无聊了就和自己视频聊天(附源码 | Mirror | 多人视频 | 详细教程)

游戏开发实战Unity从零开发多人视频聊天功能,无聊了就和自己视频聊天(附源码 | Mirror | 多人视频 | 详细教程)

Unity3D 官方移动游戏优化指南9.用户界面

JQuery在画布图像上画线并重置线而不影响图像

关于unity3D UI 对canvas的操作问题(鼠标拖动改变画布大小)