WHCSRL 技术网

Unity3D初级工程师面试题及知识点

Unity3D初级工程面试题目整合

C#基础部分

一、修饰符

1.访问修饰符

  • public:同一程序集中的任何代码或引用该程序集的其他程序集都可以访问该类型或成员。
  • private:只有同一类或结构中的代码可以访问该类型或成员。
  • protected:只有同一类或结构或此类的派生类中的代码可以访问该类型或成员。
  • internal:同一程序集中的任何代码都可以访问该类型或成员,但访问权限限制在程序集之内。
  • protect internal:在同一程序集体现internal的性质,在其他程序集体现protected的性质。

2.类型修饰符

  • abstract:使用abstract修饰的类为抽象类,抽象类只能是其他类的基类,不能与sealed、static一起使用。abstract可以修饰抽象类中的方法或属性,此时,方法或属性不能包含实现,且访问级别不能为私有。抽象类不能被实例化。
  • sealed:使用sealed修饰的类为密封类,密封类无法被继承,不能和abstract、static一起使用。当sealed用于方法或属性时,必须始终与override一起使用。
  • static:使用static修饰的类为静态类,静态类所有成员都必须是静态的,不能与abstract、sealed一起使用。static可以修饰方法、字段、属性或事件。静态类不能被实例化。
  • const:使用const关键字来声明某个常量字段或常量局部变量,必须在声明常量时赋初值。不能与static一起使用,常量默认是static的,常量字段只有一个副本。
  • readonly:使用readonly关键字来声明只读字段。
  • virtual:virtual关键字用于修饰方法、属性、索引器或事件声明,并使它们可以在派生类中被重写。默认情况下,方法是非虚拟的。 不能重写非虚方法。virtual修饰符不能与static、abstract、private或override修饰符一起使用。
  • override:要扩展或修改继承的方法、属性、索引器或事件的抽象实现或虚实现,必须使用override修饰符。重写的成员必须是virtual、abstract或override的。

二、委托、事件和回调函数

1.委托的作用:委托就像一个函数的指针,在程序运行时可以使用它们来调用不同的函数。通俗点就是把你所不能做的事情交给其他人去做。
2.事件:在某件事情发生的时候,一个对象可以通过事件通知另外一个事件。就事件而言,关键点就是什么时候,让谁去做。
3.回调函数:回调函数就是把一个方法传给另外一个方法去执行。回调函数的好处和作用,那就是解耦合。

三、接口跟抽象类的区别是什么?什么时候使用接口?什么时候使用抽象类?

抽象类是对类的抽象(是什么),接口是对行为的抽象(做什么)。抽象类与接口最大的区别在于抽象类的属性是可以继承的。而接口只有常量。因为抽象的概念是将不可变的东西提取出来封装到一起,将可变的东西放到实现中去,而接口的设计理念是高层的抽象,那么就应该定义为不可变的东西,如果接口中定义了变量,就说明带了可变的成分,就不是高层抽象了,体现的是OCP开闭原则(对修改关闭,对扩展开放)的设计原则。那么我们什么时候要调用到抽象类呢?当子类必须含有父类的某个特征属性。

四、值类型跟引用类型的区别?

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

五、游戏优化,如何优化减少GC?

产生的原因:GC会定期执行垃圾回收,回收没有有效引用的对象的内存。
如何去减少:

  • 减少new产生对象的次数
  • 使用公用的对象(静态成员)
  • 使用对象池GamObject Pool,减少对象频繁创建所占用的内存空间和初始化时间。
  • (unity优化)避免使用foreach,尽量使用for循环 注释:使用foreach会有装箱操作,自然会在堆上分配对象
  • 将String换为StringBuilder 注释:String每次使用都会在内存中产生一个String对象,而StringBuilder修改字符串是直接修改堆内存里的字符串,不修改引用,除非字符串的长度超出了容量。

六、三元运算符

(关系表达式) ? 表达式1 : 表达式2;
int x = 10;
int y = 5;
int z;
如果x大于y 则是true,将x赋值给z;
如果x不大于y 则是false,将y赋值给z;
z = (x > y) ? x : y;
System.out.println("x = " + x);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

七、面向对象编程的六大原则?

1.单一原则:一个类只负责一个职责。
2.开闭原则:一个类一旦开发完成,后续增加新功能就不应该通过修改这个类来完成,而是通过继承,增加新的类。
3.里氏替换原则:所有用到一个类的地方都替换其子类,系统仍可以正常工作。
4.依赖倒置原则:将高层所依赖的底层中间加一个接口为双方依赖项,面向接口多态编程。
5.接口隔离原则:使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口。
6.迪米特原则:一个软件应该尽可能的少地与其他实体发生相互作用。

