Unity面试手册:Unity应届生面试题

第一部分 判断题

1 C#支持继承多个类,达到重用代码功能的效果。 (×)

2 修改Renderer的sharedMaterial,所有使用这个材质球的物体都会被改变,并且也改变储存在工程里的材质设置。 (√)

3 Unity中可以创建子线程,并在子线程中直接修改UI对象。 (×)

4 Unity不支持在协程中嵌套调用协程。 (×)

5 C#不同命名空间中可以存在相同类名。 (√)

6 Unity会自动为MonoBehaviour子类的public变量做序列化。 (√)

7 每个枚举成员均具有相关联的常数值,可以设置为负数常数。 (√)

8 只带有 get 访问器的属性称为只读属性,无法对只读属性赋值。 (√)

9 protected成员只能被本类内部访问,无法被子类直接访问。 (×)

10 父物体发生Transform变化的时候,子物体跟随一起变化,但是子物体发生变化的时候,父物体不动。 (√)

第二部分 填空题

1 Unity中 (Game) 视图可以设置分辨率,在该视图中呈现的就是摄像机渲染的画面。

2 gameObject.AddComponent()的时候,Test脚本的 (Awake) 函数会立即被调用。

3 任何游戏对象在创建的时候都会附带 (Transform) 组件,用于储存并操控物体的位置、旋转和缩放。

4 只在编辑器环境下运行的代码,可以使用(UNITY_EDITOR) 宏把代码包起来。

5 Unity中可用四元数Quaternion表示 (旋转) ,不受万向锁影响,可以进行插值运算。

6 Unity协程中可以使用( yield return null )实现暂缓一帧,在下一帧接着往下处理。

7 transform.forward表示物体的(z) 轴的方向。

8 C#中的委托类似于C/C++中的 (函数指针) ,委托类型的声明以 delegate 关键字开头。

9 Unity中的 (Plugins) 目录用于放置Native插件文件,Android平台的jar文件必须放置在( Assets/Plugins/Android/libs) 目录中。

10 在移动平台,Resources目录中的资源通过 (Resources.Load) 接口来加载,如果想实现资源增量更新,则一般考虑把资源打包成 (AssetBundle) 资源类型。

11 定义对象间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新,可以使用 (观察者 设计模式)

12 Unity中每个材质球必须绑定一个 (shader (脚本)),它决定了该材质的渲染方式以及可配置属性。

13 Unity中 (StreamingAssets )文件夹是只读的,里面的所有文件将会被原封不动地复制制到目标平台机器上的特定文件夹里,不会被压缩。在Android或iOS平台,通过 (WWW)类来读取其中的文件。

14 当场景中有多个摄像机时,可以设置摄像机的 (depth) 值,调整相机的渲染顺序。

15 为了加快渲染速度和减少图像锯齿,贴图被处理成由一系列被预先计算和优化过的图片组成的文件,这样的贴图被称为 (MipMap) 。

第三部分 问答题

1、C#中的委托是什么?

delegate int MyDelegate(int value); //声明委托类型

C#所有的委托派生自 System.Delegate 类,委托是存有对某个方法的引用的一种引用类型变量,委托变量可以当作另一个方法的参数来进行传递,实现事件和回调方法。

有点类似C中的函数指针,但是又有所不同。在C中,函数指针不是类型安全的,它指向的是内存中的某一个位置,我们无法判断这个指针实际指向什么,对于参数和返回类型难以知晓。

而C#的委托则完全不同,它是类型安全的,我们可以清晰的知道委托定义的返回类型和参数类型。
委托和事件:

本质区别:从定义上说,委托被编译器编译成一个类,所以它可以像类一样在任何地方定义,而事件被编译成一个委托类型的私有字段和两个公有add 和 remove 方法(有点类似于属性的定义)不过这两个方法都有一个参数,这个参数就是委托,所以,它只能定义在一个类里面。

event MyDelegate myevent; //定义事件

委托相当于一系列函数的抽象类,这一系列函数要求拥有相同的参数和返回值;而事件(event)相当于委托的一个实例,事件是委托类型的成员,委托可以定义在类外面,而事件只能定义在类里面。

事件使用 发布-订阅(publisher-subscriber) 模型。

