interview-doc-unity面试题

计算机基础

什么是序列化?

序列化是通过将对象转换为字节流,从而存储对象或将对象传输到内存,数据库或文件的过程。主要用途是保存对象的状态,包括对象的数据,以便能够在需要是重建对象。反向过程称为反序列化。

IL是什么?

IL的全称是Intermediate Language(中级语言),很多时候我们看到的是CIL(Common Intermediate Language,特指在.NET平台下的IL标准),其实大部分文章中提到的IL和CIL表示的是同一个东西,即中间语言。IL是一种低阶(lowest-level)的人类可读的编程语言。我们可以将通用语言翻译成IL,然后汇编成字节码,最后运行在虚拟机上。也可以把IL看作一个面向对象的汇编语言,只是它必须运行在虚拟机上,而且是完全基于堆栈的语言。
IL有三种转译模式:
Just-in-time(JIT)编译:在编译的时候,把C#编译成CIL,在运行时,逐条读入,逐条解析翻译成机器码交给CPU再执行。
Ahead-of-Time(AOT)编译:在编译成CIL之后,会把CIL再处理一遍,编译成机器码,在运行的时候交给CPU直接执行,Mono下的AOT只会处理部分的CIL,还有一部分CIL采用了JIT的模式。
Full AOT 完全静态编译:在编译成CIL之后,把所有的CIL编译成机器码,在运行的时候直接执行,这个模式适用于iOS操作系统。

什么是运行时(Runtime)?

一个程序在运行(执行)的过程中所需要的硬件和软件环境
运行时的主要作用是提供程序运行所需要的环境和基础设施,通过为程序提供内存分配、线程管理、类型检查、对象实例化和垃圾回收等操作来支持程序的运行
公共语言运行时(Common Language Runtime,CLR)是整个.NET框架的核心,它为.NET应用程序提供了一个托管的代码执行环境。它实际上是驻留在内存里的一段代理代码,负责应用程序在整个执行期间的代码管理工作,比较典型的有:内存管理、线程管理、安全管理、远程管理、即时编译、代码强制安全类检查等

Mono和IL2CPP有什么区别?为什么要使用IL2CPP?

C#主要运行在.NET平台上,但.NET跨平台支持不好。
Mono是.NET的一个开源,跨平台的实现,它包含一个C#编译器,mono运行时(CLR)和一组类库,Mono使得C#有了很好的跨平台能力。C#这种遵循CLI规范的高级语言,会被编译器编译成中间语言IL(CIL),当需要运行它们时就会被实时地加载到运行时库中,由虚拟机动态地编译成汇编代码(JIT)并执行。
IL2CPP的编译和运行过程:首先还是由Mono将C#语言翻译成IL,IL2CPP在得到中间语言IL后,将它们重新翻译成C++代码,再由各个平台的C++编译器直接编译成能执行的机器码。
为什么要使用IL2CPP:
1)Mono虚拟机维护成本过大。
2)Mono版本授权受限。
3)提高运行效率。换成IL2CPP以后,程序的运行效率有了1.5~2.0倍的提升。

什么是托管代码,什么是非托管代码?

托管代码:托管代码就是执行过程交由运行时(公共语言运行时,CLR)管理的代码。不管使用的是哪种实现(例如
Mono、.NET Framework或.NET Core/.NET 5+)。CLR负责提取托管代码、将其编译成机器代码,然后执行它。除此之外,运行时还提供多个重要服务,例如GC管理、安全边界、类型安全,把托管代码理解成IL中间语言也行

非托管代码:非托管代码会直接编译成目标计算机的机器码,这些代码包含C/C++或C#中以不安全类型写的代码。非托管代码不受CLR管理,需要手动释放内存

一般情况下,我们使用托管代码来编写游戏逻辑,非托管代码通常用于更底层的架构、第三方库或者操作系统相关接口

Mono的垃圾回收机制

Mono将Simple Generational GC(SGen-GC)设置为默认的垃圾回收器,当我们向垃圾回收器申请内存时,如果发现内存不足,就会自动触发垃圾回收,或者也可以主动触发垃圾回收,垃圾回收器此时会遍历内存中所有对象的引用关系,如果没有被任何对象引用则会释放内存。SGen-GC的主要思想是将对象分为两个内存池,一个较新,一个较老,那些存活时间长的对象都会被转移到较老的内存池中去。这种设计是基于这样的一个事实:程序经常会申请一些小的临时对象,用完了马上就释放。而如果某个对象一段时间没被释放,往往很长时间都不会释放。
IL2CPP的虚拟机的内存管理仍然采用类似Mono的方式,因此程序员在使用IL2CPP时无须关心Mono与IL2CPP之间的内存差异。

栈内存和堆内存的区别?

栈空间比较小,栈遵循先进后出的原则。它是一段连续的内存,所以对栈数据的定位比较快速,栈创建和删除的时间复杂度则是O(1);
堆空间比较大,堆是随机分配的空间,处理的数据比较多,无论情况如何,都至少要两次才能定位。堆内存的创建和删除节点的时间复杂度是O(lgn)。
栈是由系统管理的,栈中的生命周期必须确定,销毁时必须按次序销毁,即从最后分配的块部分开始销毁,创建后什么时候销毁必须是一个定量,所以在分配和销毁上不灵活,它基本都用于函数调用和递归调用这些生命周期比较确定的地方。
相反,堆内存可以存放生命周期不确定的内存块,满足当需要删除时再删除的需求,所以堆内存相对于全局类型的内存块更适合,分配和销毁更灵活。

装箱和拆箱的区别,哪个会产生gc,什么时候会发生装箱?

装箱:把值类型实例转换为引用类型实例。拆箱:把引用类型实例转换为值类型实例。
装箱的内部操作:
第一步:在堆内存中新分配一个内存块(大小为值类型实例大小加上一个方法表指针和一个SyncBlockIndex类)。
第二步:将值类型的实例字段复制到新分配的内存块中。
第三步:返回内存堆中新分配对象的地址。这个地址就是一个指向对象的引用。

拆箱的操作:先检查对象实例,确保它是给定值类型的一个装箱值,再将该值从实例复制到值类型变量的内存块中。
由于装箱、拆箱时生成的是全新的对象,不断地分配和销毁内存不但会大量消耗CPU,同时也会增加内存碎片,降低性能。装箱需要消耗的托管堆内存,如果有大量的对象产生,会增加gc的压力。

发生装箱的情况:
1.当程序、逻辑或接口为了更加通用把参数定义为object,一个值类型(如Int32)传入时,就需要装箱。
2.一个非泛型的容器为了保证通用,而将元素类型定义为object,当值类型数据加入容器时,就需要装箱。
3.当结构体实现接口,而接口又持有该结构体时会发生装箱。

内存碎片

内存碎片是指内存中存在的一些不连续的小块空闲内存,由于它们不连续,所以无法被利用。
内存碎片分为外碎片和内碎片:
外碎片:外部碎片指的是还没有被分配出去(不属于任何进程),但由于太小了无法分配给申请内存空间的新进程的内存空闲区域。
内碎片:内部碎片就是已经被分配出去(能明确指出属于哪个进程)却不能被利用的内存空间;

编程基础

