文章目录

C#语言

堆和栈

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

拆箱和装箱

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

反射实现原理

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

List与数组的区别

数组(Array)的特点:

  • 静态大小: 数组在创建时需要指定大小,且大小不能更改。例如,int[] numbers = new int[3]; 创建了一个包含3个整数的数组。

  • 类型固定: 数组中的元素类型是固定的,无法在运行时更改。

  • 直接访问元素: 数组提供了通过索引直接访问元素的方式,例如,int value = numbers[1];。

  • 性能: 由于数组大小固定,可以在内存中分配连续的空间,因此在某些情况下可能具有更好的性能。

List 的特点:

  • 动态大小: List 是动态数组,可以根据需要动态调整大小,通过 Add、Remove 等方法进行操作。
  • 泛型支持: List 是泛型集合,可以存储任意类型的元素,例如,List numbers = new List();。
  • 灵活性: 提供了丰富的方法和属性,如 Add、Remove、Count 等,使操作更加灵活。
  • 类型灵活: List 可以存储不同类型的元素,而数组中的元素类型是固定的。

比较:
灵活性: List 更灵活,可以根据需要动态调整大小,而数组的大小是固定的。

类型灵活性: List 可以存储不同类型的元素,而数组要求所有元素是相同类型的。

性能: 数组在某些情况下可能具有更好的性能,因为它们在内存中分配连续的空间,而 List 的元素可能分散在堆上的不同位置。

直接访问 vs 方法调用: 数组通过索引直接访问元素,而 List 则需要通过方法调用来进行操作。

总体而言,如果需要静态大小和直接访问元素,并且元素类型相同且固定,数组可能更合适。如果需要动态大小、灵活性和可以存储不同类型的元素,List 则是更好的选择。在实际应用中,选择取决于具体的需求和性能要求。

C#委托与事件

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

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

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

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

射线检测的基本原理

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

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

协程

协程,即为协同程序. Unity中的协程由协程函数和协程调度器两部分构成.协程函数使用的是C#的迭代器, 协程调度器则利用了MonoBehaviour中的生命周期函数来实现. 协程函数实现了分步, 协程调度器实现了分时.

因为协程分时分步执行的特性,当多个协程的耗时操作挤在同一时间执行也会造成卡顿。

我们先来实现一个最简单的协程

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class lalal : MonoBehaviour
{
    
    IEnumerator CoroutineFunc()
    {
        Debug.Log("第一次进入");
        yield return null;
        Debug.Log("第二次进入");
        yield return null;
    }
    void Start()
    {
        //获取迭代器接口
        IEnumerator enumerator = CoroutineFunc();
        //返回corouine对象保存起来可以用于停止协程
        Coroutine corouine = StartCoroutine(enumerator);

        StopCoroutine(corouine);

        Coroutine corouine1 = StartCoroutine(enumerator);
        StopCoroutine(corouine1);
    }
}

常用返回值

含义代码
下一帧再执行后续代码yield return null; yield retun x(x代表任意数字)
结束该协程yield break;
等待固定时间执行后续代码yield return new WaitForSeconds(0.3f);yield return new WaitForSecondsRealtime(0.3f); //不受timescale影响

协程与多线程联系与区别

协程多线程
切换时机自定CPU时间片为单位的系统调度
CPU核心与主线程在同一核心根据操作系统调度
对主线程的影响卡顿会影响主线程卡死不会影响主线程
线程同步问题不存在线程同步问题需要i注意线程同步问题
线程开销不存在线程开销存在线程创建、销毁、切换的开销
书写方式与普通函数一致回调函数

首先执行协程函数,但不会阻塞当前线程,而是返回一个Coroutine对象。
然后,Coroutine对象被加入到协程队列中,等待执行。
每一帧Unity引擎都会执行协程队列中的协程,如果该协程没有被暂停,则会一直执行到协程结束,直到下一帧才会执行下一个协程。
如果该协程被暂停,那么该协程会被挂起,等待下一帧继续执行。
协程函数的缺点主要是代码可读性差,逻辑复杂时难以维护。此外,协程函数在多线程操作中可能会导致不可预测的结果,需要谨慎使用。