八、struct 里面如果包含一个类的对象,该对象是存在堆区还是栈区?

堆区

九、委托和接口的区别与应用?

接口是约束类应遵守实现要求的一个集合声明,不能实例化。委托是一个特殊类型,一般用于传递对方方法的引用。

十、对于多态的理解?

通过继承实现的不同对象调用相同的方法,表现出不同的行为,称之为多态。

十一、list和array的区别?

list和array都可以根据索引来取其中的元素。其中list是列表,array是数组,list是可以包含不同类型的数据,array是必须相同类型的数据。

十二、List和ArrayList的区别?

ArrayList存在不安全类型(ArrayList会把所有插入其中的数据都当做Object来处理),装箱拆箱的操作(费时)。
List是接口,ArrayList是一个实现了该接口的类,可以被实例化。

十三、如何在C#中使用LitJson对Json数据解析?

LitJson是一个开源项目,比较小巧轻便,安装也很简单,在Unity里只需要把LitJson.dll放到Plugins文件夹下,并在代码的最开头添加 “Using LitJson”就可以了。
使用步骤:

  1. 读取文本数据并实例化(此处文本为我的路径下的文本)
 string s = File.ReadAllText(Application.dataPath + ("/resource/assets/config/config.merge.json"));
  • 1
  1. 通过JsonMapper转换为JsonData
  JsonData jd = JsonMapper.ToObject(s);
  • 1
  1. 将Json数据赋值(此处我赋值为我所创建的地图信息对象)
   levelList = JsonMapper.ToObject<LevelInfo>(jd["map"][level-1].ToJson());
  • 1

十四、面向对象和面向过程

面对对象:封装、继承、多态,面向对象的三大特性。把构成问题的事物分解成各个对象,而建立对象的目的也不是为了完成一个个步骤,而是为了描述某个事物在解决整个问题的过程中所发生的行为。
面向过程:是分析解决问题的步骤,然后用函数把这些步骤一步一步地实现,然后在使用的时候一一调用即可。强调的是完成这件事的动作,更接近我们日常处理事物的思维。
通俗点来说,我们可以把面对过程看成份炒饭,炒饭放什么料,之前都要想好,而面对对象看成一份盖浇饭。炒饭都是炒完菜放饭开始炒,炒好了就行。而盖浇饭是炒菜,然后把菜放到之前做好的饭上面,盖上即可。我们要吃什么盖浇饭,只要换份菜就行。而吃不同的炒饭,必须倒掉重新炒。面向对象的好处之一就是显著的改善了软件系统的可维护性。

十五、几种常用的设计模式

1.单例模式:一个类只能有一个实例,单例非常重要,比如我需要管理游戏的UI,现在我要关闭某个界面,页面上可能打开很多子窗口也需要一起关闭,这个时候使用单例可以很好管理,单例可以实现跨类调用。
2.命令模式:命令模式就是将对象的状态和行为进行封装后按照一定的规则进行处理的模式。可以把行为的请求者和行为的实现者分开,比如现在需要在游戏中开启录制,则需要记录输入的指令,如果 行为的请求者和实现者紧耦合(按下按键就执行)是不利于实现录制的。这个模式的优点第一个是减低耦合度。
3.观察者模式:对应的就是事件机制,通过添加监听者来获取状态变化。简单理解就是,使用观察者模式可以避免类之间的直接调用,减少耦合,就是说类A需要监听类B,但是即使类B不在,也是可以运行的。
4.MVC模式:

  • 控制器:处理数据,计算数据
  • 模型:存储数据,可以说是一个数据模型
  • 视图:呈现Model的数据

5.工厂模式:当一种类型需要经常被大量创建和销毁的时候,为了提高效率可以不销毁,而是先保存起来,等需要的时候再拿去用。即通过使用一个共同的接口来指向新创建的对象。也可以理解为由继承基类的子类去具体实现。

Unity基础部分

一、monobehaviour的update、fixedupdate、lateupdate的区别?

1.fixedupdate用于做物理检测,一般是固定间隔帧执行。
2.update是每一帧执行。
3.lateupdate是晚于update执行,一般可用于摄像机移动或者一些强置后逻辑。

二、什么是协程?什么是线程?两者的区别?