游戏中常用的设计模式

  1. 简单工厂模式:把对象的创建封装到类中,根据不同的参数生成不同的对象,如根据建筑的类型生成不同的建筑。
  2. 观察者模式:C#的event。
  3. 状态模式:使用有限状态机,将行为抽象成一个个状态,通过状态管理器控制状态之间的转换,同一时间只能处于某一个状态。
  4. 组合模式:将一些功能抽象成一个个组件,对象创建时根据需求添加不同的组件,增强代码复用性。
  5. 单例模式:全局为一,游戏中的管理器。
  6. 外观模式:对多个子系统进行封装,通过外观类来获取这些系统,减少系统的互相依赖,减少和其他系统的耦合。
  7. 策略模式:定义了一组同类型的算法,在不同的类中封装起来,每种算法可以根据当前场景相互替换,从而使算法的变化独立于使用它们的客户端。
  8. 命令模式:将一个命令封装为一个对象,从而实现解耦,改变命令对象,撤销功能
  9. 原型模式:在不需要创建新对象的情况下复制现有对象,并根据需要修改一些属性

值类型与引用类型的区别

  1. 值类型存储在栈(stack)中,引用类型数据存储在堆(heap)中,内存单元中存放的是堆中存放的地址。
  2. 值类型存取快,引用类型存取慢。
  3. 值类型表示实际数据,引用类型表示指向存储在内存堆中的数据的指针和引用。
  4. 栈的内存是自动释放的,堆内存是.NET中会由GC来自动释放。
  5. 值类型继承自System.ValueType,引用类型继承自System.Object。

类和结构体的区别?使用环境?

结构体是值类型,类是引用类型。结构体存储在栈中,类存储在堆中,栈的空间小但是访问快,堆的空间大但是访问速度较慢。

结构体不能继承,不能创建默认构造函数和析构函数。结构成员不能指定为 abstract、virtual 或 protected。结构体的构造函数必须为所有值赋初值。

结构体一般存储较为轻量的数据,类一般存储具有较为复杂逻辑结构的数据。

使用环境:

  • 当堆栈的空间很有限,且有大量的逻辑对象时,创建类要比创建结构好一些。
  • 对于点、矩形和颜色这样的轻量对象,假如要声明一个含有许多个颜色对象的数组,则CLR需要为每个对象分配内存,在这种情况下,使用结构的成本较低。
  • 在表现抽象和多级别的对象层次时,类是最好的选择,因为结构不支持继承。

结构体和类的区别

1.结构体是值类型,类是引用类型
2.结构体成员不能使用protected访问修饰符,而类可以
3.结构体成员变量申明不能指定初始值,而类可以
4.结构体不能申明无参的构造函数,而类可以
5.结构体不能申明析构函数,而类可以
6.结构体不能被继承,而类可以
7.结构体需要在构造函数中初始化所有成员变量,而类随意
8.结构体不能被静态static修饰(不存在静态结构体),而类可以
9.使用 new 操作符创建一个结构体,会调用构造函数来创建结构体。与类不同,结构可以不使用 new 操作符即可被实例化。
如果不使用 new 操作符,只有在所有的字段都被初始化之后,字段才被赋值,对象才能使用
10.结构体比较特殊,他不能使用比较运算符(==),使用 Equals() 方法进行比较时,当两个结构体对象的所有字段的值都相等时返回 true,否则返回 false

抽象类和接口的区别?

  • 抽象类和接口都不能实例化。
  • 抽象类可以有抽象的的方法和未抽象的的方法,可以通过子类来重写。
  • 抽象类主要是子类的通用结构。常量、字段、运算符、实例构造函数、析构函数或类型、不能包含静态成员。
  • 接口不能有实现的方法,接口主要是作为规范来使用。

请描述你所了解的设计模式,并说明在你的项目中哪里使用过?

单例:对象池,游戏管理器,抽象工厂
状态:有限状态机
桥接:有限状态机
策略:AI自动行为操控中每种操控算法的独例

请说出4种面向对象的设计原则,并分别简述它们的含义。

单一职责原则:一个类,最好只做一件事,只有一个引起它的变化。
开放-封闭原则:对于扩展是开放的,对于更改是封闭的。
里氏替换原则:子类必须能够替换其基类。
依赖倒置原则:设计应该依赖于抽象而不是具体实现。
接口隔离原则:使用多个小的专门的接口而不要使用一个大的总接口。

虚方法和抽象方法的区别?

  • 抽象方法是只有方法名称,没有方法体,即没有方法的具体实现,子类必须重写父类抽象方法才能实现具体功能;虚函数有方法名称也也有方法体,但是子类可覆盖,也可不覆盖。
  • 抽象方法是一种强制派生类覆盖的方法,否则派生类将不能被实例化。
  • 抽象方法只能在抽象类中声明,虚方法不是。
  • 派生类必须重写抽象类中的抽象方法,虚方法则不必要。

委托,事件,UnityEvent

委托是一种类(class),我们在创建委托时,其实就是创建一个Delegate类实例。委托可以看作是一个函数指针数组,保存一个或多个函数地址,当使用 =(等号)操作时,就会把函数地址保存到这个数组中,多播委托就会保存多个函数地址。当委托被调用时,会把数组中的函数依次调用一遍。
事件(event)是委托(delegate)的封装,用户不能再直接用 =(等号)操作来改变委托变量,用户只能通过 “+=” 和 “-=” 操作来注册或删除委托函数的数量。公开的delegate会直接暴露在外,随时会被 “=” 赋值而清空前面累积起来的委托函数,封装后就保证了 “谁注册就必须谁负责销毁” 的目的,更好地维护了delegate的秩序。委托可以作为方法参数传递,事件不行。
UnityEvent使用Serializable序列化,方便开发者直接在检视面板中编辑事件及事件回调函数,简化开发流程。使用event需要手动编写代码且无法直接编辑。UnityEvent首次触发事件时会产生垃圾,而C# event不会产生任何垃圾,且前者的速度比后者慢两倍之多。

在C#中using和new这两个关键字有什么意义?

using 关键字有两个主要用途:

  • 作为指令,用于为命名空间创建别名或导入其他命名空间中定义的类型。
  • 作为语句,用于定义一个范围,在此范围的末尾将释放对象。

new 关键字:新建实例或者隐藏父类方法。

System.String 和System.StringBuilder有什么区别?

  • System.String是不可变的字符串。
  • System.StringBuilder存放了一个可变的字符串,并提供一些对这个字符串修改的方法。
  • String类在执行字符串拼接的操作上,用“+”会产生新的对象,占用内存。
  • StringBuilder类只是修改字符串的内容,不建立新的对象。

const和readonly有什么区别?

  • const 字段只能在该字段的声明中初始化。
  • 不允许在常数声明中使用 static 修饰符。
  • readonly 字段可以在声明或构造函数中初始化。

请简述sealed关键字用在类声明时与函数声明时的作用?

sealed修饰的类为密封类,类声明时可防止其他类继承此类,在方法中声明则可防止派生类重写此方法。

如果不想new,但又想获取对象实例,有哪几种方法?

  1. 使用反射。
    Assembly assembly = Assembly.Load(“xxx”);
    Type type = assembly.GetType(“yyy”);
    return Activator.CreateInstance(type);
  2. 使用原型模式克隆。
  3. 反序列化

请简述GC(垃圾回收)产生的原因,并描述如何避免?

GC回收堆上的内存
避免:

  1. 减少new产生对象的次数
  2. 使用公用的对象(静态成员)
  3. 如果字符串拼接多的将String换为StringBuilder

MVC

