🟡: 代表个人还有一些理解上的问题
🟢: 代表自己面试中被问到过
🔴: 代表问题内容未完成

Unity 工作原理 & 脚本基础

Unity 引擎中哪些功能使用了 C#的反射功能?至少说出一点

通过反射,Unity 能够动态地访问和操作代码中的元数据,实现了很多自动化和灵活的功能,使得开发者在使用 Unity 进行游戏开发时能够更加方便和高效。

Inspector 窗口中显示的内容:Unity 通过反射来显示和编辑脚本中定义的字段和属性。在 Inspector 窗口中显示的内容是通过反射自动生成的,开发者不需要手动编写 Inspector 窗口的代码。

预设体文件:预设体(Prefabs)中的字段和属性值也通过反射进行存储和恢复。这使得在编辑器中可以方便地保存和加载复杂的对象状态。

场景文件:场景文件中对象的序列化和反序列化过程也依赖于反射。通过反射,Unity 可以在场景文件中保存对象的状态,并在加载场景时恢复这些状态。

Unity 中的各种特性(Attributes):Unity 使用自定义特性(如[Serializable]、[SerializeField]、[ContextMenu]等)来标记和控制类、字段和方法的行为。反射用于读取这些特性并执行相应的操作。

🟡 Unity 中生命周期函数的设计

Unity 中的生命周期函数,为什么设计为反射调用,而不是通过继承重写生命周期函数的形式去实现呢?(至少答出一种原因)

Unity 中生命周期函数设计为反射调用的原因有多种:

并非所有继承 MonoBehaviour 的类都需要使用所有生命周期函数。如果使用继承,就会有大量的空虚函数被调用,而使用反射,则只会调用已声明的生命周期函数。Unity 只需要维护有对应生命周期函数的脚本列表,就可以避免空虚函数的调用。

Unity 采用组件式设计,通过使用反射,可以将某些逻辑解耦,将组件的功能模块化,使得逻辑更加灵活和可复用。当触发一个生命周期时,需要通知相应 GameObject 的所有组件。如果使用继承多态来实现,则所有组件都要派生自包含对应生命周期的基类,或者是筛选出派生自此基类的组件逐一通知。这样一来容易带来复杂的继承关系,并且很麻烦,与组件式设计倡导的聚合代替继承的设计相悖。

Unity 提供插件和外部脚本支持,这些脚本可能不是在 Unity 中编写的,而是通过第三方库或外部工具生成的。为了支持这些脚本,可以使用反射机制调用其生命周期函数。

反射调用生命周期函数使得 Unity 更加灵活,并且能够更好地支持不同的开发场景和需求。

Unity 生命周期函数中的 OnEnable 和 Start,我们在使用时应该如何选择?

可以举例说明

Unity 中 yield return 的含义

请说明下面这些 yield return 的含义

1
2
3
4
5
6
yield return 数字;//表示在下一帧执行。
yield return null;//表示在下一帧执行。
yield return new WaitForSeconds(数字);//表示等待指定秒数后执行。
yield return new WaitForFixedUpdate();//表示等待下一个固定物理帧更新时执行。
yield return new WaitForEndOfFrame();//表示等待摄像机和GUI渲染完成后执行。
yield break;//表示跳出协程。

Unity 协同程序进行异步加载时的底层原理

使用 Unity 协同程序进行异步加载时,底层是否会使用多线程?

底层可能会使用多线程。协同程序的原理是分时分步完成指定逻辑。在其中的某一步骤中,是可以使用多线程来完成某些加载操作的。多线程加载完成后,再进入协同程序的下一步继续执行。

Unity 中 Awake 和 Start 两个生命周期函数,分别在什么时候被调用?

Awake:在运行时,当脚本被动态添加到对象上时立即被调用。当对象被实例化时,依附它的脚本会立即调用 Awake。它类似于构造函数。

Start:在第一次 Update 之前被调用。

Unity 场景中脚本生命周期函数执行顺序控制

Unity 场景上有多个对象,都分别挂载了 n 个脚本。我们如何控制不同脚本间生命周期函数 Awake 的执行先后顺序?