1.协程:Unity协程长的有点像线程,但却不是线程.因为协程仍然是在主线程中执行,且在使用时不用考虑同步与锁的问题。协程只是控制代码等到特定的时机后再执行后续步骤。
2.线程:线程是异步运行的,线程可以多线程,性能相比较协程更优,但是消耗大。
3.区别:同一时间只能执行一个协程,线程却可以开辟多线程。

三、Resources和 AssetBundle 的区别?

Resource:是动态内部调用,Resources在编辑环境下是project窗口的一个文件夹,调用里面的资源,可以用Resources类,比如Resources.Load,打包后这个文件夹是不存在的,会统一生成assets资源。
AssetBundle:是外部调用,要用AssetBundle 首先要先把资源打包为.assetbundle文件,再动态的去加载这个文件,本地或者网络服务器都可以。
需要注意:Resource文件下,如果资源过多,会影响游戏的启动时间;其次,Resource文件夹下的资源本身不会在启动后加载到内存中,资源是Resources.Load后被加载进内存的。但是需要指出的是,引擎启动时会创建一个Resource索引表,Resource文件夹下文件越多,其索引表也就越大,这个是会占用内存的;资源打包成Assetbundle加载也快,也方便更新,Resources下可以放一些无需更新且启动时就需要的资源,譬如登陆场景的loading图等,游戏启动的逻辑简洁不易出错。

四、简述四元数的作用,四元数对欧拉角的优点?

1.作用:四元数用于表示旋转
2.相对于欧拉角的优点:

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

五、如何实现游戏存档和读档的功能?

1.Unity提供了一个用于本地持久化保存与读取的类——PlayerPrefs。工作原理:以键值对的形式将数据保存在文件中,然后程序可以根据这个名称取出上次保存的数值。支持3种数据类型的保存和读取。
SetInt();保存整型数据;
GetInt();读取整形数据;
SetFloat();保存浮点型数据;
GetFlost();读取浮点型数据;
SetString();保存字符串型数据;
GetString();读取字符串型数据;
2.XML,sql,txt等配置文件都可以实现存读等功能。

六、Prefab的作用?

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

七、如何安全的在不同工程间安全地迁移asset数据?三种方法

1.将Assets目录和Library目录一起迁移。
2.导出包。
3.用unity自带的assets Server功能。

八、unity生命周期

1.Reset() 组件重设为默认值时(只用于编辑状态)
2.Awake() 脚本组件载入时 (调用一次)
3.OnEnable() 是在游戏对象可以调用时调用
4.Start()第一个Update发生之前 (调用一次)
5.FixedUpdate() 固定时间调用,常用于物理相关的计算,比如对Rigidbody的操作
6.Update()大部分游戏行为代码被执行的地方,除了物理代码
7.LateUpdate() 每帧Update调用之后
8.OnGUI() 绘制GUI时调用
9.OnDisable() 当对象设置为不可用时
10.OnDestroy()组件销毁时调用

九、unity如何制作小地图(MiniMap)?

原理:添加多一个相机,设置垂直投影,然后将投影的画面送到显示在UI的Textrue上面。
步骤:

  • 为自己和NPC添加小图标(添加Quad,材质为带小图标的MinimapIconMaterial,这样可以顺便在生成图标的时候改变颜色),并标记为Layer为Minimap,只在小地图中显示。
  • 为自己添加跟随的相机(MinimapCamera),相机的内容就是小地图的内容。culling mask选择MiniMap,projection选择垂直投影Orthographic,Target Texture为相机投影内容的一张Texture。
  • 在游戏里添加小地图,MinimapMask,就是一个白色圆,添加遮罩。在他的子对象中,只能显示在这个圆里。MinimapContent,地图内容,就是一张Textrue(由先前操作投影而来)。

十、CharacterController和Rigidbody的区别?

Rigidbody具有完全真实物理的特性,而CharacterController可以说是受限的Rigidbody,具有一定的物理效果但不是完全真实的。

十一、什么是游戏框架?使用框架的好处是什么?

1.游戏框架是比较上层的封装,是一套快速搭建游戏的代码结构。
2.使用游戏框架开发能节约工程时间,易维护,易扩展,易阅读开发,可复用,我的理解框架就是让代码的编写难度和耦合难度降低,从而更简便地去

数据结构与算法基础部分

以下大部分图片转自他人

一、Unity3d常用的排序算法时间复杂度与空间复杂度