view初始化时从model中获取数据,并监听model数据变化,用户操作view触发事件,发送给control,control处理后更新model数据,model再通知view刷新。
数据部分分为配置表数据和网络数据,配置表数据相对固定,在界面中通过事件管理器监听网络数据的变化。
界面会监听对应数据的变化,比如背包界面监听背包道具的变化。view和model的关系只是查询,并不会改变数据,数据的变化只能来自于服务器的协议驱动。

using关键字的作用

  1. 引用命名空间,也可using 别名
  2. 释放资源,实现了IDisposiable的类在using中创建,using结束后会自定调用该对象的Dispose方法,释放资源。

算法

四元数是什么?主要作用什么?对欧拉角的优点是什么?

所谓四元数,就是把4个实数组合起来的东西。4个元素中,一个是实部,其余3个是虚部。

作用:四元数用于表示旋转。

优点:

  • 能进行增量旋转
  • 避免万向锁
  • 给定方位的表达方式有两种,互为负(欧拉角有无数种表达方式)

叉乘和点乘的意义?

叉乘:
几何意义:得到一个与这两个向量都垂直的向量,这个向量的模是以两个向量为边的平行四边形的面积。
在同一平面内,结果>0表示B在A的逆时针方向,结果<0表示B在A的顺时针方向, 结果=0表示B与A同向。

点乘:
几何意义:可以用来表征或计算两个向量之间的夹角,以及b向量在a向量方向上的投影。
两个向量的点乘所得到的是两个向量的余弦值,也就是-1到1之间,0表示垂直,-1表示相反,1表示相同方向。

点乘,叉乘和归一化的意义

  1. 点乘主要用于计算角度和投影
    θ是向量A和向量B的夹角。
    计算两个向量的夹角:cosθ = A·B /(|A||B|)
    计算A向量在B向量上的投影:|A| * cosθ = A·B / |B|
  2. 叉乘用于计算旋转的轴,判断向量的相对位置
    计算旋转的轴:比如我面向前方,我要向右转,这时朝前的这个向量和朝右的这个向量叉乘得到了我需要的旋转轴。注意数学上叉乘用右手法则,Unity当中叉乘用左手法则。
    判断相对位置:向量A和向量B做叉乘,如果结果向上,说明B向量在A向量的右边,否则B向量在A向量的左边。
    |a X b| = |a||b|sinθ 几何意义:两个向量构成的平行四边形的面积。
  3. 归一化:归一化就是要把需要处理的数据经过处理后限制在你需要的一定范围内,用在只关系方向,不关心大小的时候。

Stack底层如何实现的?

Stack内部也是数组实现的,与List一样,也是按照2倍的容量去扩容,只是默认容量不一样,Stack默认构建一个容量为10的数组。

数组和List两者效率之间哪个好?

数组: 数组在C#中是最早出现的。它在内存中是连续的存储的,所以索引速度很快,而且赋值与修改元素也很简单。可以利用偏移地址访问元素,时间复杂度为O(1);删除时间复杂度为O(n),数组没有添加数据选项。

List:基于数组,时间复杂度相同,插入为O(n);不过在数据少量的时候跟数组差不多,数据庞大的时候效率会低于数组。

请简述ArrayList和List<>的主要区别。

ArrayList是非泛型列表,存储数据是把所有的数据都当成object类型数据,存在装箱问题,取出来使用的时候存在拆箱问题,装箱拆箱会使性能变差,而且存在数据安全问题,但是优点在于可以让值类型和引用类型相互转换。

List是泛型列表,在使用的时候才会去定义数据类型,泛型避免了拆装箱的问题,存入读取熟读较快,类型也更安全。

哈希表与字典的区别?

字典:内部用了Hashtable作为存储结构。

  • 如果我们试图找到一个不存在的键,它将返回 / 抛出异常。
  • 它比哈希表更快,因为没有装箱和拆箱,尤其是值类型。
  • 仅公共静态成员是线程安全的。
  • 字典是一种通用类型,这意味着我们可以将其与任何数据类型一起使用(创建时,必须同时指定键和值的数据类型)。
  • Dictionay 是 Hashtable 的类型安全实现, Keys和Values是强类型的。Dictionary遍历输出的顺序,就是加入的顺序。

哈希表:

  • 如果我们尝试查找不存在的键,则返回 null。
  • 它比字典慢,因为它需要装箱和拆箱。
  • 哈希表中的所有成员都是线程安全的。
  • 哈希表不是通用类型。
  • Hashtable 是松散类型的数据结构,我们可以添加任何类型的键和值。
  • HashTable是经过优化的,访问下标的对象先散列过,所以内部是无序散列的。

欧拉角,四元数,旋转矩阵优缺点

欧拉角
优点:直观,容易理解。3个数据可以节省内存空间
缺点:万向节死锁问题,必须严格按照顺序进行旋转(顺序不同结果就不同)
应用:只涉及到一个方向的简单旋转可以用欧拉角

四元数
优点:没有万向节死锁。存储空间小,计算效率高。平滑插值,
缺点:单个四元数不能表示在任何方向上超过180度的旋转。四元数的数字表示不直观。
应用:物体旋转的过渡

矩阵旋转:
优点:旋转轴可以是任意向量,没有万向节死锁
缺点:元素多,存储空间大

四元数的作用

四元数用于表示旋转。
其相对于欧拉角的优点:
1.避免万向锁。
2.只需要一个4维的四元数就可以执行绕任意过原点的向量的旋转,方便快捷,在某些实现下比旋转矩阵效率更高。
3.可以提供平滑插值。

延伸
什么是欧拉角?
用一句话说,欧拉角就是物体绕坐标系三个坐标轴(x,y,z轴)的旋转角度。
1,静态:即绕世界坐标系三个轴的旋转,由于物体旋转过程中坐标轴保持静止,所以称为静态。
2,动态:即绕物体坐标系三个轴的旋转,由于物体旋转过程中坐标轴随着物体做相同的转动,所以称为动态。
物体的任何一种旋转都可分解为分别绕三个轴的旋转,但分解方式不唯一。
unity 3D欧拉角的旋转顺序(父子关系)是y-x-z。
unity中最简单的万向锁就是先让X轴旋转90度,z轴旋转和y轴旋转效果是一样。

讲讲万向锁:
万向锁(英语:Gimbal lock)是在使用动态欧拉角表示三维物体的旋转时出现的问题。
万向节死锁的根本问题是欧拉角(EulerAngles)保存的信息不足以描述空间中的唯一转向。

逆矩阵的作用

当我们将一个向量经过旋转或其他的变换后,如果想撤销这个变换,就乘以变换矩阵的逆矩阵。

解决哈希冲突的方法

  1. 开放定址法:冲突位置向后移动一个单位,直到不发生冲突。
  2. 平方探测法:按照+1,-1,+2²,-2²,+3²…顺序查找
  3. 再哈希法:对于冲突的哈希值再次进行哈希处理,直至没有哈希冲突。
  4. 拉链法

常用的hash算法

  1. 加法Hash,就是把输入元素一个一个的加起来构成最后的结果
  2. 位运算Hash,这类型Hash函数通过利用各种位运算(常见的是移位和异或)来充分的混合输入元素
  3. 乘法Hash,这种类型的Hash函数利用了乘法的不相关性
  4. 混合Hash,混合以上方式

A星寻路

