渲染流水线概述

什么是渲染流水线?

计算机从一些列的顶点数据、纹理等信息出发,把这些信息最终转换成一张人眼可以见到的图像的过程叫做渲染流水线,这个工作通常是由CPU和GPU共同完成的。

渲染流水线分为三个阶段:

  • 应用阶段
  • 几何阶段
  • 光栅化阶段

应用阶段

从名字可以看出,这个阶段是由我们主导的,因此通常由CPU负责实现。换句话说开发者具有这个阶段的绝对控制权。

首先,我们需要准备好场景数据(摄像机位置、视锥体、场景中包含那些模型、光源等)。

其次,为了提高渲染性能,通常需要做一个粗颗粒度剔除(culling)工作,把不可见的物体剔除出去,这样就不需要再移交给几何阶段处理。

最后,我们需要设置好每个模型的渲染状态,包括但不限于材质(漫反射颜色、高光反射颜色)使用的纹理、shader等。

这一阶段最重要的输出是渲染所需的几何信息 即渲染图元(rendering primitives)。

通俗来讲,渲染图元可以是点、线、三角面等。这些渲染图元会传递给下一个阶段——几何阶段。


几何阶段

几何阶段用于处理所有我们要绘制的几何相关的事情,例如决定需要绘制的图元是什么,怎样如何绘制他们。这一阶段通常再GPU上进行。

几何阶段负责和每个渲染图元打交道,进行逐顶点、逐多边形的操作。这个阶段可以进一步分成更小的流水线阶段。

几何阶段的一个重要任务就是把顶点坐标变换到屏幕空间中,再交给光栅器进行处理,通过对输入的渲染图元进行多步处理后,

将会输出屏幕空间的二维顶点坐标、每个顶点对应的深度值、着色器相关信息,并传递给下一阶段。


光栅化阶段

这一阶段将会使用几何阶段传递的数据来产生屏幕上的像素,并渲染出最终的图像。

这一阶段也是在GPU上进行,光栅化的任务 主要是决定每个渲染图元中的哪些像素应该被绘制再屏幕上。

他需要对上一个阶段得到的 逐顶点数据 (例如纹理坐标、顶点颜色等)进行插值,然后再进行逐像素处理。

和上一阶段类似,光栅化阶段也可以分成更小的流水线阶段


注意:

我们需要把上面的3个流水线阶段和GPU流水线阶段区分开来。这里的流水线均是概念流水线,是我们为了给一个渲染流程进行基本的功能划分而提出的。


来源作者:《UnityShader入门精要》冯乐乐

矩阵-坐标空间转换

坐标空间转换

  1. 渲染管线(流水线)的基本概念是将数据分阶段的变为屏幕图像的过程
  2. 在渲染管线中的顶点阶段和光栅化阶段会对数据进行处理其中最重要的一部分工作就是坐标转换、顶点变换等等
  3. 矩阵主要就是来完成变换的 可以通过平移、旋转、缩放矩阵来完成对点或向量的变换

坐标空间是什么

坐标空间是一个用于描述和定位物体位置的数学概念。

一般由一个基础参照物(原点)和轴线(相互垂直的轴线)组成

常见的坐标空间包括二位平面坐标空间和三维空间

在三维空间中 通常决定一个原点(0,0,0)并使用三个坐标轴(通常是xyz轴)来描述点的位置


Unity中的坐标空间

  • 世界坐标系
  • 物体坐标系
  • 屏幕坐标系
  • 等等

之所以存在这么多不同的坐标空间,主要是因为不同的问题需要不同的坐标系来描述和解决特定的空间问题


坐标空间的变换

在Shader开发中 为了方便制作模型、使用模型、渲染模型 也存在很多不同的坐标空间

比如:模型空间、世界空间、观察空间、裁剪空间、屏幕空间

在设计模型时 使用的是模型空间(顶点、法线相关数据都是基于模型空间坐标系的)

当模型被导入Unity后,最终能够在屏幕上显示 就经历了一系列坐标转换

模型空间->世界空间->观察空间->裁剪空间->屏幕空间


坐标空间的变换规则

Unity中大部分的坐标空间 都是世界坐标空间的子坐标空间

这些子坐标空间的原点和轴向相关表示数据都是基于世界坐标空间的

因此在Unity的坐标空间中会形成一种层级结构

矩阵-变换

矩阵的几何意义

矩阵的几何意义

点和向量能在图像中画出来,那么矩阵可以吗? 答案是肯定的,矩阵的可视化结果就是:变换

在游戏开发中,如果你看到了一个矩阵,那么基本上你可以认为你看到的是一个 变换,

这些变换一般包含:平移、旋转、缩放