通过 Inspector 窗口:选中脚本文件,点击 Inspector 窗口右上角的 Execution Order(执行顺序)按钮。
通过 Project Settings 窗口:打开 Project Settings 窗口,选择 Script Execution Order 选项。

Unity 中多线程执行代码报错

Unity 中多线程执行下面哪些代码会报错?

A. Application.persistentDataPath
B. File.Exists(“文件名”)
C. transform.Translate
D. Object.Destroy(对象)
A、C、D

解释:

A. Application.persistentDataPath
Application.persistentDataPath 返回持久化数据的路径,通常用于读写文件。在多线程环境中,对文件系统的访问可能导致竞态条件或不确定的行为,因此在多线程中使用会报错。

C. transform.Translate
transform.Translate 用于移动游戏对象的位置。在多线程环境中,直接操作游戏对象的 transform 可能会导致不可预测的结果,因为 Unity 的 API 大多不是线程安全的。

D. Object.Destroy(对象)
Object.Destroy 用于销毁游戏对象。在多线程环境中,直接操作游戏对象的销毁可能会导致不确定的结果,因为 Unity 的销毁操作通常需要在主线程执行,而不是在后台线程执行。

🟢🟡 请简述 Unity 中协程的原理

Unity 中协程是一种特殊的函数,能够在一段时间内挂起执行,然后在之后的某个时间点恢复执行。

协程函数本体(迭代器函数):

协程函数本体是指用于实现协程逻辑的函数,通常使用 C# 中的迭代器函数来定义。这些函数通过 yield 关键字来暂停执行,并在之后的某个时间点继续执行。

协程调度器(协程管理器):
协程调度器是 Unity 的一个内置系统,用于管理所有的协程。它负责协调协程函数的执行顺序和时间点,以确保它们能够按照预期的方式执行。
协程调度器会根据迭代器函数的返回值来决定下一次执行函数逻辑的时间点,从而实现逻辑的分时分步执行。
综上所述,Unity 中的协程利用迭代器函数的分步执行特性,结合协程调度器对协程函数的统一管理,根据迭代器函数的返回值来决定下一次执行函数逻辑的时间点,从而实现逻辑分时分步执行的目的。这种机制使得开发者能够编写出简洁、清晰、高效的异步逻辑代码,提升了游戏的性能和用户体验。

Unity 多线程

Unity 是否支持写成多线程程序?如果支持的话需要注意什么?

Unity 支持多线程编程,但在使用多线程时需要注意:

主线程访问限制:只能从主线程访问 Unity 相关组件、对象以及 UnityEngine 命名空间中的绝大部分内容。直接在多线程中操作这些对象会导致不可预测的行为或崩溃。
例如,不能在非主线程中直接修改 GameObject、Transform 或调用 UnityEngine 的 API。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
using UnityEngine;
using System.Threading;

public class MultiThreadExample : MonoBehaviour
{
private void Start()
{
// 创建并启动一个新的线程
Thread thread = new Thread(ThreadedMethod);
thread.Start();
}

private void ThreadedMethod()
{
// 错误示例:直接在非主线程中访问Unity对象
// 这会导致错误,因为只能在主线程中访问UnityEngine命名空间中的内容
// transform.position = new Vector3(0, 0, 0); // 错误用法

// 正确示例:使用主线程来访问Unity对象
// 可以使用委托和Invoke方法在主线程中执行
UnityMainThreadDispatcher.Instance().Enqueue(() => {
transform.position = new Vector3(0, 0, 0); // 正确用法
});
}
}

数据同步:如果多线程中要与 Unity 主线程同时修改一些数据,需要通过 lock 关键字进行加锁,确保线程安全。
这样可以避免多个线程同时访问和修改共享数据时产生的竞争条件和数据不一致问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
using UnityEngine;
using System.Threading;

public class DataSyncExample : MonoBehaviour
{
private int sharedData;
private readonly object dataLock = new object();

private void Start()
{
// 创建并启动一个新的线程
Thread thread = new Thread(ThreadedMethod);
thread.Start();
}

private void ThreadedMethod()
{
for (int i = 0; i < 10; i++)
{
// 使用lock关键字确保线程安全
lock (dataLock)
{
sharedData++;
Debug.Log("ThreadedMethod: " + sharedData);
}
Thread.Sleep(100);
}
}

private void Update()
{
// 使用lock关键字确保线程安全
lock (dataLock)
{
sharedData++;
Debug.Log("Update: " + sharedData);
}
}
}