f(寻路消耗) = g(离起点的距离)+ h(离终点的距离)
起点添加到关闭列表,将起点周围的点添加到开放列表中,开放列表中选出一个消耗最小的点放入关闭列表中,如果这个点是终点则路径找完了,否则这个点作为新起点再循环找。每次从新起点找周围的点时,如果周围的点已经在开放列表或者关闭列表中则忽略。除了起点每个格子都会存其父对象,当找到终点后回溯父对象的格子直到起点,连成路径。
优化:预存路径,地图分块,优化开放列表的排序,最小堆数据结构非常适合A星寻路的open排序。

unity基础

Addcomponent后哪个生命周期函数会被调用?

对于AddComponent添加的脚本,其Awake,Start,OnEnable是在Add的当前帧被调用的,其中Awake,OnEnable与AddComponent处于同一调用链上,Start会在当前帧稍晚一些的时候被调用,Update则是根据Add调用时机决定何时调用:如果Add是在当前帧的Update前调用,那么新脚本的Update也会在当前帧被调用,否则会被延迟到下一帧调用。

Image和RawImage的区别?

  • Imgae比RawImage更消耗性能。
  • Image只能使用Sprite属性的图片,但是RawImage什么样的都可以使用。
  • Image适合放一些有操作的图片(裁剪、平铺、旋转等),针对Image Type属性。
  • RawImage只放单独展示的图片就可以,性能会比Image好很多。

简述prefab的用处?

在游戏运行时实例化,prefab相当于一个模板,对你已经有的素材、脚本、参数做一个默认的配置,以便于以后的修改,同时prefab打包的内容简化了导出的操作,便于团队的交流。

使用Unity3d实现2d游戏,有几种方式?

  1. 使用本身的GUI,在Unity4.6以后出现的UGUI
  2. 把摄像机的Projection(投影)值调为Orthographic(正交投影),不考虑z轴
  3. 使用2d插件,如:2DToolKit,和NGUI

mesh与shareMesh,material与shareMaterial,materials和sharedMaterials的区别?

mesh和material都是实例型的变量,对mesh和material执行任何操作,都是额外复制一份后再重新赋值,即使只是get操作,也同样会执行复制操作。也就是说,对mesh和material进行操作后,就会变成另外一个实例,虽然看上去一样,但其实已是不同的实例了。
sharedMesh和sharedMaterial与前面两个变量不同,它们是共享型的。多个3D模型可以共用同一个指定的sharedMesh和sharedMaterial,当你修改sharedMesh或sharedMaterial里面的参数时,指向同一个sharedMesh和sharedMaterial的多个模型就会同时改变效果。也就是说,sharedMesh和sharedMaterial发生改变后,所有使用sharedMesh和sharedMaterial资源的3D模型都会表现出相同的效果。
materials与sharedMaterials类似,只不过变成了数组形式。materials和sharedMaterials可以针对不同的子网格,material和sharedMaterial只针对主网格。也就是说,material和sharedMaterial等于materials[0]和sharedMaterials[0]。

FixedUpdate原理

Unity的主要逻辑是单线程的,Update和FixedUpdate都是在主线程上调用的,如果某一帧的Update卡了很长时间,下一帧的FixedUpdate肯定会受影响,那么是怎么保证FixUpdate的更新频率?
FixedUpdate在累计的时间大于一次物理更新时才会调用一次,当经过的时间大于多个物理更新时间就会按更新间隔分成多次调用。比如物理更新间隔设置的是15毫秒,但运行时,实际的帧间隔是30毫秒,30毫秒大于两次物理更新时间,所以fixedupdate会调用两次,update只调用一次。

修改Time.timeScale会影响什么?

timeScale改变时,会对以下值产生影响:time、deltaTime、fixedTime以及fixedUnscaledDeltaTime

timeScale会影响 FixedUpdate 的执行速度,当timeScale为0时,FixedUpdate完全停止。但不会影响Update、LateUpdate的执行速度,如果Update、LateUpdate中使用了deltaTime,则也会影响这部分逻辑的执行

timeScale 不会影响 Coroutine本身的执行速度。当timeScale为0时,如果Coroutine中yield了某个WaitForSeconds或者WaitForFixedUpdate,那么该Coroutine会在此处停下。如果想要等待一个不受timeScale影响的时间,请用WaitForSecondsRealtime

Material和Shader的区别

Material是模型的材质,包含贴图,shader等。 Shader是Material的一部分,本质是一小段程序,它负责将输入的Mesh(网格)以指定的方式和输入的贴图或者颜色等组合作用,然后输出。

Material和Physic Material区别?

PhysicMaterial 物理材质:主要是控制物体的摩擦,弹力等物理属性。
Material材质:主要是控制一个物体的颜色,质感等显示。

材质、贴图、纹理的关系

材质 Material 包含贴图 Map,贴图包含纹理 Texture。
纹理是最基本的数据输入单位,游戏领域基本上都用的是位图。此外还有程序化生成的纹理 Procedural Texture。
贴图的英语 Map 其实包含了另一层含义就是“映射”。其功能就是把纹理通过 UV 坐标映射到3D 物体表面。贴图包含了除了纹理以外其他很多信息,比方说 UV 坐标、贴图输入输出控制等等。
材质是一个数据集,主要功能就是给渲染器提供数据和光照算法。贴图就是其中数据的一部分,根据用途不同,贴图也会被分成不同的类型,比方说 Diffuse Map,Specular Map,Normal Map 和 Gloss Map 等等。另外一个重要部分就是光照模型 Shader ,用以实现不同的渲染效果。

LineRenderer的实现原理是什么?

Line Renderer组件是一种用于在3D空间中绘制线的工具。它使用一个点的数组来确定线条的形状和位置,然后在每个点之间插值生成顶点和三角形

动画

模型动画有哪些?

骨骼蒙皮动画
特点:文件格式复杂,文件体积小,耗CPU,需要大量矩阵运算。

GPU动画
特点:体积小,运算快,动画相对简单,缺乏物理交互,因为顶点的移动是GPU处理的,CPU端的物理引擎认为点并没有移动。
骨骼本质是一个4*4的变换矩阵,可以将变换矩阵的数据当作颜色存储在贴图上,GPU读取贴图上的信息还原为变换矩阵。

请描述游戏动画有哪几种,以及其原理?

主要有关节动画、单一网格模型动画(关键帧动画)、骨骼动画。

  1. 关节动画:把角色分成若干独立部分,一个部分对应一个网格模型,部分的动画连接成一个整体的动画,角色比较灵活,Quake2中使用这种动画。
  2. 单一网格模型动画由一个完整的网格模型构成,在动画序列的关键帧里记录各个顶点的原位置及其改变量,然后插值运算实现动画效果,角色动画较真实。
  3. 骨骼动画,广泛应用的动画方式,集成了以上两个方式的优点,骨骼按角色特点组成一定的层次结构,有关节相连,可做相对运动,皮肤作为单一网格蒙在骨骼之外,决定角色的外观,皮肤网格每一个顶点都会受到骨骼的影响,从而实现完美的动画。

骨骼动画原理

在mesh中添加骨骼,骨骼的两端为关节,骨骼只能以关节为轴心旋转,把mesh上的点绑定到骨骼上,即刷权重,这样mesh就能够随着骨骼的动作而变形。骨骼动画的本质,便是在不同的时间点为某节骨骼定义了特定的位置、缩放、旋转。动画的运作便是根据两个时间点之间的骨骼数据做数值变化,这种行为称之为补间(Tweens),同理骨骼动画也就是一种补间动画

骨骼包含哪些信息

