当前位置:   article > 正文

游戏客户端开发面经

游戏客户端开发

帧同步与状态同步,它们的区别是什么,什么类型的游戏适合帧同步,什么类型的游戏适合状态同步。

答:帧同步中,服务器广播玩家的操作; 状态同步,服务器同步玩家的属性;对于玩家数量少,非常需要注重手感的游戏适合帧同步(王者荣耀、魔兽争霸,格斗类型游戏),对与玩家数量多,手感要求没那么高适合用状态同步(qq飞车、荒野行动等);它们的重要区别是战斗的核心逻辑写在哪里,帧同步的战斗逻辑写在客户端,状态同步的战斗逻辑写在服务端;由于玩家的属性比较多,所以状态同步需要发送的数据比较多,而且通常都是采用分批次发送,所以网络延迟会更高,而帧同步只需要转发玩家的操作,延迟比较低。详细的文章可以参考这里

map和unordered_map的实现原理。

答:unordered_map使用hash表实现的,效率高,无序;map的底层实现是红黑树,有序,对于红黑树,删除、插入、查找的时间复杂度都是logN,通过自旋和变色达到平衡;红黑树特点有如下:

  • 只有红黑两种节点;
  • 叶子节点和根节点是黑节点;
  • 两个红节点不能直接相连(每个红节点的这两个子节点都是黑子节点);
  • 每个叶子节点到根节点所包含的黑节点数目是一样的;

指针会导致崩溃?

答:首先导致崩溃的原因是非法读写内存地址,有如下可能:

  • 空指针,当读写空指针时,操作系统不允许读写空指针,然后程序崩溃,空指针指向的地址一般是0(windows是的),这个地址一般是操系统自己的地址,不允许程序自己去访问,所以当遇到读写时,程序就会崩;
  • 指针指向无效地址,例如当指针所指向的地址已经释放,去读写该指针,程序会崩溃。

右值引用的问题,介绍下右值应用。

答:对于左值右值问题,平时开发可能不怎么会用到这个,但是面试有可能会被问到,其实左值右值简单说就是不同的类型;在STL中,一般会用到move这个函数,它的作用就是把左值转换为右值,就是一个类型转换函数,在STL库中,经常将右值作参数类型,做浅拷贝,提升新性能,例如STL中Vector的push_back,当您传右值进去的时候,就会作为浅拷贝,eg:

#include<iostream>
#include<string>
#include<vector>


using namespace std;