🟢 在 Unity 中,什么是协程(Coroutine)它有什么作用,以及如何使用它

协程(Coroutine)是一种在 Unity 中常用的编程技术,用于在运行时控制代码的执行顺序。协程可以将代码执行分为多个阶段,可以在其中暂停和恢复代码执行,从而实现异步执行和任务管理等功能。

协程的主要作用包括:

延迟执行:协程可以延迟执行某个任务,从而在指定时间后执行相应的操作。

异步执行:协程可以在后台执行任务,从而避免卡顿和阻塞主线程。

任务管理:协程可以管理多个任务,从而实现更灵活和可控的代码执行顺序。

在 Unity 中,协程的使用非常简单,可以使用 C#的 yield 语句来实现协程。以下是一个使用协程延迟执行某个操作的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using UnityEngine;
using System.Collections;

public class CoroutineExample : MonoBehaviour
{
IEnumerator DelayCoroutine(float delayTime)
{
yield return new WaitForSeconds(delayTime);
Debug.Log("Delayed log after " + delayTime + " seconds");
}

void Start()
{
StartCoroutine(DelayCoroutine(3.0f));
}
}

以上代码将会在 3 秒后输出一条日志信息。通过使用协程,我们可以方便地控制代码执行顺序和实现异步执行等功能。

Application.streamingAssetsPath 和 Application.persistentDataPath

两个路径有何区别?对于我们的意义是什么?
Application.streamingAssetsPath:这个路径指向的是只读的 StreamingAssets 文件夹。这个文件夹下的内容会随着构建(Build)一起打包到最终的应用程序中。因此,这些文件在运行时是只读的。适合存放一些默认的、不需要修改的资源文件,比如配置文件、初始配置等。

Application.persistentDataPath:这个路径指向的是应用程序的持久化数据路径,可以读写。在运行时,这个路径下的文件是可以被应用程序读取和写入的,并且会在应用程序重新启动后保留。
适合处理数据的持久化,比如保存用户设置、保存游戏进度、缓存下载内容等。同时也可以作为热更新下载内容的存放目录,因为它允许应用程序在运行时对其中的文件进行修改和更新。

🟡 Unity 底层如何处理 C# 代码?

在 Unity 中,处理 C#代码的底层机制主要有两种方案:Mono 和 IL2CPP

Mono 是一个开源的跨平台实现的.NET Framework。Unity 在早期版本中主要使用 Mono 来处理 C# 代码。适用于开发周期中频繁调试和快速迭代的场景。

工作原理:

  • 编译:C#代码首先被编译成中间语言(Intermediate Language, IL)。
  • 运行:Mono 运行时将 IL 代码即时编译(JIT)成本地机器代码,然后在目标平台上执行。

优点:

  • 跨平台:支持多种平台,开发和调试效率高。
  • 即时编译:允许动态代码生成,适合快速开发和调试。

缺点:

  • 性能:相比于预先编译的本地代码,运行时性能稍逊,尤其在移动平台上。

IL2CPP(Intermediate Language To C++)是 Unity 提供的一种高级编译技术,将 IL 代码转换为 C++代码,再编译成本地机器代码。适用于性能要求高、发布构建的场景,如移动设备和控制台游戏的最终发布版本。

工作原理:

  • 编译:C#代码首先被编译成 IL 代码。
  • 转换:IL 代码被 IL2CPP 工具转换为 C++代码。
  • 编译:生成的 C++代码通过平台原生的 C++编译器编译成本地机器代码。

优点:

  • 性能:生成的本地机器代码性能优越,适合性能要求较高的场景,尤其是在移动和控制台平台上。
  • 安全性:转换为 C++代码可以提高代码的安全性和难以逆向工程的复杂性。

缺点:

  • 编译时间:转换和编译过程较为耗时,影响开发周期中的构建时间。
  • 调试难度:由于转换为 C++代码,调试复杂性增加,不如 Mono 方便。