发布器(publisher) 是一个包含事件和委托定义的对象。事件和委托之间的联系也定义在这个对象中。发布器(publisher)类的对象调用这个事件,并通知其他的对象。

订阅器(subscriber) 是一个接受事件并提供事件处理程序的对象。在发布器(publisher)类中的委托调用订阅器(subscriber)类中的方法(事件处理程序)。

为什么需要事件?

事件最常用的应用场景是图形用户界面(GUI),如一个按钮点击事件,菜单选择事件,文件传输完成事件等。简单的说,某件事发生了,你必须要作出响应。你不能预测事件发生的顺序。只能等事件发生,再作出相应的动作来处理。触发事件的类本身对怎样处理事件不感兴趣。按钮说:“我被点过了”,响应类作出合适的响应。

2、值类型与引用类型的区别?

1.值类型存储在栈(stack)中,引用类型数据存储在堆(heap)中,内存单元中存放的是堆中存放的地址。

2.值类型存取快,引用类型存取慢。

3.值类型表示实际数据,引用类型表示指向存储在内存堆中的数据的指针和引用。

4.栈的内存是自动释放的,堆内存是.NET中会由GC来自动释放。

5.值类型继承自System.ValueType,引用类型继承自System.Object。

数据结构的堆和栈:

堆和栈都是一种数据项按序排列的数据结构。

栈就像装数据的桶,具有后进先出性质;堆像一棵倒过来的树,堆是一种经过排序的树形数据结构,每个结点都有一个值。堆的存取是随意,这就如同我们在图书馆的书架上取书,虽然书的摆放是有顺序的,但是我们想取任意一本时不必像栈一样,先取出前面所有的书。

内存结构:

栈中分配局部变量空间;

堆区是向上增长的用于分配程序员申请的内存空间;

静态区是分配静态变量、全局变量空间的;

只读区是分配常量和程序代码空间的;

3、接口Interface与抽象类abstract class的区别?

接口和抽象类是支持抽象定义的两种机制。

接口是完全抽象的,只能声明方法,而且只能声明public的方法,不能声明private及protected的方法,不能定义方法体,也不能声明实例变量。抽象类是可以有私有的方法或者私有的变量,如果一个类中有抽象方法,那么就是抽象类。

一个类可以实现多个接口,但一个类只能继承一个抽象类。

接口强调特定功能的实现,具有哪些功能,而抽象类强调所属关系。

尽管接口实现类及抽象类的子类都必须要实现相应的抽象方法,但实现的形式不同。接口中的每一个方法都是抽象方法,都只是声明的, 没有方法体,实现类必须都要实现;而抽象类的子类可以有选择地实现,只实现其中的抽象方法,覆盖其中已实现了的方法。

4.如何弱化代码依赖关系?

在代码的控制流中,调用关系和依赖关系几乎是完全吻合的,如果缺乏良好的封装与接口提取,那么调用者必须掌握被调用者的代码实现。

而抽象良好的接口,能够使控制流对代码的依赖实现反转,比如面向同一个接口协议,被调用者需要在协议的约束下对提供的服务进行实现,它的代码依赖协议的制定,而调用者只用依据协议按需获取服务即可,在控制流上依赖接口,而不再需要在代码上依赖被调用者,此即是从接口到被调用者的控制流-代码依赖关系反转。

代码依赖关系弱化,意味着业务可以模块化、组件化,拆分的功能组团可以以“插件”的方式并行独立开发维护,这种隔离大大提升开发运维效率,同时独立部署的能力也更加符合软硬件发展的趋势。

5、Unity实现跨平台的原理?

Unity的跨平台技术是通过一个Mono虚拟机实现的。就是通过Mono将C#脚本代码编译成CIL,然后Mono运行时利用JIT或者AOT将CLI编译成目标平台的原生代码实现的。

不过这个虚拟机更新太慢,不能很好地适应众多的平台,所以后来推出了IL2CPP,把本来应该再mono的虚拟机上跑的中间代码转换成cpp代码,这样再把生成的cpp代码,利用c的跨平台特性,在各个平台上通过对各平台都有良好优化的native c编译器编译,以获得更高的效率和更好的兼容性。

IL是.NET框架中间语言(Intermediate Language)的缩写。使用.NET框架提供的编译器可以直接将源程序编译为.exe或.dll文件,但此时编译出来的程序代码并不是CPU能直接执行的机器代码,而是一种中间语言IL(Intermediate Language)。

