一些Unity面试题 agile Posted on Oct 2 2021 优秀博文 > 本文由 [简悦 SimpRead](http://ksria.com/simpread/) 转码, 原文地址 [zhuanlan.zhihu.com](https://zhuanlan.zhihu.com/p/375376042) 记录一些自己面试遇见的问题吧,通过面试也可以发现自己和大佬们之间的一些差距,方便对自己的查漏补缺。如果后面在学习的过程中遇到一些好的知识点,也会记录一下,万一自己哪天有本事当上面试官了,这不就是题库了,哈哈。 个人感觉关于 U3D 方面的面试主要分如下四大块: * Unity 功能相关,主要结合面试者简历上介绍的一些功能模块,了解对 Unity 的使用情况,例如 Timeline,Assetbundle,各种 Package,以及一些性能优化等等。 * 语法相关,例如 C++,C#,Lua,HLSL/GLSL。 * 算法相关,可以了解面试者的逻辑能力,脑子灵不灵光。 * 图形学相关,可以了解面试者对渲染方面的知识掌握。 Unity 功能相关 ---------- 这块没什么好说的,在团队里每个人负责的模块都不一样,如果自己连自己写在简历上的模块都说不清的话,还是洗洗睡吧。 当然了,很多模块可能是你一年甚至更久前做的,如今忘得一干二净,临时要能把它说的清清楚楚也是蛮困难,反正我肯定不行。俗话说:好记性不如烂笔头,建议大家可以像我一样平时把划水摸鱼的时间抽一部分出来,记记笔记。这样一方面可以加深自己的理解,另一方面在以后需要它的时候,可能较快的回忆起来。还有比如我不会的,但是你会,这样我不就可以百度到了,嘻嘻。 ### 1. 性能优化 可参考: [王江荣:Unity 的内存管理与性能优化](https://zhuanlan.zhihu.com/p/362941227) ### 2. ILRuntime 的原理 ILRuntime 借助 Mono.Cecil 库来读取 DLL 的 PE 信息,以及当中类型的所有信息,最终得到方法的 IL 汇编码,然后通过内置的 IL 解译执行虚拟机来执行 DLL 中的代码。 [ILRuntime 的实现原理 - ILRuntime](http://ourpalm.github.io/ILRuntime/public/v1/guide/principle.html) 还有例如 ILRuntime 中绑定(Binding)的原理:CLR 绑定借助了 ILRuntime 的 **CLR 重定向机制**来实现,因为实质上也是将对 CLR 方法的反射调用重定向到我们自己定义的方法里面来。这样我们可以选着性的对经常使用的 CLR 接口进行直接调用,从而尽可能的消除反射调用开销以及额外的 GC Alloc。 ### 3. 协程的原理 简单来说:主要是通过 **IEnumerator** 来实现一个**迭代器**,然后分帧执行 **MoveNext()** 方法。 参考 [undefined](https://www.cnblogs.com/yespi/p/9847533.html) ### 4. 因物体速度太快碰撞体被穿透的问题 首先是刚体组件中 Collision Detection 属性的设置:  其次我们还可以利用代码,增加射线检测来避免穿透的问题。 ### 5. UGUI 合批的一些问题 简单来说在一个 Canvas 下,需要相同的 material,相同的纹理以及相同的 Z 值。例如 UI 上的字体 Texture 使用的是字体的图集,往往和我们自己的 UI 图集不一样,因此无法合批。还有 UI 的动态更新会影响网格的重绘,因此需要动静分离。 详细可参照: [Unity3D UGUI 系列之合批](https://blog.csdn.net/sinat_25415095/article/details/112388638) 语法相关 ---- 语法也是非常重要的,也是自己很欠缺的一部分。大部分的 U3D 程序员应该都是用 C# 做开发,如果你不能很清晰的了解 class 与 interface,delegate 与 action,sealed 和 virtual 等等的适用场景,那么在需要搭一些框架的时候,可能会写的奇臭无比。 还有就是 C++,也是我的惨痛经历,由于长期使用 C# 做 Gameplay,导致几乎没用过 C++,因此想加入某大厂的一丝丝希望也破灭了。很多大厂都会有引擎开发部门,而且 Unity 也是一个 C++ 引擎,因此如果不会 C++ 那么压根不能看懂 Unity 的一些底层代码,从而去修改优化。因此今后划水摸鱼的时间又得抽一部分出来学习 C++ 了。 **学好 C++,走遍天下都不怕。** 废话说了这么多,来正式看看一些有关的面试题: ### 1. C++ 的多态原理 简单来说,如果当派生类重写了基类的函数,然后用基类指针指向派生类时候(Base* p = Child()),要实现仍能正确调用派生类重写后的函数而不是基类对应的函数,需要在基类对应函数处使用 **virtual** 关键字。对于使用了 virtual 关键字的方法,c++ 会在对应的类下维护一个虚函数表(**__vfptr**),通过该表来找到对应的函数。并且只有在 Runtime 才知道 p 实际指向了子类,而在 Compile time 只知道 p 是 base 类的,因此还涉及到动态查找(dynamic lookup)。 参考: [KarK.Li:C++——来讲讲虚函数、虚继承、多态和虚函数表](https://zhuanlan.zhihu.com/p/136478734) ### 2. Lua 中 ipairs 和 pairs 的区别 简单来说,pairs 会遍历表中全部的键值对(key,value),其中键可以是字符串也可以是整数(例如:array['wjr'],array[2])。而 ipairs 则从下标为 1 开始遍历,然后下标累加 1(例如:array[1],array[2],...),如果某个下标元素不存在就终止遍历。 这就导致如果下标不连续或者不是从 1 开始的表,如果使用 ipairs 遍历就会中断或者遍历不到所有元素。 3. Lua 实现面向对象 ------------- LUA 中的类可以通过 table + function 模拟出来,而类的继承,可以通过 metetable 模拟出来。 参考: [Lua 面向对象 | 菜鸟教程](https://www.runoob.com/lua/lua-object-oriented.html) ### 4. Lua 中 require 的原理 简单来说,当我们要 require 一个 .lua 文件的时候,首先会查找 **package.loaded** 表,检测是否被加载过(预防重复加载)。如果被加载过,require 得到之前保存的值,即:package.loaded['name']。 如果没有被加载过,则通过 package 的 **searchers** 表中的四个**加载器**进行查找,四个加载器按顺序查找,找到则返回得到的结果,没找到则执行下一个加载器。 * 预加载器,执行 package.preload[modname],一些特殊模块会有预加载器。 * lua 加载器,查找 package.path 路径下的文件。 * c 加载器,查找 package.cpath 路径下的文件。 * 一体化加载器,本质上他是 cpath 加载器的延伸,允许多个导出函数绑定在一个 c 库里。 参考: [lua require 机制_专注于网络编程, 游戏后台, 高并发 - CSDN 博客_lua require](https://blog.csdn.net/zxm342698145/article/details/80607072) 如果我们有个 UI 相关的模块已经被加载过,即在 package.loaded 中了。但是此时出现了一点 BUG,需要我们对此模块进行修改,修改后我们自然希望重新加载该模块。因此只需要我们在加载该模块前,将 package.loaded 中对应的值设为空,这样就可以实现即使不重启游戏就可以加载更新后的模块,非常方便。 ### 5. CLR 机制 参考 [C#、.NET Framework、CLR 的关系](https://blog.csdn.net/lidandan2016/article/details/77868043) ### 6. 热更新的原理 首先要了解上面的 CLR,然后了解 **JIT** 和 **AOT**,IOS 不支持 JIT。 参考 [Unity 将来时:IL2CPP 是什么? - decode126 - 博客园](https://www.cnblogs.com/decode1234/p/10270911.html)[Unity 热更新](https://www.jianshu.com/p/f9d90edf4a7c) ### 7. 弱引用 通常情况下我们声明一个引用类型的变量都是**强引用**的关系,如下: ``` Object obj = new Object(); ``` 此时**引用计数**会 + 1,在 GC 时该对象并不会被释放。此时若我们用一个新的引用指向它,那么引用计数还会再次增加,如下: ``` Object otherObj = obj; ``` 此时生成的 Object 对象引用计数为 2,如果我们执行 obj = null; Object 对象引用计数会变为 1,而 otherObj 依旧引用着,因此还是不会被 GC。 C# 为我们提供了 **System.WeakReference** 类型来声明一个弱引用对象,如下: ``` System.WeakReference weakObj = new System.WeakReference(obj); ``` 这种方式就**不会导致引用计数增加**,因此 Object 对象引用计数依旧为 1(没有 otherObj 那步操作)。所以如果我们执行 obj = null; Object 对象引用计数会变为 0,就会被 GC,可以有效的避免内存泄漏问题。 可以利用**.IsAlive** 属性来判断弱引用引用的对象是否还存在,使用**.Target** 属性来获取弱引用引用的对象。 弱引用常用于一些游戏内的统计或者异步对象等。 ### 8. StringBuilder 原理 在 C# 中,在处理**字符串拼接**的时候,使用 StringBuilder 的效率会比硬拼接字符串高很多。StringBuilder 并不会重新创建一个 string 对象,它的效率源于预先以**非托管**的方式分配内存。如果 StringBuilder 没有先定义长度,则默认分配的长度为 16。当 StringBuilder 字符长度小于等于 16 时,StringBuilder 不会重新分配内存;当 StringBuilder 字符长度大于 16 小于 32 时,StringBuilder 又会重新分配内存,使之成为 16 的倍数。使用时如果预先判断字符串的长度将大于 16,则可以为其设定一个更加合适的长度(如 32)。StringBuilder 重新分配内存时是按照上次的容量**加倍**进行分配的。 因此在做字符串拼接操作时,不会导致频繁的分配内存的操作。需要注意的是,StringBuilder 指定的长度要合适,太小了,需要频繁分配内存;太大了,浪费空间。此外由于是非托管的方式,使用完后需要主动释放这部分内存。 ### 9. C# Dictionary 的实现 参考: [数据结构基础温故 - 6. 查找(下):哈希表 - EdisonZhou - 博客园](https://www.cnblogs.com/edisonchou/p/4706253.html)[浅谈 C# Dictionary 实现原理](https://www.cnblogs.com/xiaomowang/p/12405639.html) 源码: [undefined](https://referencesource.microsoft.com/#mscorlib/system/collections/generic/dictionary.cs,dc94bb2ee9650189) 算法相关 ---- 想要算法好,无脑刷力扣: [力扣](https://leetcode-cn.com/) 虽然平时一些功能,即使用简单的 if/else,循环,递归等也能实现,但是好的算法可以大大提高程序的运行效率,降低内存的消耗等,也能让你觉得自己很牛,得到爽快感。 ### 1. 自动寻路的原理 如今常见的寻路有 A star,D star,Dijkstra 等算法,正好看见一个牛逼的网址,可以看出各种算法的计算过程: [PathFinding.js](http://qiao.github.io/PathFinding.js/visual/) A * 算法介绍 [王江荣:A-Star(A*)寻路算法原理与实现](https://zhuanlan.zhihu.com/p/385733813) ### 2. 排序算法 最常见的就是问**快速排序**了,百度一下一大堆,就不多说了 参考: [王江荣:常用排序算法(C#)](https://zhuanlan.zhihu.com/p/363639152) ### 3. 最大公约数 这么不起眼的题,有三个算法,参考如下: [浅析求两个数的最大公约数的三种算法_小葱的博客 - CSDN 博客_最大公约数怎么求算法](https://blog.csdn.net/qq_34992845/article/details/53043339) 想到自己当时 freestyle 的回答:搞个循环从 2 开始慢慢往上除,卧槽,真想找个坑钻进去。 ### 4. 斐波那契数列(Fibonacci sequence) 斐波那契数列简单来说,就是一个数组 array,我们可以自定义 array[0] 和 array[1] 的值,然后数组后续的值都等于前两项之和,即 array[n] = array[n-1] + array[n-2],那么怎么求 array[n] 的值。 freestyle 的第一反应自然就是循环: ``` int Fibonacci(int a0, int a1, int n) { if (n == 0) return a0; if (n == 1) return a1; int value1 = a0;//第一个被加数 int value2 = a1;//第二个被加数 for (int i = 2; i <= n; i++) { //做一个换位,例如对于a2而言,value1 = a0,value2 = a1。而对于a3而言,value1 = a1,value2 = a2。 //其中后者的value1等于前者的value2,后者的value2等于前者的value1+value2 int temp = value2; value2 = value1 + value2; value1 = temp; } return value2; } ``` 实际上还有看起来更舒服的递归算法: ``` int Fibonacci(int a0, int a1, int n) { if (n == 0) return a0; if (n == 1) return a1; return Fibonacci(a0, a1, n - 2) + Fibonacci(a0, a1, n - 1); } ``` 不过明显可以发现,递归算法里虽然看着简洁,但是有个很致命的缺点,无限多的重复计算。时间上的漏洞,那么我们就牺牲一下空间来弥补,如下: ``` Dictionary<int,int> FibonacciDic = new Dictionary<int, int>(); int Fibonacci(int a0, int a1, int n) { if (n == 0) return a0; if (n == 1) return a1; if (FibonacciDic.ContainsKey(n)) return FibonacciDic[n]; return Fibonacci(a0, a1, n - 2) + Fibonacci(a0, a1, n - 1); } ``` ### 5. 最多可以参加的会议数目 给你一个数组 events,其中 events[i] = [startDayi, endDayi] ,表示会议 i 开始于 startDayi ,结束于 endDayi 。你可以在满足 startDayi <= d <= endDayi 中的任意一天 d 参加会议 i 。注意,一天只能参加一个会议。请你返回你可以参加的最大会议数目。 示例 1:输入:events = [[1,2],[2,3],[3,4]] 输出:4 示例 2:输入:events= [[1,2],[2,3],[3,4],[1,2]] 输出:4 思路:可以利用哈希表来存储,日期作为键,可以有效去重。 ### 6. 求阶乘末尾有多少个 0 实现一个函数,传入参数为正整数 N,返回 N 的阶乘末尾有多少个 0 示例 1:输入:10 输出:2 示例 2:输入:25 输出:6 最简代码: ``` Int rst = 0; while(N > 0) { N /=5; rst += N; } ``` 思路:计算因子 5,为什么末尾会出现 0,是因为存在 5 的倍数与 2 的倍数相乘,因为 5 的倍数明显少于 2 的,因此只需要计算 5 的倍数即可。 例如 25 的阶乘 25!=25*24*23...*1,我们可以分解成 (5*5)*24*...*(5*4)*...*1 ### 7. 奇偶链表 题目链接:[力扣](https://leetcode-cn.com/problems/odd-even-linked-list/) 具体思路就是用两个新的链表来存储奇数位和偶数位的节点,然后把两个链表相连。其中原链表偶数位节点的后面必然是奇数位节点,然后这个奇数位节点后面又必然是偶数位,这样就可以实现类似 +=2 的效果。 ``` public class Solution { public ListNode OddEvenList(ListNode head) { if(head == null) return head; ListNode otherStartNode = head.next; ListNode oddNode = head;//存放奇数 ListNode otherNode = otherStartNode;//存放偶数 while(otherNode != null && otherNode.next != null){ //原本偶数后面肯定是奇数,所以放到奇数列表后。 oddNode.next = otherNode.next; //奇数列表后移一位 oddNode = oddNode.next; //新的那个奇数后面又肯定是偶数,放到偶数列表后 otherNode.next = oddNode.next; //奇数列表后移一位 otherNode = otherNode.next; } //整体效果类似于 otherNode += 2 //奇数列表的屁股连上事先存好的偶数列表的头 oddNode.next = otherStartNode; return head; } } ``` 贴一个自己一开始写的错误案例: ``` public ListNode OddEvenList(ListNode head) { if(head == null) return head; ListNode oddNode = new ListNode(1); ListNode otherNode = new ListNode(1); ListNode oddStartNode = head; ListNode otherStartNode = head.next; int count = 1; while(head != null) { if(count % 2 == 1) { oddNode.next = head; oddNode = oddNode.next; } if(count % 2 == 0) { otherNode.next = head; otherNode = otherNode.next; } head = head.next; count++; } oddNode.next = otherStartNode; return oddStartNode; } ``` 咋一看除了逻辑蠢了点,没啥问题,实际上当链表为奇数个时会出现死循环。例如输入 1->2->3,得到的奇数位链表位 head->1->3,偶数位链表为 head->2->3,并不是 head->2,因为上面的逻辑没有把 2.next 设为 null。这样连接起来后造成死循环 1->3->2->3->2... 解决方法即不管什么情况,偶数链表的最后一项的 next 必定是 null,因此在最后加上一句即可: ``` otherNode.next = null; ``` 图形学相关 ----- 在图形学方面,自己更是一个实打实的萌新了,但是它真的很重要,能够让你看清引擎中很多功能的本质,比如缩放旋转是矩阵的相乘,物体表面的颜色有它的计算公式。 ### 1. 用 shader 实现描边效果 参考: [undefined](https://blog.csdn.net/puppet_master/article/details/54000951) ### 2. 放大镜效果 最简单的方法就是新增一个 Camera,然后利用 **RenderTexture** 来实现。 ### 3. 光栅化过程以及着色模型公式等 [王江荣:光栅化与深度缓存](https://zhuanlan.zhihu.com/p/363245957)[王江荣:着色与 Blinn-Phong 反射模型](https://zhuanlan.zhihu.com/p/364086530) ### 4. 菲涅尔方程 (**F**resnel Rquation) 菲涅尔方程描述的是在不同的表面角下**表面所反射的光线所占的比率**,在 BRDF 中也有它的存在。具体介绍可以参考: [王江荣:菲涅尔方程(Fresnel Equation)](https://zhuanlan.zhihu.com/p/375746359) ### 5. 图片扭曲效果 如下图,实现图片扭曲的效果:  说下简单思路,我们先来看下原图,如下:  如果正常的 uv 采样,我们 A 点应该显示的是肩膀部分,但是扭曲后显示的确实眼睛部分。也就是说 **A 点所在的像素采样的时候用的是 B 点的 uv**。因为我们是绕中心点的扭曲,那么就可以通过半径、三角函数这些,利用 A 点原本的 uv 来计算出 B 点的 uv,然后在 A 点的像素用 B 的 uv 采样,从而实现扭曲。如果 AB 两点到中心点的角度越大,那么扭曲就越严重。 fragment shader 的代码如下: ``` fixed4 frag (v2f i) : SV_Target { float center = (0.5, 0.5);//中心点uv float2 dt = i.uv - center;//原本uv和中心点的偏移 float len = length(dt);//半径 float theta = len * _Amount;//要扭曲的角度,_Amount为自定义的扭曲强度,值越大扭曲越严重 float2x2 rot = { cos(theta), -sin(theta), sin(theta), cos(theta) };//旋转矩阵 dt = mul(rot, dt) + center;//计算出扭曲后的uv float4 col = tex2D(_MainTex, dt);//采样 return col; } ``` Unity中Animator Override的性能问题 菲涅尔方程(Fresnel Equation)