八股文(Unity篇)

文章目录

C#语言

堆和栈

  • 值类型的数据被保存在栈(stack)上,而引用类型的数据被保存在堆(heap)上,当值类型作为参数传递给函数时,会将其复制到新的内存空间中,因此在函数中对该值类型的修改不会影响原始值类型。当引用类型作为参数传递给函数时,传递的是其引用,即内存地址,因此在函数中对引用类型的修改将影响原始引用类型。

拆箱和装箱

拆箱是将一个值类型转换为一个对象类型,而装箱是将一个对象类型转换为一个值类型。

反射实现原理

反射是指程序在运行时可以访问、检测和修改它本身状态或行为的一种能力。在 C# 中,反射主要是通过 System.Reflection 命名空间提供的类和接口来实现的。
反射的优点是能够实现程序的动态性和灵活性,缺点是反射操作较慢,因为反射的操作需要在运行时进行动态解析和调用,而不是在编译期确定。此外,反射还可能导致安全性问题,因为它可以访问私有成员和调用私有方法。因此,在使用反射时需要谨慎处理,并遵守相关的安全规则。

List与数组的区别

  • 大小的可变性:数组的长度是固定的,一旦创建之后,就不能再改变它的大小。而List的长度可以动态增加或减少,可以根据实际情况灵活地调整。
  • 引用类型:数组是值类型,它们存储的是数据本身。而List是引用类型,存储的是对象的引用,它们只是在堆上分配了一块内存来存储元素,而引用则存储在栈中。

C#委托与事件

委托是一个类,它封装了一个或多个方法,并可以将这些方法作为参数传递给其他方法,从而实现回调函数的功能。委托可以看作是函数指针的一种类型安全的替代,它使得我们可以在运行时动态地调用方法。委托可以用来定义事件处理函数的类型。

事件是在委托的基础上实现的,它是一种在程序中发生的事情的表示,比如按钮被点击、鼠标移动等等。事件需要一个触发器和一个或多个处理程序,当事件发生时,触发器会调用相应的处理程序来处理事件。

在C#中,事件是委托的特殊用法,通过使用关键字event来声明一个事件,可以将委托类型限定为只能被事件使用。事件可以用+=和-=来添加和移除事件处理程序,而委托则可以直接调用。

总的来说,委托和事件是C#中非常重要的概念,它们使得程序可以更加灵活地响应用户的操作和外部事件,同时也方便了代码的编写和维护。

射线检测的基本原理

射线检测是一种在3D图形学中常用的技术,可以检测出射线与场景中的物体是否有相交。其基本原理是从一个起点发射一条射线,然后检测该射线是否与场景中的任何物体相交。如果射线与某个物体相交,则可以得到该物体的相关信息,如位置、旋转、缩放等,并且可以用这些信息进行后续操作,比如进行碰撞检测、选择操作等。

在Unity中,可以通过Ray和RaycastHit两个类来实现射线检测。其中,Ray类表示射线,包括起点和方向;RaycastHit类表示射线与物体相交的信息,包括相交点、相交物体等。通过将射线投射到场景中,然后检测射线是否与场景中的物体相交,可以实现各种功能,如拾取、碰撞检测、射线照射等。

协程过程

Unity中的协程是一种能够在主函数中暂停和恢复执行的功能,它使用了C#的迭代器。要实现协程,你需要继承MonoBehaviour类,并使用IEnumerator类型的函数。你还需要使用StartCoroutine()方法来开启协程,并使用yield return语句来指定协程恢复执行的条件。
首先执行协程函数,但不会阻塞当前线程,而是返回一个Coroutine对象。
然后,Coroutine对象被加入到协程队列中,等待执行。
每一帧Unity引擎都会执行协程队列中的协程,如果该协程没有被暂停,则会一直执行到协程结束,直到下一帧才会执行下一个协程。
如果该协程被暂停,那么该协程会被挂起,等待下一帧继续执行。
协程函数的缺点主要是代码可读性差,逻辑复杂时难以维护。此外,协程函数在多线程操作中可能会导致不可预测的结果,需要谨慎使用。

序列化

序列化是将数据结构或对象转换为可在网络上传输或持久化到磁盘的格式的过程。在C#中,常用的序列化方式包括二进制序列化、XML序列化和JSON序列化。

继承关键字

sealed关键字用于声明类、方法或属性,以防止它们被子类继承或重写。一旦将类或成员标记为sealed,它们就不能被继承或重写。这通常用于确保代码的安全性或性能优化,因为它可以防止其他程序员通过继承或重写类来更改代码的行为。
internal关键字用于声明只能在同一程序集中访问的类、方法或属性。这使得程序员可以创建一些只能在程序集内部使用的辅助类或方法,从而隐藏复杂性并提高代码安全性。

