查看: 1166|回复: 0

2021游戏大厂Unity应届生面试题

[复制链接]
发表于 2022-11-12 17:04:43 | 显示全部楼层 |阅读模式
一、判断题

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、Unity实现跨平台的原理

Unity的跨平台技术是通过一个Mono虚拟机实现的。就是通过Mono将C#脚本代码编译成CIL,然后Mono运行时利用JIT或者AOT将CLI编译成目标平台的原生代码实现的。
不过这个虚拟机更新太慢,不能很好地适应众多的平台,所以后来推出了IL2CPP,把本来应该再mono的虚拟机上跑的中间代码转换成cpp代码,这样再把生成的cpp代码,利用c++的跨平台特性,在各个平台上通过对各平台都有良好优化的native c++编译器编译,以获得更高的效率和更好的兼容性。

延伸
讲讲IL:
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:静态编译,它在程序运行之前就编译好了。

5、四元数的作用

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

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

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

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

名称触发时机用途
Awake脚本实例被创建时调用用于游戏对象的初始化,注意Awake的执行早于所有脚本的Start函数
OnEnable当对象变为可用或激活状态时被调用
StartUpdate函数第一次运行之前调用用于游戏对象的初始化
Update每帧调用一次用于更新游戏场景和状态
FixedUpdate每个固定物理时间间隔调用一次用于物理状态的更新
LateUpdate每帧调用一次(在update之后调用)用于更新游戏场景和状态,和相机有关的更新一般放在这里
OnGUI渲染和处理OnGUI事件
OnDisable当前对象不可用或非激活状态时被调用
OnDestroy当前对象被销毁时调用


延伸
Awake与Start:
Awake和Start只会调用一次,当游戏过程中调整脚本的可见状态时,会分别调用OnEnable, OnDisable函数,而Awake和Start将不会再调用。
Start可能不会被立刻调用,比如我们之前没有让其enable,当脚本被enable时,Start才会被调用。
不同对象之间的Awake顺序是不得而知的。
如下,MyBhv脚本在Awake中初始化speed=1f;执行完下面的代码,speed的值是多少?

var bhv = go.AddComponent<MyBhv>()
bhv.speed = 3f;

答: 3f,因为Awake会先执行。

Update与FixedUpdate:
同:当MonoBehaviour启用时,其在每一帧被调用。都是用来更新的。
异:Update()每一帧的时间不固定,受场景渲染的复杂程度,还有输入的一系列事件等等各种原因影响,游戏画面的帧率是在不断变化的。
FixedUpdate()每帧与每帧之间相差的时间是相对固定的(值为Time.fixedDeltaTime),默认是0.02s,可以通过Edit->ProjectSettings->Time来设置。物理相关的处理(比如Rigidbody)一般在FixedUpdate()中。

Update与LateUpdate:
LateUpdate是在所有Update函数调用后被调用。可用于调整脚本执行顺序。例如当物体在Update里移动时,跟随物体的相机可以在LateUpdate里实现。
有2个不同的脚本同时在Update中控制一个物体,那么当其中一个脚本改变物体方位、旋转或者其他参数时,另一个脚本也在改变这些东西,那么这个物体的方位、旋转就会出现一定的反复。如果还有个物体在Update中跟随这个物体移动、旋转的话,那跟随的物体就会出现抖动。如果是在LateUpdate中跟随的话就会只跟随所有Update执行完后的最后位置、旋转,这样就防止了抖动。

7、讲讲你对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 的条件是否满足。

协程不是只能做一些简单的延迟,如果只是单纯的暂停几秒然后在执行就完全没有必要开启一个协程。
协程的真正作用是分步做一些比较耗时的事情,比如加载游戏里的资源。

延伸
讲讲进程、线程、协程:
进程拥有自己独立的堆和栈,既不共享堆,亦不共享栈,进程由操作系统调度。
线程拥有自己独立的栈和共享的堆,共享堆,不共享栈,线程亦由操作系统调度(标准线程是的)。
协程和线程一样共享堆,不共享栈,协程由程序员在协程的代码里显示调度。

协程和线程的区别是:协程避免了无意义的调度,由此可以提高性能,但也因此,程序员必须自己承担调度的责任,同时,协程也失去了标准线程使用多CPU的能力。
打个比方吧,假设有一个操作系统,是单核的,系统上没有其他的程序需要运行,有两个线程 A 和 B ,A 和 B 在单独运行时都需要 10 秒来完成自己的任务,而且任务都是运算操作,A B 之间也没有竞争和共享数据的问题。现在 A B 两个线程并行,操作系统会不停的在 A B 两个线程之间切换,达到一种伪并行的效果,假设切换的频率是每秒一次,切换的成本是 0.1 秒(主要是栈切换),总共需要 20 + 19 * 0.1 = 21.9 秒。如果使用协程的方式,可以先运行协程 A ,A 结束的时候让位给协程 B ,只发生一次切换,总时间是 20 + 1 * 0.1 = 20.1 秒。如果系统是双核的,而且线程是标准线程,那么 A B 两个线程就可以真并行,总时间只需要 10 秒,而协程的方案仍然需要 20.1 秒。

讲讲状态机:
状态机是有限状态自动机的简称,是现实事物运行规则抽象而成的一个数学模型。一个状态机至少要包含两个状态。例如一个自动门,就有 open 和 closed 两种状态,我们通常所说的状态机是有限状态机,也就是被描述的事物的状态的数量是有限个,例如自动门的状态就是两个 open 和 closed 。

2021游戏大厂Unity应届生面试题-6212

closed 状态下,如果读取开门信号,那么状态就会切换为 open 。open 状态下如果读取关门信号,状态就会切换为 closed 。
状态机的四大概念:
1 State ,状态。一个状态机至少要包含两个状态。例如上面自动门的例子,有 open 和 closed 两个状态。
2 Event ,事件。事件就是执行某个操作的触发条件或者口令。对于自动门,“按下开门按钮”就是一个事件。
3 Action ,动作。事件发生以后要执行动作。例如事件是“按开门按钮”,动作是“开门”。编程的时候,一个 Action 一般就对应一个函数。
4 Transition ,变换。也就是从一个状态变化为另一个状态。例如“开门过程”就是一个变换。
总结一下,状态机不是实际机器设备,而是一个数学模型,通常体现为一个状态转换图。涉及到的相关概念是 State 状态,Event 事件,Action 动作,Transition 转换。状态机是计算机科学的重要基础概念之一,也可以说是一种总结归纳问题的思想,应用范围非常广泛。

四、场景题

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

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

延伸
讲讲ADB:
ADB(Android Debug Bridge)是Android SDK中的一个工具, 使用ADB可以直接操作管理Android模拟器或者真实的Andriod设备。
ADB主要功能有:
1、在Android设备上运行Shell(命令行);
2、管理模拟器或设备的端口映射;
3、在计算机和设备之间上传/下载文件;
4、将电脑上的本地APK软件安装至Android模拟器或设备上;

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

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

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

版权声明:本文为CSDN博主「林新发」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:

https://blog.csdn.net/linxinfa/article/details/106822744

您需要登录后才可以回帖 登录 | 加入联盟

本版积分规则

快速回复 返回顶部 返回列表