比如: 我们想要将一个点、一个向量进行一种变换(平移、旋转、缩放) 那么我们可以利用矩阵来进行数学计算,从而达到变换的目的


变换的种类

矩阵的几何意思是变换,在数学中常用的几何变换有两种:

线性变换 和 仿射变换

他们的主要区别在于是否保持直线的平行性和原点位置

线性变换:

保持直线的平行性和原点位置不变,比如缩放、旋转等操作 简而意之就是只对向量进行旋转、缩放等操作,而不影响方向和原点的位置

仿射变换:

由一个线性变换和一个平移组成,比如缩放后、旋转后再平移 简而言之就是缩放后、旋转后平移,会改变原点的位置


齐次坐标

齐次坐标是一种在计算机图形学中常用的表示坐标的方式。 它是通过引入一个额外的维度来扩展传统的笛卡尔坐标系, 就是将一个原本是 n 维的向量或矩阵用 n + 1维来表示, 让我们可以更方便的进行几何变换和矩阵运算。

举例:

三维空间中有一个向量或点 ( x , y , z),它对应的齐次坐标就是给它加一维,变成 (x , y , z , w),其中 w 值的改变可以让它有具有不同的含义。


为什么要使用齐次坐标进行矩阵运算

主要原因1:

通过齐次坐标,我们可以明确的区分向量和点

三维空间中的 ( x , y , z) ,它既可以表示点,也可以表示向量。 可以利用齐次坐标给它加一维,变成 (x , y , z , w),其中 w = 1 时 代表是一个点,w = 0 时 代表一个向量。 这样就可以明确它是点还是向量了

有限状态机

状态机基类实现

using System;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 状态接口
/// </summary>
public interface IState
{
    /// <summary>
    /// 状态进入
    /// </summary>
    void OnEnter();
    /// <summary>
    /// 状态退出
    /// </summary>
    void OnExit();
    /// <summary>
    /// 状态执行
    /// </summary>
    void OnUpdate();
    /// <summary>
    /// 状态执行
    /// </summary>
    void OnFixedUpdate();
}

/// <summary>
/// 在不同状态之间共享数据 
/// </summary>
[Serializable]
public class BlackBorad
{
}

/// <summary>
/// 状态机
/// </summary>
/// <typeparam name="T">枚举状态Type</typeparam>
public abstract class FSM<T> where T: Enum
{
    /// <summary>
    /// 状态字典
    /// </summary>
    private readonly Dictionary<T, IState> stateDict;
    /// <summary>
    /// 当前状态
    /// </summary>
    private IState curState;
    /// <summary>
    /// 共享数据 黑板
    /// </summary>
    public BlackBorad BlackBorad { get; private set; }
    public FSM(BlackBorad blackBorad)
    {
        stateDict = new Dictionary<T, IState>();
        BlackBorad = blackBorad;
    }
    /// <summary>
    /// 状态添加
    /// </summary>
    /// <param name="stateType"></param>
    /// <param name="state"></param>
    public virtual void AddState(T stateType, IState state)
    {
        if (stateDict.ContainsKey(stateType))
        {
            Debug.Log("[AddState]已存在状态:" + stateType.ToString());
            return;
        }
        stateDict.Add(stateType, state);
    }
    /// <summary>
    /// 状态切换
    /// </summary>
    /// <param name="stateType"></param>
    public virtual void SwitchState(T stateType)
    {
        if (!stateDict.ContainsKey(stateType))
        {
            Debug.Log("[SwitchState]不存在状态:" + stateType.ToString());
            return;
        }
        curState?.OnExit();
        curState = stateDict[stateType];
        curState.OnEnter();
    }
    /// <summary>
    /// 状态执行
    /// </summary>
    public virtual void OnUpdate()
    {
        curState?.OnUpdate();
    }
    /// <summary>
    /// 状态执行
    /// </summary>
    public virtual void OnFixedUpdate() 
    {
        curState?.OnFixedUpdate();
    }
}

游戏状态机

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 游戏状态枚举
/// </summary>
public enum E_GameStateType
{
    Loading,
    Start,
    Stop,
    End
}
/// <summary>
/// 黑板
/// </summary>
public class GameFSMBlackBorad : BlackBorad
{

}
/// <summary>
/// 游戏状态状态机
/// </summary>
public class GameFSM : FSM<E_GameStateType>
{
    public GameFSM(BlackBorad blackBorad) : base(blackBorad)
    {
        
    }
}

使用

HTTP类

HttpWebRequest类

命名空间:System.Net

HttpWebRequest是主要用于发送客户端请求的类

主要用于:发送HTTP客户端请求给服务器,可以进行消息通信、上传、下载等等操作


HttpWebRequest方法

创建WebRequest

HttpWebRequest req = HttpWebRequest.Create(new Uri("http://192.168.50.109:8000/Http_Server/")) as HttpWebRequest;

