殷奥辉Unity 游戏客户端 面试精粹

作者: cayman 分类: 未分类,编程技术 发布时间: 2022-06-17 11:00

点乘与叉乘?

     向量(向量:既有大小又有方向的量叫向量)的点乘也叫向量的内积,数量级,对两个向量执行点乘运算,就是对这两个向量对应位一一相乘之后求和的操作,点乘的结果是一个标量(标量:只有大小没有方向的量)。

 叉乘的几何意义

在三维集合中向量a和向量b的结果是一个向量,该向量垂直于a和b向量构成的平面。【在二维空间中,叉乘还有另外一个几何意义就是:aXb的等于向量b构成的平行四边形的面积】

点乘的运算结果是一个标量,等于两个向量长度相乘再乘两者夹角的余弦值

叉乘的运算结果是一个向量,长度等于原来向量长度相乘后夹角的正弦值

Unity中的应用

点乘:根据点乘大小,得到夹角大小范围>0则夹角(0,90)、<0则夹角(90,180),可以利用这点判断一个多边形是面向摄像机还是背向摄像机。

叉乘:根据叉乘得到a,b向量的相对位置,和顺时针或逆时针方向。

打AB包时需要注意什么?

·经常更新和不经常更新的对象拆到不同的AB包中。

·同时加载的对象放在同一个AB包中。

·根据项目逻辑功能来分组打AB包。

·根据同一类型对象来分组打AB包。

·公共资源和非公共资源拆分到不同的AB包中。

IOS会自动将persistentDataPath路径下的文件上传到ICloud,会占用用户的ICloud空间,如果persistentDataPath路径下的文件过多,苹果审核可能被据,所以IOS平台,有些数据得放temporaryCachePath路径下。

AB包需要加密

加密大体思路:生成AB包,然后对AB包文件的byte[]使用加密算法 。

解密大体思路:先以byte[]形式读取AB包内容,之后使用对应的加密算法对该byte[]进行解密 ,解密后通过AssetBundle.LoadFromMemory()来进行加载。

存在问题:解密时会多占一份内存。

暗坑:打包资源名称不要有空格,空格在PC上支持,在Android上不支持。

名称用小写、下划线、数字,尽量不要用大写。

基于C/S的网络架构分构

Peer-to-Peer 在网络通信服务的形式上,一般采用浮动服务器的形式,即其中一个玩家的机器即是客户端又扮演服务器的角色,一般由创建游戏对局的玩家担任服务器。

基于游戏大厅代理的结构,通过会话大厅(lobby)结构为不同玩家牵线搭桥,既直接管理客户端,也管理游戏局,是回合制网络游戏的常见类型。

Socket连接

TCP是一种面向连接的,可靠的,基于字节流的传输通信协议,与TCP相应的UDP协议是无连接的,不可靠的,但传输效率较高的协议。

TCP注重传输的可靠性,确保数据不会丢失,但速度慢。

UDP注重传输速度,但不保证所有发送的数据对方都能够收到。

协议相关,字节流到客户端

TCP Socket在内核中都有一个发送缓冲区和一个接受缓冲区,TCP的全双工的工作模式以及TCP的流量(拥塞)控制便是以来于这两个独立的Buffer以及buffer的填充状态。接收缓冲区把数据缓存如内核,应用进程一直没有调用recv()进行读取的话,此数据会一直缓存在Socket的接收缓冲区内。

在编写网络游戏的时候,到底使用UDP还是TCP的问题迟早都要面对。

一般来说你会听到人们这样说:“除非你正在写一个动作类游戏,否则你就用TCP吧” 或者是 “你能够在MMO游戏中用TCP,因为魔兽世界就用的TCP!”

遗憾的是,这些观点都没有反映这个问题的复杂性。

背景

首先,说明一下,我之前主要是用TCP进行网络编程。我曾为一个流行的在线纸牌游戏编写服务器了好几年,在高峰期我们的每台服务器能够承受4000到10000个连接(同一台物理机器上有多个服务器进程在跑)都没有问题。在我来看,TCP是一种安全而且常见的选择。

尽管如此,我们最新的项目却是使用UDP协议,而且我们的项目无法通过任何方式在TCP下工作。事实上,项目一开始使用的TCP,但是后来发现我们使用TCP无法达到我们需求的连接数量时,我们只能换成UDP了。