常用的排序算法的时间复杂度与空间复杂度
常用的排序算法的时间复杂度与空间复杂度

  • 时间频度: 一个算法执行所耗费的时间。记为T(n)。
  • 时间复杂度:描述时间频度T(n)变化的规律。其中n称为问题的规模。常见的时间复杂度有:常数阶O(1),对数阶O(log2n),线性阶O(n), 线性对数阶O(nlog2n),平方阶O(n2),立方阶O(n3),…, k次方阶O(nk),指数阶O(2n)。
  • 渐进时间复杂度评价算法时间性能:渐进时间复杂度是指对于一个算法来说,我们常常需要计算其复杂度来决定我们是否选择使用该算法。算法的渐进分析就是要估计:n逐步增大时资源开销T(n)的增长趋势。
  • 空间复杂度:一个算法的空间复杂度(Space Complexity)S(n)定义为该算法所耗费的存储空间,它也是问题规模n的函数。

二、数组

线性表: 线性表就是数据排成像一条线一样的结构.每个线性表上的数据最多只有前和后两个方向.常见的线性表结构:数组,链表、队列、栈等。
线性表
什么是数组:

  • 数组(Array)是一种线性表数据结构。它用一组连续的内存空间,来存储一组具有相同类型的数据。
  • 连续的内存空间和相同类型的数据(随机访问的前提)
  • 优点:两限制使得具有随机访问的特性
  • 缺点:删除,插入数据效率低
  • 为何删除,插入效率低:因为每当插入或者删除,该数后面的数字都要跟着移动,而链表只需更改指针的指向。

三、链表

链表
什么是链表:

  • 和数组一样,链表也是一种线性表。
  • 从内存结构来看,链表的内存结构是不连续的内存空间,是将一组零散的内存块串联起来,从而进行数据存储的数据结构。
  • 链表中每个内存块称为一个节点Node,节点Node除了存储数据,还负责记录链表上下一个节点的地址,即后继指针Next。
  • 链表的特点:插入、删除数据效率高O(1)级别(只需更改指针指向即可),随机访问效率低O(n)级别(需要从链头至链尾进行遍历)。
  • 和数组相比,内存空间消耗更大,因为每个存储数据的节点都需要额外的空间存储后继指针。

常用链表

1.单链表:

  • 每个节点只包含一个指针,即后继指针。
  • 单链表有两个特殊的节点,即首节点和尾节点。为什么特殊?用首节点地址表示整条链表,尾节点的后继指针指向空地址null。
  • 性能特点:插入和删除节点的时间复杂度为O(1),查找的时间复杂度为O(n)。

单链表
2.循环链表:

  • 除了尾节点的后继指针指向首节点的地址外均与单链表一致。
  • 适用于存储有循环特点的数据。
    循环链表

3.双向链表

  • 节点除了存储数据外,还有两个指针分别指向前一个节点地址(前驱指针prev)和下一个节点地址(后继指针next)。
  • 首节点的前驱指针和尾节点的后继指针均指向空。
  • 性能特点:与单链表相比,消耗更多的存储空间。删除、插入操作比单链表效率更高O(1)级别。
    双向链表

4.双向循环链表:

  • 首节点的前驱指针指向尾节点,尾节点的后继指针指向首节点。
    双向循环链表

选择数组还是链表?
1.插入、删除和随机访问的时间复杂度

  • 数组:插入、删除的时间复杂度是O(n),随机访问的时间复杂度是O(1)。
  • 链表:插入、删除的时间复杂度是O(1),随机访问的时间复杂端是O(n)。

2.数组缺点

  • 假设申请100M内存空间,若内存空间没有100M的连续空间时,则申请失败,即便内存空间超过100M。
  • 大小固定,若内存空间不足,则需要扩容,一扩容就需要进行数据复制,相当的费时。

3.链表缺点

  • 内存空间消耗更大,因为需要额外的空间去存储指针的信息。
  • 对链表频繁的插入删除,会导致内存频繁的申请和释放,容易造成内存碎片。

4.如何选择二者?
数组在实现上使用的是连续的内存空间,可借助CPU缓存机制预读数组中的数据,所以访问效率高,而链表在内存中并不是连续存储,所以CPU缓存机制无法预读。

四、队列

什么是队列:队列是一种受限的线性表数据结构,栈只支持两个操作:入栈push()和出栈pop(),队列跟栈非常的相似,支持的操作也很有限,最基本的操作也是两个:入队enqueue(),放一个数据到队列尾部,出队dequeue(),从队列头部取一个元素。
队列
队列的特点:

  • 队列跟栈一样,也是一种抽象的数据结构。
  • 具有先进先出的特性,支持在队尾插入元素,在队头删除元素。

