Unity开发经验小结--1.C#基础
Posted 丁小未
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Unity开发经验小结--1.C#基础相关的知识,希望对你有一定的参考价值。
一、C#语言基础
1、C#的值类型和引用类型
值类型分配在栈上,性能高,继承自ValueType。结构体就是值类型,struct。
引用类型分配在堆上,引用类型的new会产生GCAlloc。继承自Object。
2、ArrayList、List
ArrayList类型是object。可以放任意对象,会有装箱和拆箱的操作。装箱就是把值类型转换成引用类型(object)。拆箱就是把引用类型转换成值类型。
List是泛型,性能更好。推荐使用。
ArrayList和List的内存都是连续的,都是数组结构。需要跟LinkedList区分开。
3、循环遍历List中删除元素,常用的方式是倒序遍历,删除指定元素。如果是正序遍历,删除元素后,List后面的元素前移,然后迭代器再跳转到下一个元素,那么就会跳过删除元素的下一个元素。倒序遍历就不会有这个问题。
4、善用数据结构。比如我们的玩家列表保存了两个数据结构,一个List结构用于遍历,一个Dictionary结构用于查询。
当我们查询比较频繁的时候,通过HashSet或者Dictionary做判定或查询会大大提高性能。
以正常游戏的数量级来说,LinkedList几乎没有使用的必要。即便是有插入删除元素的需求,一般也都是List比较快。除非列表中有几百个对象。
二、线程、协程与进程
(一) 线程
1、多线程可以很好的利用多核,提高执行效率。
2、子线程的计算不会卡住主线程,所以一些复杂的计算放到子线程可以减少游戏卡顿。
3、有一些阻塞性质的代码需要放到子线程,比如下载代码,socket通信等等。
4、子线程受到的限制比较多,大多数Unity的API都无法调用。
5、编写多线程代码需要关注线程同步。稍不留意可能出现死锁或者因为线程不安全导致逻辑错误甚至闪退。
6、如果是大量线程创建,可以使用线程池进行优化。服务器比较常见。客户端用的比较少。
7、线程数过多,可能会造成频繁的上下文切换。它会保存一个线程的状态,然后处理另外一个线程的东西,再然后恢复第一个线程的状态以继续处理该线程。这个操作是资源密集型过程,因此应该尽可能的避免。
(二) 协程
1、协程的好处是可以以同步代码的方式处理异步逻辑,避免复杂的回调代码。
2、协程虽然也是异步执行的,但都是在主线程执行。所以依然不能做可能会导致主线程卡顿的操作,比如大量物体实例化。正确的操作应该是分帧执行。
3 、直接StartCoroutine依然是有一定开销的,应该尽可能合并协程。比如使用CoroutineManager。
(三) 进程
1、Mutex 是进程互斥量。当一段逻辑只能一个进程执行的时候,可以使用Mutex来控制。
(四) 锁
1、lock(object) xxxxx
2、比如网络通信中,对一个buffer,主线程不停的读取,子线程不停的写入。如果读取的时候不加锁的话,就可能会跟子线程冲突,导致结果错误或者数据错乱。
3、lua state不是线程安全的。主线程创建的lua state,如果我们在子线程调用其接口,比如Call一个lua函数,那么堆栈可能会错乱掉。很有可能导致游戏闪退。之前我们犯的错误就是在socket.BeginReceive的回调线程中,如果网络异常则通知lua。这个操作就是非常危险的。
(五) JobSystem
1、这个是由Unity调度的多线程代码。更加高效。且可以不用关心线程安全。
2、IJob
(六)、ECS
1、Entity–Component–System
Entity持有Component
Component是数据的容器
System处理Component,可以理解为函数集合。与对象无关。
2、守望先锋使用ECS系统,方便的实现了战斗回滚。
3、ComponentSystem、NativeArray
4、Unity的ECS模式更好的进行数据对齐,有利于CPU缓存命中。性能更好。
(七)、DOTS
1、ECS+JobSystem+Burst
三、其他
1、Xml
1.1、解析Xml分文档对象模型(DOM)和流模型两种。
1.2、前者写起来比较简单,可以随机读取xml中的内容,缺点是比较耗性能,需要一次加载整个的xml文件到内存中。
1.3、流模型是只读的,且只能顺序读取xml的内容。优势是性能好。
2、Json
2.1、优势是速度快。缺点是不支持注释。
2.2、我们在解析json配置的时候,多数是直接使用Unity提供的JsonUtility API。这个接口速度很快。缺点是格式固定,不能解析成Dictionary这样的任意结构。
2.3、在需要处理服务器数据返回这样的任意结构的时候,使用LitJson或者MiniJson。
3、YAML
3.1、Unity中所有序列化的文本格式都是YAML。比如prefab、material、.unity场景文件等等。编辑器下设置 Force Text 可以强制指定这些序列化为文本格式。否则可能是二进制格式。
4、AES加密
4.1、高级加密标准(AES,Advanced Encryption Standard),是现在比较流行的对称加密算法。对称加密算法加密和解密用的秘钥是相同的。常见的还有DES加密,不过DES加密速度相对比较慢,不再推荐使用。
4.2、根据秘钥和向量,把明文加密为密文,服务器收到密文之后,再根据同样的秘钥和向量解密为明文。
4.3、AES加密的速度是非常快的。我们的消息通信、Lua文件加密都使用了AES加密。
4.4、还有一种非对称加密算法。秘钥分公钥和私钥。常见的是RSA算法。优势是安全,极难被破解。缺点是速度较慢。所以正常游戏中使用RSA的地方不多。不过推送通知服务器、Https服务器等地方都会用到类似的加密证书。同时我们常用的git的用户认证也是这种算法。我们的公钥是配置在服务器上的,私钥自己本地保存。私钥加密,公钥解密。
5、LZ4 压缩
5.1、常用的压缩算法有 Deflate (zlib、zip)、lzma (7z)和lz4。还有一些压缩算法特性跟这三个重复,所以不做说明。
5.2、zlib是非常常见的压缩算法库。几乎每个游戏引擎或者游戏都会用到。它实现的是 Deflate算法。常见的压缩存档格式,比如zip、tar.gz等,大多是这个算法。
补充说明的是 png 也是用到这个压缩算法。事实上最早 zlib 就是用于png压缩的。png是无损压缩格式。最终通过zlib压缩图片大小。
5.3、lzma,也就是7z格式,压缩比非常高。缺点是压缩和解压速度比较慢。Unity中ab包的可选格式就包含lzma,5.0之前的版本就只有lzma和不压缩两种选择。Unity的ab包使用lzma的缺点是,ab包会整体在内存中解压。如果一个ab包有100mb,解压内存也要至少100mb。
5.4、lz4。它的优势是速度极快。尤其是解压速度。可以达到zlib的几倍到几十倍。虽然作为存档格式并不常用。但是游戏中应用的非常广泛。我们对lua文件加密后,就又进行了一次lz4的压缩。Unity的ab包的可选格式 ChunkBaseCompression就是lz4的格式。优势除了快,还有就是ab包本身加载并不耗内存,只有文件头的内存消耗。用到哪个资源,再解压哪个部分。我们的资源管理模块的设计很大程度上依赖了这个特性。
如果没有lz4这个特性,那么我们可能做的方案选择就是:ab包选择不压缩的格式。然后打包的时候使用zip压缩到apk内部。等游戏第一次启动的时候,再解压到可写目录。曾经很多游戏都会选择这这个方案。主要考量点还是加载速度和内存占用。
6、字符编码
6.1、ASCII码。0-128就代表我们常见的英文字母、数字等。
6.2、ANSI标准。ASCII码只有128个,算上扩展码也只有256个。显然无法表示世界上所有的文字。
中国制定的ANSI标准的编码就是 GB2312。它包含7000多个汉字和符号。
GBK全面兼容GB2312,包含3万多个汉字。
GB18030在GBK的基础上进一步扩展了汉字,增加了藏、蒙等 少数民族文字。
6.3、Unicode标准。ANSI标准下每个国家的编码含义不同。而Unicode是全世界的统一标准。包含了全世界的所有文字。
6.4、MBCS (Multi-Byte Character System)。多字节字符集。ASCII码占用一个字节,汉字绝大多数占两个字节。
6.5、CodePage。代码页。这个也是个非常常见的概念。通过设置代码页,告诉操作系统ANSI到底是哪个国家或者地区的标准。比如Windows规定,65001的代码页代表UTF-8,936代表的代码页代表GBK。
6.6、UTF-8、UTF-16、UTF-32
Unicode是一个字符集标准,具体计算机上怎么进行编码,有不同的实现方案。
UTF-8是可变字节的,兼容ASCII码部分,汉字一般是占3个字节,最多4个字节。
UTF-16,使用2个字节或4个字节表示一个字符,常用的字符比如ASCII码和绝大多数汉字都是2个字节。
UTF-32,与Unicode码表基本一致,用4个字节表示一个字符。
6.7、在Windows上,wchat_t 就代表Unicode字符,是2个字节,也就是说Windows系统采用的是UTF-16的方案。
各种Windows API,比如 SetWindowTitle和SetWindowTitleW,就分别对应ANSI接口和Unicode接口。
在中文环境下,Windows系统默认的字符集是GBK。比如我们创建文件的文件名,应该是GBK编码,如果代码中写的是UTF-8编码,那么创建的文件就可能会出现乱码。这种情况在Windows和Mac下转换文件时很常见。
类似的还有cmd窗口的默认代码页是936,cmd输出的结果就是GBK编码,Jenkins默认的字符编码是UTF-8,那么Jenkins在Windows上执行bat,输出结果可能会出现乱码。解决方法是在执行bat命令之前,调用 chcp 65001 将代码页切换为UTF-8。
6.8、在Linux上,默认的字符集是 UTF-8。而对应的 wchar_t 是4个字节。这是因为Linux选择的字符编码是 UTF-8而不是UTF-16。对于Windows选择的UTF-16而言,绝大多数文字都可以用2个字节表示,需要用4个字节表示的文字非常罕见。当出现4个字节的文字的时候,实际上是2个wchar_t表示一个文字,这种情况下多数计算长度等函数都是有问题的,不过因为这种情况及其罕见,所以也没什么大问题。
而Linux使用的UTF-8编码,大多数汉字都是3个字节,就无法像Windows一样,用2个字节表示 wchart_t 就能处理绝大多数文字,所以Linux下 wchar_t 就只能用4个字节来表示。
6.9、C#中处理字符编码比较简单,使用Encoding类就行。不过需要注意,如果要处理汉字的UTF-8转GBK这样的操作,需要把几个dll拷贝到工程中,并且link.xml中配置其不被裁剪。然后才能正常调用。
I18N.dll I18N.West.dll I18N.CJK.dll。
因为我们代码中是用 Encoding.GetEncoding(936),这样的写法获取转换对象的,所以如果不在link.xml中配置,这几个dll就会因为没有被引用到而被裁剪掉。
6.10、最后补充下,Lua的字符编码是 UTF-8。C#调用C++接口的时候,可以指定CharSet,来说明我们传递的参数是什么字符编码。比如,
[DllImport(LUADLL, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
7、字节序
7.1、字节序是只占内存多余1个字节的类型,数据在内存中如何存放。
小端,Little Endian。低字节数据存放在内存低地址处。我们常见的PC、移动平台都是小端。
大端,Big Endian。高字节数据存放在低地址处。很多嵌入式平台是大端。
7.2、假设,内存地址增长方向是 从左到右,由低到高。
比如,UTF-16的\\n,数据是 0x000D,00为高位,数字更加重要,想象成102中的1是高位,更加重要。
则小端的内存排布为 0D00,大端的内存排布为000D。对应关系可以看上面的描述。
这个知识在我们查看一个文件的二进制编码的时候很实用。从左到右的显示顺序就可以理解为内存地址增长顺序。
7.3、UTF-16就是2个字节代表1个字符,它同样可以是大端,也可以是小端。所以就规定在文件头加个标识符,说明文件的字节顺序。这就是 BOM (Byte Order Mark)。
FE FF 代表 UTF-16BE,是大端文件。
FF FE 代表 UTF-16LE,是小端文件。
其他还有UTF-32的BOM标记,不过基本不怎么使用。UTF-16的这两个标记是应用最广的。
7.4、UTF-8也有一个BOM头,EF BB BF。不过这个是微软自己定的,实际上UTF-8跟GBK一样,没有字节序的问题,它们都是多字节编码。所以有些语言比如Go语言,曾经就不支持带BOM头的UTF-8编码的源文件。
7.6、我们Lua文件都是不带BOM的UTF-8格式。C#和C++的源文件,都是带BOM的UTF-8格式。
使用UTF-8编码是因为,VSCode或者Sublime默认支持的就是UTF-8格式,如果源代码是GBK格式的话,还要进行转换才能正常显示,否则就是乱码。另外源文件如果是GBK编码的话,在Mac下显示也会乱码,所以还是UTF-8更加通用一些。
Lua文件不带BOM头是因为,Luajit支持带BOM头的lua文件,但原生的Lua5.1不支持带BOM头的文件。所以如果我们使用的Lua版本是原生lua的话,加载代码之前还要先移除BOM头。方式就是如果判定文件前三个字节是 EF BB BF,那么就移除这三个字节。
C++文件带BOM头是因为我们C#和C++的查看和编译工具都是微软自家的,我们既然使用UTF-8的格式,又不带BOM的话,微软自己的编译器遇到中文可能会编译报错。
8、换行符
这个虽然很简单,但是很常见,也有一些坑,所以单独提一提。
8.1、回车符 \\r,ASCII码为 13,即0D。
换行符 \\n,ASCII码为 10,即0A。
8.2、Windows下的换行符是 \\r\\n,表示为 CRLF
Linux下的换行符是 \\n,表示为 LF
Mac下的换行符是 \\r,表示为 CR
不同系统下的换行符不一样,所以一些linux下编辑的文本文件,拿到Windows下用文本编辑器打开会没有换行。VSCode也可以修改当前文件的换行符。
8.3、Git默认在检出的时候会转换换行符。提交入库的时候也会统一设置换行符。这个经常会有一些坑。所以我们设置开发环境的时候,会设置git,关掉这个 autoCrlf 的功能。检出和提交代码的时候都保持原样。
有的时候我们新仓库没有关掉autoCrlf,如果后面再有一些复制文件或者其他操作,可能出现文件没有修改,但是提交的时候提示文件变动,打开又不会显示出任何修改内容。这就是因为虽然文件内容没变,但是换行符变了。
8.4、因为我们都是在Windows下进行开发,所以我们的代码和配置的换行符都统一为 CRLF。
8.5、Unity可以在 Project Setting – Editor 中设置换行符。默认好像是LF。
以上是关于Unity开发经验小结--1.C#基础的主要内容,如果未能解决你的问题,请参考以下文章