ArrayList与List区别

类型安全:ArrayList可以存储任意类型的对象,而List是泛型类型,可以指定存储的对象类型,提高了类型安全性和代码可读性。
性能:ArrayList是基于Object数组实现的,因此存储和检索元素时需要进行拆箱和装箱操作,影响了性能,而List是泛型类型,避免了这个问题,性能更好。
ArrayList的扩容方式是以当前容量的两倍进行扩容,而List则是按照指定的增量进行扩容,这也是List在处理大量数据时性能更好的原因之一。

抽象类和接口

定义方式:抽象类是一个普通的类,其中包含一个或多个纯虚函数(没有实现的虚函数),而接口则是纯虚类,其中的所有函数都是纯虚函数(没有实现的虚函数)。
实现方式:派生类继承抽象类时必须实现其纯虚函数,否则也会变成抽象类,而接口则要求实现类必须实现所有的纯虚函数。
类可以实现多个接口,但只能继承一个抽象类

String和StringBuffer

因为String是不可变类型,每次对其进行操作都会创建新的对象,这会产生很多的内存分配和垃圾回收,导致性能下降。

而StringBuilder则是可变类型,它可以在一个内存块中进行多次操作,减少内存分配和垃圾回收的次数,因此效率更高。

因为String是不可变类型,它的值是不能被修改的,所以可以安全地作为方法的参数或返回值。

而StringBuilder是可变类型,如果作为方法的参数或返回值,可能会被修改,从而导致不可预料的结果,因此不能作为方法的参数或返回值。

Hash和字典的区别

字典是泛型的,而HashTable是非泛型的。这意味着字典可以指定键和值的类型,而HashTable则不需要。
字典在存储和检索值类型时比HashTable更快,因为没有装箱和拆箱的开销。
字典在查找不存在的键时会抛出异常,而HashTable则会返回null。
字典使用KeyValuePair<K,T>来表示键值对,而HashTable使用DictionaryEntry来表示键值对。

接口是否是引用类型

是的,接口是引用类型。这意味着接口类型的变量存储的是对数据(对象)的引用,而不是数据本身。实现接口的任何对象都可以被接口类型的变量引用。接口类型的变量只能访问接口声明中定义的成员。

Debug.Log为什么可以后面无限添加参数

Debug.Log为什么可以后面无限添加参数在Unity中,Debug.Log 函数被重载了多次,其中一个重载函数可以传入任意数量的参数。这是通过使用 C# 中的可变参数列表(varargs)来实现的,其语法为在类型后面加上三个点(…)。

可变参数列表实际上被封装成了一个数组,因此可以在函数中像操作数组一样处理这些参数。在 Debug.Log 中,这样的设计允许开发人员传入任意数量的参数,从而方便调试信息的输出。

Using除了名称空间之外,还有在哪里使用

在C#中,using关键字可以用于引入命名空间中的类型或为它们创建别名,也可以用于定义一个范围,在该范围的末尾将释放对象。

可空变量

可空变量(Nullable Variables)是指可以赋予 null 值的变量,它的类型称为可空类型(Nullable Type)。在 C# 中,所有的值类型(Value Type)都有相应的可空类型。可空类型通过添加 ? 后缀来定义。
例如,int 类型的可空类型为 int?,bool 类型的可空类型为 bool?,DateTime 类型的可空类型为 DateTime?,等等。
可空类型的主要作用是为了解决在值类型中无法表示 null 值的问题。它可以使得值类型可以赋予 null 值,从而在某些场景下具有更大的灵活性。同时,C# 也提供了一些语法糖来方便开发者使用可空类型。

string是引用类型吗?

C#的string是引用类型。这意味着string类型的变量存储的是字符串对象的引用,而不是字符串对象本身。但是,string与其他引用类型有一些区别,例如字符串是不可变的,修改其中一个字符串,就会创建一个全新的string对象,而另一个字符串不会发生任何变化。

如何获取协程的两个yield return

协程其实就是一个迭代器,它可以在运行到yield return语句时返回一个表达式并保留当前的代码位置。如果你想获取两个yield return的值,你可以使用send方法来向协程发送参数,并赋给yield的返回值。或者你可以使用UnityWebRequest来处理远程数据访问,并在yield return后面的代码直接处理其结果

Const和readonly的区别