🟢Unity 中动态加载资源的方式

Resources 类:使用 Resources 类中的相关方法加载 Resources 文件夹下的资源。

1
GameObject prefab = Resources.Load<GameObject>("Prefabs/MyPrefab");

AssetBundle 类或 Addressables 类:使用 AssetBundle 类中的相关方法加载 AB 包(Asset Bundle)中的资源。使用 Addressables 类中的相关方法加载地址化资源(Addressables)。

1
2
AssetBundle assetBundle = AssetBundle.LoadFromFile("path/to/assetbundle");
GameObject asset = assetBundle.LoadAsset<GameObject>("MyAsset");

UnityWebRequest 类:使用 UnityWebRequest 类中的相关方法加载本地或远端资源。

1
2
3
UnityWebRequest request = UnityWebRequest.Get("http://example.com/texture.png");
yield return request.SendWebRequest();
Texture2D texture = DownloadHandlerTexture.GetContent(request);

C#原生文件加载:使用 C#原生的一些文件加载相关类,如 File、FileStream 等。

1
byte[] bytes = File.ReadAllBytes("path/to/file");

Unity 底层是单线程还是多线程

Unity 工程文件中,meta 后缀的文件中主要存了什么信息?(最少说出 2 点)

Unity 当中存在多线程时,继承 MonoBehaviour 的脚本是否有必要对其中内容加锁?为什么?

Unity 中鼠标、键盘、触屏、手柄等输入事件会在 Update 之前、还是之后、还是同时执行?

在 Unity 中,输入事件的处理优先级较高,它们会在每一帧开始时立即触发。这意味着输入事件的处理会在 Update 方法之前执行,因此我们可以在 Update 方法中立即响应用户的输入操作。

在 Unity 中使用指针

想要在 Unity 中使用指针我们需要进行哪些操作?

在 Player Settings 中的 Other Settings 中勾选 Allow ‘unsafe’ code 选项。使用指针时,必须在 unsafe 修饰的代码块中编写相关代码。
在 C#中,使用指针通常被认为是一种不安全的操作,因为它们可以直接访问内存地址,可能会导致内存泄漏或潜在的安全漏洞。因此,默认情况下,C#是禁止使用指针的,需要显式开启 unsafe 选项,并在代码中使用 unsafe 关键字来标记包含指针操作的代码块。

Unity 核心系统 & 重要组件

Unity 中当一个细小高速物体撞击另一个较大物体时,会出现什么情况?如何避免?

当一个细小高速物体(例如子弹)在短时间内移动了很大的距离,Unity 的物理引擎可能会错过碰撞检测,从而导致该物体穿过另一个较大物体,而不是发生碰撞。这种情况在需要精确碰撞检测的应用场景(例如射击游戏)中尤为明显。这是因为物体在每一帧之间移动的距离太大,导致物理引擎无法正确检测到碰撞。

尽量用射线检测来替代细小物体的物理系统碰撞。射线检测通过发射一条射线,并检查射线是否碰撞到任何物体,来模拟高速物体的运动和碰撞。射线检测在传统的 FPS 游戏中非常常用,因为它可以精确地判断高速物体的碰撞和伤害。

1
2
3
4
5
6
7
8
9
10
11
12
13
void Update()
{
// 定义射线
Ray ray = new Ray(transform.position, transform.forward);
RaycastHit hit;

// 检测射线是否碰撞到物体
if (Physics.Raycast(ray, out hit, 100f))
{
Debug.Log("碰撞到物体: " + hit.collider.name);
// 在这里处理碰撞逻辑,例如计算伤害
}
}

修改 Rigidbody 参数:

修改 Rigidbody 刚体中的 Interpolate(插值)和 CollisionDetection(碰撞检测)两个参数,以提高碰撞检测的准确性。

Interpolate:将 Rigidbody 的 Interpolate 属性设置为 Interpolate 或 Extrapolate,可以在物体之间进行平滑插值,减少帧间移动的跳跃现象。