在使用中TCP表现怎么样呢

从原理上,TCP的优势有:

· 简单直接的长连接

· 可靠的信息传输

· 数据包的大小没有限制

任何一个和TCP打过交道的人都知道,要实现一个稳定的TCP网络连接,需要处理各种隐藏的坑,比如断线检测、慢速客户端响应阻塞数据包,对开放连接的各种dos攻击,阻塞和非阻塞IO模型等等。

除了上面列出的这些问题外,一个好的TCP模块确实不好编码实现。

但是,TCP最糟糕的特性是它对阻塞的控制。一般来说,TCP假定丢包是由于网络带宽不够造成的,所以发生这种情况的时候,TCP就会减少发包速度。

在3G或WiFi下,一个数据包丢失了,你希望的是立马重发这个数据包,然而TCP的阻塞机制却完全是采用相反的方式来处理!

而且没有任何办法能够绕过这个机制,因为这是TCP协议构建的基础。这就是为什么在3G或者WiFi环境下,ping值能够上升到1000多毫秒的原因。

为什么不用UDP

UDP相对TCP来说既简单又困难。

举个例子来说,UDP是基于数据包构建,这意味着在某些方面需要你完全颠覆在TCP下的观念。UDP只使用一个socket进行通信,不像TCP需要为每一个客户端建立一个socket连接。这些都是UDP非常不错的地方。

但是,大多数情况下你需要的仅仅是一些连接的概念罢了,一些基本的包序功能,以及所谓的连接可靠性。可惜的是,这些功能UDP都没有办法简单的提供给你,而你使用TCP却都可以免费得到。

这也是人们为什么经常推荐TCP的原因。在用TCP的时候你可以不考虑这些问题,直到你需要同步连接的数量级达到500以上的时候。

所以,是的,UDP没有提供所有的解决方法,但是就像你看到的那样,这也正是UDP好用的地方。在某种意义上来说,TCP对UDP就好比是Hibernate和手写SQL的区别。

使用TCP失败的地方

人们经常给你建议,让你去使用TCP,比如“TCP跟UDP一样快”或者“游戏X用TCP如此成功,所以TCP当然是首选”,然而,他们完全没有理解为什么在那个特定的游戏中TCP是有效的,为什么UDP不按照顺序发送数据包呢?

那么为什么魔兽世界采用TCP呢?首先我们需要解释这个问题。这个问题其实是“为什么魔兽世界有的时候1000毫秒以上的延迟还能够运行?”这是TCP的性质决定的,在发生丢包的时候,会产生巨大的延迟,因为TCP首先会去检测哪些包发生了丢失,然后重发所有丢失的包,直到他们都被接收到。

可靠的UDP也是有延迟的,但是由于它是在UDP的基础之上建立的通信协议,所以可以通过多种方式来减少延迟,不像TCP,所有的东西都要依赖于TCP协议本身而无法被更改。

就这一点来讲,一些人要开始提到Nagle算法了,实际上它是你在实现任意一个对延迟敏感的TCP模型时首先需要禁止使用的。

那么魔兽世界以及其他的一些游戏是怎么处理延迟问题的呢?

方法也很简单,他们能够隐藏掉延迟带来的影响。

在魔兽世界中,玩家和玩家是无法碰撞的:因为这类碰撞是无法通过一些预测来处理的,但是玩家和环境之间的碰撞却是可以通过预测来处理的,所以这里使用TCP是没有问题的。

我们来看一下魔兽世界的战斗就会发现,玩家的攻击指令发送给服务器的操作是放在比如“attack_entity(entity_id)”或者”cast_spell(entity_id, spell_id)“的接口中来做的,换句话说,瞄准操作是独立于进行的。如此一来,一些类似发起攻击动作和释放技能特效就能够在没有收到服务器确认的情况下就直接执行,比如展现冰冻技能的效果就可以在服务器没有返回数据前在客户端就做出来。

客户端直接开始进行计算而不等待服务端确认是一种典型的隐藏延迟的技术。

几年前,我为一个叫“Five Card Jazz”的纸牌游戏编写过客户端。它使用的是http协议,它比直接的TCP协议连接的延迟更加严重。