const 是编译时常量,必须在声明时进行初始化,且不能被修改,而 readonly 是运行时常量,可以在声明时或构造函数中初始化,且只能在初始化的时候赋值,之后不能再修改。

const 适用于简单数据类型,如整数和枚举值,而 readonly 可以用于任何数据类型。

const 值在编译时就确定了,所以可以在编译时被嵌入到代码中,不需要在运行时进行计算,因此运行速度较快。而 readonly 值在运行时才被计算,因此会有一定的运行时开销。

const 只能在类的内部和静态成员中使用,而 readonly 可以在任何成员中使用。

总之,如果需要定义一个在运行时确定的常量,使用 readonly 更加合适;如果需要定义一个在编译时就确定的常量,使用 const 更加合适。

Unity相关

动态加载资源与方式

Resources方式:Resources方式:使用Resources.Load方法加载Resources文件夹下的资源,这种方式的好处是简单易用,无需手动打包资源,但是会导致应用包变得较大,加载速度较慢,不适合较大的资源文件。
AssetBundle方式:使用Unity提供的AssetBundle打包工具将资源打包成二进制文件,再通过AssetBundle.LoadFromFile或者AssetBundle.LoadFromMemory方法加载资源。这种方式可以动态地加载和卸载资源,适合较大的资源文件,但是需要手动打包资源,增加了工作量。

引擎中有哪些坐标空间

世界坐标 局部坐标 屏幕空间 UI空间

相机中的Clipping Plane、Near、Far数值有什么意义

相机的 Clipping Plane 是相机能够看到的最近和最远距离,而 Near 和 Far 分别是相机能够看到的最近和最远的物体到相机的距离。

在渲染场景时,所有距离相机在 Near 和 Far 范围内的物体将会被渲染出来,而距离相机小于 Near 或大于 Far 的物体则不会被渲染。

调整 Clipping Plane、Near 和 Far 可以影响相机视野的大小和渲染的效率。如果将 Near 和 Far 的值设置过小,则可能无法看到远处的物体;如果将其设置过大,则会浪费资源渲染远处不必要的物体,从而降低渲染性能。

四元数的作用

  • 避免万向锁问题:在使用欧拉角或旋转矩阵进行旋转时,存在万向锁问题,即在某些情况下无法进行预期的旋转,而使用四元数可以避免这个问题。
  • 插值平滑:在游戏中需要进行物体的插值平滑,例如在进行摄像机跟随时需要平滑过渡,四元数可以更好地实现这个效果。
  • 通过将两个四元数进行乘法运算,可以将它们表示的旋转进行组合,得到一个新的旋转。

OnEnbale、Awake、Start运行时的先后顺序

OnEnable(): 当脚本所在的游戏对象被激活或者脚本实例化时,会调用该函数。
Awake(): 在脚本实例化时被调用,用于初始化一些变量等操作,但是它并不保证所有脚本都已经初始化完成。
Start(): 在所有脚本的 Awake 函数都被调用后执行,用于开始游戏。
因此,它们的先后顺序为 OnEnable -> Awake -> Start。需要注意的是,如果脚本所在的游戏对象已经处于激活状态,那么只会执行 Awake 和 Start 函数。如果脚本所在的游戏对象在 Awake 函数调用后变为非激活状态,那么 Start 函数不会被调用,只有当它重新激活时才会执行

相机的移动在Update函数中好吗?物理更新一般是在Update中吗?

尽量都放在Fixupdate中

说说prefab

在Unity中,Prefab(预制体)是一种可重用的游戏对象集合,它们允许在游戏中实例化多个相同的对象。Prefab可以包含游戏对象及其组件,以及对它们的属性和设置的引用。通过使用Prefab,开发人员可以在多个场景和不同的游戏对象之间共享和重用代码和资源,从而提高游戏开发效率。在开发过程中,Prefab也常常用于制作场景中的物体、怪物和角色等游戏元素。

说说垃圾回收机制和如何避免

  • 避免在Update函数中使用大量的临时对象。每当创建对象时,都会产生垃圾,并触发垃圾回收机制。
  • 避免使用字符串拼接操作。字符串拼接操作也会产生临时对象,从而引发垃圾回收。
  • 使用对象池。对象池可以预先创建一些对象,并在需要时重复使用,从而避免频繁的对象创建和销毁。
  • 避免使用过多的装箱和拆箱操作。装箱和拆箱操作会引起对象的创建和销毁,从而导致频繁的垃圾回收。
  • 尽可能使用值类型。值类型的对象不需要通过堆来分配内存,而是直接在栈上分配内存,从而避免频繁的堆内存分配和垃圾回收。

