Unity 面试高频题
CSharp 部分
值类型和引用类型的区别
值类型:整数, 浮点, bool, char, struct, enum 继承自 System.ValueType
引用类型:string, object, class, interface, delegate, array 继承自 System.Object
- 值类型存储在内存栈中,引用类型数据存储在内存堆中,而内存单元中存放的是堆中存放的地址。
- 值类型存取快,引用类型存取慢。
- 值类型表示实际数据,引用类型表示指向存储在内存堆中的数据的指针和引用。
- 栈的内存是自动释放的,堆内存是.NET 中会由 GC 来自动释放。
重载和重写的区别
封装、继承、多态所处位置不同,重载在同类中,重写在父子类中。
定义方式不同,重载方法名相同参数列表不同,重写方法名和参数列表都相同。
调用方式不同,重载使用相同对象以不同参数调用,重写用不同对象以相同参数调用。
多态时机不同,重载时编译时多态,重写是运行时多态。
ArrayList 和 List 的主要区别
ArrayList 不带泛型 数据类型丢失
List 带泛型 数据类型不丢失
ArrayList 需要装箱拆箱 List 不需要
ArrayList 存在不安全类型(ArrayList 会把所有插 ⼊其中的数据都当做 Object 来处理)装箱拆箱的 操作(费时)IList 是接⼝,ArrayList 是⼀个实现了 该接⼝的类,可以被实例化
List 类是 ArrayList 类的泛型等效类。它的大部分用法都与 ArrayList 相似,因为 List 类也继承了 IList 接口。最关键的区别在于,在声明 List 集合时,我们同时需要为其声明 List 集合内数据的对象类型。
CSharp 的 GC
const 和 readonly 的区别
在 C#中,const
和 readonly
都用于定义不可修改的字段或变量,但它们有一些重要的区别:
- 定义时机
const
: 必须在声明时进行初始化。const
是编译时常量,也就是说其值在编译时就已经确定,不能通过代码在运行时修改。1
const int maxItems = 100;
readonly
: 可以在声明时或构造函数中进行初始化。这意味着它的值可以在运行时设置,但一旦初始化后就不能再修改。1
2
3
4
5
6readonly int maxItems;
public MyClass()
{
maxItems = 100;
}
- 修饰符适用范围
const
: 只能用于值类型(如整数、布尔值等)或字符串。此外,它总是隐式的static
,不需要显式声明。1
public const double Pi = 3.14159; // const 隐式 static
readonly
: 可以用于引用类型,如类和数组。它也可以是静态的或实例级的变量,但需要显式声明static
,如果需要它在类级别共享。1
2public readonly string myString;
public static readonly MyClass sharedInstance = new MyClass();
- 内存分配
const
: 因为它是编译时常量,编译器会将其值直接嵌入到使用它的代码中,这意味着const
不会占用额外的内存。readonly
: 是在运行时初始化的,因此它的值是保存在内存中的(和普通字段类似),但一旦被初始化后,它的值不可修改。
- 版本兼容性
const
: 由于编译器将常量的值直接嵌入到使用它的代码中,如果常量值被改变,必须重新编译所有引用此常量的代码,否则引用的代码可能仍然使用旧的值。readonly
: 因为它的值在运行时确定,如果值发生改变,不需要重新编译引用此readonly
字段的代码。
- 使用场景
const
: 适用于定义从不变化的值,如数学常量或配置常量。readonly
: 适用于需要在运行时初始化的常量,或者需要引用复杂对象的情况。
1 | public class Example |
总结来说,const
主要用于编译时已知的常量,而 readonly
用于在运行时确定且不再改变的值。
Unity 部分
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 的条件是否满⾜
在 Unity 中,Draw Call 是指 GPU 绘制对象的次数。每个 Draw Call 代表一次从 CPU 向 GPU 发出的绘制指令,这些指令告诉 GPU 渲染某个物体或者一组物体。减少 Draw Call 是优化游戏性能的重要步骤,尤其是在移动平台和低性能设备上。Mask
和 RectMask2D
是 Unity UI 系统中常用的两种遮罩组件,它们用于控制某些 UI 元素的可见性,但在使用场景和性能上有所不同。以下是它们的主要区别及适用场景:
1. Mask
Mask
是一个通用的遮罩组件,它基于图形元素的形状来裁剪子元素。它的主要特点是支持任意形状的遮罩。
主要特性:
- 任意形状遮罩:
Mask
可以基于父级对象的图形形状来裁剪子元素,不局限于矩形。它通常依赖于父对象的Image
组件,遮罩形状由父对象的图形形状(如圆形、任意多边形等)决定。 - Alpha 遮罩:
Mask
可以使用父对象的 Alpha 通道来裁剪子元素,这意味着可以基于半透明图像的 Alpha 通道进行遮罩。
适用场景:
- 当你需要遮罩任意形状的 UI 元素时,例如圆形头像、复杂的 UI 元素形状时。
- 如果你的遮罩需要基于图像的透明度(Alpha 通道),如在某些特定形状的边缘模糊或淡入淡出。
示例:
假设你有一个圆形的头像框,你可以在头像框对象上添加 Mask
组件,子对象(如头像图片)将仅显示在这个圆形区域内。
性能:
Mask
的开销相对较大,尤其是在需要遮罩复杂图像或进行多层遮罩时,因为它涉及到更多的渲染计算,特别是使用 Alpha 遮罩时。
2. RectMask2D
RectMask2D
是一个用于裁剪矩形区域的遮罩组件,它只能裁剪矩形区域内的子元素,且不使用 Alpha 通道。
主要特性:
- 矩形遮罩:
RectMask2D
仅支持矩形区域裁剪。它基于RectTransform
的大小来确定遮罩区域,因此比Mask
更加轻量级。 - 无 Alpha 遮罩:
RectMask2D
不支持基于图像 Alpha 通道的遮罩,只能进行简单的矩形区域裁剪。
适用场景:
- 当你只需要裁剪矩形区域时,
RectMask2D
是更高效的选择。它适合用于滚动列表、滚动视图(ScrollView)等场景中,尤其是当内容在一个矩形区域内需要被裁剪时。 - 比如,在实现聊天框、物品列表等 UI 元素的滚动区域时,用
RectMask2D
裁剪超出滚动视图范围的内容。
示例:
假设你有一个滚动视图,里面的内容可能会超出可视区域。可以在滚动视图的容器上添加 RectMask2D
,超出矩形区域的内容将被裁剪掉。
性能:
RectMask2D
的性能开销较小,适合在需要裁剪矩形区域的情况下使用,因为它不涉及复杂的图像处理和 Alpha 遮罩,因此在处理简单矩形区域裁剪时,比 Mask
更高效。
总结:
**
Mask
**:- 支持任意形状遮罩(依赖图像形状或 Alpha 通道)。
- 适用于需要复杂形状遮罩的场景。
- 开销相对较大,适合在需要精确遮罩控制时使用。
**
RectMask2D
**:- 仅支持矩形遮罩,基于
RectTransform
的大小进行裁剪。 - 适用于滚动视图等简单矩形区域裁剪场景。
- 性能开销小,适合矩形裁剪需求。
- 仅支持矩形遮罩,基于
根据具体需求,选择合适的遮罩组件来实现相应功能。对于大多数涉及矩形裁剪的 UI 场景,RectMask2D
是更好的选择,而对于复杂形状的裁剪需求则需要使用 Mask
。
什么是 Draw Call?
每次 Unity 渲染场景中的一个对象时,都会触发一次 Draw Call。Draw Call 的开销主要体现在 CPU 和 GPU 之间的通信上。通常,每个 Draw Call 都会涉及以下内容:
- 设置渲染状态(例如,材质、光照、纹理等)。
- 提交几何数据给 GPU。
- 执行 GPU 渲染。
大量 Draw Call 会对 CPU 造成负担,因为每个 Draw Call 都需要从 CPU 发送命令给 GPU。如果 Draw Call 过多,CPU 就会成为性能瓶颈,导致帧率下降。
减少 Draw Call 的方法
合并网格(Mesh)
- 静态批处理(Static Batching):如果场景中的对象是静态的(即它们不会移动、旋转或缩放),可以启用静态批处理。Unity 会将所有使用相同材质的静态对象合并为一个大网格,从而减少 Draw Call 数量。
- 要使用静态批处理,你需要在 Inspector 中勾选对象的
Static
选项。
- 要使用静态批处理,你需要在 Inspector 中勾选对象的
- 动态批处理(Dynamic Batching):对于小型、动态的对象,Unity 可以使用动态批处理来将它们合并为一个 Draw Call。然而,动态批处理仅适用于小型对象,且对模型的顶点数有限制(通常为 300 顶点以下)。
- 确保对象使用相同的材质,并且模型的顶点数在批处理限制范围内。
- 静态批处理(Static Batching):如果场景中的对象是静态的(即它们不会移动、旋转或缩放),可以启用静态批处理。Unity 会将所有使用相同材质的静态对象合并为一个大网格,从而减少 Draw Call 数量。
材质合并和使用共享材质
- 减少材质种类:不同的材质需要单独的 Draw Call,因此减少材质种类可以减少 Draw Call。尽量在多个对象之间共享材质。
- 合并纹理(Texture Atlasing):通过将多个小纹理合并到一个大纹理(称为 Texture Atlas),可以减少材质切换次数,从而减少 Draw Call。例如,所有需要使用同一材质的对象可以共享同一张纹理图集。
使用 GPU Instancing
- GPU Instancing:对于多个使用相同网格和材质的对象,GPU Instancing 可以让 GPU 一次性渲染多个实例,而不必每个实例都发起一次 Draw Call。这对大量相同对象(如树木、草丛、敌人等)非常有用。
- 要启用 GPU Instancing,需要在材质的设置中勾选 “Enable GPU Instancing” 选项。
- GPU Instancing:对于多个使用相同网格和材质的对象,GPU Instancing 可以让 GPU 一次性渲染多个实例,而不必每个实例都发起一次 Draw Call。这对大量相同对象(如树木、草丛、敌人等)非常有用。
减少光源和阴影
- 每个光源,特别是动态光源,都会增加额外的 Draw Call。尽量减少场景中动态光源的数量,或者使用烘焙光照(Baked Lighting)。
- 阴影的渲染也会增加 Draw Call。根据需要调整阴影质量,减少使用高质量的实时阴影。
LOD(Level of Detail)
- 使用 LOD 级别来降低远处物体的细节。当对象离相机较远时,使用简化版本的模型(低多边形)可以减少渲染负担,间接减少 Draw Call 数量。
Occlusion Culling(遮挡剔除)
- 启用遮挡剔除可以防止相机渲染被其他物体挡住的对象,减少不必要的渲染和 Draw Call。Unity 提供了内置的遮挡剔除系统,可以在场景中设置。
- 对于复杂的场景,尤其是有大量隐藏对象的场景,遮挡剔除可以显著减少 Draw Call。
减少 Canvas 重绘(对于 UI 元素)
- Unity 的 UI 系统(Canvas)在更新时会触发大量的 Draw Call。为了减少 UI Draw Call,可以尽量避免频繁更新整个 Canvas。如果某个小部分 UI 频繁更新,可以将其放到单独的子 Canvas 中,这样更新时不会影响整个 UI。
- 尽量减少 Canvas 的层级,合并相似的 UI 元素,避免过度嵌套。
使用前向渲染(Forward Rendering)
- 前向渲染比延迟渲染(Deferred Rendering)更容易控制 Draw Call,特别是在光源数量有限的情况下。对于移动平台和简单的场景,前向渲染通常可以带来更好的性能。
总结
减少 Draw Call 是优化 Unity 性能的关键步骤之一。以下是一些主要策略:
- 合并网格(静态和动态批处理)。
- 合并材质和纹理(使用 Texture Atlasing)。
- 使用 GPU Instancing 来渲染相同的对象。
- 减少光源和阴影的数量。
- 使用 LOD 和遮挡剔除优化场景。
- 对于 UI,优化 Canvas 的重绘频率。
通过这些优化措施,可以显著减少 Draw Call 的数量,提升游戏的性能和帧率。
在 Unity 中,Update
、FixedUpdate
和 LateUpdate
是三种常用的事件函数,它们的调用时机和频率有所不同,适用于不同的场景。理解它们之间的区别对于优化游戏逻辑和性能至关重要。
1. Update
Update
是最常见的帧更新函数,它在每一帧被调用一次。这个函数用于处理需要每帧都更新的逻辑,例如输入检测、移动、状态更新等。
特点:
- 按帧调用:
Update
函数是基于帧率的调用,意味着它的调用频率与游戏的帧率(FPS)直接相关。如果帧率为 60 FPS,那么Update
函数每秒会调用 60 次。 - 适用场景:
Update
适合处理与游戏帧同步的逻辑,例如检测用户输入、对象的非物理移动、动画状态更新等。
示例:
1 | void Update() |
使用场景:
- 处理输入检测:如键盘、鼠标或触摸屏输入。
- 对象的非物理移动或动画更新。
- 更新 UI 元素或播放音效。
2. FixedUpdate
FixedUpdate
函数在一个固定的时间间隔内被调用。它的调用频率与游戏帧率无关,而是由物理系统的时间步长 (Time.fixedDeltaTime
) 决定。默认情况下,FixedUpdate
每秒被调用 50 次(即 Time.fixedDeltaTime
为 0.02 秒)。
特点:
- 固定时间间隔调用:无论帧率如何,
FixedUpdate
都会按照固定的时间间隔调用。这使得它非常适合处理物理相关的更新逻辑。 - 适用场景:
FixedUpdate
主要用于处理物理系统中的逻辑,例如物体的刚体(Rigidbody)运动、力的应用等。
示例:
1 | void FixedUpdate() |
使用场景:
- 更新物理系统中的物体位置和旋转。
- 应用力和其他物理相关操作。
- 确保物理行为一致,独立于帧率。
3. LateUpdate
LateUpdate
函数在每一帧的所有 Update
函数调用完之后执行。它用于处理依赖于其他更新逻辑的操作,例如在所有对象更新后再进行的相机跟随等。
特点:
- 在
Update
之后调用:LateUpdate
确保所有对象的Update
函数已经执行完毕,非常适合依赖于这些更新结果的逻辑。 - 适用场景:
LateUpdate
常用于相机跟随、动画处理、调整 UI 布局等需要确保所有对象已经更新完状态的场景。
示例:
1 | void LateUpdate() |
使用场景:
- 相机跟随逻辑:确保相机位置在所有对象更新之后调整。
- 在帧结束前处理的动画或粒子效果更新。
- 调整 UI 布局或处理依赖于其他对象更新的操作。
总结:
- **
Update
**:- 基于帧率,每帧调用一次。
- 用于处理非物理的游戏逻辑,如输入检测、对象移动等。
- **
FixedUpdate
**:- 基于固定时间间隔,与物理引擎同步。
- 用于处理物理相关的逻辑,如刚体运动、力的应用等。
- **
LateUpdate
**:- 在
Update
之后调用。 - 用于依赖于其他更新逻辑的操作,如相机跟随、动画调整等。
- 在
选择合适的更新函数可以确保你的游戏逻辑和性能得到优化。在处理物理相关的内容时使用 FixedUpdate
,对于大多数游戏逻辑使用 Update
,而在需要在所有游戏逻辑更新完成后执行某些操作时使用 LateUpdate
**
FixedUpdate
**:物理计算具有一致的时间步长,不受帧率波动影响,避免物理行为的不稳定性。**
Update
**:处理与帧率相关的任务,确保响应用户输入和更新游戏状态时的流畅性。**
LateUpdate
**:在所有Update
逻辑处理完成后进行操作,确保后处理操作如相机跟随和动画更新能正确应用到最终渲染的帧中。
在 Unity 中,加载资源有多种方式,具体选择取决于资源的类型、项目的规模和加载需求(如同步或异步)。以下是几种常见的资源加载方式:
1. Resources.Load
Resources.Load
是 Unity 中加载资源的最常见方式之一,它从项目的 Resources
文件夹中同步加载资源。
优点:
- 简单直接。
- 适合加载小型项目中的静态资源。
缺点:
- 在大型项目中,如果
Resources
文件夹中的资源过多,可能会导致内存使用量增加。 - 资源打包到最终项目时,
Resources
文件夹中的所有资源都会被包含在内,无法按需剔除。
示例:
1 | GameObject myPrefab = Resources.Load<GameObject>("Prefabs/MyPrefab"); |
2. AssetBundle
AssetBundle
是一种打包资源的方式,它允许你将资源打包成独立的文件,通常用于减少项目的初始大小,并在运行时按需加载资源。
优点:
- 可以显著减少初始安装包的大小。
- 允许从远程服务器按需下载资源,适合大型项目或需要动态更新的游戏。
缺点:
- 创建和维护
AssetBundle
需要额外的构建步骤。 - 对于初学者而言,使用相对复杂,需要处理版本控制和依赖关系。
示例:
1 | IEnumerator LoadAssetBundle() |
3. Addressable Assets
Addressable Assets
是 Unity 提供的一个现代化的资源管理系统,旨在解决资源加载和管理的复杂性。它基于 AssetBundle
,但提供了更高级别的抽象和功能,如按需加载、异步操作和更好的版本控制。
优点:
- 资源可以按地址进行加载,而不必关心资源的存储位置。
- 自动处理依赖关系和异步加载,简化资源管理。
- 适合需要灵活加载资源的中大型项目。
缺点:
- 设置复杂度比
Resources.Load
略高,但比AssetBundle
更易用。
示例:
1 | using UnityEngine.AddressableAssets; |
4. StreamingAssets
StreamingAssets
文件夹可以用于存储任何你不希望被压缩或者需要在运行时使用的文件。它不会被 Unity 的资源系统压缩或转换,因此可以用于存储原始文件(如视频、文本、配置文件等),并在运行时读取。
优点:
- 可用于存放视频、音频、文本文件等无法通过
Resources
加载的资源。 - 适合需要在运行时读取的文件,尤其是不可压缩的文件。
缺点:
- 不能直接加载 Unity 的原生资源(如材质、Prefab 等),需要自行管理文件读取。
示例:
1 | string path = Path.Combine(Application.streamingAssetsPath, "myFile.txt"); |
5. WWW
/UnityWebRequest
UnityWebRequest
是 Unity 用于处理网络请求的类,通常用于从服务器下载资源或与在线服务进行交互。
优点:
- 可以从远程服务器下载资源,如文本、图像、音频等。
- 支持异步加载,适合动态更新内容的项目。
缺点:
- 需要处理网络连接、超时和错误情况。
示例:
1 | IEnumerator LoadFromWeb() |
6. TextAsset
如果你需要加载文本文件,可以使用 TextAsset
。这种方式适用于加载 CSV 文件、JSON 文件等文本数据。
优点:
- 适合小型文本资源。
- 简单方便,适合脚本、配置文件等用途。
缺点:
- 不适合大型文本文件,因为它会将整个文件加载到内存中。
示例:
1 | TextAsset textAsset = Resources.Load<TextAsset>("myTextFile"); |
总结
在 Unity 中,选择合适的资源加载方式取决于你的项目需求。如果是简单的本地静态资源,Resources.Load
足够用;如果需要更复杂的资源管理或网络下载,可以使用 AssetBundle
、Addressable Assets
或 UnityWebRequest
。
在 Unity 中,如果你希望相机只看到指定的对象,可以通过以下几种方法来实现。以下方案包括使用图层和相机的层级裁剪(Culling Mask)。
1. 使用图层和相机的层级裁剪 (Culling Mask)
这是最直接的方法。你可以将目标对象分配到特定的图层,然后设置相机的 Culling Mask
以仅渲染该图层的对象。
步骤:
创建新图层:
- 在 Unity 的
Inspector
面板顶部,点击Layers
下拉菜单,选择Add Layer...
。 - 添加一个新图层(例如
TargetLayer
)。
- 在 Unity 的
将目标对象设置到该图层:
- 选择你希望相机看到的对象。
- 在
Inspector
面板的顶部,选择Layer
并将其分配到你刚创建的TargetLayer
。
设置相机的
Culling Mask
:- 选择相机对象。
- 在
Inspector
面板中,找到相机的Culling Mask
属性。 - 取消选中所有其他图层,只保留你想看到的目标图层(例如
TargetLayer
)。
这样,相机会只渲染你设置的图层中的对象,其他图层上的对象将不可见。
2. 使用多个相机
如果你需要更复杂的场景管理或效果(例如,一部分相机只显示指定对象,另一部分相机显示其他场景),可以使用多个相机,并分配不同的图层给每个相机。
步骤:
设置第一台相机:
- 让第一台相机只渲染目标对象的图层(例如
TargetLayer
),如上面描述的方式。 - 设置
Clear Flags
为Depth Only
或者Don't Clear
,以便其他相机会渲染不同的图层。
- 让第一台相机只渲染目标对象的图层(例如
设置第二台相机:
- 第二台相机渲染其他图层(如背景或环境),并将其
Culling Mask
设置为其他的图层。 - 确保它的
Depth
值小于第一台相机,这样它会在第一台相机之后渲染。
- 第二台相机渲染其他图层(如背景或环境),并将其
这种方法允许你创建复杂的渲染效果,比如前景只显示某些对象,而背景显示其他场景。
3. 使用遮挡剔除 (Occlusion Culling)
虽然这个方法不完全是让相机只看到某个对象,但可以通过遮挡剔除来优化场景中可见的内容。遮挡剔除会自动剔除被遮挡的对象,减少不必要的渲染。但请注意,这不保证相机只渲染指定的对象。
4. 使用自定义渲染管线
如果你使用的是自定义渲染管线(如 URP 或 HDRP),你可以通过编写自定义渲染设置来控制相机只渲染特定对象。
- 在自定义渲染管线中,创建一个渲染层或过滤器,使其只渲染特定的对象。
- 使用
RenderObjects
或类似的功能在渲染管线中指定需要渲染的对象。
总结:
最常见和有效的方法是使用图层和相机的 Culling Mask
来控制相机只渲染特定图层的对象。这不仅简单直接,而且在性能上也非常高效。
图形学
depth buffer 和 stencil buffer 有什么作用
在计算机图形学中,depth buffer
(深度缓冲区)和 stencil buffer
(模板缓冲区)是两个重要的缓冲区,用于处理图形渲染中的不同方面。
Depth Buffer(深度缓冲区)
- 作用:用于处理深度测试,决定哪些像素在前景,哪些像素在背景。
- 工作原理:每个像素都有一个深度值(即距离观察者的距离),在渲染场景时,深度缓冲区记录每个像素的深度值。当一个新像素被渲染时,它的深度值会与当前深度缓冲区中的值进行比较。如果新像素的深度值较小(表示更靠近观察者),则新像素会被渲染,并更新深度缓冲区;否则,新像素会被丢弃。
Stencil Buffer(模板缓冲区)
作用:用于控制像素的绘制,通过模板测试决定是否绘制某些像素。
工作原理:每个像素都有一个模板值。模板缓冲区用于存储这些模板值,通过模板测试来决定是否进行绘制操作。模板测试可以根据模板缓冲区中的值和当前渲染操作的模板值进行比较,从而允许或拒绝绘制操作。模板缓冲区常用于实现一些复杂的图形效果,例如遮罩、剪裁和图形叠加。
Depth Buffer:控制图像的深度,确保前景物体遮挡背景物体。
Stencil Buffer:控制图像的渲染区域,提供更复杂的像素处理和效果实现。