我们用简单的纸牌绘制和抽牌的动画来掩盖延迟的问题,所以延迟的问题只在非常糟糕的连接下才会被看出来。这种方法也非常的典型:发送请求的同时开始播放牌桌的动画,一直播放翻动最后一张牌直到接收到了服务端传回来的数据为止。魔兽世界的战斗特效就是使用类似的原理。

这也意味着,我们到底是使用TCP还是UDP取决于我们能否隐藏延迟。

TCP在什么时候失效

一个采用TCP的游戏必须能够处理好突发的延迟问题(纸牌客户端就很典型,对突发性的一秒的延迟,玩家也不会产生什么抱怨)或者是拥有缓解延迟问题的好方法。

但是如果你运行的是一个无法使用任何减缓延迟措施的游戏呢?玩家对玩家的动作类游戏通常就属于这个范畴,但是这也不仅仅限于动作类游戏。

举个例子:

我目前正在写一个多人游戏(War Arcana)。

一种常见的操作是,你快速的移动你的角色通过一张充满战争迷雾的世界地图,但是一旦你探索过,迷雾就会被打开。

为了确保游戏的规则,防止玩家作弊,服务器只能显示玩家当前位置附近的信息。这意味着不像魔兽世界,玩家无法在没有得到服务器响应的情况下,做出完整的动作。和Five Card Jazz相比,我们即使允许500毫秒的延迟,也已经非常困难了。

在实现了游戏的原型后,在局域网内一切都进行的非常顺利,但当我们在WiFi环境下测试时,操作会间歇性的卡起来或者延迟高起来。写了一些测试程序之后发现,WiFi环境下偶尔会发生丢包行为,每当发生丢包的时候,服务器的响应速度就从100-150毫秒上升到1000-2000毫秒。

没有任何办法可以绕过TCP的这个设置来避开这个问题。

我们替换了TCP的代码,用了自定义的可靠的UDP来实现,把大量的丢包产生的延迟降到了仅仅只有50毫秒,甚至比以前TCP不丢包的情况一个来回的延迟还要小。当然,这只可能建立在UDP之上,这样我们才对可靠性拥有完全的掌控力。

困惑:可靠的UDP只是TCP的一种简单的实现?

你有没有听过这种说法:“可靠的UDP就像TCP一样,所以还是用TCP吧”。

问题是这种说法是错误的。可靠的UDP一点也不像TCP,要去实现一个特殊的阻塞控制。事实上,这也是你使用可靠UDP代替TCP的最大的原因,避免TCP的阻塞控制。

另一个重点是可靠的UDP的可靠性是如何保证的。这里有很多种方法去实现。我非常喜欢Quake3网络库代码里的一些想法,它们也激发了我在War Arcana中使用UDP协议。

你也可以使用许多支持可靠通信的UDP库,当然,这样在可靠性方面,相比自己手动实现全部的代码而言,可能会更加通用而失去了一些性能优势。

底线

那么到底是用UDP还是TCP呢?

· 如果是由客户端间歇性的发起无状态的查询,并且偶尔发生延迟是可以容忍,那么使用HTTP/HTTPS吧。

· 如果客户端和服务器都可以独立发包,但是偶尔发生延迟可以容忍(比如:在线的纸牌游戏,许多MMO类的游戏),那么使用TCP长连接吧。

· 如果客户端和服务器都可以独立发包,而且无法忍受延迟(比如:大多数的多人动作类游戏,一些MMO类游戏),那么使用UDP吧。

这些也应该考虑在内:你的MMO客户端也许首先使用HTTP去获取上一次的更新内容,然后使用UDP跟游戏服务器进行连接

图集打断合批的原因及情况

渲染时性能的瓶颈主要是在提交渲染数据(Mesh),每次提交Mesh数据时还需要对描画State的状态进行一次设置,设置每次都会消耗大量的CPU时间。

减少提交次数,把具有相同state状态的数据合并到一起,一次性提交描画的Mesh的数据给GPU。

RenderShadowMap时合批失败。

RenderLoop时合批失败

静态合批

在GameObject上勾选Static,然后在BuildTarget时会对Mesh进行Combine的操作(MakeBatch)。合并成一个总的Mesh。每个单位的Mesh为SubMesh。