队列的实现:
1.队列可以用数组来实现,称为顺序队列。实现队列需要两个指针:一个是head指针,指向队头;一个是tail指针,指向队尾。你可以结合下面这幅图来理解。当a,b,c,d依次入队之后,队列中的head指针指向下标为0的位置, tail指针指向下标为4的位置。
数组实现队列
出队的时候不用搬移数据,在入队的时候,如果没有空闲空间了,我们只需要在入队时,再集中触发一次数据的搬移操作。
数据搬移
2.队列可以用链表来实现,称为链式队列。需要两个指针: head指针和tail指针,它们分别指向链表的第一个结,点和最后一个结点。
链表实现队列
循环队列:原本数组是有头有尾的,是一条直线。现在我们把首尾相,连,板成了一个环。能够避免数据搬移。
循环队列
阻塞队列:简单来说,就是在队列为空的时候,从队头取数据会被阻塞。因为此时还没有数据可取,直到队列中有了数据才能返回;如果队列已经满了,那么插入数据的操作就会被阻塞,直到队列中有空闲位置后再插入数据,然后再返回。
并发队列:就是队列的操作多线程。·

五、二叉树

二叉树(binary tree)是树的一种特殊形式。二叉,指的是这种树的每个节点最多有2个孩子节点。这里最多有2个,也可能只有1个,或者没有孩子节点。
树有以下概念:

  • 根节点、父节点、兄弟节点、孩子节点。
  • 树的最大层级数,被称为树的高度或深度。

满二叉树:每一个分支都是满的。
满二叉树
完全二叉树:如果这个树所有节点和同样深度的满二叉树的编号为从1到n的节点位置相同。则这个二叉树为完全二叉树。
完全二叉树
二叉树可以用链式存储数组两种物理存储结构来表达。
二叉树的应用:最主要的应用在于进行查找操作和维持相对顺序两个方面。
1.查找:二叉查找树
二叉查找树在二叉树的基础上增加了以下几个条件:

  • 若左子树不为空,则左子树所有节点的值均小于根节点的值;
  • 若右子树不为空,则右子树所有节点的值均大于根节点的值;
  • 左、右子树也都是二叉查找树。

二叉堆:只要求父节点比它的左右孩子都大。
二叉树遍历
可分为4种:

  • 前序遍历:根左右
  • 中序遍历:左根右
  • 后序遍历:左右根
  • 层序遍历:一层一层遍历

实现方法:递归与非递归
拓展:可以用递归解决的方法,同样可以用栈解决,递归和栈都有回溯的特性。

六、图

图(Graph):由顶点集V(Vertex)和边集E(Edge)组成。
有向图与无向图:

  • 如果给图的每条边规定一个方向,那么得到的图称为有向图。 在有向图中,从一个顶点出发的边数称为该点的出度,而指向一个顶点的边数称为该点的入度。
  • 边没有方向的图称为无向图

有权图与无权图:

  • 如果图中的边有各自的权重,得到的图是有权图
  • 如果图的边没有权重,或者权重都一样(即没有区分),称为无权图

连通图:如果图中任意两点都是连通的,那么图被称作连通图。
深度优先遍历:选择一条支路,尽可能不断地深入,如果遇到死路就往回退,回退过程中如果遇到没探索过的支路,就进入该支路继续深入。
广度优先遍历:类似二叉树的层级遍历,把相邻最近的先遍历,从近到远。

七、哈希表

哈希表可以弥补数组的一些缺点,所以就可以在数组的基础上做一些改动,来实现哈希表。通过秦九韶算法得到一个特别大的数,然后取余,余数作为该元素在数组中的下标值。
优点:

  • 无论数据有多少,处理起来都特别的快
  • 能够快速地进行 插入修改元素 、删除元素 、查找元素 等操作
  • 代码简单

缺点:

  • 哈希表中的数据是没有顺序的
  • 数据不允许重复

冲突时如何处理:
1.拉链法:在下标相同的地方放一个数组,则可同时存放了。
2.开放地址法:元素下标值发生冲突时,寻找空白的位置插入数据。

八、常用算法

几种最经典、最常用的排序方法:冒泡排序、插入排序、选择排序、快速排序、归并排序、计数排序、基数排序、桶排序。

推荐阅读