终止传输

req.Abort();

获取用于上传的流

Stream s = req.GetRequestStream();

返回HTTP服务器响应

HttpWebResponse res = req.GetResponse() as HttpWebResponse;

异步获取用于上传的流

HTTP工作原理

HTTP是什么?

超文本传输协议,是因特网上应用最为广泛的一种网络传输协议。

最初设计HTTP的 目的是为了提供一种发布和接收由文本文件组成的HTML页面的方法,后来发展到除 了文本数据外,还可以传输图片、音频、视频、压缩文件以及各种程序文件等。

HTTP主要用于超文本传输,因此相对FTP显得更简单一些,目前常见的HTTP标准是 HTTP/1.1


HTTP的工作原理

HTTP的本质也是TCP通信

HTTP定义了Web客户端(一般指浏览器)如何从Web服务器请求Web页面,以及服务器如何把Web页面传送给客户端。

HTTP客户端首先与服务器建立TCP连接 然后客户端通过套接字发送HTTP请求,并通过套接字接收HTTP响应 由于HTTP采用TCP传输数据,因此不会丢包、不会乱序。


HTTP以TCP方式工作

在HTTP/1.0中,客户端和服务器建立TCP连接后,发送一个请求到服务器,服务器发 送一个应答给客户端,然后立即断开TCP连接,他们的主要步骤为:

  1. 客户端与服务端建立TCP连接
  2. 客户端向服务端发出请求
  3. 若服务端接受请求,则回送响应码和所需的信息
  4. .客户端与服务端断开TCP连接

需要注意,HTTP/1.1 支持持久连接,即客户端和服务端建立连接后,可以发送请求和接收应答,然后迅速地发送另一个请求和接收另一个应答。

持久连接也使得在得到上一个请求的应答之前能够发送多个请求,这就是HTTP/1.1 与HTTP/1.0的明显不同之处,除此之外,HTTP/1.1可以发送的请求类型也比 HTTP/1.0多。

目前市面上的Web服务器软件和浏览器软件基本都是支持HTTP/1.1版本的。

目前使用的基本上都是HTTP/1.1版本


HTTP是无状态的

客户端发送一次请求后,服务端并没有存储关于该客户端的任何状态信息 即使客户端再次请求同一个对象,服务端仍会重新发送这个对象,不会在意之前是否已经向客户端发送过这个对象。


HTTP使用元信息作为标头

HTTP通过添加标头(header)的方式向服务端提供本次HTTP请求的相关信息,即在主要数据前添加一部分额外信息,称为元信息(metainformation)

元信息里主要包含:传送的对象属于哪种类型,采用的是哪种编码等等


HTTP的工作原理(总结)

HTTP的工作原理主要有以下三个特点

  1. HTTP是以TCP方式工作 连接—>请求—>响应—>断开(目前的HTTP/1.1支持长久连接)
  2. HTTP是无状态的 客户端要什么来什么,想要多少来多少,服务端不会因为你要过了而不给你
  3. HTTP使用元信息作为标头 主要数据前添加一部分额外信息(元信息) 包含传送的对象属于哪种类型,采用的是哪种编码等等

HTTP协议的请求类型和响应状态码

HTTP/1.0中:

GET、POST、HEAD

HTTP/1.1中:

GET、POST、HEAD、PUT、DELETE、OPTIONS、TRACE、CONNECT

响应状态码:

1xx、2xx、3xx、4xx、5xx


请求方法名说明
GET请求获取特定的资源,比如请求一个Web页面或请求获取一个资源
POST请求提交数据进行处理,比如请求上传一个文件
HEAD请求获取和GET一致的内容,但是不会返回具体内容,只会返回消息头
PUT向指定位置上传最新内容
DELETE删除指定资源
OPTIONS返回服务器针对特定资源支持的HTTP请求方法
TRACE回显服务端收到的请求
CONNECT预留给能够将连接改为管道方式的代理服务器

HTTP协议的请求类型

每一种请求方法,其实就是在HTTP请求的头部信息包含的内容不同而已 内容发送的格式为:

UDP同步通信

实现UDP服务端通信 收发字符串

//1.创建套接字
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
//2.绑定本机地址
IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8081);
socket.Bind(ipPoint);
Console.WriteLine("服务器开启");
//3.接受消息
byte[] bytes = new byte[512];
//这个变量主要是用来记录 谁发的信息给你 传入函数后 在内部 它会帮助我们进行赋值
EndPoint remoteIpPoint2 = new IPEndPoint(IPAddress.Any, 0);
int length = socket.ReceiveFrom(bytes, ref remoteIpPoint2);
Console.WriteLine("IP:" + (remoteIpPoint2 as IPEndPoint).Address.ToString() +
    "port:" + (remoteIpPoint2 as IPEndPoint).Port +
    "发来了" +
    Encoding.UTF8.GetString(bytes, 0, length));