通常包含以下信息:
骨骼名称:每个骨骼都有一个唯一的名称,用于标识该骨骼和在程序中引用它。
骨骼的旋转、位移和缩放:这些变换信息指示了骨骼在动画中的变化,如旋转方向、位置和大小。
骨骼的层次结构:骨骼可以是单个骨骼,也可以是层次结构中的父骨骼或子骨骼。这些关系确定了骨骼在空间中的位置和姿态。
骨骼的绑定信息:这些信息指示了哪些网格顶点与该骨骼相关联,以及它们的权重。这些权重指示了骨骼对网格的影响程度,决定了网格的变形方式。

动画融合是怎么实现的

混合树(Blend Tree)是一种将多个动画片段以位置、速度、角速度为依据经行线性混合的方式,可以将几个动画文件很好的融合在一起
还可以通过动画层(Layer)的方式实现,每一个动画层只对动画主体的部分进行控制,其他部分通过遮罩屏蔽

Animation和Animator的区别

Animation需要通过代码手动控制动画的播放和迁移。而Animator拥有动画状态机,可以通过动画状态机来设置动画之间的状态,并且可以为单个动画设置脚本代码来控制事件。

什么情况下会用到Animator Override Controller?

如果A,B两个角色使用的Animator Controller结构完全相同,只是用到Animation Clip不一样,这时可以使用Animator Override Controller。实现A的状态机后,B使用Animator Override Controller覆盖掉A中的Animation Clip

动画层(AnimationState Layers)的作用是什么?

动画层作为一个具有层级动画编辑概念的工具,可以用来制作和处理任何类型的动画

简述SkinnedMesh的实现原理

骨骼蒙皮动画,模型本身是静态的,是因为通过蒙皮,使模型每个点都有Skin数据,Skin数据包括顶点受到哪些骨骼影响以及这些骨骼影响顶点的权重值,还有动画数据,有了Skin数据的模型就可以根据动画数据进行显示动画了。

ai

有限状态机和行为树区别

有限状态机将游戏AI行为分为一个一个的状态,状态与状态之间通过状态管理器切换,某一个时刻只能处于其中一种状态
状态机的问题:随着状态的增多,需要考虑任意两个状态之间是否可以切换,逻辑复杂,复用性不好,如果设计一个全新的敌人,又需要重写一套状态节点和切换逻辑

行为树把行为抽象成一棵树,它是一种“轮询式机制”,即每次更新都会遍历树,判定逻辑是否成立,是否该继续往下执行。行为树从上到下,从左到右遍历节点,行为树的每个节点会有返回一个执行状态,一种设置方式是 {Running,Success,Failure } 三种状态,Running代表正在运行,Success,Failure对应执行成功和失败

物理

MeshCollider和其他Collider的一个主要不同点?

MeshCollider是网格碰撞器,对于复杂网状模型上的碰撞检测,比其他的碰撞检测精确的多,但是相对其他的碰撞检测计算也增多了,所以一般使用网格碰撞也不会在面数比较高的模型上添加,而会做出两个模型,一个超简模能表示物体的形状用于做碰撞检测,一个用于显示。

Unity3d中的碰撞器和触发器的区别?

碰撞器是触发器的载体,而触发器只是碰撞器身上的一个属性。当Is Trigger=false时,碰撞器根据物理引擎引发碰撞,产生碰撞的效果,可以调用OnCollisionEnter/Stay/Exit函数;当Is Trigger=true时,碰撞器被物理引擎所忽略,没有碰撞效果,可以调用OnTriggerEnter/Stay/Exit函数。如果既要检测到物体的接触又不想让碰撞检测影响物体移动或要检测一个物件是否经过空间中的某个区域这时就可以用到触发器。

CharacterController和Rigidbody的区别?

Rigidbody具有完全真实物理的特性,Unity中物理系统最基本的一个组件,包含了常用的物理特性,而CharacterController可以说是受限的的Rigidbody,具有一定的物理效果但不是完全真实的,是Unity为了使开发者能方便的开发第一人称视角的游戏而封装的一个组件。

场景中有个物体(坐标未知),摄像机可以看到这个物体,已知摄像机的位置,怎么得到物体的坐标?

射线检测

怎么判断两个平面是否相交?不能用碰撞体,说出计算方法

在两个平面上分别取一个向量,然后看是否相交

当一个细小的高速物体撞向另一个较大的物体时,会出现什么情况?如何避免?

穿透(碰撞检测失败)
避免的方法:把刚体的实时碰撞检测打开Collision Detection修改为Continuous Dynamic

<愤怒的小鸟>给予初速度以后,怎么让小鸟受到重力和空气阻力的影响而绘制抛物线轨迹,说出具体的计算方法.

添加刚体使小鸟模拟受到重力影响。

资源

Resources和AssetBundle使用区别?

Resources是动态内部调用,Resources在编辑环境下是project窗口的一个文件夹,调用里面的资源,可以用Resources类,比如Resources.Load,打包后这个文件夹是不存在的,会统一生成assets资源。

AssetBundle是外部调用,要用AssetBundle首先要先把资源打包为.assetbundle文件,再动态的去加载这个文件,本地或者网络服务器都可以。

图集打包怎么分类?

  • 按业务功能的预制,寻找依赖,收集所有预制引用的图片。
  • 如果有多个预制使用了同一张图片,我们就把它扔到common文件夹。
  • 让图集尽量紧凑,没有太多空白,尽量让图集处于2的n次方大小。

热更新方案

  1. 整包:将完整更新资源放在Application.StreamAssets目录下,首次进入游戏将资源释放到Application.persistentDataPath下。
    优点:首次更新少。缺点:下载时间长,首次安装时间久。
  2. 分包:少部分资源放在包里,其他资源存放在服务器上,进入游戏后将资源下载到Application.persistentDataPath目录下。
    优点:安装包小,安装时间短,下载快。缺点:首次更新下载时间久。

热更新的大致流程

  1. 首先,需要将资源或代码打包成AssetBundle文件,生成一个热更配置文件记录版本号及所有资源信息(地址,MD5,大小等),并上传到服务器
  2. 然后,应用程序启动时,会从服务器下载这个热更配置文件,与本地的文件进行对比,如果服务器版本号更高,则需要进行热更新,版本号一致也需要检测文件的大小和MD5,保证资源没有丢失或修改
  3. 如果有需要更新的内容,会遍历本地资源计算MD5,如果与服务器不一致,则下载相应的AssetBundle文件,并替换现有的资源,下载完成后会进行文件校验,对比本地资源和服务器资源的大小和MD5,不一致则重新下载
  4. 热更新完成后,客户端会把最新的热更配置文件存储在本地,方便下一次更新检测

资源管理器需要注意哪些?

开发模式:编辑器下使用AssetDatabase.LoadAssetAtPath从StreammingAsset下加载资源。
发布模式:从ab包中加载资源,使用AssetBundle.LoadFromFile(Async optional)
加载的资源需要引用计数,当引用计数为0时,如果是GameObject就销毁或者回收到对象池,如果是ab包就unload。

AssetBundle.Unload()参数为true和false有什么区别?

Unload(false)表示只卸载ab包,并破坏了资源和AB之间的链接
Unload(true)表示把ab包和加载的资源都卸载掉

ab包中的资源冗余怎么处理?

如果两个ab包A和B中的一些资源都依赖了资源C,那么C就会同时被打进A和B中,造成资源的冗余
资源C又可以分为两种类型,一种是我们自己创建的资源;另一种是Unity内置的资源,例如内置的Shader,Default-Material和UGUI一些组件如Image用的一些纹理资源等等