Render quere index-Material队列中的排序Render可能有一组Material

Renderer priotity 可以通过代码来设置优先级,影响排序前后顺序。

Distance -从后向前排序

SortOrder-如果过Distance相同则通过SortOrder来排序,通常通过SortGroup来设置例如Canvas、Sprite、Shape、Renerer,但其实每个Renderer都有SortOrder属性来进行设置,可以通过代码来设置Materialindex-引擎生产Material时自动分配一个ID作为Index来排序。但其实唯一可以起到作用的时判断两个Material是否时同一个Material。

静态合批是将一些足够小的网格,在CUP上转换它们的顶点,将许多相似的顶点组合在一起并一次性绘制它们。

动态合批处理的GameObject的每个顶点都有一定的开销,因此动态合批处理仅应用于不超过900个顶点和不超过300个顶点的网格。

如果shader中使用的vertes,positon,normal,SingleUv,可以批量才处理最多300个顶点。

而如果shader中使用Vertes Positon Normal UV0,UV1和Tangent,则只能使用180个顶点

Unity多线程方案

一, 主线程的回调操作交个Update每帧调用进行执行。

二, 子线程的回调就直接C#的ThraadPool动态获取一个没用的线程进行执行,由于ThreadPool并没有设置线程上限,所以需要通过变量控制线程数,并对变量进行原子锁加减工作,超过上限的调用处于等待锁状态。

架构(MVC)

M,指业务模型      Model模型层  

V, 指用户界面 View  视图层

C, 指控制器 Controller 控制器

欧拉角,四元数,Vetor3

欧拉角

欧拉角是物体绕坐标三个坐标轴(x,y,z)的旋转角度。

·静态:即绕世界坐标系中的三个轴的旋转,由于物体旋转过程中坐标轴保持静止,所以称为静态

动态:即绕物体坐标系三个轴的旋转,由于物体旋转过程中坐标轴随着物体做相同的转动,所以坐标为动态

万向锁

对于动态欧拉角,即绕物体坐标旋转,无论heading,和bank为多少角度,只要pitch为正负90°(即绕第二个轴的旋转)就会出现万向锁现象。

四元数

一个旋转的向量,+一个旋转的角度

优点:避免万向锁。

  只需要一个4维的四元数就开会执行绕任意过远点的向量旋转。

缺点:比欧拉角多一个维度,理解困难不直观。

Vetor3

可以表示三位向量,也可表示三维坐标,值为三个float

加载大资源策略

分块加载,延迟加载,预加载

1. 一些场景中必然会出现静态资源,例如山水建筑物,预先就部署在场景(Scene)中。

2. 根据玩家属性有所变化的动态资源,按需加载,例如只加载玩家视界范围内的资源,或者将地图划分多块,

3. 使用LoadAsync加载

4. 预先加载。

异步加载注意要点:

延后0.1s执行异步否则可能进度条显示不正常,跳转场景不受asynOperation.allowSceneActivaticon控制。

如何使用LOD

 性能优化是对物体精选处理,来让游戏跑起来更流畅,多层次细节处理就是让一个物体在相机距离不同的情况下显示不同的模型,从而节省性能开销。

添加LoDGroup组件,然后把显示模型拖到对应级别

动态字体与态字体

动态字体即使用TTF格式字体库

静态字体需要自己打包图集

动态字体如果使用自己没有的字,会使用系统字,而静态字体不会,静态字体是图片,通过缩放来改变。

渲染流程

应用程序阶段(CPU)识别出潜在可视化网格实例,并将它们及其材质交给GPU以供渲染。

几何阶段(GPU)进行顶点变换等计算,并将三角形旋转到齐次空间并进行裁切。

光栅阶段(GPU)把三角形转换为片元,并对片元实行着色,片元经过多次测试(深度测试,alpha测试)之后,最终与帧缓冲融合。

CPU,准备好需要渲染的对象。设置每个对象的渲染状态,包括着色器,光源,材质等。

发送DrawCall,当给定一个DrawCall是,GUP会根据渲染状态输入顶点数据进行计算。

ZBuffer,ZTest,ZWrite

ZBuffer,缓冲区类型,对depthbuffer即深度缓冲区其中存在的是视点到每个像素所对于的空间点的坐标衡量称之为Z值或者深度值