场景中Static对象有什么作用

Static对象通常指的是不会在运行时改变的对象,例如场景中的地形、建筑、树木等。这些对象在场景中都是静态的,它们在编译时就已经被确定,并且它们的位置、大小、形状等都不会在运行时发生改变。Static对象可以通过设置其静态属性,让Unity引擎进行一些优化,以提高游戏的性能。具体来说,Static对象可以帮助Unity引擎进行以下方面的优化:

Unity实现移动有哪些方法

  • 修改Transform
  • 使用刚体
  • 使用动画

协程是如何实现的

在Unity中,协程是基于迭代器(Iterator)实现的。具体来说,协程本质上是一种可以被暂停和恢复的函数,可以在执行过程中暂停自身的执行,等待一段时间后再继续执行。在Unity中,协程是通过迭代器实现的,即将协程函数的返回值类型定义为IEnumerator,然后使用yield语句暂停协程的执行,并在需要恢复执行时通过MoveNext()方法继续执行。

ScriptableObject如何存储在硬盘中

在Unity中,ScriptableObject是可以被序列化的对象,可以被存储为.asset文件
当使用CreateAsset()或SaveAssets()方法时,Unity会将ScriptableObject对象序列化为二进制数据,并写入磁盘文件。这些文件通常存储在项目的Assets文件夹下,可以通过Unity编辑器中的Project视图进行访问。
在运行时,可以使用AssetDatabase类的LoadAsset()或LoadAllAssets()方法来加载.asset文件中的ScriptableObject对象。此时,Unity会将二进制数据反序列化为ScriptableObject对象,并将其加载到内存中。

BFS查找Hierarchy中游戏对象,查找中途停止搜索后如何获取到停止搜索的这个目录(层次遍历)

创建一个队列,并将Hierarchy中的根节点(一般是场景Scene)添加到队列中。

对队列进行循环,每次从队列的头部取出一个游戏对象,并检查该对象是否为目标对象。

如果该对象是目标对象,则停止搜索并返回该对象。

如果该对象不是目标对象,则将该对象的所有子对象添加到队列的尾部。

当队列为空时,表示已经搜索完整个Hierarchy,但仍然没有找到目标对象。

常用碰撞检测算法

  • collider
  • Trigger
  • Raycast
  • Overlap

游戏开发

游戏AI常用什么方法实现?

  • 有限状态机
  • 行为树
  • NavMesh
    是一种用于游戏场景中的路径规划的技术。NavMesh将游戏场景中的空间划分为网格,然后对每个网格进行遍历、连接,形成一个图形结构。NavMesh将游戏场景中的物体抽象为网格节点,它将提供可行走区域的网格与连通性,这些信息将被用于寻路。相比传统的寻路算法,NavMesh寻路的优势在于它可以避免一些在复杂地形中不必要的计算,例如山峰、高墙、河流等不可行走的区域。NavMesh还能够支持更多的寻路策略,例如绕路、躲避、穿越等。

A*寻路算法的原理

A*寻路算法是一种启发式搜索算法,用于在图中寻找两点之间的最短路径。它在寻路效率和准确性方面表现良好,被广泛应用于游戏开发等领域。

A算法的原理是,在每次迭代中选择一个距离起点最近的未探索节点,同时估算该节点到终点的距离。这个距离可以用一个启发式函数(如曼哈顿距离、欧几里得距离等)来计算,因此A算法也被称为启发式搜索算法。

在A*算法中,每个节点有两个值:f值和g值。其中,f值等于该节点到起点的距离(g值)加上该节点到终点的估算距离;g值等于该节点到起点的距离。每次迭代时,从开启列表中选择f值最小的节点,将它从开启列表中移除并加入关闭列表中。然后,对该节点周围未探索的节点进行扩展,计算它们的f值并加入开启列表中。这样不断迭代,直到找到终点或开启列表为空为止。

A算法的优点是,在不考虑障碍物的情况下,它能够找到最短路径;在考虑障碍物的情况下,它可以在较短的时间内找到一条可行路径。缺点是,在有些情况下,A算法可能会探索过多的节点,导致搜索效率降低。

是否了解多个AI都在自动寻路时的动态避障算法