对于我们自己创建的资源,解决方案就是将这些被多个ab包依赖的资源打包到一个公共ab包中,处理过程如下:

  1. 使用 EditorUtility.CollectDependencies() 得到ab依赖的所有资源的路径
  2. 统计资源被所有ab引用的次数,将被多个ab引用的资源打包为公共ab包

对于内置资源:
将内置资源提取或者下载到本地,打成ab包,检测其他ab包是否引用内置资源,如果引用了内置资源,则修改引用关系

打ab包时,LZMA和LZ4这两种压缩方法有什么区别?

LZMA压缩的ab包较小,它是流式压缩,只支持顺序读取,获取ab包中的某个资源需要完全解压后再加载,加载时间较慢
LZ4压缩的ab包较大,它是块压缩,支持随机读取,加载时间较快

网络

TCP和UDP的区别

TCP和UDP是TCP/IP协议簇中传输层的传输协议。

Tcp是面向连接的,可靠的,面向字节流的传输。TCP在连接时需要三次握手,断开时需要四次挥手。TCP的连接是点到点的连接。Tcp收到的数据保证顺序,TCP有着拥塞控制,超时重发,丢弃重复数据,检验数据等机制。

UDP是面向数据包的,不可靠,包头简单,传输速度快。可以一对一,一对多,多对多,多对一发送,无需建立连接,没有拥塞控制,即使网络拥塞了也会不断的发送数据。目前在实时应用中,如游戏直播等,虽然UDP不可靠,但是得益于网速的提升以及可以自己编写重传机制来保证UDP的可靠性。

Http和Https的区别

简单来说,Http属于明文传输,不安全,Https属于加密传输,较安全。

Http是无状态的连接,通过明文传输,信息可能被拦截,篡改等等。Https是HTTP的安全加强版,Https 协议是由 SSL+HTTP 协议构建的可进行加密传输、身份认证的网络协议,比 Http 协议安全。

帧同步是如何同步的?

服务器把玩家的操作同步给所有的玩家,玩家在本地客户端,根据服务器发过来的操作,来推进游戏。
同样的代码 + 同样输入 —> 同样的结果
服务器,每隔一端时间,将采集的玩家的操作,发给所有的客户端,继续采集下一次的操作,等下一次时间到,又把采集到的操作发送給所有客户端。
客户端:收到服务器的操作 —> 计算游戏逻辑 —> 上报下一帧的操作给服务器。

状态同步和帧同步区别

状态同步 帧同步
消息传输
回放 较难还原 容易
安全性 较多逻辑在服务器,安全 主要逻辑在客户端,难以避免外挂
战斗校验 较难精确校验 服务器可进行完整战斗模拟
服务器压力 重逻辑,大 转发为主,小
网络卡顿表现 瞬移、闪回、血量不一致、各种异常 战斗卡顿
断线重连 无负担 游戏时长越长恢复压力越大
实现难点 客户端无需要做一些插值,或者行为预测等方式来优化卡顿体验。较多的逻辑要在服务器实现,调测压力较大 需要规避浮点数问题,逻辑要与表现进行分离,对设计有一定要求

设计一个与服务器进行socket通信的包结构

要进行socket通信,包结构基本原则,固定包头长度+包体内容。
包头:

  1. 消息数据id。这个id是用于反序列化时的标识,比如1代表开始战斗StartBattleMsg,那么服务器收到这个包头以后就根据双方协议StartBattleMsg的定义去反序列化后面的数据。
  2. 包体长度。因为每个数据包长度不一致,所以要知道后续包体有多长,才能进行对应的粘包拆包操作。

包体:序列化后的数据。

如何判断客户端与服务器是否保持连接

使用心跳包,每隔一段时间,客户端向服务器发送一条指定的心跳协议。

什么是黏包

收到的数据包不完整,这种现象称之为黏包。
出现黏包的原因:当发送端缓冲区的长度大于网卡的MTU(网络上传送的最大数据包)时,tcp会将这次发送的数据拆成几个数据包发送出去。
tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。

解决办法:

  1. 首先将所有接收到的字节流,塞入缓冲池
  2. 根据包头信息,判断是否能获取当前完整协议包,若能获取完整,则根据协议头提取数据。此时若有缓冲池还有其他协议数据,若能继续提取就继续,不能则跳过,继续等待接收新字节流,塞入缓冲池。
  3. 循环1和2

线程

Unity中协程是如何实现的?

协程不是线程,也不是异步执行的。协程和MonoBehaviour的Update函数一样也是在MainThread中执行的。使用协程你不用考虑同步和锁的问题。

协程其实就是一个IEnumerator(迭代器),IEnumerator 接口有两个方法Current和MoveNext() ,只有当MoveNext()返回 true时才可以访问Current,否则会报错。迭代器方法运行到yield return语句时,会返回一个expression表达式并保留当前在代码中的位置,当下次调用迭代器函数时执行从该位置重新启动。

协程的用法?

在主线程运行的同时开启另一段逻辑处理,来协助当前程序的执行,协程很像多线程,但不是多线程。Unity的协程是在每帧结束之后去检测yield的条件是否满足。

  • 用来延时
  • 用来异步加载等待
  • 加载WWW
  • 基本就是控制代码在特定的时机执行。

什么时候用协程,什么时候用线程?

协程:实现一个任务在不同时间内分段执行,使用它来控制运动,以及对象的行为,或者实现延迟操作

线程:(1) 大量耗时的数据计算
(2) 网络请求
(3) 复杂密集的I/O操作
Unity支持多线程,有main Thread和renderer thread,但是组件和调用mono相关的接口只能运行在主线程上。

Unity3D是否支持写成多线程程序?如果支持的话需要注意什么?

Unity支持多线程,如果同时要处理很多事情或者与Unity的对象互动小可以用thread,否则使用coroutine。

注意:

  1. 虽然支持多线程,但是仅能从主线程中访问Unity3D的组件,对象和Unity3D系统调用,所以如果使用的话需要把组件中的数值传到开启的新线程中。
  2. C#中有lock这个关键字,以确保只有一个线程可以在特定时间内访问特定的对象

渲染

对渲染管线的理解?

渲染管线流程:

  • 应用阶段(由CPU负责,输出是渲染所需要的几何信息,即渲染图元)
  • 几何阶段(由GPU负责,处理渲染图元,这一阶段中最重要的就是把顶点坐标变换到屏幕空间中交给光栅器处理,这阶段输出的是屏幕空间中二维顶点坐标、每个顶点对应的深度值、着色等相关信息)
  • 光栅化阶段(由GPU负责,这一阶段会使用上个阶段传递的数据来产生屏幕上的像素,并渲染出最终的图像)

什么是drawcall?

CPU在每次通知GPU进行渲染之前,都需要提前准备好顶点数据(如位置、法线、颜色、纹理坐标等),然后调用一系列API把它们放到GPU可以访问到的指定位置,最后调用一个绘制命令。而调用绘制命令的时候,就会产生一个drawcall。过多的drawcall会造成CPU的性能瓶颈,这是因为每次调用drawcall时,为了把一个对象渲染到屏幕上,CPU需要检查哪些光源影响了该物体,绑定shader并设置它的参数,再把渲染命令发送给GPU。当场景中包含了大量对象时,这些操作就会非常耗时。降低drawcall应该避免使用过多材质,尽量共享材质,尽量合并网格。

UI上的特效怎么被裁剪?