CollisionDetection:将 Rigidbody 的 CollisionDetection 属性设置为 Continuous 或 Continuous Dynamic,可以提高物体在高速运动时的碰撞检测精度。

1
2
3
4
5
6
7
8
9
10
void Start()
{
Rigidbody rb = GetComponent<Rigidbody>();

// 设置插值模式
rb.interpolation = RigidbodyInterpolation.Interpolate;

// 设置碰撞检测模式
rb.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic;
}

Unity 中判断两个 2D 矩形是否相交

请至少说出两种方式

方法 1:使用 Unity 物理系统进行碰撞检测,Unity 的 2D 物理系统中的 Collider2D 组件和 Rigidbody2D 组件来进行碰撞检测。

1
2
3
4
5
6
7
8
9
10
11
12
13
using UnityEngine;

public class CollisionCheck : MonoBehaviour
{
// 碰撞检测
void OnCollisionEnter2D(Collision2D collision)
{
if (collision.gameObject.CompareTag("OtherRectangle"))
{
Debug.Log("两个矩形相交了!");
}
}
}

方法 2:使用 Unity 中范围检测相关 API,Unity 提供了一些用于检测范围重叠的 API,比如 Rect.Overlaps 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using UnityEngine;

public class RangeCheck : MonoBehaviour
{
public Rect rect1;
public Rect rect2;

void Start()
{
if (rect1.Overlaps(rect2))
{
Debug.Log("两个矩形相交了!");
}
}
}

Transform 中的本地坐标系的方向 转换 为相对世界坐标系的方向 的两个 API

TransformVector() 和 TransformDirection()
有什么区别?

Unity 中摄像机组件中的投影(Projection)参数

其中的两个选项透视投影(Perspective)和 正交投影(Orthographic)有什么区别?

Unity 中如果想要改变物理系统中默认重力的方向或大小,应该在哪里修改?

Unity 制作物理游戏相关功能时,我们采用哪种方式处理位移?为什么?

Unity 性能优化

Unity 中动态批处理和静态批处理的区别?

在 Unity 中,有时在第一次执行 GameObject.Instantiate 的时候有明显卡顿,该怎么解决?

在 Unity 中如何控制渲染优先级?(谁先渲染谁后渲染,分情况回答)

我们在编写代码时,有什么常用的优化代码性能的手段?至少说出 3 点

DrawCall 优化

什么是 DrawCall?DrawCall 为什么会影响游戏运行效率?如何减少 DrawCall?

什么是 DrawCall?

每次 CPU 准备渲染相关数据并通知 GPU 的过程称为一次 DrawCall。简而言之,DrawCall 是 CPU 向 GPU 发出绘制命令的过程,涉及渲染数据的准备和传输。

DrawCall 为什么会影响游戏运行效率?

如果 DrawCall 次数较高,意味着 CPU 需要花费更多的时间准备和传输渲染数据,这会增加 CPU 的负担,进行更多的计算。高频次的 DrawCall 会导致 CPU 瓶颈,进而影响游戏的整体运行效率,可能导致帧率下降、游戏卡顿等性能问题。

如何减少 DrawCall?

2D 和 UI 层面:

  • 打图集:将多个小纹理合并成一个大纹理,可以减少纹理切换的次数,从而减少 DrawCall。
    层级优化:在面板中,不同图集的图片的层级不要穿插,保持同一图集内的图片连续渲染,避免不必要的图集切换。

3D 模型层面:

  • 动态批处理(Dynamic Batching):对于小且相似的网格,Unity 可以在运行时将多个物体的网格合并成一个网格,以减少 DrawCall。需要注意的是,动态批处理对顶点数量有限制,适用于小型网格。
  • 静态批处理(Static Batching):对于不移动的静态物体,可以在场景构建时将多个物体的网格合并成一个网格。静态批处理没有顶点数量限制,但要求物体在运行时不移动。
  • 光照和阴影优化:尽量不使用实时光照和实时阴影,因为它们会增加额外的 DrawCall。可以使用预计算的光照贴图(Lightmap)和烘焙阴影来代替。