ZTest,深度测试, 在深度测试中,默认情况是将要绘制的新像素的z值与深度缓冲区中对应位置的z值进行比较,如果比深度缓存中的值小,那么用新像素的颜色更新深度缓存中对应像素的颜色值。

ZWrite,深度写入 取值为On/Off,默认为On,代表是否要将像素深度写入深度缓存中。

Unity先将渲染队列中较前的进行渲染,然后再执行ZWrite,ZTest。

C#多线程

一个进程可以保护多个线程,在进程中执行的第一个线程被视为这个进程的主线程,在.Net中Main()为入口,当调用时系统会自动创建一个主线程。

线程主要是有CPU寄存器调用栈和线程本地储存器,CPU寄存器主要记录当前所执行线程的状态,调用栈主要用于维护线程所调用到的内存与数量,TLS主要用于存放线程的状态信息。

多线程的优点:

·可以同时完成多个任务,可以使程序的速度加快,可以让大量的处理时间的任务或当前没有进行处理的任务定期将处理时间让给别的任务,可以随时停止任务,可以设置每个仍无的优先级以优化程序性能。

缺点:

·线程也是程序,所以线程需要占用内存也会多,多线程需要协调管理,所以需要占用cpu时间以便跟踪线程,线程之间对共享资源的访问会互相影响。

线程太多会导致控制太复杂,最终可能造成很多程序缺陷。

创建线程,编写线程锁需要执行的方法,实例化Thread类,并传入一个指向线程所需要执行的方法的委托。

调用Thread实力话的Start方法,标记线程可以被CPU执行了,但具体执行时间由CPU决定。

哈希表

支持任何类型的KeyVlaue键值对。

应用场景

某些数据会被高频率查询,数据量大,查询字段包含字符串类型,类型不唯一。

Protobuf

一种轻便高效的结构化数据储存格式,可以用于结构化数据的串行化,也称作序列号,主要用于数据存储或是RPC数据交换,比较类似XML,JSON,具有更快更简单,更轻量等级特性,支持多语言,只需要定义好数据结构,利用Protobuf的框架生成源代码,就可以轻松实现数据结构的序列号和反序列化。

因为是二进制传输,位运算规则可以自己定义,所以不易被破解,安全性时一方面,还有高效性,兼容性,多语言性,代码生成机制。

支持向后兼容和向前兼容,当客户端和服务器同时使用一块协议的时候,当服务器新增加一个属性对客户端不影响

ArrayList与list

ArrayList存在不安全性(会把所有的插入数据当作Object处理)装箱拆箱的操作(费事)

List是泛型不纯在这个问题

数组和list之间的区别

数组: 储存,修改,读取速度快.

缺点,始化需要指定长度,无法拓展,插入数据麻烦.

List 数组基础上优化,储存通用类型的数据列表,有点:可拓展,初始化无需指定长度,可插入指定位置数据.

Arrylist 可储存不同类型的数组

优点:可拓展,无指定长度,可插入删除

缺点:因储存不同类型,执行装箱拆箱操作,读取,存储速度慢.

光源

平行光,电光,聚光灯,区域观光

碰撞产生的条件

两个物体都必须有碰撞器,当一个物体还必须带有RigidBody,脚本才能检测到碰撞。

不同工程见安全迁移asset数据(三种方式)

·迁移Assets目录,Library目录

·导出包,exportpockage

·用unity自带的assetsserver功能

Awake,OnEnable,Start执行顺序

Awake->OnEnable->Start OnEnable反复发生。

物理计算应该放在FixedUpDate中

Input检测应该放在Update中

相机应该放在lateUpdate中

多线程收到协议消息,怎么传给主线程

Socket线程收到消息,将数据储存在一个List里面,主线程每次去取。

线程通信,共享变量

关于lua闭包导致引用无法释放内存泄露

Lua闭包不当使用,对必要的引用要及时释放

注册时间未及时取消订阅,注册到C#的luafunction用完一定要dispose,委托事件要对应取消订阅或清空事件。

Lua简单介绍

Lua轻便,可拓展,支持面向过程,和函数式编程。

只提供了一种通用类型的表,可以实现数组,哈希表,集合,对象。

