Unity
游戏开发工程师,皮皮是我养的猫,会讲人话,它接到了喵星的特殊任务:学习编程,学习
Unity
游戏开发。
皮皮的简历很快得到了回复。
【XXX企业】
尊敬的皮皮猫先生。
欢迎您应聘我公司Unity3D游戏开发职位,经过初步审核觉得您比较适合我们公司,邀请您下周一上午10:00参加面试。
收到请回复,谢谢!
我:“面试不可怕,提前做好准备,放平心态,一定可以的。”
皮皮:“我没有害怕呀,小场面,Hold
得住。”
我:“不要大意,我推荐一些对你有帮助的网站给你,你这两天好好准备准备。”
网址:https://www.nowcoder.com/
找工作神器,笔试题库,面试经验,新人培训等。
网址:https://www.lintcode.com/
刷题利器,人工智能、大数据、算法。
网址:https://leetcode-cn.com/leetbook/
海量技术面试资源,帮助你高效提升编程技能。
网址:https://www.runoob.com/
前后端开发教程都有,非常不错的学习网站。
网址:https://www.w3cschool.cn/
全球最大的中文 Web 技术教程。
网址:https://www.bilibili.com/
BiliBili不仅是一个二次元视频网站,也是一个非常优质的学习网站,上面有非常多优质的学习视频。
比如搜索python
,可以看到很多python
的教程。
网址:https://www.icourse163.org/
中国大学MOOC,有丰富的大学视频教程资源,每个教程的视频都是一系列的,适合系统性地学习,质量高,而且免费。
比如计算机原理
网址:https://www.51zxw.net/
我要自学网,上面有很多不错的培训教程,包括电脑办公、平面设计、影视动画、机械设计、工业自动、程序设计、网页设计、会计课程等。
网址:https://www.csdn.net/
CSDN
是一个专业的开发者社区,在百度上搜索编程相关的问题,都会搜到CSDN的文章。
网址:https://www.cnblogs.com/
博客园,开发者的网上家园。
网址:https://www.zhihu.com/
有问题,上知乎。
网址:https://juejin.im/
掘金,帮助开发者成长的社区。
网址:https://segmentfault.com/
思否,开发者社区和技术媒体。
网址:https://gitee.com/oschina
中文开源技术社区。
网址:https://www.infoq.cn/
前沿资讯,潮流技术,文章质量高
我:“我再出一份样题给你吧。”
皮皮:“铲屎官,干脆你直接面试我好了。”
我:“不行,你这是走后门,我不能放水,要靠自己。今天给你加个罐头,把这份试题完成了就可以吃。”
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#
的委托则完全不同,它是类型安全的,我们可以清晰的知道委托定义的返回类型和参数类型。
[延伸1]: 关于委托和事件
答:
本质区别:从定义上说,委托被编译器编译成一个类,所以它可以像类一样在任何地方定义,而事件被编译成一个委托类型的私有字段和两个公有add
和 remove
方法(有点类似于属性的定义)不过这两个方法都有一个参数,这个参数就是委托,所以,它只能定义在一个类里面。
event MyDelegate myevent; //定义事件
委托相当于一系列函数的抽象类,这一系列函数要求拥有相同的参数和返回值;而事件(event
)相当于委托的一个实例,事件是委托类型的成员,委托可以定义在类外面,而事件只能定义在类里面。
事件使用 发布-订阅(publisher-subscriber
) 模型。
发布器(publisher
) 是一个包含事件和委托定义的对象。事件和委托之间的联系也定义在这个对象中。发布器(publisher
)类的对象调用这个事件,并通知其他的对象。
订阅器(subscriber
) 是一个接受事件并提供事件处理程序的对象。在发布器(publisher
)类中的委托调用订阅器(subscriber
)类中的方法(事件处理程序)。
[延伸2]: 为什么需要事件?
答:
事件最常用的应用场景是图形用户界面(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
的虚拟机上跑的中间代码转换成c++
代码,这样再把生成的c++
代码,利用c++
的跨平台特性,在各个平台上通过对各平台都有良好优化的native c++
编译器编译,以获得更高的效率和更好的兼容性。
[延伸1]: 讲讲你对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编译。
[延伸2]: 讲讲你对JIT的了解
答:
JIT
:即时编译(Just In-Time compile
),这是.NET
运行可执行程序的基本方式,编译一个.NET
程序时,编译器将源代码翻译成中间语言,它是一组可以有效地转换为本机代码且独立于CPU
的指令。当执行这些指令时,实时(JIT
)编译器将它们转化为CPU
特定的代码。部分加密软件通过挂钩JIT
来进行IL
加密,同时又保证程序正常运行。JIT
也会将编译过的代码进行缓存,而不是每一次都进行编译。所以说它是静态编译和解释器的结合体。
5 四元数的作用
答:
四元数用于表示旋转。
其相对于欧拉角的优点:
1 避免万向锁。
2 只需要一个4维的四元数就可以执行绕任意过原点的向量的旋转,方便快捷,在某些实现下比旋转矩阵效率更高。
3 可以提供平滑插值。
6 Unity脚本生命周期与执行顺序
答:
名称 | 触发时机 | 用途 |
---|---|---|
Awake | 脚本实例被创建时调用 | 用于游戏对象的初始化,注意Awake的执行早于所有脚本的Start函数 |
OnEnable | 当对象变为可用或激活状态时被调用 | |
Start | Update函数第一次运行之前调用 | 用于游戏对象的初始化 |
Update | 每帧调用一次 | 用于更新游戏场景和状态 |
FixedUpdate | 每个固定物理时间间隔调用一次 | 用于物理状态的更新 |
LateUpdate | 每帧调用一次(在update之后调用) | 用于更新游戏场景和状态,和相机有关的更新一般放在这里 |
OnGUI | 渲染和处理OnGUI事件 | |
OnDisable | 当前对象不可用或非激活状态时被调用 | |
OnDestroy | 当前对象被销毁时调用 |
[延伸1]: 讲讲关于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
会先执行。
[延伸2]: 讲讲关于Update与FixedUpdate
答:
同:当MonoBehaviour
启用时,其在每一帧被调用。都是用来更新的。
异:Update()
每一帧的时间不固定,受场景渲染的复杂程度,还有输入的一系列事件等等各种原因影响,游戏画面的帧率是在不断变化的。
FixedUpdate()
每帧与每帧之间相差的时间是相对固定的(值为Time.fixedDeltaTime
),默认是0.02s
,可以通过Edit->ProjectSettings->Time
来设置。物理相关的处理(比如Rigidbody
)一般在FixedUpdate()
中。
[延伸3]: 讲讲关于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 的条件是否满足。
协程不是只能做一些简单的延迟,如果只是单纯的暂停几秒然后在执行就完全没有必要开启一个协程。
协程的真正作用是分步做一些比较耗时的事情,比如加载游戏里的资源。
[延伸3]: 讲讲进程、线程、协程
答:
进程拥有自己独立的堆和栈,既不共享堆,亦不共享栈,进程由操作系统调度。
线程拥有自己独立的栈和共享的堆,共享堆,不共享栈,线程亦由操作系统调度(标准线程是的)。
协程和线程一样共享堆,不共享栈,协程由程序员在协程的代码里显示调度。
协程和线程的区别是:协程避免了无意义的调度,由此可以提高性能,但也因此,程序员必须自己承担调度的责任,同时,协程也失去了标准线程使用多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
秒。
现在要开发一个点击屏幕开炮发射子弹的功能,说下你的做法?
答:
首先把子弹进行抽象,把属性和行为方法提炼出来,比如具有速度、威力、碰撞大小等属性,具有飞行、碰撞和伤害等行为。
封装子弹的抽象类,可以不继承MonoBehaviour
。
监听屏幕点击事件,触发开炮逻辑。子弹通过对象池管理,复用子弹,防止因为频繁创建销毁带来的性能问题。另外,子弹的坐标更新,可以统一由一个弹道控制器的Update
遍历每个子弹对象来计算,而不是每个子弹都挂一个MonoBehaviour
去更新,因为MonoBehaviour
的Update
是通过反射被调用的,如果有1000
颗子弹,就会调用1000
次反射,这样性能上比较差。
如果要做好几种弹道的子弹,可以继承子弹基类,拓展出多种子弹子类,子类中各自实现自己的UpdatePosition
接口,弹道管理器通过Update
遍历每个子弹调用基类的UpdatePosition
接口。