获取RectMask2D或者Mask的RectTransform,接着去调用GetWorldCorners获得该UI在世界坐标的信息坐标,然后设置参数给Shader,让其根据Rect坐标进行裁剪。而Shader的实现很简单,将超出部分的透明度设置为0。

NGUI渲染过程

Unity在制作一个图元,或者一个按钮,或者一个背景时,都会先构建一个方形网格,网格的绘制单位是图元(点,线,三角面),再将图片放入网格中。可以理解为构建了一个3D模型,用一个网格绑定一个材质球,材质球里存放要显示的图片。
渲染过程:UI元素都继承自UIWidget,UIPanel遍历自己子物体的UIWidget组件,放入到一个List中,按照depth排序。List中相邻元素如果material,texture,shader相同,就传递它们的material,texture,shader,Geometry缓存都传给同一个UIDrawCall,否则就再创建一个新的UIDrawCall。每次有新的UIDrawCall产生,UIPanel就会调用上一个UIDrawCall的UpdateGeometry()函数,来创建渲染所需的对象。这些对象分别是MeshFilter,MeshRender,和最重要的Mesh(Mesh的顶点,UV,Color,法线,切线,还有三角面)。UIDrawcall是渲染UI元素的载体,UIPanel生成UIDrawcall,UIDrawcall是一个组件,挂载在一个GameObject,这个GameObject上再挂载MeshRender、Mesh、MeshFilter、材质等Unity组件,通过这些组件将UI元素渲染出来。我们在Editor中是看不到这个GameObject的,是因为创建的时候设置了HideFlags.HideAndDontSave。

UGUI渲染过程

UGUI的depth是动态算出来的,按照Hierarchy的节点顺序从上向下进行depth分析,最下层的元素depth = 0,元素相交会先判断是否能合批,材质id一样,图片id一样才能合批,比如元素A和元素B相交且B盖住了A,如果A,B可以合批,那么depthB = depthA,否则 depthB = depthA + 1,如果一个元素盖住了多个元素,则选取下面depth最大的元素进行合批判断。从规则中可以看出,depth值与是否相交有关,与是否为子节点无关。相同depth的元素会根据Material ID和Texture ID(字体的Texture ID就是其字体的ID)进行升序排序。
UGUI的渲染过程和NGUI类似,UI组件的基类是Graphic,Graphic保存了当前元素的mesh和material,Graphic实现接口ICanvasElement主要用于重绘,CanvasRenderer用于传递这些数据给Canvas,CanvasRenderer并不是直接渲染,而是交给Canvas,Canvas还要做合批等操作,Canvas会对节点下的Graphic进行合批,所以一个Graphic设置dirty,整个canvas都需要重新计算合批。

Unity中三角面正面,背面是如何渲染的?

三角面正面是顶点顺时针,背面是顶点逆时针,如
a
|
b——c
a,b,c为逆时针,渲染背面。a,c,b为顺时针,渲染正面。

如何让粒子在界面上正确显示?

方法1:修改ParticleSystem的Order in Layer参数,如果特效粒子勾选Render属性,这个特效就会有Order in Layer的概念,就会跟Canvas的order进行混合影响显示层级。
方法2:在Prefab根节点上挂Sorting Group,然后根据情况设置Order in Layer。
方法3:每个特效挂上脚本,脚本中的类继承MaskableGraphic重写OnPopulateMesh函数,该类是模拟Particle,将其转换成UGUI的Graphic,融入到UGUI体系,所以可以将其当做lmage一样控制。

垂直同步对游戏帧率有什么影响

垂直同步是一种调整显示器和GPU之间帧率同步的技术。在启用垂直同步时,GPU将帧率锁定为与显示器刷新率相同的数值。例如,如果你的显示器刷新率是60Hz,那么GPU会将帧率锁定为60fps
启用垂直同步有以下影响:

  1. 减少画面撕裂
  2. 如果游戏的帧率无法达到显示器的刷新率,那么垂直同步将会导致帧率下降。这是因为在垂直同步的情况下,每当显示器完成一次刷新,图形卡必须等待下一次刷新才能开始呈现下一个帧,因此在某些情况下,帧率将被降低到不到60帧以下

什么时候用Reflection Probe(反射探针)

反射探针可在场景中的关键点对视觉环境进行采样。通常将这些探针放置在反射对象外观发生明显变化的每个点上(例如,隧道、建筑物附近区域和地面颜色变化的地方)。当反射对象靠近探针时探针采样的反射可用于对象的反射贴图。此外,当几个探针位于彼此附近时,Unity可在它们之间进行插值,从而实现反射的逐渐变化。因此,使用反射探针可以产生非常逼真的反射,同时将处理开销控制在可接受的水平。
反射探针捕获的是间接光

出光照计算中的diffuse的计算公式

实际光照强度l=环境光(lambient)+漫反射光(Idiffuse)+镜面高光(lspecular)
环境光:lambient=环境光强度(Aintensity)环境光颜色(Acolor)
漫反射光:ldiffuse=镜面光照强度(Dintensity)
镜面光颜色(Scolor)*(光的反射向量(R).观察者向量(V))^镜面光指数(n)

alpha blend工作原理

Alpha Blend是 实现透明效果,Color = 原颜色alpha/255+目标色(255-alpha)/255

光照贴图 的优势是什么?

  1. 使用光照贴图比使用实时光源渲染要快
  2. 可以降低游戏内存消耗
  3. 多个物体可以使用同一张光照贴图

两种阴影判断的方法、工作原理。

自身阴影:因物体自身的遮挡而使光线照射不到它上面的某些可见面
工作原理:利用背面剔除的方法求出,即假设视点在点光源的位置。

投射阴影:因不透明物体遮挡光线使得场景中位于该物体后面的物体或区域收不到光照照射而形成的阴影。
工作原理:从光源处向物体的所有可见面投射光线,将这些面投影到场景中得到投影面,再将这些投影面与场景中的其他平面求交得出阴影多边形,保存这些阴影多边形信息,然后在按视点位置对场景进行相应处理得到所要求的师徒(利用空间换时间,每次只需依据视点位置进行一次阴影计算即可,省去了一次消隐过程)若是动态光源此方法就无效了。

什么是渲染管道?

是指在显示器上为了显示出图像而经过的一系列必要操作。
渲染管道中的很多步骤,都要将几何物体从一个坐标系中变换到另一个坐标系中去。
主要有三步:应用程序阶段,几何阶段 光栅阶段
本地坐标->视图坐标->背面裁剪->光照->裁剪->投影->视图变换->光栅化。

优化

简述一下对象池,你觉得在FPS里哪些东西适合使用对象池?

对象池就存放需要被反复调用资源的一个空间,当一个对象回大量生成的时候如果每次都销毁创建会很费时间,通过对象池把暂时不用的对象放到一个池中(也就是一个集合),当下次要重新生成这个对象的时候先去池中查找一下是否有可用的对象,如果有的话就直接拿出来使用,不需要再创建,如果池中没有可用的对象,才需要重新创建,利用空间换时间来达到游戏的高速运行效果,在FPS游戏中要常被大量复制的对象包括子弹,敌人,粒子等。

遮挡剔除原理?

遮挡剔除会使用一个虚拟的摄像机来遍历场景,从而构建一个潜在可见的对象集合的层级结构。在运行时刻,每个摄像机将会使用这个数据来识别哪些物体是可见的,而哪些被其他物体挡住不可见。使用遮挡剔除技术,不仅可以减少处理的顶点数目,还可以减少overdraw,提高游戏性能。