//4.发送到指定目标
//由于我们先收 所以 我们已经知道谁发了消息给我 我直接发给它就行了
socket.SendTo(Encoding.UTF8.GetBytes("欢迎发送消息给服务器"), remoteIpPoint2);

//5.释放关闭
socket.Shutdown(SocketShutdown.Both);
socket.Close();

实现UDP客户端通信 收发字符串

//1.创建套接字
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);

//2.绑定本机地址
IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080);
socket.Bind(ipPoint);

//3.发送到指定目标
IPEndPoint remoteIpPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8081);
//指定要发送的字节数 和 远程计算机的 IP和端口
socket.SendTo(Encoding.UTF8.GetBytes("唐老狮来了"), remoteIpPoint);

//4.接受消息
byte[] bytes = new byte[512];
//这个变量主要是用来记录 谁发的信息给你 传入函数后 在内部 它会帮助我们进行赋值
EndPoint remoteIpPoint2 = new IPEndPoint(IPAddress.Any, 0);
int length = socket.ReceiveFrom(bytes, ref remoteIpPoint2);
print("IP:" + (remoteIpPoint2 as IPEndPoint).Address.ToString() +
    "port:" + (remoteIpPoint2 as IPEndPoint).Port +
    "发来了" +
    Encoding.UTF8.GetString(bytes, 0, length));

//5.释放关闭
socket.Shutdown(SocketShutdown.Both);
socket.Close();

FTP关键类

NetworkCredential类

命名空间:System.Net

NetworkCredential通信凭证类

用于在Ftp文件传输时,设置账号密码

创建通信凭证

NetworkCredential n = new NetworkCredential("username", "password");

FtpWebRequest类

命名空间:System.Net

Ftp文件传输协议客户端操作类

主要用于:上传、下载、删除服务器上的文件


FtpWebRequest方法

创建FtpWebRequest

FtpWebRequest req = FtpWebRequest.Create(new Uri("ftp://127.0.0.1/Test.txt")) as FtpWebRequest;

终止传输

如果正在进行文件传输

req.Abort()

获取用于上传的流

Stream s = req.GetRequestStream();

返回FTP服务器响应

为Typecho添加LateX公式渲染支持

Typecho原生并不支持TaTex公式的渲染,可以通过导入KaTaXMathJax 这两个库来实现,本文以KataX为例。

此方法不适用于Pjax加载页面的主题

为文章添加自定义字段按需渲染LaTeX

修改functions.phpthemeFields函数中添加如下代码

//公式支持
    $layout->addItem(new Typecho_Widget_Helper_Form_Element_Radio('isLatex', array(
        1 => _t('启用'),
        0 => _t('关闭')),
        0,_t('LaTeX 渲染'), _t('默认关闭增加网页访问速度,如文章内存在LaTeX语法则需要启用')));

导入KataX库

下载最新的Releases版 解压到 主题文件夹中

header.php中添加(themeUrl中的参数填入KataX库的位置)

<?php if ($this->is('post') && $this->fields->isLatex == 1): ?>
<script type="text/javascript" src="<?php $this->options->themeUrl('assets/katex/katex.min.js'); ?>"></script>
<link rel="stylesheet" type="text/css" href="<?php $this->options->themeUrl('assets/katex/katex.min.css'); ?>" />
<script type="text/javascript" src="<?php $this->options->themeUrl('assets/katex/contrib/auto-render.min.js'); ?>"></script>
<?php endif; ?>

footer.php中的合适位置添加:

如何在 markdown 中表示矩阵

基本语法

  • 数学公式放在 $$ 之间。
  • 起始标记 \begin{matrix},结束标记 \end{matrix}
  • 每一行末尾标记 \\,行间元素之间用 & 分隔。
$$\begin{matrix}
0&1&1\\
1&1&0\\
1&0&1\\
\end{matrix}$$
$$ \begin{matrix} 0&1&1\\ 1&1&0\\ 1&0&1\\ \end{matrix} $$

矩阵边框

  • 在起始、结束标记用下列词替换 matrix
  • pmatrix:小括号边框
  • bmatrix:中括号边框
  • Bmatrix:大括号边框
  • vmatrix:单竖线边框
  • Vmatrix:双竖线边框

省略元素

  • 横省略号:\cdots
  • 竖省略号:\vdots
  • 斜省略号:\ddots