协程的执行

在yield waitforsecond的期间,如果读秒还在继续,程序的控制权会交给Unity,只有阻塞结束了,并且到下一个Update时执行权才会交给协程
我们来看这段代码

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class lalal : MonoBehaviour
{
    void Update()
    {
        Debug.Log(1);
        StartCoroutine(f1());
        Debug.Log(3);
    }
     void LateUpdate()
    {
        Debug.Log(5);
    }

    IEnumerator f1()
    {
        Debug.Log(2);
        yield return null;
        Debug.Log(4);
    }

}

第一帧没有输出4,因为return了,在下一帧yield return null执行了4,然后又执行了lateupdate中的5
在这里插入图片描述

协程的注意点

  • 开启协程时如果用字符串调用,那么关闭时也要用字符串调用
  • 如果想让协程效果停止,不能禁用脚本,而是要销毁脚本组件,或禁用游戏对象组件,或销毁游戏对象

序列化

序列化是将数据结构或对象转换为可在网络上传输或持久化到磁盘的格式的过程。在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 中,这样的设计允许开发人员传入任意数量的参数,从而方便调试信息的输出。

可空变量

可空变量(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后面的代码直接处理其结果
使用 foreach 循环迭代执行 Execute 方法,并输出每个 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 函数都被调用后执行,用于开始游戏。
因此,它们的先后顺序为 Awake->OnEnable -> Start。需要注意的是,如果脚本所在的游戏对象已经处于激活状态,那么只会执行 Awake 和 Start 函数。如果脚本所在的游戏对象在 Awake 函数调用后变为非激活状态,那么 Start 函数不会被调用,只有当它重新激活时才会执行

说说prefab

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

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

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

场景中Static对象有什么作用

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

Unity实现移动有哪些方法

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

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

游戏开发

Unity的执行顺序

在这里插入图片描述

游戏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应用到角色上。

Unity中点乘和叉乘对我们来说作用是什么

点乘:可以用于计算投影、判断两个向量是否平行(夹角)、计算光照
叉乘:判断一个点是否在三角形内部,获取平面的法向量

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

A. Application.persistentDataPath

B. File.Exists("文件名")

C. transform.Translate

D. Object.Destroy(对象)

ACD,UnityEngine命名空间中相关类基本都不能被Unity多线程使用

Application.streamingAssetsPath 和 Application.persistentDataPath两个路径有何区别?对于我们的意义是什么?

Application.streamingAssetsPath只读
Application.persistentDataPath可读可写
Application.streamingAssetsPath适合放置一些默认的二进制文件
Application.persistentDataPath用于处理数据持久化或是热更新下载内容的存放目录,因为它可读可写

Unity中协程的原理

Unity中的协同程序分为两部分
1.协程函数本体(迭代器函数)
2.协程调度器(协程管理器)
协程利用迭代器函数的分步执行的特点
协程调度器对迭代器函数们进行统一管理
根据迭代器函数的返回值来决定下一次执行函数逻辑的时间点
从而实现逻辑分时分步执行的目的
将协程函数的返回值类型定义为IEnumerator,然后使用yield语句暂停协程的执行,并在需要恢复执行时通过MoveNext()方法继续执行。

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

Mono和IL2CPP
Mono:
Mono是Unity早期使用的默认工具,它是一个跨平台的.NET运行时环境,负责解释C#代码并将其转换为本地机器代码。
在运行时,Mono即时(JIT)编译IL代码,并将其转换为适用于特定平台的本地机器代码。
优点是跨平台性和快速开发,但有时在性能方面可能存在一些限制。
IL2CPP(Intermediate Language to C++):

IL2CPP是Unity的另一种选项,它将C#代码编译成C++代码,而不是像Mono那样将其编译成本地机器代码。
IL2CPP通过将C#代码转换为C++代码,然后使用本地平台的编译器(如Visual Studio编译器)将其编译成本地机器代码。
这种方法的优点是提高了应用程序的性能和稳定性,因为它使用了底层操作系统和硬件的优化,而且不再依赖于Mono运行时

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

可能会反弹,可能会相互推动
我们可以尽量用射线检测来替代细小物体的物理系统碰撞,因为传统的FPS游戏都是通过射线检测加模拟计算来判断伤害的
或者修改Rigidbody中的插值和碰撞检测参数,提高碰撞检测的准确性

请简述一下Prefab(预制体)的本质是什么?

Prefab的本质就是一个配置文件

其中记录了一个GameObject对象上挂载的脚本信息
并且记录了脚本信息中的可配置的属性信息

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

1.只能从主线程访问Unity相关组件、对象以及
UnityEngine命名空间中的绝大部分内容
2.如果多线程中要和Unity主线程同时修改一些数据
可以通过lock关键词加锁

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

  1. 对象池的主要作用是 避免大量创建对象再释放对象时造成的内存消耗,可以有效降低GC发生的频率。我们把不用的对象放入对象池中而不是让它直接变为垃圾,下次需要用到对象时再从对象池中获取,我们通过占用内存来避免更多的内存消耗和GC的发生

  2. 在游戏中频繁创建对象、实例化对象的地方,都可以用到对象池。
    比如前端开发中,游戏中的子弹、伤害字体、特效等等
    比如后端开发中线程池等等

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

每次CPU准备渲染相关数据并通知GPU的过程称为一次DrawCall

  • 2D和UI层面:打图集,并且注意面板中不同图集图片的层级不要穿插
  • 3D模型层面:利用动态批处理和静态批处理,尽量不使用实时光照和实时阴影 等等
  • 动态批处理是将多个具有相同材质和网格的游戏对象合并为一个批次,以减少渲染调用的数量。静态批处理是在编辑器中预先计算和合并游戏对象的批次,通常应用于不会经常变化的对象,如地形、静态装饰物等。

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

可以通过协同程序,分时分步创建或删除
原理是避免一帧中处理太多对象

请简述热更新的流程

  1. 下载资源服务器中的对比文件
  2. 将下载下来的远端对比文件和本地的做对比,记录需要更新的资源和要移除的资源
  3. 根据第二步中记录的信息,进行资源下载和移除
  4. 更新本地对比文件,其内容和刚才下载的远端对比文件一致

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

性能上

  1. 打图集,将同一画面的图片放入一个图集中,目的是减少DrawCall
  2. 面板中的图片和文字尽量不要交叉,因为这样会产生多余的DrawCall
  3. 取消勾选不必要的射线检测,UI组件上的
  4. 减少透明图片的重叠使用

内存上

  1. 大图尽量使用9宫格缩放,让美术设计UI面板底图时不要过于复杂
    尽量是有规律的纹理和颜色变化
  2. 图片的RGBA通道分离(方便并行处理)

单机游戏中,我们如何避免玩家利用修改器修改客户端数据?(至少说出2点方案)

  1. 数据加密,对存档数据和内存中关键数据进行加密
  2. 检测修改器,运行时检测常见修改器,警告或强行退出游戏
  3. 服务器验证存储

网络游戏中,我们如何避免外挂的产生?(至少说出2点方案)

  1. 服务器验证,尽量将游戏逻辑和关键数据通过服务器进行处理
  2. 通讯数据加密,避免数据被篡改
  3. 客户端和服务器进行频繁的数据验证
  4. 使用反外挂服务,利用第三方的一些定制反外挂软件和服务来检测和组织常见外挂工具
  5. 玩家行为数据行为分析,识别异常数据和行为进行封号等惩罚

lua语言中的 upvalue 是什么?

upvalue是一个闭包概念
被局部函数所捕获的外部变量就叫upvalue
一般情况下,一个函数的外部作用域中定义了一个变量,但是其被该函数所引用了
那么这种变量就被称为upvalue

function outerFunction()
  local outerVariable = 10

  return function innerFunction()
    print(outerVariable) -- 这里的 outerVariable 是 UpValue
  end
end

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

UGUI中的EventTrigger

EventTrigger

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

方案一:制作自动生成代码的工具,将声明控件、查找控件、监听控件这些事情自动生成
方案二:创建面板基类,在基类中进行统一处理,新面板只需继承该基类,基类代码就可以帮助我们完成声明控件、查找控件、监听控件这些事情

正交矩阵的基本概念是什么?它对我们来说有什么意义?

如果一个矩阵是正交的,那么它的逆矩阵等于其转置矩阵
可以非常方便的进行逆运算,用于取消之前的变换

Shader和材质球的关系是什么?

  1. 一个Shader可以与无数个材质关联。

  2. 一个材质同一时刻只能关联于一个Shader。(为什么说是同一时刻,因为我们可以通过代码去动态改变材质所关联的Shader)

  3. 材质可以赋与模型,但是Shader不行。

  4. 材质就像是Shader的实例,每个材质都可以参数不一样呈现不同的效果,但是当Shader改变时,关联它的所有材质都会相应的改变。

任务系统中,某任务是要采集某一种植物n株后才能完成。说一说制作思路

  1. 接受任务时,根据策划需求决定是否判断身上已有的物品
    如果需要,则遍历有用物品是否有满足条件的物品,并统计数量
    如果不需要,则从0开始计数

  2. 执行任务时,在采集物品结束或获取物品时利用事件中心分发事件
    任务管理器监听事件,当获得满足条件物品时数量累加

  3. 判断任务是否可以交付,当物品数量达到目标时,任务状态变为可以交付

数据结构中的树,一般会在游戏开发中用来处理什么?至少说出3点

  1. 光照,碰撞
  2. 行为树(逻辑上)
  3. 技能树,任务树(逻辑上)

Unity中为Sprite前后关系排序的常用方式有哪些?(至少说出3种)

  1. Sprite Renderer上的Sorting Layer按层排序
  2. Sprite Renderer上的Order in Layer(同层时)
  3. 坐标轴(一般为Z轴)排序
  4. Soring Group排序组组件排序
  5. 自定义材质球使用的Shader去控制

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

资源上:

  1. 压缩纹理
  2. 减小纹理图片大小
  3. 降低模型质量(减少顶点)
  4. 使用压缩格式的音效,比如AAC、ADPCM等
    等等

代码相关:
5. 缓存池
6. 减少静态成员
7. 少new
8. string和StringBuilder

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

渲染相关:
主要方向减少DrawCall,减少着色器计算量

  1. 批处理
  2. GPU Instancing(当场景中有大量使用 相同材质和网格 的物体时,通过GPU Instancing可以大幅降低Draw Call数量。
    GPU Instancing是指由GPU和图形API支持的,用一个Draw Call同时绘制多个Geometry相同的物体的技术。)
  3. 合并图集
  4. 减少模型顶点数
  5. LOD
  6. MipMaps
  7. 减少光源、阴影
  8. 光照烘焙贴图
  9. 遮挡剔除
    等等

逻辑相关:
1.缓存池
2.利用多线程处理复杂逻辑
3.避免过多的Update
4.异步加载
5.预加载
6.定时主动GC(比如过场景时)
等等

Unity在发布Android平台项目时,在加载Application.streamingAssetsPath中文件时我们应该注意什么?

无法使用File中相关API进行加载
只能使用Unity中WWW、UnityWebRequest、AssetBundle相关API进行加载

Unity中安卓主流的纹理压缩格式ETC和ETC2的主要区别是什么?

1.透明通道
ETC不支持透明通道
ETC2支持透明通道
2.压缩效率
ETC2相对ETC在相同压缩比下图像质量更好
3.支持设备
ETC基本支持所有Android设备,ETC2不支持低端设备

目前ETC2在逐渐取代ETC

Android平台常用压缩格式ETC不支持透明通道,那我们的半透明纹理应该如何处理?

将纹理的透明通道分离
1张存RGB,一张存A
利用自定义Shader将其组合使用

ETC的内存占用相对RGBA32会降低为之前的1/4,效率也会提升
因此即使分成2张图,内存占用也会减少到之前的1/2

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

在传统的前向渲染路径中,每个像素都需要遍历所有的光源来计算光照效果。而在延迟渲染路径中,光照计算是延迟到后处理阶段进行的,因此不需要在每个像素上都进行光照计算,而是在需要时只对可见的像素进行计算。这样可以大大减少渲染时对光源的遍历次数,从而降低了渲染负担。
当我们真正进行光照计算时,只会计算G缓冲区中存储的各片元信息
会减少很多不必要的光照计算

在C#当中,我们想要用一个字典容器存储场景中的所有敌对对象,比如 怪物、Boss、可被攻击的场景物件等等,应该如何存储

里式替换原则
父类容器装载子类对象

假设游戏中有一个怪物管理器管理所有怪物,那么在开发时,为了避免内存泄漏,我们需要注意什么?

当某一怪物真正需要移除时
我们需要清除怪物管理器对该怪物的引用(置为null GC会自动回收)

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

OnEnable在对象激活时都会自动调用一次
Start在第一次Update调用之前只会调用一次

我们需要根据执行时机的这个特点来决定何时调用他们
OnEnable可以将一些想要重置的数据 放在其中处理
Start一般放只会初始化一次的数据内容

网络通讯中分包黏包指的是什么?我们应该如何解决这些问题?

分包:一个独立的数据包(消息)被拆分成了多个小数据,分别发送出去
黏包:多个数据包(消息)被粘合在了一起,变成了一个更大的数据包,发送出去
解决:加入消息头,在消息头中加入长度等其他信息,用于接收后判断消息长度,进行对应的逻辑处理

使用C#制作游戏存档功能,请问有几种做法?(至少说出三种)

  1. xml
  2. json
  3. 2进制
  4. 自定义文档结构
  5. 数据库

C#中是否可以通过反射获取到类内部的私有成员?

可以
在获取成员的相关方法中,可以通过传入参数,指定获取非公共的成员

在制作游戏存档功能时 C#中反射主要可以发挥出哪些作用(至少说出三点)

  1. 序列化时:动态获取数据结构类信息,可以动态获取字段用于存储
  2. 反序列化时:可以通过反射实例化对象,写入数据
  3. 结构发生变化时:我们可以利用反射机制进行判断,多的数据抛弃,少的数据自定义初始化

如果在Unity当中制作FPS游戏,如何模拟枪械开枪时的后坐力

每次开枪时
可以给枪械、手臂IK、摄像机等(根据实际视角情况决定)
一个绕x轴(往上偏移)和y轴(左右偏移)的旋转角度
并且每帧都会归位(角度四元数不停向原始位置归位)

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

  1. 如果是想要通过多线程控制或访问场景上对象的相关行为,比如移动旋转、资源加载、动态销毁等等,那么不用加锁
    因为Unity的整个执行模型是单线程的,所有的游戏逻辑和渲染都在主线中进行。
    大部分的UnityAPI都只能在主线程上调用,如果通过多线程去调用Unity场景上物体的相关API,或改变场景中对象的相关属性,会直接出现报错。因此加锁也没有意义

  2. 如果是想要通过多线程改变继承MonoBehaviour脚本中的某些公共成员,比如一个int,一个List。那么可以为这些公共成员加锁,防止线程并发带来的问题

但是建议尽量不要通过多线程和继承MonoBehaviour的挂载在场景对象中的脚本打交道

Shader当中的Blend渲染命令主要用来干什么?

用来进行颜色混合的
将当前片元的颜色和颜色缓冲区中的颜色进行混合计算

一般可以用来实现半透明相关效果,光照颜色叠加相关效果等等

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

  1. 文件的全局唯一标识,用于确保资源文件在Unity中的唯一性
  2. 导入设置相关信息,比如纹理在Inspector窗口中的相关设置信息
  3. 关联脚本,如果资源和脚本关联,会包含相关脚本信息
  4. 资源依赖关系
  5. 编辑器状态,比如在Unity编辑中是否被锁定等

meta后缀文件在进行项目版本管理时比较重要,因为它可以确保资源的一致性

不同后缀的各种文件的本质是什么?

不同后缀的文件表示文件的类型或格式不同
一般情况下,不同后缀的文件都有着自己的一套序列化和反序列化的规则

UnityWebRequest可以用来做什么?

  1. 用于上传下载数据(网络通讯,网络传输)
  2. 用于本地数据加载
  3. 支持异步执行,它支持多种协议,比如HTTP、HTTPS、FTP、本地文件系统

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

  1. 网络通讯
  2. A星寻路
  3. 文件读写操作
  4. 使用协同程序异步加载(协同程序底层,部分API也是开启了多线程的)

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

卡顿、发热:性能
闪退:内存

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

代码相关:
1.代码中预加载的数据(比如数据表中加载出来的数据)
2.代码中的成员数据(执行逻辑的代码中的相关成员)

资源相关:
模型
3.模型数据
4.纹理贴图
5.动画数据
2D
6.UI
7.各种游戏图片(角色、道具、背景等等)
其他
8.音效
9.特效

网络游戏中,有一个数据统计界面,这些数据是由最近1000场战斗计算出来的平均数据你认为以下处理方式合理吗?如果不合理,应该如何改进?我们通过向后端请求这1000场战斗的数据,然后在客户端计算相关平均数据,更新显示到统计界面上

不合理
应该让后端计算好,前端需要数据时请求获取计算好的数据后更新到界面上即可
无需前端来进行计算处理

Unity中摄像机组件中的投影(Projection)参数其中的两个选项透视投影(Perspective)和 正交投影(Orthographic)有什么区别?

透视投影:
模拟人眼看世界的方式,近大远小,并呈现出显示世界中的景深和透视效果
通常用于制作3D游戏

正交投影:
不考虑对象到摄像机的距离,所有对象无论在场景中任何位置,都以相同大小显示
通常用于制作2D游戏

FSM和BT指什么?他们是用来处理什么的?

FSM(finite state machine):有限状态机
BT(Behavior Tree):行为树

他们都是用来制作游戏中AI功能的设计方案

OpenGL和DX的最大区别是什么?

OpenGL和DX的主要区别就是在于

OpenGL是一个跨平台、跨语言的开放图形库,可以支持各种平台,比如Windows、Linux、MacOS等等
DX是微软创建的,它不跨平台,只针对微软相关平台,比如Windows操作系统,xBox主机系

Unity中的Lerp和Slerp分别是什么?

Lerp和Slerp是两个方法,在Mathf和Vector等类中都提供了对应的方法
1.Lerp:
线性插值函数,接受三个参数,起始值、目标值和插值比例
Lerp用于在两个值之间进行平滑的线性插值,适用于直线运动、颜色过渡等场景

2.Slerp:
球面插值函数,接受三个参数,起始值、目标值和插值比例
Slerp在插值过程中会在两个旋转之间沿着曲线(球面)插值,从而保持较为自然的旋转过渡

他们都是在Unity常用的插值函数,能够在动画、平滑移动、旋转等场景中产生自然和平滑的效果

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

Unity底层实现是基于单线程的,这个线程通常被称为 主线程 或 渲染线程
意味着大部分的游戏逻辑、渲染和更新都在主线程上执行。

但是Unity也支持自定义多线程处理复杂逻辑,并且Unity目前的版本也提供了一些多线程技术来改善性能,比如Job System、Dots等

如何在Unity中实现多语言支持?(不同国家的人,看到的游戏内的语言是不一样的)

  1. 单包:将所有设计多语言的图片、文本通过配置表去配置
    当显示这些内容时,根据配置表中的信息去动态的加载
    所有地区通用一个安装包,只是游戏中进行地区判断或者语言选择

  2. 分包:根据不同地区发布不同的安装包

Shader当中的顶点和片元着色器有什么作用?

顶点着色器:
主要作用是对物体的顶点位置进行变换和投影,从本地坐标系转换到世界坐标系和相机坐标系等。在这个阶段,顶点着色器还可以计算光照的影响、法线的变换,以及传递一些数据供后续阶段使用

片元着色器:
主要处理像素的颜色、纹理和光照。在顶点着色器之后,渲染管线会对物体进行光栅化,将物体的几何形状转换为像素。在片元着色器中,针对每个像素,会进行插值和纹理采样,以及计算光照、阴影、反射等效果。片元着色器最终输出的颜色将决定每个像素的显示效果。

这两个着色器阶段协同工作,将3D场景中的几何信息转换为2D屏幕上的像素颜色,从而实现图形的渲染和显示。

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