在开发过程中哪些地方比较容易造成内存泄漏问题?如何避免?

造成内存泄漏的可能原因:
1.你的对象仍被引用但实际上却未被使用。 由于它们被引用,因此GC将不会收集它们,这样它们将永久保存并占用内存。
2.当你以某种方式分配非托管内存(没有垃圾回收)并且不释放它们。
3.过度使用委托会导致内存泄漏,多播委托会引用多个方法,而当这个方法是实例方法(非静态方法)的话,也就是说这个方法隶属于一个对象。一旦我们使用委托引用这个方法的话,那么这个对象就必须存在于内存当中。即便没有其他地方引用这个对象,因为委托的关系,这个对象也不能释放。因为一旦释放,委托就不再能够间接调用到这个方法了,所以没有正确删除委托的方法会导致内存泄漏。
4.静态对象没有及时释放。
如何避免:
1) 在架构上,多添加析构的abstract接口,提醒团队成员,要注意清理自己产生的“垃圾”。
2) 严格控制static的使用,非必要的地方禁止使用static。
3) 强化生命周期的概念,无论是代码对象还是资源,都有它存在的生命周期,在生命周期结束后就要被释放。如果可能,需要在功能设计文档中对生命周期加以描述。

如何优化内存?

  1. 压缩自带类库
  2. 将暂时不用的以后还需要使用的物体隐藏起来而不是直接Destroy掉
  3. 释放AssetBundle占用的资源
  4. 降低模型的片面数,降低模型的骨骼数量,降低贴图的大小
  5. 使用光照贴图
  6. 使用多层次细节(LOD)
  7. 使用着色器(Shader)
  8. 使用预设(Prefab)等

纹理加载进内存以后占用内存如何计算?

纹理内存大小(字节) = 纹理宽度 x 纹理高度 x 像素字节
像素字节 = 像素通道数(R/G/B/A) x 通道大小(1字节/半字节)
举例:比如一个1024 * 1204的RGBA 32bit的纹理占用多大内存?
占用总位数 :allbits = 1024 * 1024 * (4*8bit)
占用总字节数:allbytes = allbits / 8bit

Lod是什么,优缺点是什么?

LOD是Level of detail简称,意为多层次细节,是最常用的游戏优化技术,LOD技术指根据物体模型的几点在显示环境中所处的位置和重要性,决定物体渲染的资源分配,降低非重要物体的面数和细节度,从而获得高效率的渲染运算。
优点:可根据距离动态的选择渲染不同细节的模型
缺点:增加美工工作量,增大了游戏的容量。

MipMap是什么,作用?

MipMapping:在三维计算机图形的贴图渲染中有常用的技术,为加快渲染进度和减少图像锯齿,贴图被处理成由一系列被预先计算和优化过的图片组成的文件,这样的贴图被称为MipMap。

场景模拟

假设一个回合制战斗,战斗过程均由客户端计算,请问使用什么方式使得服务器可以验证此场战斗的数据是合法的?

1.最简单的方式,根据公式计算当前队伍的伤害上限,只要低于此伤害上限就认为战斗数据合法。
2.可以将整个战斗过程上传到服务器进行验算。数值计算涉及到随机结果的情况下,客户端、服务器使用同一随机种子及随机算法,保证数值结果的正确合法性。

现在打出的Android包启动闪退,应该怎么定位问题?

使用ADB真机调试,通过日志定位问题。

延伸
讲讲ADB:
ADB(Android Debug Bridge)是Android SDK中的一个工具, 使用ADB可以直接操作管理Android模拟器或者真实的Andriod设备。
ADB主要功能有:

  1. 在Android设备上运行Shell(命令行);
  2. 管理模拟器或设备的端口映射;
  3. 在计算机和设备之间上传/下载文件;
  4. 将电脑上的本地APK软件安装至Android模拟器或设备上;

现在要开发一个点击屏幕开炮发射子弹的功能,说下你的做法?

首先把子弹进行抽象,把属性和行为方法提炼出来,比如具有速度、威力、碰撞大小等属性,具有飞行、碰撞和伤害等行为。
封装子弹的抽象类,可以不继承MonoBehaviour。
监听屏幕点击事件,触发开炮逻辑。子弹通过对象池管理,复用子弹,防止因为频繁创建销毁带来的性能问题。另外,子弹的坐标更新,可以统一由一个弹道控制器的Update遍历每个子弹对象来计算,而不是每个子弹都挂一个MonoBehaviour去更新,因为MonoBehaviour的Update是通过反射被调用的,如果有1000颗子弹,就会调用1000次反射,这样性能上比较差。

延伸
如果现在要做好几种弹道的子弹,可以继承子弹基类,拓展出多种子弹子类,子类中各自实现自己的UpdatePosition接口,弹道管理器通过Update遍历每个子弹调用基类的UpdatePosition接口。

正式面试题

阿里

  1. 水面波浪起伏的效果是如何实现的?波光粼粼的效果又是如何实现的?
  2. 静态合批与动态合批的原理是什么?有什么限制条件?为什么?对CPU和GPU产生的影响分别是什么?
  3. 一个正方体多少个顶点和三角形,为什么?如何合并顶点?
  4. 什么是DrawCall,如何减少DrawCall?
  5. 资源生命周期如何管理?如何加载与释放资源?资源打包颗粒是怎么设计的?资源压缩格式是什么?
  6. 一个相机中如何做分层渲染?底层原理是什么?
  7. 渲染管线的流程,后处理泛光效果如何实现?
  8. 项目中热更新流程是怎样的?热更包颗粒如何设计?资源如何加密?协议如何加密?
  9. 如何实现物体被墙遮蔽后显示轮廓的效果?shader的pass是什么东西?
  10. TCP连接的流程是怎样的?
  11. 项目中的打包工具做了什么事情?如何做自动化打包?
  12. lua的闭包是什么?闭包产生的内存泄露如何解决?
  13. A*寻路算法的原理是什么?还知道其他寻路算法吗?
  14. 求一个大型排行榜的Top100用什么算法,过程是怎么样的?
  15. lua的table的底层实现原理是什么?为什么这么设计?
  16. 如果让你自己实现C#字典,你如何设计?
  17. 3D转2D碰撞检测的实现过程,使用什么算法进行检测?
  18. 讲讲你对URP的了解。
  19. 讲讲你对ECS的了解。
  20. lua与C#或C语言相互调用的底层实现原理是什么?
  21. 代码安全具体做了什么内容?
  22. 性能优化做了什么内容?
  23. LineRenderer的底层实现原理是什么?
  24. 讲一下二维碰撞检测的算法实现,四叉树。
  25. 热更新的流程,如何做版本管理,如何校验热更包,如何确保下载过程,热更包的颗粒策略。
  26. 讲一下UGUI与NGUI有哪些不同的地方。
  27. 讲一下你搭建的游戏框架的内容。
  28. 对自研引擎的看法。

网易

  1. Udp和Tcp有什么区别
  2. 简单说下Unity AssetBundle 打包解析过程
  3. Android和Pc读取ab包的差异
  4. 简单说一下游戏对象池的作用和思路
  5. 说一下lua的基本类型
  6. lua你用的什么框架
  7. 说说xlua配置和热更新方案
  8. GPU有哪些编程语言
  9. 简单说说Z缓冲
  10. 图像识别了解吗
  11. 碰撞体和触发器的区别
  12. 简单说一下自动寻路思路
  13. 简单说一下a*算法