其他优化手段:

  • 合并网格:手动将多个小的网格合并成一个大网格,减少 DrawCall。
  • 减少材质数量:使用少量的共享材质,避免频繁切换材质。
  • LOD(Level of Detail):对于复杂的 3D 模型,可以根据距离使用不同的模型细节级别,远处使用低细节模型,近处使用高细节模型,减少渲染负担。

请介绍一些在 Unity 中减少内存的方法。(至少说出 3 种方法)

请介绍一些在 Unity 中提升性能的方法(至少说出 5 种方法)

如何在 Unity 中进行多线程编程以提高性能?(至少说出 3 点可以使用多线程提高性能的内容)

游戏开发完成后 1.游戏运行卡顿、设备发热一般往哪个大方向进行排查? 2.游戏运行一段时间后闪退,一般往哪个大方向进行排查?

游戏项目中,运行时主要占内存的内容有哪些?(至少说出 5 点)

游戏项目中,主要消耗性能的内容有哪些?(至少说出 3 点)

游戏在设备上运行时,出现崩溃闪退现象,排查方向有哪些?(至少说出 2 点)

游戏在设备上运行时,出现卡顿掉帧现象,排查方向有哪些?(至少说出 2 点)

在进行游戏开发时,我们一般如何测试项目在目标设备的最大内存上限和性能上限?

在 Unity 中,什么是常见的性能优化技巧?请举例说明。

LOD(多细节层次)和 MipMap(纹理图)的作用是什么?

LOD(多细节层次)和 MipMap(纹理图)的作用是优化游戏性能。在不同距离渲染对象时,使用的是质量不同的模型和贴图。一般情况下,随着距离的增加,采用的模型的面数越低,贴图的尺寸也越小。这样可以在保证画面质量的前提下,减少需要渲染的面数和贴图尺寸,从而提高游戏的性能。

在没有使用遮挡剔除的情况下,图中 A 和 B 都是默认标准材质

图中的小球最终是否会被渲染,是否会产生 DrawCall

图中的小球是否被渲染了?是否会产生 DrawCall?

Unity 中如何解决过多创建和删除对象带来的卡顿问题?

  1. 使用协程(Coroutine):通过协程实现分时分步的对象创建或删除,避免一帧中处理过多对象。分时分步处理,减少单帧中的工作量,降低卡顿风险。可以在协程的每一步中加入一定的等待时间,以便让主线程有足够的时间处理其他任务,提高游戏的流畅度。

  2. 使用对象池(Object Pool):事先创建一批对象并保存在池中,需要时从池中取出,用完后再放回池中重复利用,而不是频繁地创建和销毁对象。减少了对象的创建和销毁次数,节省了资源和时间。可以避免频繁的内存分配和回收,减少了 GC(垃圾回收)的压力,提高了性能。

第一次执行 GameObject.Instantiate 时可能出现明显的卡顿

如何解决该问题?

程序优化:

资源加载优化:使用 Unity 自带的性能分析工具 Profiler 分析实例化时造成卡顿的具体原因,通常是由于资源加载引起的。在进入场景时,进行资源预加载,尤其是较大的资源,可以提前或者分帧加载,避免实时加载导致的卡顿。

脚本初始化优化:实例化对象时,会同步执行其身上挂载的所有脚本的初始化工作。避免在 Awake 和 Start 方法中做较复杂的逻辑,或者将复杂逻辑提前或者分帧处理,以减少实例化时的初始化开销。

对象缓存池:对于会频繁使用的对象,可以使用对象池进行管理。对象池可以避免频繁创建和销毁对象,从而减少卡顿。

美术优化:

资源消耗优化:美术制作时,要根据项目的实际情况设定模型的骨骼数、面数以及贴图的数量和大小上限。不能只追求好的美术效果,而不考虑资源的消耗。

粒子特效优化:制作粒子特效时,要尽量减少粒子数、粒子面积、贴图等资源的使用,保持资源的轻量化,以减少性能消耗。

10000 个 monobehavior,每个各自执行 update,和放到一个 update 里执行,哪个效率更高?

为什么?

Unity 中的 Destroy 和 DestroyImmediate 的区别是什么?

在 Unity 中,Destroy 和 DestroyImmediate 方法都用于销毁对象,但它们之间有几个关键区别:

Destroy 方法:

可以指定删除的延迟时间,如果第二个参数不填写,最快也会在下一帧前完成删除。
实际的对象销毁操作始终延迟到当前更新循环结束,但始终在渲染前完成。
即使调用了 Destroy 方法,对象也不会立即变为 null。因此,如果在 Destroy 对象后立即检查该对象是否为 null,它仍然可能不为 null。

DestroyImmediate 方法:

会立即销毁删除对象,而不考虑当前帧的更新循环。
一旦调用 DestroyImmediate 方法,对象立即被标记为删除状态,并在当前帧的结束时立即销毁。
在 DestroyImmediate 调用后,对象将立即变为 null,因此可以立即检查对象是否已销毁。
这两种方法适用于不同的情况。如果需要在稍后的某个时间点销毁对象,并希望避免在同一帧中多次销毁大量对象而导致性能问题,则应使用 Destroy 方法。而如果需要立即销毁对象,或者在编辑器中进行一些特定操作时,可以使用 DestroyImmediate 方法。

Unity 中的对象销毁与空引用判断

请问最终打印的 s 的结果为?

1
2
3
4
5
6
7
8
9
10
11
12
string s = string.Empty;
GameObject go = new GameObject();
DestroyImmediate(go);
if (!go)
s += "A";
if (go is null)
s += "B";
if (go == null)
s += "C";
if ((System.Object)go == null)
s += "D";
Debug.Log(s);

最终打印的 s 的结果为 AC。

DestroyImmediate 方法会立即将 GameObject 对象从场景上删除。

UnityEngine.Object 中对 ==、!=、! 进行了重载。如果用 !go 和 go == null 去判断对象是否为空,由于重载了,所以能够返回正确的结果 true 和 false。
但是本质上此时的 go 还不是真正意义上的 null,所以如果用 go is null 或者将其转换为万物之父 Object (System.Object) go == null 去判断时,并不会为 true。因此只会进入 AC 的 if 语句。

这里的重点内容就是 UnityEngine.Object 中重载了逻辑非 ! 和 ==、!= 运算符,因为使用它们来判断 null 是可以的,但是此时的 GameObject 在内部并不是真正意义的 null。我们在使用时最好手动置空。

我们在 Unity 中进行一些复杂逻辑处理时,比如网路通讯、寻路算法

往往会开启多线程进行处理。我们如何保证数据能够和 Unity 主线程进行正常交互?(请至少说出 1 种方式)

请问为什么延迟渲染路径能够优化有大量光源的场景渲染

Unity UGUI & 优化

我们应该如何优化 UI(基于 UGUI)

性能优化策略

  • 打图集:将同一画面的图片合并到一个图集中,以减少 DrawCall 的数量,提高渲染效率。
  • 避免交叉使用图片和文字:图片和文字尽量不要交叉使用,因为这样会增加额外的 DrawCall,降低性能。
  • 取消不必要的射线检测:在 UI 组件上取消勾选不需要进行射线检测的功能,减少额外的计算开销。
  • 减少透明图片的重叠:尽量避免透明图片的重叠使用,因为这会增加渲染负担,降低性能。

内存优化策略

  • 使用 9 宫格缩放:对于大图,尽量使用九宫格缩放,避免过于复杂的图片设计,以减少内存占用。
  • 图片的 RGBA 通道分离:对于一些颜色较为简单的图片,可以将 RGBA 通道分离,只使用其中的部分通道,以降低内存占用。

请写出 UGUI 中两种处理异形按钮的具体方法

我们在进行 UI 开发时,每个面板都会有很多控件(Button、Toggle、Slider 等等)

每新写一个面板逻辑,都会为这些控件做一些相同的事情,比如:声明控件、查找控件、监听控件等等
请问:我们应该如何提升我们的开发效率,让这些事情不用每次都去做?(至少说出两种方案)

如何为 UGUI 中的某一个控件添加自定义事件监听(比如为一个 Image 添加点击事件)

Unity 中 DrawCall、Batches、SetPass Calls 的意思是什么

设计模式

简述对象池

简述一下对象池,在游戏开发中我们什么时候会用到它?

