Bootstrap

Unity游戏开发进阶面试经验

目录

C#

1.反射

2.闭包

3.C#中常规容器和泛型容器有什么区别,哪种效率高?

4.哪些情况会引起装箱(对应即是拆箱)操作?

5.C# 中unsafe关键字是用来做什么的?什么场合下使用?

Unity相关

1.MonoBehaviour生命周期

2.animator组件的拥有物体太多时有哪些问题?

3.override Animator Controller在什么情况下用到?

性能相关

1.C# 哪些情况会触发gc?

2.C# 中gc性能消耗在哪里?

性能优化

渲染

1.Unity渲染流程

2.SRP和BRP的区别

3.渲染管线优化的策略

其他知识

1.A*算法

2.A*算法优化

3.B*算法

4.Dijkstra算法(单源最短路径算法)

5.路径平滑算法有哪些?

设计模式

场景题

规范相关

游戏结构相关

一些面经参考

一些比较好的博客


——我将自己的遇到的一些面试题以及网上我觉得有深度的面试题一并归纳整理出面经以飨读者。

C#

1.反射

反射是.NET框架提供的一个功能强大的机制,它允许程序在运行时检查和操作对象的类型信息。通过使用反射,程序可以动态地创建对象、调用方法、访问字段和属性,无需在编译时显式知道类型信息。在.NET中,所有类型的信息最终都是存储在元数据中的。反射就是.NET提供的一组API,允许我们在运行时访问这些元数据,从而获得关于程序集、模块、类型、成员等的详细信息。

  • 反射的优缺点

        反射机制的优点包括提高了程序的灵活性和扩展性,降低了耦合性,增强了自适应能力。它使得程序能够创建和控制任何类的对象,而无需在编写代码时就确定目标类。

        然而,反射也有其缺点。首先是性能问题,反射操作的性能通常比直接代码慢,因为它需要解释操作。因此,反射主要应用于对灵活性和扩展性要求较高的系统框架中,而不推荐在普通程序中广泛使用。其次,反射可能会使程序内部逻辑变得模糊,增加了代码的复杂性和维护难度。

  • 反射用途

    1. 它允许在运行时查看特性(attribute)信息。

    2. 它允许审查集合中的各种类型,以及实例化这些类型。

    3. 它允许延迟绑定的方法和属性(property)。

    4. 它允许在运行时创建新类型,然后使用这些类型执行一些任务。

  • 反射的基本使用场景

    1. 动态加载程序集:在某些情况下,我们可能在编译时并不知道需要加载哪些程序集,反射可以在运行时根据配置或其他条件动态加载特定的程序集,实现插件式的功能扩展。例如,一个图像编辑软件可以通过反射加载不同格式图像的处理插件。

    2. 访问私有成员:正常情况下,类的私有成员在类外部是无法直接访问的,但通过反射可以突破这种限制,这在一些需要深度调试或特殊框架开发中非常有用,比如在单元测试框架中访问被测试类的私有方法进行更全面的测试。

    3. 创建对象实例:当我们只知道类型的名称(字符串形式)时,可以利用反射动态地创建该类型的对象,这使得程序能够根据运行时的信息灵活地生成对象,而不是在编译时就确定所有对象的创建。

2.闭包
  • 闭包内存管理的风险:

    1. 延长变量生命周期: 闭包可以访问并操作外部函数的变量,这意味着即使外部函数执行完毕,这些变量仍然被闭包引用,从而延长了它们的生命周期。这种特性可以导致内存中的变量不会被及时释放,如果不当使用,可能会造成内存泄漏。相应地因为可能持有大量外部变量导致占用大量内存。

    2. 内存泄漏的风险: 如果闭包持续引用一个对象,那么垃圾回收器无法回收该对象,即使该对象在其他情况下已经不再需要。这可能导致内存占用不断上升,特别是在闭包被频繁创建和销毁的情况下。

    3. 循环引用问题: 闭包可能会形成循环引用,即闭包引用外部变量,同时外部变量也引用闭包,这会导致垃圾回收器无法正确识别和回收内存,从而导致内存泄漏。

  • 闭包的缺点:

    1. 因为持有外部变量,可能导致内存变大。

    2. 闭包只有外部变量的延迟性,可能带来的计算问题。

    3. 并发环境下可能引发锁竞争。如果闭包中包含了对共享资源的访问,而这些资源需要通过锁来保护,那么在高并发情况下,多个goroutine可能会同时尝试获取同一个锁,从而引发锁竞争。

  • 闭包的优化

    1. 避免高频代码频繁创建闭包;

    2. 避免闭包的嵌套;

    3. 闭包内引用资源不需要则直接释放;