$$\begin{bmatrix}
{a_{11}}&{a_{12}}&{\cdots}&{a_{1n}}\\
{a_{21}}&{a_{22}}&{\cdots}&{a_{2n}}\\
{\vdots}&{\vdots}&{\ddots}&{\vdots}\\
{a_{m1}}&{a_{m2}}&{\cdots}&{a_{mn}}\\
\end{bmatrix}$$ 
$$ \begin{bmatrix} {a_{11}}&{a_{12}}&{\cdots}&{a_{1n}}\\ {a_{21}}&{a_{22}}&{\cdots}&{a_{2n}}\\ {\vdots}&{\vdots}&{\ddots}&{\vdots}\\ {a_{m1}}&{a_{m2}}&{\cdots}&{a_{mn}}\\ \end{bmatrix} $$

阵列

  • 需要array环境:起始、结束处以{array}声明
  • 对齐方式:在{array}后以{}逐行统一声明
  • 左对齐:l;居中:c;右对齐:r
  • 竖直线:在声明对齐方式时,插入 | 建立竖直线
  • 插入水平线:\hline
$$\begin{array}{c|lll}
{↓}&{a}&{b}&{c}\\
\hline
{R_1}&{c}&{b}&{a}\\
{R_2}&{b}&{c}&{c}\\
\end{array}$$
$$ \begin{array}{c|lll} {↓}&{a}&{b}&{c}\\ \hline {R_1}&{c}&{b}&{a}\\ {R_2}&{b}&{c}&{c}\\ \end{array} $$

方程组

  • 需要cases环境:起始、结束处以{cases}声明
$$\begin{cases}
a_1x+b_1y+c_1z=d_1\\
a_2x+b_2y+c_2z=d_2\\
a_3x+b_3y+c_3z=d_3\\
\end{cases}
$$
$$ \begin{cases} a_1x+b_1y+c_1z=d_1\\ a_2x+b_2y+c_2z=d_2\\ a_3x+b_3y+c_3z=d_3\\ \end{cases} $$

矩阵

矩阵是什么?

矩阵是一种用来表示和处理数据的数学工具

它可以帮助我们有效的管理和计算大量的数据


矩阵的数学表示

可以通过 方括号内的数值表格来表示

$$ \begin{bmatrix} {4}&{6}&{1} \end{bmatrix} \begin{bmatrix} {4}\\ {6}\\ {1}\\ \end{bmatrix} \begin{bmatrix} {1}&{3}&{2}\\ {5}&{6}&{7}\\ \end{bmatrix} \begin{bmatrix} {1}&{3}&{2}\\ {5}&{6}&{7}\\ {5}&{6}&{7}\\ \end{bmatrix} $$

矩阵是由m ×n(m和n大于0)个标量组成的,可以通过方块好内的数值表格来表示

矩阵的表示

$$ Matrix= \begin{pmatrix} {m_{11}}&{\cdots}&{m_{1n}}\\ {\vdots}&{\ddots}&{\vdots}\\ {m_{m1}}&{\cdots}&{m_{mn}}\\ \end{pmatrix} $$

在程序中,我们用于存储矩阵结构的容器类型有很多选择,最常见的的为:

  1. 数组(一维二维都可以)
  2. 嵌套列表(两个List嵌套)
  3. 开发工具提供的类或结构体(Unity中的Matrix4x4、Matrix3x2结构体)

矩阵的乘法

$$ 2× \begin{bmatrix} 1&2&3\\ 1&2&3 \end{bmatrix} {=} \begin{bmatrix} 2&4&6\\ 2&4&6 \end{bmatrix} $$

相乘规则:

让矩阵中的每一个标量和标量相乘即可


矩阵和矩阵相乘的条件

$$ A= \begin{matrix} {\color{Red}a_{11}}&{\color{Red}a_{12}}&{\color{Red}a_{13}}\\ {\color{green}a_{21}}&{\color{green}a_{22}}&{\color{green}a_{23}}& \end{matrix} B= \begin{matrix} {\color{green}b_{11}}&{\color{Red}b_{12}}\\ {\color{green}b_{21}}&{\color{Red}b_{22}}\\ {\color{green}b_{31}}&{\color{Red}b_{32}} \end{matrix} \\AB=C= \begin{matrix} {c_{11}}&{\color{blue}c_{12}}\\ {c_{21}}&{c_{22}} \end{matrix} \\{\color{blue}C12}=\color{Red}{a_{11}}×{b_{12}}+{a_{12}}×{b_{22}}+{a_{13}}×{b_{32}} $$

相乘规则:

TextMeshPro

TextMesh Pro是什么

TextMesh Pro 最初是由游戏开发者Stephan Bouchard(斯蒂芬·布沙尔)开发的, 是一个主要用于处理2D、3D文本的工具。

后来Unity注意到了这款强大的文本渲染工具,于2017年对该工具进行了收购