使用中间语言的优点有两点,一是可以实现平台无关性,既与特定CPU无关;二是只要把.NET框架某种语言编译成IL代码,就实现.NET框架中语言之间的交互操作(这就是为什么unity3D里面可以c#和js混编)。

在Mac OS上,因为iOS的现有限制,面向iOS的C#代码会通过AOT编译技术直接编译为ARM汇编代码。而在Android上,应用程序会转换为IL,启动时再进行JIT编译。

讲讲JIT:

JIT:即时编译(Just In-Time compile),这是.NET运行可执行程序的基本方式,编译一个.NET程序时,编译器将源代码翻译成中间语言,它是一组可以有效地转换为本机代码且独立于CPU的指令。

当执行这些指令时,实时(JIT)编译器将它们转化为CPU特定的代码。部分加密软件通过挂钩JIT来进行IL加密,同时又保证程序正常运行。JIT也会将编译过的代码进行缓存,而不是每一次都进行编译。所以说它是静态编译和解释器的结合体。

AOT:静态编译,它在程序运行之前就编译好了。

6、四元数的作用?

四元数用于表示旋转。

其相对于欧拉角的优点:

1.避免万向锁。

2.只需要一个4维的四元数就可以执行绕任意过原点的向量的旋转,方便快捷,在某些实现下比旋转矩阵效率更高。

3.可以提供平滑插值。

7.什么是欧拉角?

用一句话说,欧拉角就是物体绕坐标系三个坐标轴(x,y,z轴)的旋转角度。

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

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

物体的任何一种旋转都可分解为分别绕三个轴的旋转,但分解方式不唯一。

unity 3D欧拉角的旋转顺序(父子关系)是y-x-z。

unity中最简单的万向锁就是先让X轴旋转90度,z轴旋转和y轴旋转效果是一样。

讲讲万向锁:

万向锁(英语:Gimbal lock)是在使用动态欧拉角表示三维物体的旋转时出现的问题。

万向节死锁的根本问题是欧拉角(EulerAngles)保存的信息不足以描述空间中的唯一转向。

8、Unity脚本生命周期与执行顺序?

unity脚本执行顺序

9、讲讲你对Unity的协程的理解?

协程不是线程。协程的实现原理是迭代器,而迭代器的实现原理是状态机。

unity中协程执行过程中,通过 yield return XXX,将程序挂起,去执行接下来的内容。在遇到 yield return XXX语句之前,协程方法和一般的方法是相同的,也就是程序在执行到 yield return XXX语句之后,接着才会执行的是 StartCoroutine()方法之后的程序,走的还是单线程模式,仅仅是将 yield return XXX语句之后的内容暂时挂起,等到特定的时间才执行。

那么挂起的程序什么时候才执行?协同程序主要是Update()方法之后,LateUpdate()方法之前调用的。

通过设置MonoBehaviour脚本的enabled对协程是没有影响的,但如果gameObject.SetActive(false)则已经启动的协程则完全停止了,即使在Inspector把gameObject激活还是没有继续执行。也就说协程虽然是在MonoBehvaviour启动的(StartCoroutine),但是协程函数的地位完全是跟MonoBehaviour是一个层次的,不受MonoBehaviour的状态影响,但跟MonoBehaviour脚本一样受gameObject控制,也应该是和MonoBehaviour脚本一样每帧轮询yield 的条件是否满足。

第四部分 场景题

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

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

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

首先把子弹进行抽象,把属性和行为方法提炼出来,比如具有速度、威力、碰撞大小等属性,具有飞行、碰撞和伤害等行为。
封装子弹的抽象类,可以不继承MonoBehaviour。

监听屏幕点击事件,触发开炮逻辑。子弹通过对象池管理,复用子弹,防止因为频繁创建销毁带来的性能问题。

另外,子弹的坐标更新,可以统一由一个弹道控制器的Update遍历每个子弹对象来计算,而不是每个子弹都挂一个MonoBehaviour去更新,因为MonoBehaviour的Update是通过反射被调用的,如果有1000颗子弹,就会调用1000次反射,这样性能上比较差。

更多学习请参考

张林

唯美图片

posted @ 2021-07-22 21:49  镜子-眼泪  阅读(910)  评论(0)    收藏  举报