3.C#中常规容器和泛型容器有什么区别,哪种效率高?

一般情况下,泛型容器的效率更高。因为它避免了装箱和拆箱操作带来的性能损耗,并且在编译时就进行类型检查,能够提供更好的性能和类型安全性。但在某些简单的、对性能要求不高且需要存储多种不同类型元素的场景下,常规容器可能会更方便使用,但这种情况相对较少。在实际开发中,为了提高性能和代码质量,泛型容器是更好的选择。例如:在 C# 中,常规容器如ArrayList是一种可以存储不同类型对象的集合。泛型容器是在 C# 2.0 引入的,像List<T>就是一种典型的泛型容器。

4.哪些情况会引起装箱(对应即是拆箱)操作?

分别列举如下:

  • 赋值给object类型

    int number = 42; object obj = number; // 装箱
  • 赋值给接口类型

    IDisposable disposable = new FileStream("file.txt", FileMode.Open); // 装箱
  • 存储到非泛型集合中

    ArrayList list = new ArrayList(); list.Add(42); // 装箱
  • 返回值类型的函数返回object

    public object GetData() { return 42; // 装箱 }
  • 使用反射时

    MethodInfo method = typeof(MyClass).GetMethod("MyMethod"); object result = method.Invoke(obj, null); // 装箱
  • 使用动态语言特性时

    dynamic dynObj = new ExpandoObject(); dynObj.Value = 42; // 装箱
  • 使用可空类型(Nullable<T>)时

    int? nullableInt = 42; object obj = nullableInt; // 装箱 int? nullableInt = 42; int number = nullableInt.Value; // 拆箱

因此,在一些遍历或者Update中需要特别注意这些情况,可能引起拆装箱问题而导致性能问题。

5.C# 中unsafe关键字是用来做什么的?什么场合下使用?
  1. unsafe 关键字的作用

    1. unsafe关键字主要用于启用不安全代码。它允许程序员绕过C#的一些安全机制,例如自动内存管理和类型安全检查。

    2. 当使用unsafe关键字时,你可以在代码块、方法、类或结构中直接处理指针。这为程序员提供了对内存的底层访问,就像在 C 或 C++ 等语言中操作指针一样。例如,你可以使用指针来高效地处理数组,直接访问内存中的数据结构,或者与一些需要直接内存访问的外部库(如某些硬件驱动程序接口)进行交互。

  2. 使用场合

    1.高效的内存操作。当需要对大量数据进行高性能处理时,使用指针可以更高效地访问内存。例如,在图像处理程序中,你可能需要直接访问图像数据的字节数组。如果使用安全的 C# 代码,通过索引访问数组元素可能会有一定的性能开销。而使用unsafe关键字和指针,可以直接在内存中移动指针来访问相邻的像素数据,提高处理速度。在这个例子中,不安全代码通过指针直接在内存中访问数组元素,在某些情况下可能会更高效,特别是当处理大量数据时。不过,这种性能提升的程度取决于具体的场景和编译器优化。
// 安全代码(无unsafe关键字):
int[] array = new int[1000];
for (int i = 0; i < array.Length; i++)
{
    array[i]++;
}
    
// 不安全代码(使用unsafe关键字):
unsafe{
    int[] array = new int[1000];
    fixed (int* pArray = array)
    {
        int* current = pArray;
        for (int i = 0; i < 1000; i++)
        {
            (*current)++;
            current++;
        }
    }
}

        2.与外部代码交互。当调用一些非托管的外部函数(如 C 或 C++ 编写的动态链接库中的函数)时,这些函数可能需要指针作为参数或者返回指针。unsafe关键字就可以用于在 C# 代码中正确地传递和处理这些指针。

class Program{[DllImport("yourLibrary.dll")]unsafe static extern void ModifyValue(int* value);static void Main(){unsafe{int number = 10;int* pNumber = &number;ModifyValue(pNumber);
            Console.WriteLine(number); 
        }}}

        3.实现特定的数据结构和算法。在一些高级的数据结构和算法实现中,如某些自定义的树结构或者图算法,可能需要直接操作内存地址来优化存储和访问。例如,在实现一个自定义的哈希表时,为了更好地控制内存布局和提高查找性能,可能会使用unsafe关键字来直接处理内存中的桶(bucket)和键 - 值对存储。

Unity相关

;