Unity 2018 版本时,TextMesh Pro成为了Unity的内置软件包,可以通过 Package Manager 对其进行安装和管理。

Unity 2020 版本开始,UGUI默认的文本控件变成了TextMesh Pro。

为什么要使用TextMesh Pro替代UGUI中的Text

  1. TMP的渲染效果更好,可以提供更好的文本质量 文本的清晰度更高、抗锯齿更强、字形更准确
  2. TMP提供了更强大的富文本支持 在文本中添加样式、颜色、链接、图片等更灵活
  3. TMP可以使用自定义字体和样式
  4. TMP提供了更灵活的文本布局控制
  5. TMP虽然内存占用更多,但是性能表现更好,可以提高渲染效率等等

导入TMP相关资源

Untiy2020以上版本不需要导入

两种方式:

  1. Window ——> TextMeshPro ——> Import TMP Essential Resources(导入TMP基本资源)
  2. 在Hierarchy窗口中右键创建TMP相关的对象时,在弹出窗口中点击 Import TMP Essentials

导入成功后会在Assets窗口中看到TextMesh Pro文件夹

其中就是TMP的相关基础资源

导入TMP事例和附加功能

两种方式:

  1. Window ——> TextMeshPro ——> Import TMP Examples and Extras(导入TMP事例和附加功能)
  2. 在Hierarchy窗口中右键创建TMP相关的对象时,在上一步弹出的窗口中一起导入

导入成功后在刚才的TextMesh Pro文件夹中会多出一个 Examples & Extras文件夹

导入或更新TMP

对于老版本的Unity,并没有集成TMP

如果你想要自己获取它

只需要在Package Manager中进行下载即可

如果TMP存在版本更新,也可以在这里进行更新

TMP的官方文档

https://docs.unity3d.com/Packages/com.unity.textmeshpro@4.0/manual/index.html

FTP概述

FTP 是什么

定义:文件传输协议(File Transfer Protocol),支持通过网络在主机之间传输文件。

功能

  1. 上传和下载文件。
  2. 提供登录验证。
  3. 目录查询和会话控制。

FTP 的工作原理

基础概念

FTP 本质

  • 基于 TCP 的通信协议。

连接类型

  • 控制连接:用于传输 FTP 命令。
  • 数据连接:用于传输文件数据。

数据连接与控制连接方向

  • 控制连接:客户端主动连接服务器,发送下载命令。
  • 数据连接:服务器主动连接客户端,传输数据。

传输模式

主动模式 (Port 模式)

  • 服务器主动连接客户端传输文件。
  • 可能被客户端防火墙阻止。

被动模式 (Passive 模式)

  • 客户端主动连接服务器。
  • 控制连接与数据连接均由客户端发起,更常用。

数据传输方式

ASCII 传输

  • 以 ASCII 编码传输,适合纯英文文件。

二进制传输

  • 支持非英文文件传输,推荐使用。

登录机制

  • 默认:客户端需要提供用户名和密码。
  • 匿名登录:服务器可允许用户无需账号登录。

学习 FTP 的方法

C# 实现 FTP 通信相关类

  1. FtpWebRequest
  2. FtpWebResponse
  3. NetworkCredential

学习内容

  1. 搭建 FTP 服务器。
  2. 上传文件到 FTP 服务器。
  3. 从 FTP 服务器下载文件。

总结

FTP 是什么

UDP通信概述

主要内容

  1. 服务端和客户端代码应该怎么写
  2. UDP 相对 TCP 的区别
  3. UDP 的分包、黏包问题

服务端和客户端代码开发流程

服务端与客户端需要完成的操作

  1. 创建套接字(Socket)
  2. 使用 Bind 方法将套接字与本地地址绑定
  3. 使用 ReceiveFromSendTo 方法进行消息收发
  4. 使用 Shutdown 方法释放连接
  5. 关闭套接字

UDP 相对 TCP 的区别

  • UDP:无连接、不可靠、高效,适合频繁发送小数据包。
  • TCP:连接可靠、流控机制完整,适合需要数据一致性的场景。

UDP 的分包与黏包问题

黏包问题

UDP 不会黏包

  • 发送方发送的数据不会被合并。
  • 一端发什么,另一端收到的就是独立的包。

分包问题

自动分包风险

  • UDP 消息可能在传递过程中因超出 MTU 而被分包。
  • 自动分包可能导致消息丢失或乱序,处理起来非常困难。

MTU(Maximum Transmission Unit)

MTU 概念

最大传输单元,通知对方所能接受的最大数据包尺寸。

常见 MTU 值

  • 局域网:1472 字节(1500 减去 UDP 头部 28 字节)。
  • 互联网:548 字节(576 减去 UDP 头部 28 字节)。

避免分包建议