对象池(Object Pool)是一种优化内存管理的设计模式,主要作用是:

  • 避免大量创建和释放对象时造成的内存消耗:在游戏开发中,频繁地创建和销毁对象会导致频繁的垃圾回收(GC),从而影响游戏性能。对象池通过将不再使用的对象存放在池中,下次需要时再取出使用,避免了反复创建和销毁对象的开销。
  • 降低 GC 发生的频率:由于对象池复用已有对象,减少了垃圾的产生,从而降低了 GC 的频率。通过占用一些内存来存储闲置对象,从而避免更多的内存消耗和 GC 的发生,提高游戏的整体性能和流畅度。 2. 游戏开发中的使用场景

对象池在游戏开发中的应用非常广泛:

频繁创建对象的地方:

  • 子弹:射击类游戏中,子弹对象的创建和销毁非常频繁。使用对象池可以复用子弹对象,减少开销。
  • 特效:例如爆炸、火焰等特效对象,通常在游戏中会大量使用,复用这些对象可以提高性能。
  • UI 元素:例如伤害数字、提示信息等短时间内频繁出现和消失的 UI 元素,使用对象池可以避免反复创建和销毁 UI 对象。

后端开发:

  • 线程池:服务器端为了处理大量并发请求,通常会使用线程池来管理线程。线程池可以复用线程,避免频繁创建和销毁线程的开销。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
using System.Collections.Generic;
using UnityEngine;

public class Bullet : MonoBehaviour
{
// 子弹行为的相关代码
}

public class BulletPool : MonoBehaviour
{
public GameObject bulletPrefab;
private Queue<GameObject> bulletPool = new Queue<GameObject>();
public int poolSize = 20;

void Start()
{
// 初始化对象池
for (int i = 0; i < poolSize; i++)
{
GameObject bullet = Instantiate(bulletPrefab);
bullet.SetActive(false);
bulletPool.Enqueue(bullet);
}
}

public GameObject GetBullet()
{
if (bulletPool.Count > 0)
{
GameObject bullet = bulletPool.Dequeue();
bullet.SetActive(true);
return bullet;
}
else
{
// 如果池中没有可用的子弹,创建新的子弹
GameObject bullet = Instantiate(bulletPrefab);
return bullet;
}
}

public void ReturnBullet(GameObject bullet)
{
bullet.SetActive(false);
bulletPool.Enqueue(bullet);
}
}

游戏中的成就系统,我们一般会使用设计模式中的哪种模式来制作?为什么?

在游戏开发中,实现成就系统时一般会使用观察者模式(Observer Pattern)。

松耦合:观察者模式可以实现对象之间的松耦合关系。成就系统中,成就的达成不应该直接依赖于其他系统的状态,而是应该通过观察者模式,让成就系统作为观察者,观察游戏中各种事件的发生,从而实现对应的成就解锁。

扩展性:观察者模式具有很好的扩展性,如果需要新增一种成就或者修改成就的条件,只需要添加或者修改相应的观察者,而不需要修改被观察者或者其他模块的代码。

可维护性:观察者模式使得系统中的各个模块之间的关系更加清晰明了,易于维护和管理。

在 Unity 中使用 C# 中的委托和事件来实现观察者模式。比如,游戏中的各种事件(如玩家击杀敌人、完成任务等)可以定义为事件,而成就系统则订阅这些事件,当事件发生时,成就系统就会收到通知并做出相应的处理,例如解锁对应的成就。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
using UnityEngine;
using System;

public class AchievementSystem : MonoBehaviour
{
// 定义事件
public event Action<string> OnAchievementUnlocked;

// 模拟事件触发
public void PlayerKilledEnemy()
{
// 触发事件
OnAchievementUnlocked?.Invoke("Killer");
}
}

public class AchievementUI : MonoBehaviour
{
void Start()
{
AchievementSystem achievementSystem = FindObjectOfType<AchievementSystem>();
achievementSystem.OnAchievementUnlocked += UnlockAchievement;
}

// 事件处理函数
void UnlockAchievement(string achievementName)
{
Debug.Log("解锁成就:" + achievementName);
// 在 UI 中显示解锁的成就
}
}