Lua是动态型语言,变量不需要类型定义,只需要为变量赋值,值可以存储在变量中,作为传递或者返回。

Pairs迭代table,可以遍历表中所有key,可以返回nil。

IPairs迭代数组,不能返回nil,如果遇到nil则退出。

结构体和类的区别

结构体中可以声明字段,但是声明的时候是不能给初始值

不能继承其它的结构或类

不能作为其他结构或类的基础结构

类是引用类型,结构体是指类型

Xlua对值类型有哪些优化?

String优化

字符串比较使用的是引用比较,并不需要比较字符串字节,因同一字符串中是同一个引用。

XLua对Vector3优化

Vector3的方法属性都是成员方法(如x,y,z,Slerp),那么调用这些方法前需要先获取Vector3对应的对象。比如Vector3()新建,transform.position获取等。

在函数之间传递三个float,比传递Vector3要快

在函数中传递三个float,比传递vector3快,void SetPos(GameObject obj,Vector3pos)改为void SetPos( GameObject obj ,float x,float y,float z 

Xlua交互过程中类型传递优化

C#对象类型优化:将所有的交互中的对象存放到一个object_pool里,将当前对象的索引映射到lua的userdata,这样传递时,只需要传递索引,c#侧根据索引找到对应对象

C#复杂值类型优化:

a. 复杂值类型,比如Vector3这样的,也可以采用object_pool的方式优化,但这样也会涉及到拆装箱问题,应为object_pool里面存放的时object

b. 一中方法时在lua侧实现一个lua版的vector3,这样的话,C#toLua只传xyz,在lua侧构建一个luaVector3,luaToC#也只传xyz,在C#侧构建一个Vector3

c. Xlua方法,值拷贝#Tolua,把struct的值拷贝到lua的userdata中;把userdata中的解出来。

Hashtable 代表了一系列德给予哈希代码组织起来的键值对.使用键;来访问集合中的元素.

使用键访问元素时,则使用hash表,哈希表中的每一项都有一个键值对.键用于访问集合中的项目.

Hashtable,HshMap是一个散列表,它储存的内容是键值对映射.

Hashtbale的函数都是同步的,意味着它是线程安全的.key\value都不可为null.此外,hashtable中的映射不是有序的.

面向对象概述

面向对象其实是在处理事务时,对事务的特征,行为进行改款,归纳,总结,抽象成一个类,然后再使用时由内再生成对象,在编程时使用,这个过程就可以面向对象编程.

面向对象主要三大特征:封装,继承,多态

封装 ,面向对象的思想特征之一,它是指通过具体功能封装到方法中,在我们学习对象的时候,也提过将方法封装在类中,其实这些都是封装..

封装提高了代码的复用性,隐藏了实现细节,还要对外提供可以访问的方式,便于调用者的使用提高了安全性.

继承 ,继承在现实生活中一般指子女继承父辈的遗产财务.但在程序中,继承是指事物之间的所属关系,通过继承可以使多种事物之间形成一种关系体系.

当一个类是另一个类中的一种时,可以通过继承,来继承属性和功能.如果父类具备的功能需要子类特殊定义时,需进行方法重写.

多态,就是值在一个类实例中相同方法在不同情况下有不同表现形式.多态机制使具有不同内部结构的对象可以共享相同的外部接口.这意味着,虽然针对不同对象的具体操作不同,但通过一个公共的类,他们可以通过相同的方式予以调用.同一操作作用于不同的对象,可以有通透的解释,蚕食不同的执行结果..

重载和重写是实现多态主要两种方式.

其实最主要的就是子类对父类的重构,子类出了继承,重构父类方法,也可以自定义新法.

观察者模式

当对象间存在一对多关系时,则使用观察者模式.比如,当一个对象被修改是,则会自动通知依赖他的对象.观察者模式属于行为型模式.

意图 定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于他的对象得到通知并被自动更新.

主要解决 一个对象状态改变给其它对象通知的问题,而且要考虑到易用和低耦合,保证高度的协作.

何时使用:一个对象(目标对象)的状态发生,所有依赖的对象(观察者对象)都将的到通知,进行广播通知.

基于C/S的网络架构分构

来源网络转载

发表评论

您的电子邮箱地址不会被公开。