确保消息大小小于 MTU:

  • 局域网环境下:1472 字节以内。
  • 互联网环境下:548 字节以内。

手动分包与黏包

黏包

TCP异步通信

异步方法和同步方法的区别

同步方法:

方法中逻辑执行完毕后,再继续执行后面的方法

异步方法:

方法中逻辑可能还没有执行完毕,就继续执行后面的内容

注意:Unity中的协同程序中的某些异步方法,有的使用的是多线程有的使用的是迭代器分步执行


举例说明异步方法原理

我们以一个异步倒计时方法举例

1.线程回调

CountDownAsync(5, ()=> 
               {
                    print("倒计时结束");
               });
print("异步执行后的逻辑");

2.async和await 会等待线程执行完毕 继续执行后面的逻辑

//相对第一种方式 可以让函数分步执行
CountDownAsync(5);
print("异步执行后的逻辑2");

TCP通信中的异步方法(begin,end)

服务器监听

Socket socketTcp = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.TCP);
socketTcp.BeginAccept(AcceptCallBack,socketTcp);
private void AcceptCallBack(IAsyncResult result)
{
    try
    {        
        //获取传入的参数
        Socket s = result.AsyncState as Socket;
        //得到连入的客户端
        Socket clientSocket = s.EndAccept(result); 
        //继续监听
        s.BeginAccept(AcceptCallBack, s);
    }
    catch(SocketException e)
    {
        print(e.SocketErrorCode);
    }
}

客户端连接

粘包/分包消息处理

byte[] catchBytes = new byte[1024 * 1024];
int catchNum = 0;
private void HandleReceiveMessage(byte[] bytes, int receiveLength)
{
    //消息ID
    int msgId = 0;
    //消息长度
    int msgLength = -1;
    //当前的索引
    int nowIndex = 0;
    //将接收到的byte写入缓存
    bytes.CopyTo(catchBytes, catchNum);
    catchNum += receiveLength;
    while (true)
    {
        if (catchNum > 8)
        {
            //解析id
            msgId = BitConverter.ToInt32(catchBytes, nowIndex);
            nowIndex += 4;
            //解析长度
            msgLength = BitConverter.ToInt32(catchBytes, nowIndex);
            nowIndex += 4;
        }
        else
        {
            msgLength = -1;
        }
        //粘包
        if (catchNum - 8 >= msgLength && msgLength != -1)
        {

            //解析消息体
            BaseMsg baseMsg = null;
            switch (msgId)
            {
                case 1:
                    PlayerMsg msg = new PlayerMsg();
                    //读取消息
                    msg.Reading(catchBytes, nowIndex);
                    baseMsg = msg;
                    break;
            }
            if (baseMsg != null)
            {
                //receiveMsgQueue.Enqueue(baseMsg);
                Console.WriteLine((baseMsg as PlayerMsg).playerData);
            }
            nowIndex += msgLength;
            if (nowIndex == catchNum)
            {
                catchNum = 0;
                break;
            }
        }
        //分包
        else
        {
            if (msgLength != -1) 
            {
                nowIndex -= 8;
            }  
            //写缓存
            Array.Copy(catchBytes, nowIndex, catchBytes, 0, catchNum - nowIndex);
            catchNum = receiveLength - nowIndex;
        }
    }
}

TCP的粘包和拆包

为什么会产生粘包?

因为TCP是面向流,没有边界,而操作系统在发送TCP数据时,会通过缓冲区来进行优化,例如缓冲区为1024个字节大小。

如果一次请求发送的数据量比较小,没达到缓冲区大小,TCP则会将多个请求合并为同一个请求进行发送,这就形成了粘包问题。

如果一次请求发送的数据量比较大,超过了缓冲区大小,TCP就会将其拆分为多次发送,这就是拆包。

关于粘包和拆包可以参考下图的几种情况:

/usr/uploads/2024/12/4200448999.jpeg

上图中演示了以下几种情况:

  • 正常的理想情况,两个包恰好满足TCP缓冲区的大小或达到TCP等待时长,分别发送两个包;
  • 粘包:两个包较小,间隔时间短,发生粘包,合并成一个包发送;
  • 拆包:一个包过大,超过缓存区大小,拆分成两个或多个包发送;
  • 拆包和粘包:Packet1过大,进行了拆包处理,而拆出去的一部分又与Packet2进行粘包处理。

UDP不会产生粘包

粘包拆包问题在数据链路层、网络层以及传输层都有可能发生。日常的网络应用开发大都在传输层进行,由于UDP有消息保护边界,不会发生粘包拆包问题,因此粘包拆包问题只发生在TCP协议中。

常见的解决方案