int main()
{
	vector<string> v;
	string s = "asd";
	v.push_back(s);
	cout << "s = " << s << endl; // 输出 s = asd
	v.push_back(move(s));
	cout << "s = " << s << endl; //输出 s = 

	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

此时相当于做了一次浅拷贝,将s的指针指向的值转移到了数组s中,并且将s指向空;另外就是在unique_ptr中也经常会用到move,其目的是保证unique_ptr是指向该地址的唯一指针,这样就可以避免刚开始提到的出现野指针的情况,对于详细的左右值介绍请看这里, 文中提出了一个重要的结论,就是std:move单独使用,是不会提升性能的,它只是一个类型转换函数,效果和 static_cast(x) 是一样的效果,需要和其他功能配合使用,才有可能会往性能提升方向靠拢。还有一个c++11中提到的中道模板函数 forward,这个模板函数可以用于实现完美转发,在我们给函数传参数时,如果这个函数同时可以接受左值和右值,当对应参数传到这个函数中,无论这个值是左值还是右值,在这个函数里面都是左值(因为有地址了),如果这个时候用std::forward进行转发,就可以传递到接收对应类型的函数里面去 关于完美转发的详细介绍可以看这里

为什么解释型语言能够热更新,编译型得不能热更新呢(其实这个问题比较局限)?

答: 对于android手机而言,系统是基于linux开发的,所以可以通过动态连接库加载热更的代码(.so文件)。对于ios手机而言,运行app的时候会为appstore审核过的代码开辟专用的内存空间,而其他app中的数据或者通过代码从线上下载的数据加载的时候会将存放的内存空间定义描述符定义为禁止运行,这样ip寄存器将不能够跳转到该空间,因此这部分代码不能运行;但是为什么解释型的语言又可以呢,例如lua、python,对于这两种语言,在不开启JIT的情况下,是解释运行的,就是直接将代码输入给虚拟机,然后虚拟机边读取边执行,虚拟机这部分代码在包体提交到appstore之后是合法的,所以对于ios来说,并没有新的要执行的代码,只是虚拟机输入的数据有变化而已。当然,像其他同学说的那样,使用C++/C#解释器仍然是可以热更的。其实使用lua的主要原因是lua的字节码执行比较快,并且模拟器(虚拟机)运行比较稳定,维护足够好。同时lua模拟器足够轻量,方便扩展。也有部分项目组是用IL_RunTime热更,好处是热更用的是C#,不用再加其它脚本语言进行开发了,当然可能用的项目不多,需要踩的坑很多,所以大部分项目还是用的C++/C# + 脚本语言(lua、Python)进行开发。

C#以及mono的内存回收方式。

答:它们两个都是通过GC方式进行回收的,

  • 对于C#:首先资源分为托管资源和非托管资源,非托管资源需要自己去实现对应的回收函数接口IDispose;托管资源,指的是引用类型的对象,这一部分是需要GC去管理的;GC时从根集遍历,把能找到应引用的和不能找到引用的都标记出来,最后将没有引用的对象清除;GC的算法是采用的分代处理,这样做的目的是为了加快回收速度,减少GC是的卡顿现象,主要分为0、1、2代,每一代都有自己的内存预算,超过预算之后触发GC,GC之后0代中还存活的对象变为1代,依次内推,一般GC的调用机制时系统控制的,当然也可在代码中手动调用GC:GC.Collect();总体思路就是标记+分代处理垃圾对象。

  • 对于mono,采用的方式就比较简单些,mono的内存分为空闲内存+已分配的内存,当需要分配内存时,检查空闲内存是否够,如果够进行分配,如果不够,进行GC,GC完不够,向操作系统申请内存;其中mono内存有个特点就是向操作系统申请的内存是不会还给操作系统的;为了避免GC峰值造成的卡顿,unity也可以设置分步GC机制,也叫做步进式GC。

什么是虚拟内存

答:虚拟内存是一种计算机系统内存管理技术,它使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换;例如我们在Windows系统中,通常将虚拟内存设置为真是内存的1.5倍。由于物理内存有限,对于一些大型程序(游戏),需要占用的空间就比较大,此时就可以把一些资源放在磁盘中进行IO,实质上就是为了方便管理程序的地址以及利用磁盘的部分空间增大内存。

C++多态原理,虚表及其重载的实现原理。

答:C++多态分为静态联编和动态联编;静态联编就是指在编译的时候知道调用什么函数,例如函数的重载(参数类型、参数个数不同),动态联编是需要在程序具体执行的时候才能确定函数的调用,例如父类指针指向子类指针,父类存在虚函数;重载的原理就是编译器在编译的时候,会根据函数的参数类型将对应的函数重新命名,这一样就保证了函数的调用唯一性;对于虚函数,是通过虚表实现的,每一个存在虚函数的类或者其父类存在虚函数,在实例中都会有一个虚表,这个虚表记录了其指向的虚函数的调用地址,如果是多继承,就会有多个虚表。如果调用的是虚函数,那么就去看这个类指针实际指向的是谁,如果不是虚函数,调用的就是这个指针类型对应的函数。更详细的关于C++多态的介绍的内容可以看这里。

二维平面问题及其拓展 ----- 点到直线的距离,点是否在在扇形内,怎么判断扇形和圆相交。

点到直线的距离:直线上两个点A、B, 直线外的点P,通过向量法做,求出向量AP在直线AB上的投影向量AM,然后通过向量减法求出点P到线最近点M的向量:PM = AP - AM, C# 代码如下:

float getPointLineDis(float2 p, float2 a, float2 b)
{
	float2 v1 = p- a; // AP
	float2 v2 = b- a;  
	float2 v3 = dot(v1,v2) * v2 / pow(length(v2), 2); // AM
	return length(v1 - v3); // length(PM)
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

判断点是否在扇形内部: 首先计算扇形圆心O和所求点P的距离d,如果大于扇形半径,则一定不在扇形内部,其次,计算点P与圆心的连线向量与扇形角平分线的夹角,这个加角一定是小于扇形角度的一半,当然前提是扇形的角度theta小于180,当大于180时可以转换为求360 - theta;

判断扇形和圆是否相交: 通过分离轴的方式,判断扇形是否和圆相交,大概方式就是通过求圆心到扇形两条边(线段)的最短距离以及扇形圆心到所求圆的圆心的距离可以判断,简化可以通过对称性以及坐标转化求的,具体的详细算法可以参考这里

物理碰撞计算中,如果速度非常快,那么相邻两帧有时候就会出现穿插而导致没有检测到碰撞,这种情况怎么处理?

答:可以通过保留上一帧的位置信息,计算是否碰撞,例如:一个子弹飞向一面墙,可以通过子弹这一帧的位置和上一帧的位置连线是否与墙相交,相交则发生了碰撞,没有相交则没有发生碰撞。如果是两个运动很快的物体,则先利用相邻前后帧位置计算出线段,判断线段是否相交,如果相交,算出交点,然后再用出发点到交点的距离以及运动速度和碰撞物体体积进行计算。

深拷贝和浅拷贝

答:对于C++指针而言,浅拷贝是指连个指针指向同一个地址,深拷贝是指在内存空间中新开辟一个地址,并且复制需要拷贝的内容;在很多语言中会遇到这种问题,例如Python中的copy与deeppeek,当用copy.copy时,会出现修改复制出来的对象,最后溅射到原来对象的情况,原理和C++的深浅拷贝一样。

c#中的装箱和拆箱,性能会有什么损耗

答:

  • 装箱:将值类型转变为引用类型的过程成为装箱过程,首先会为该对象分配一个指针,然后为该对象在堆上开辟一个地址空间,损耗:由于需要给引用类型分配空间,所以内存消耗会增加,同时由于操作了c#堆,会造成更多的GC,从而有可能会增加游戏的卡顿。
  • 拆箱:将引用类型转换为值类型,这其中会涉及到数据类型的转换,例如将一个整形从引用转换为值,会有一个向整形转换的过程,int a = (int)(obj),这里也会有一定的性能损耗,相对于装箱损耗会小一些。

unity生命周期中的fixupdate,什么场合用,及其设置的参数

答:fixupdate固定时间间隔更新的函数,一般为0.02s更新一次;一般在start执行之后,update执行之前,由于其固定的执行频率,所以适合用于物理逻辑的计算,它不会随着帧率的变化而变化,例如,由于GC的原因,导致游戏卡顿,本来60帧的,变为了30帧,update的执行会和帧率保持一致,但是fixupdate不会,当帧率从60变为30时,执行到fixupdate时,会执行两次;设置的参数:Edit->Project Settings->Time 找到弹出面板的Fixed Timestep设置。
在这里插入图片描述

C#的异步操作

答:C#中的异步操作,主要是通过 Async 和await关键字配合一起使用,以及需要配合using System.Threading.Tasks下面的Task模板类实现多任务。C#异步编程应用举例可以看这里

多线程从编程,需要注意什么

答:

  • 一、线程池,对于多线程编程,一定程度上可以提高程序运行的并发性,但是同一时刻运行的线程数目不能多于CPU核心数,否者会导致线程相互竞争 CPU 资源,进而造成频繁的上下文切换,上下文切换是资源密集型的过程,因此应尽可能避免,所以超出核心数之后不一定能够提升程序的性能;
  • 二、安全性,多线程编程中特别需要注意的是 race condition,也就是竞争条件,例如多个线程都在写一个共享变量,这时候的结果是不确定的,所以会有问题;通常解决办法是通过加锁或者使得代码块的原子性操作。

AB包常用的压缩方式

LZMA, LZ4;
LZMA压缩是比较流行的压缩格式,能使压缩后文件达到最小,但是解压相对缓慢,导致加载时需要较长的解压时间;
LZ4压缩后的文件大小比LZMA稍大,但是解压速度快,如果只需要其中的某一部分,可以不用全部解压。

unity mask增加几个dp

加倍,先画原来的,在用模板(遮罩)进行剔除

unity对象的生命周期

awake,OnEnable,start,FixUpdate,Update,LateUpdate,OnDisable,OnDestroy

常用的图片压缩方式

etc pvr;etc1 不支持透明通道,解决办法是通过渲染另外一张图片作为渲染的透明度。etc2 支持透明通道,但是只有支持opengl3.0的才能用,大部分是支持opengl2.0。

为什么要做场景烘培,对于做烘培的场景有什么要求

不能做实时光照,增加效果,只能对静态物理做烘培

1024*1024的8位图片多大

(102410244*8b) 一个通道8位,所以是4m

前向渲染和延迟渲染特点

前向渲染:流水线的各个组成部分是:VS->TS->GS->FX->apphaTest->DepthTest->RT;支持半透明物体的渲染;光源数量不能太多,否者FS阶段的计算量会暴增;可以支持MSAA;只需要一个RT。

延迟渲染:流水线的各个组成部分是:VS->TS->GS->FX->MRT;需要设备支持多RT渲染,由于需要多RT,所以带宽要求高;由于光照计算是在FX之后,所以不支持MSAA,但是可以使用FXAA;不支持半透明度的物体渲染,如果需要,可以和前项渲染混合使用;支持多光源;需要 支持OpenGL ES 3.0以上的设备,metal支持延迟渲染。

UGUI的常用优化方式

动静分离;不用交互的取消勾选raycast;层级不要太深;有效利用合批(比如加了mask,会打断合批,以及增加一次dp)

图集有什么作用

图集的目的是为了提升渲染性能;例如GameObject A 和GameObject B,他们的材质除了引用的贴图不一样,其他的都一致,由于合批是需要GameObject的材质一致,所以这样会打断合批;如果这个时候把A和B所需要的贴图打包到一个大的贴图上,此时GameObject A 和GameObject B材质就是一致的,就可以使用合批提升渲染新能(这里不一定是减小drawcall,更加详尽介绍的可以查看这篇文章

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小蓝xlanll/article/detail/123215
推荐阅读
相关标签
  

闽ICP备14008679号