多个AI同时进行自动寻路时,需要考虑到它们之间的碰撞避免问题,这就需要动态避障算法。动态避障算法基于静态障碍物避让算法,考虑到其他运动物体对于路径规划的影响。常见的动态避障算法包括VO算法、HRVO算法、ORCA算法等。
一个比较简单的动态避障算法是基于力的方法,如Social Force Model(社交力模型)。该算法模拟了每个移动的对象(如人、车辆等)之间的相互作用力,其中包括个体之间的斥力、个体与目标点之间的引力、个体之间的吸引力等,从而计算每个移动对象的加速度,并根据加速度更新位置和速度。在这个过程中,算法会根据对象的速度和加速度动态调整对象的运动轨迹,以避免与其他对象相撞。该算法简单易懂,容易实现,但需要根据具体的场景和需求进行调整和优化。

如何处理子弹特别快造成的异常伤害问题

如果子弹速度特别快,可能会出现一些问题。比如,子弹速度非常快,可能会在一个时间段内跨越多个格子,如果这时候一个敌人恰好处于多个格子的交界处,就可能导致一个子弹在一个时间段内同时对多个敌人造成伤害,从而破坏了上面的伤害处理逻辑。

为了避免这种情况,可以在子弹的运动过程中,对每个子弹进行分段处理,每一段距离都对应一个时间段。在每个时间段内,只允许子弹与一个敌人对象进行碰撞检测和伤害处理,而不允许与多个敌人对象同时进行处理。这样可以保证每个子弹只对一个敌人对象造成一次伤害。

场景管理加速结构(BVH 八叉树)

场景管理加速结构是指在游戏或计算机图形学中,用于加速场景中物体之间的碰撞检测和光线追踪等计算的数据结构。由于在场景中存在大量的物体,直接对所有物体进行计算会造成计算量大、效率低下的问题。而采用加速结构可以将物体按照一定的规则组织起来,使得检测两个物体之间的碰撞或者光线是否穿过物体的计算效率大大提高。

状态同步和帧同步的原理

状态同步是指服务器将所有玩家的状态进行汇总,然后广播给所有客户端。客户端收到广播后,更新自己的状态,以保持和服务器一致。状态同步适用于对实时性要求不是很高的游戏,例如策略游戏、棋牌游戏等。

帧同步是指服务器以一定的帧率(通常为每秒 30 帧或 60 帧)将游戏状态广播给所有客户端。客户端按照服务器广播的状态进行模拟,然后将自己的操作发送给服务器,服务器再将所有玩家的操作进行汇总,计算新的游戏状态并广播给所有客户端。帧同步适用于对实时性要求比较高的游戏,例如射击游戏、竞速游戏等。
在帧同步中,为了避免网络延迟导致的卡顿和掉帧,常常采用平滑插值、延迟补偿、预测等技术。平滑插值是指在两帧之间,将玩家的状态进行插值,以平滑过渡;延迟补偿是指服务器根据玩家的网络延迟,向后推迟一定的帧数来接收玩家的操作,以避免操作过期;预测是指客户端预测自己的操作结果,以提高游戏的响应速度。

上面两个在反作弊、断线重连、实时性等等场合,用哪种同步策略好

在反作弊、断线重连和实时性等场合,帧同步是更好的选择。

在帧同步中,客户端只能执行服务器发送过来的操作,减少了作弊的可能性。而状态同步是客户端主动更新自己的状态,容易被作弊者恶意利用。此外,断线重连也更适合在帧同步中实现,因为客户端可以重新连接到服务器并接收新的帧数据来进行同步,而状态同步需要重新将所有状态数据发送到客户端,增加了网络带宽的负担。

场景

跳跃到最高点自动开枪,这个功能应该怎么做

定义一个布尔变量 jumping 表示玩家是否正在跳跃。
在每帧中检测玩家是否按下了跳跃键,如果按下了,则将 jumping 设为 true。
如果 jumping 为 true,则检测玩家是否已经到达最高点。可以通过判断玩家的竖直速度是否小于等于0来判断是否到达最高点。
如果到达了最高点,则自动开枪。可以调用开枪的函数或发送一个开枪的指令给服务器。

游戏分辨率变换、窗口尺寸变换时,UI应该怎么适配

  • 自适应布局
  • 锚点

角色可以穿脱装备,每个装备对角色的血量有不同的buff,该如何设计这个功能

  • 装备属性的实现:装备可以为角色提供一些属性加成(例如加血、加攻击力等)。可以通过设计一个装备属性接口,让装备继承该接口,并实现对应的属性加成函数。在角色穿戴或卸下装备时,调用对应的属性加成函数即可。
  • 装备buff的实现:当角色穿戴某个装备时,可以获得该装备的buff(例如加血量)。可以在装备属性接口中添加一个获取buff的函数,在角色穿戴该装备时,调用该函数获取buff,并将buff应用到角色上。

猜你喜欢

转载自blog.csdn.net/m0_50816320/article/details/129306706