对于粘包和拆包问题,常见的解决方案有四种:

  • 发送端将每个包都封装成固定的长度,比如100字节大小。如果不足100字节可通过补0或空等进行填充到指定长度;
  • 发送端在每个包的末尾使用固定的分隔符,例如\r\n。如果发生拆包需等待多个包发送过来之后再找到其中的\r\n进行合并;例如,FTP协议;
  • 将消息分为头部和消息体,头部中保存整个消息的长度,只有读取到足够长度的消息之后才算是读到了一个完整的消息;
  • 通过自定义协议进行粘包和拆包的处理。

Netty对粘包和拆包问题的处理

Netty 对解决粘包和拆包的方案做了抽象,提供了一些解码器(Decoder)来解决粘包和拆包的问题。如:

  • LineBasedFrameDecoder:以行为单位进行数据包的解码;
  • DelimiterBasedFrameDecoder:以特殊的符号作为分隔来进行数据包的解码;
  • FixedLengthFrameDecoder:以固定长度进行数据包的解码;
  • LenghtFieldBasedFrameDecode:适用于消息头包含消息长度的协议(最常用);

基于 Netty 进行网络读写的程序,可以直接使用这些 Decoder 来完成数据包的解码。对于高并发、大流量的系统来说,每个数据包都不应该传输多余的数据(所以补齐的方式不可取),LenghtFieldBasedFrameDecode 更适合这样的场景。

为vps添加虚拟内存

在运行内存不足的情况下,适当配置虚拟内存(Swap)可以缓解内存压力,提高系统稳定性。本文介绍如何在 VPS 上创建并启用 512MB 的交换空间。

停用旧的swap文件

sudo swapoff /swapfile
sudo rm /swapfile

步骤 1: 创建交换文件

首先,我们需要创建一个大小为 512MB 的文件用作交换空间。可以使用以下两种方法之一:

方法 1: 使用 fallocate(推荐)

sudo fallocate -l 512M /swapfile

方法 2: 使用 dd

如果 fallocate 命令不可用,可以改用 dd

sudo dd if=/dev/zero of=/swapfile bs=1M count=512

步骤 2: 设置文件权限

为了安全起见,确保只有 root 用户可以访问交换文件:

Socket套接字TCP通信概述

服务端和客户端需要做什么

客户端

  1. 创建套接字Socket
  2. 用Connect方法与服务端相连
  3. 用Send和Receive相关方法收发数据
  4. 用Shutdown方法释放连接
  5. 关闭套接字

服务端

  1. 创建套接字Socket
  2. 用Bind方法将套接字与本地地址绑定
  3. 用Listen方法监听
  4. 用Accept方法等待客户端连接
  5. 建立连接,Accept返回新套接字
  6. 用Send和Receive相关方法收发数据
  7. 用Shutdown方法释放连接
  8. 关闭套接字

TCP协议三次握手的体现

/usr/uploads/2024/11/4230308893.png

/usr/uploads/2024/11/3507366322.png

/usr/uploads/2024/11/2552593966.png

TCP协议四次挥手的体现

/usr/uploads/2024/11/2264902490.png

/usr/uploads/2024/11/3544974811.png

/usr/uploads/2024/11/1814195855.png

域名解析

什么是域名解析?

域名解析就是将域名解析成IP地址

IP地址是网络上标识站点的数字地址,但是IP地址相对来说记忆困难

所以为了方便记忆,采用域名来代替IP地址标识站点地址。

域名解析就是域名到IP地址的转换过程。域名的解析工作由DNS服务器完成

域名系统(英文:Domain Name System,缩写:DNS)是互联网的一项服务

IPHostEntry类

命名空间:System.Net

类名:IPHostEntry

主要作用:域名解析后的返回值 可以通过该对象获取IP地址、主机名等等信息

该类不会自己声明,都是作为某些方法的返回值返回信息,我们主要通过该类对象获取返回的信息

关键成员

IPHostEntry host = new IPHostEntry();
//获取关联IP
host.AddressList
//获取主机别名列表
host.Aliases
//获取DNS名称
host.HostName

Dns类

命名空间:System.Net

类名:Dns

主要作用:Dns是一个静态类,提供了很多静态方法,可以使用它来根据域名获取IP地址

常用方法

获取本地系统的主机名

print(Dns.GetHostName());

获取指定域名的IP信息

同步获取

private void GetHostEntry()
{
    IPHostEntry entry = Dns.GetHostEntry("www.baidu.com");
    //获取ip地址
    for (int i = 0; i < entry.AddressList.Length; i++)
    {
        print("IP地址:" + entry.AddressList[i]);
    }
    //获取主机名
    for (int i = 0; i < entry.Aliases.Length; i++)
    {
        print("主机别名:" + entry.Aliases[i]);
    }
    //获取DNS名称
    print("DNS服务器名称" + entry.HostName);
}

异步获取