Unity杂项

Posted by LudoArt on June 10, 2023

Unity杂项

Unity脚本生命周期

monobehaviour_flowchart

MeshRender 中 material 和 sharedmaterial 的区别 ?

  • material 属性提供了引用到游戏对象渲染器的材质的克隆实例。材质属性的任何变化只会反映在被引用的游戏对象上。

  • sharedmaterial 属性提供引用到 GameObject 渲染器的实际材质。共享材料中的任何更改都将反映到所有引用的 GameObejct。

PS:material 是拷贝,sharedmaterial 是引用

CLR是什么

当修改了C#代码,并回到Unity编辑器时,代码会自动编译,此时代码转换为通用中间语言(Common Intermediate Language, CIL),它是本地代码之上的一种抽象。

在运行时,中间代码通过Mono虚拟机运行,Mono虚拟机是.NET公共语言运行时(Common Language Runtime, CLR)的一个实现。

在CLR中,中间CIL代码实际上根据需要编译为本地代码。这种及时的本地编译可以通过AOT(Ahead-Of-Time)或JIT(Just-In-Time)编译器完成,选择哪一个取决于目标平台。两种编译器类型主要的区别在于代码编译的时间。

AOT编译是代码编译的典型行为,它发生于构建流程之前,在一些情况下则在程序初始化之前。代码都已经完成提前编译,没有后续运行时由于动态编译产生的消耗。

JIT编译在运行时的独立线程中动态执行,且在指令执行之前。通常,该动态编译导致代码在首次调用时,运行得稍微慢一点,因为代码必须在执行之前完成编译。

IL2CPP和Mono

IL2CPP是一个脚本后端,用于将Mono的CIL输出直接转换为本地C++代码。由于应用程序现在运行本地代码,因此这将带来性能提升。

https://blog.unity.com/engine-platform/an-introduction-to-ilcpp-internals

C# GC算法

Unity使用的Mono版本中的GC是一种追踪式的GC,它使用标记和清除策略。该算法分为两个阶段:

阶段一:每个分配的对象通过一个额外的数据位追踪,该数据位标识对象是否被标记。这些标记设置为false,标识它尚未被标记。当收集过程开始时,所有对程序可访问的对象(直接引用或者间接引用)会被标记为true,剩下的没有被标记的(即标记为false的)是可以被GC回收的对象。

阶段二:迭代对象,并基于它的标记状态决定是否应该回收。在该阶段,所有标记的对象都被跳过,但在下次垃圾回收扫描之前会将它们设置回false。

Unity纹理压缩格式

image-20230616003707702

https://docs.unity3d.com/cn/current/Manual/class-TextureImporterOverride.html

Mipmap思想,内存变化

MipMap的意思是分级细化纹理,也可以理解为图片的LOD(Levels of Detail)。 如果贴图的基本尺寸是256x256像素的话,它mipmap就会有8个层级。每个层级是上一层级的四分之一的大小,依次层级大小就是:128x128;64x64;32x32;16x16;8x8;4x4;2x2;1x1(一个像素)。

UI背包如何优化

  • 使用更多的画布,将常更新的部分与不常更新的部分区分开来
  • 为无交互的元素禁用 Raycaster Target
  • 通过禁用父画布组件来隐藏UI元素
  • 优化ScrollRect
    • 确保使用RectMask2D
    • 在ScrollRect中禁用Pixel Perfect
    • 手动停用ScrollRect活动,使用ScrollRect.velocity和ScrollRect.StopMovement()方法检测到帧的移动速度低于某个阈值,就可以手动冻结它的运动
  • 动态加载Item,而不是全部加载

Unity UI如何自适应

  • UI元素的适配

    Unity提供了一种自适应UI的解决方案,即Anchors和Pivot(锚点和中心点)。Anchors决定了UI元素的位置和大小随着屏幕尺寸的变化而变化,Pivot决定了UI元素的旋转和缩放的中心。

  • UI布局的适配

    Unity提供了一种灵活的UI布局系统,即UI Layout系统。它提供了多种布局组件,例如VerticalLayoutGroup和HorizontalLayoutGroup等,可以在不同分辨率和尺寸的屏幕上实现适配。

  • 使用Canvas Scaler来对UI进行缩放

    Canvas Scaler提供了三种缩放模式:Constant Pixel Size(固定像素大小)、Scale with Screen Size(根据屏幕尺寸进行缩放)和Constant Physical Size(固定物理大小)

如何防止大量GC

  • 减少对象的大小

合理安排类或结构体的字段声明顺序,以优化其对象的内存布局进而减少对象大小,结构体可以使用 StructLayout 属性

关于结构体,结构体本身是值类型。若结构体中不包含引用类型时,针对结构体的 new 操作不会造成 GCAlloc,但若结构体中包含引用类型字段,如 string 或数组等,那么在对结构体执行 new 操作时会产生 GC Alloc

  • 降低内存分配的频次, 也就是尽量减少 GCAlloc

    • 减少引用类临时对象的分配,传递结构类型的参数时,如果对象尺寸超过 IntPtr.Size 时,采用引用传递方式,参数加关键字 ref,类类型的对象本身已经是引用传值了,不会生成临时对象

    • 使用泛型优化装箱,例如 void Func(object o) 方法在传值类型参数时会进行装箱,使用 void Func(T o) 则不会产生装箱,但泛型在 IL2cpp 时会生成多种类型对应的代码

    • 可变参数的方法,先定义常用参数个数的方法,再定义可变参数方法,确保绝大多数调用是固定个数的方法。如 string.Format方法是将1个、2个和3个参数的方法单独提出,另外再实现一个可变参数的方法

    • 缓存某些 Get 类方法或属性的结果,例如不要使用 GameObject.nameGameObject.tag,但GameObject.CompareTag() 方法不会产生 GCAlloc

    • 尽量减少装箱和拆箱

    • 不要在 Update 等频次较高的方法中分配堆内存

    • 使用对象池

    • 事先申请足量的容器尺寸,避免申请尺寸不足时添加元素造成的复制和重新申请操作

    • 字符串本身是引用类型,字符串连接时会申请新的空间,所以尽量避免字符串拼接

    • 协程 yield return 0 应该使用 yield return null 来替代,避免装箱

    • 协程 yield return new WaitForSeconds 应该先将 new WaitForSeconds 缓存下来

  • 在适当的时机主动调用 GC.Collect

例如在场景切换显示加载界面时调用,用户无感知

  • 关于调试日志的字符串参数

正式版本中使用 unityLogger.logEnabled = false 仅仅只是不打日志,但字符串已经被分配内存,GC Alloc 还是产生了。解决方法是使用 Conditional 特性来处理日志输出,在正式版本中不要生成打印相关的代码,也不会有字符串的生成,减少了 GCAlloc

Unity Navmesh

NavMesh:用来描述一个可行走区域的数据结构,这个数据是需要我们手动设置生成(baked),或者动态生成(代码控制)。

如何创建一个NavMesh

  1. 选中会影响导航的场景物体
  2. 在导航系统中勾选Navigation Static,这样就可以标记好可以被用来baking导航场景
  3. 设置bake相关的数值
  4. 点击bake按钮完成导航网格的构建

Unity优化手段,dc cpu gpu

Unity资源相关问题,内存分布,是否copy等

Unity动画相关问题

如何分析程序运行效率瓶颈,log分析

借助工具,如:Unity Profiler

具体位置:

  • 从脚本代码控制Profiler:

    • 主要是UnityEngine.Profiling.Profiler类的BeginSample()EndSample()方法,分别控制运行时激活和禁用分析功能;
  • 自定义定义和日志记录方法:

    • 可以使用System.Diagnostics命名空间下的StopWatch类,来记录时间信息;

    • 可以使用缓存日志的方式来记录日志信息,因为Unity的控制台窗口日志记录机制十分昂贵;

碰撞检测算法,优化,物理引擎检测优化

连续碰撞检测

数据结构

Map怎么实现的,Dictionary如何实现

Dictionary如何实现: https://zhuanlan.zhihu.com/p/96633352?utm_id=0

实质:哈希表

常见的数据结构的增删改查的时间复杂度

https://www.bigocheatsheet.com/

image-20230617113940214

排序算法的时间复杂度

image-20230617114205016

image-20230617114310409

红黑树和avl树还有堆的区别,内存&效率

快排的时间空间复杂度以及实现

平均 O(nlog2n),以第一个数为基准数,比基准数小的到左边,比基准数大的到右边,对左右俩子集再进行一次上述操作,直到所有子集仅只有一个元素为止。

一趟快排的具体实现

  • i = start, j = end

  • 先从右向左找第一个小于基准数的值
    • 找到了,与基准数交换
    • 没找到,j–
  • 再从左向右找第一个大于基准数的值
    • 找到了,再与基准数交换
    • 没找到,i++
  • 当 i == j 时,一次快排结束

堆排的实现,时间空间复杂度

平均 O(nlog2n)

实现:

  • 将序列构建为大顶堆
  • 取出根节点,将其与序列末尾元素进行交换
  • 对交换后的n-1个序列再进行调整,使其满足大顶堆
  • 重复步骤2和步骤3,直至堆中只有1个元素

数组和链表区别

  • 数组
    • 空间连续
    • 取值O(1)
    • 查找、插入、删除O(n)(需遍历)
  • 链表
    • 空间上可以不连续,通过指针指向下一个元素
    • 取值、查找O(n)
    • 插入、删除O(1)(只需改指针)

100万个数据快速插入删除和随机

哈希表

语言基础

虚函数的实现原理,父类和子类的内存布局

协程和进程的区别,怎么实现,协程程序怎么写,有什么问题

Struct和class的区别,值类型和引用类型区别

  • 值类型:当我们将一个int类型的值赋值到另一个int类型的值时,它实际上是创建了一个完全不同的副本。换句话说,如果你改变了其中某一个的值,另一个不会发生改变。C#的所有值类型均隐式派生自System.ValueType。C#的值类型包括:结构体(数值类型、bool型、用户定义的struct),enum,可空类型。
  • 引用类型:当我们创建一个对象并且将此对象赋值给另外一个对象时,他们彼此都指向了内存中同一块区域。因此,当我们将obj赋值给obj1时,他们都指向了堆中的同一块区域。换句话说,如果此时我们改变了其中任何一个,另一个都会受到影响。C#的引用类型包括:数组,class、interface、delegate,object,string。

Struct和Class的区别:

  • struct 是值类型,class 是对象类型
  • struct 不能被继承,class 可以被继承
  • struct 默认的访问权限是public,而class 默认的访问权限是private.
  • struct总是有默认的构造函数,即使是重载默认构造函数仍然会保留。这是因为struct的构造函数是由编译器自动生成的,但是如果重载构造函数,必需对struct中的变量全部初始化。并且struct的用途是那些描述轻量级的对象,例如Line,Point等,并且效率比较高。class在没有重载构造函数时有默认的无参数构造函数,但是一被重载,默认构造函数将被覆盖。
  • struct的new和class的new是不同的。struct的new就是执行一下构造函数创建一个新实例再对所有的字段进行Copy。而class则是在堆上分配一块内存然后再执行构造函数,struct的内存并不是在new的时候分配的,而是在定义的时候分配

Struct和Class应用上的区别:

  • 值类型适合存储供 C#应用程序操作的数据
  • 引用类型应该用于定义应用程序的行为。

C#装箱和拆箱

装箱:值类型转化为引用类型的过程。在堆上为新生成的引用对象分配内存,然后将值类型的数据拷贝到分配的内存中,返回堆中新分配对象的地址,这个地址就是指向对象的引用

拆箱:引用类型转化为值类型的过程。获取引用类型的地址,将用用对象的值拷贝到栈上的值类型实例中(注意 拆箱时可能会引发“转换无效”的异常。要记住,拆箱时强转的值类型,应以装箱时的值类型一致)

  • 装箱是将值类型转换为引用类型。进行一次装箱要进行:分配内存和拷贝数据,会影响性能。
  • 拆箱是将引用类型转换为值类型。严格意义上,拆箱并不影响性能,但拷贝数据的操作会影响性能。
  • 装箱和拆箱后的值类型和引用类型的改变将互不影响。

Inline函数有啥作用,和宏定义有啥区别(C++)

内联:

编译器会将使用响应的函数代码替换函数调用,对于内联代码,程序无需跳到另一个位置处执行代码,再跳回来。

因为内联函数的运行速度比常规函数稍快,但代价是需要占用更多内存。

内联函数和常规函数一样,也是按值来传递参数的。

宏:

C语言使用预处理语句 #define 来提供宏——内联代码的原始实现。

不是通过传递参数实现的,而是通过文本替换来实现的。

举个例子:

inline double square(double x) { return x * x; }
#define SQUARE(x) x * x
double temp = 3.0;

a1 = square(5.0);		// 25.0
b1 = square(2.5 + 1.5);	// 16.0
c1 = square(temp++);	// 16.0 (temp递增一次)

a2 = SQUARE(5.0);		// 25.0 正确
b2 = SQUARE(2.5 + 1.5);	// 2.5 + 1.5 * 2.5 + 1.5 = 7.75 错误
c2 = SQUARE(temp++);	// 4.0 * 5.0 = 20.0 错误(temp递增两次)

闭包

Action和function区别,以及内部实现,注册函数如何防止重复,如何删除

两种委托类型的区别在于:Action委托签名不提供返回类型,而Func提供返回类型。

delegate和event的区别

用了个event修饰,它仍然能在当前类中被执行,但是在另外一个类中,既不能被直接调用执行,也不能被重新赋值了,只能通过+=或-=来增减函数。这就是事件存在的必要,因为事件的这两个限制条件,在某种程度上会更安全。

Enum作Key的问题

注意Enum类型的定义与前两种类型的不同,它并没有实现IEquatable接口。因此,当我们使用Enum类型作为key值时,Dictionary的内部操作就需要将Enum类型转换为System.Object,这就导致了Boxing的产生。

https://blog.csdn.net/weixin_39939034/article/details/123370610

lua dofile和require区别

Lua面向对象实现以及与区别

编程基础

MVC思想

设计模式准则

UML图

图形学

如何实现一个扇形进度条

渲染管线流程,mvp变换等,各种test

image-20230617182443592

应用阶段:

  • 把数据加载到显存:大多数显卡没有直接访问RAM的能力,将数据加载到显存中使GPU能更快的访问这些数据。当把数据加载到显存后,内存中的数据便可以释放了,但对于一些还需要使用的数据则需要继续保留在内存中,如CPU需要网格数据进行碰撞检测。
  • 设置渲染状态:渲染状态的一个通俗解释就是,定义了场景中的网格是怎样被渲染的。例如,使用哪个顶点着色器/片段着色器、光源属性、材质等。如果不设置渲染状态,那所有的网格将使用同一种渲染,显然这是不希望得到的结果。
  • 调用Draw Call:当所有的数据准备好后,CPU就需要调用一个渲染指令告诉GPU,按照上述设置进行渲染,这个渲染命令就是Draw Call。Draw Call命令仅仅会指向一个需要被渲染的图元列表,而不包含任何材质信息,因为这些信息已经在上一个阶段中完成。执行DrawCall后GPU就会按照渲染流水线进行渲染计算,并输出到显示设备中,所执行的操作便是下述GPU渲染管线的内容。

image-20230617182524539

https://zhuanlan.zhihu.com/p/627201581

点乘叉乘的几何意义

点乘:投影,单位矢量a和矢量b的点乘,会得到b在a方向上的投影;

叉乘:对两个矢量进行叉乘的结果会得到一个同时垂直于这两个矢量的新矢量;

四元数与欧拉角

欧拉角是描述方位的一种方法,是物体绕坐标系三个坐标轴(x, y, z轴)的旋转角度,在这里坐标系可以是世界坐标系,也可以是物体坐标系,旋转顺序也是任意的。(最常见的是使用Roll-Pitch-Yaw(或者x-y-z)三个自身轴表示旋转角度)

四元数通过使用四个数来表示方位。可以避免万向节死锁这样的问题。

四元数记法: 一个四元数包含一个标量和一个3D向量,经常记标量分量为w,记向量分量为单一的v 或分开的x、y、z。

image-20230617183610415

Shadowmap实现

如何高效实现阴影

几种反走样算法实现、问题、效率

前向渲染和延迟渲染的区别,什么时候用

延迟渲染需要几个buffer,需要记录什么信息

Pbr最重要的参数,几个方程

如何搭建一个pbr工作流

计算机组成原理

CPU和GPU区别,如何设计

进程与线程

进程是资源分配的最小单位,线程是CPU调度的最小单位

做个简单的比喻:进程=火车,线程=车厢

  • 线程在进程下行进(单纯的车厢无法运行)
  • 一个进程可以包含多个线程(一辆火车可以有多个车厢)
  • 不同进程间数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘)
  • 同一进程下不同线程间数据很易共享(A车厢换到B车厢很容易)
  • 进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源)
  • 进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车上中间的一节车厢着火了,将影响到所有车厢)
  • 进程可以拓展到多机,进程最多适合多核(不同火车可以开在多个轨道上,同一火车的车厢不能在行进的不同的轨道上)
  • 进程使用的内存地址可以上锁,即一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。(比如火车上的洗手间)-”互斥锁”
  • 进程使用的内存地址可以限定使用量(比如火车上的餐厅,最多只允许多少人进入,如果满了需要在门口等,等有人出来了才能进去)-“信号量”

算法

数组第k大的数

1~n有一个数字没有,找到这个数

A*寻路实现

Topk问题以及变种,各种解法

求不重复且连续的最长字符串

求链表是否有环

动态规划问题

业务系统逻辑

如何实现战争迷雾

如何设计一个技能系统以及buff系统

设计个UIManager,ui层级关系,ui优化

消息管理器实现

体素的思想和实现

如何实现一个延时运行功能

内存管理器实现

如何实现热更

游戏AI如何实现,行为树要点

网络

帧同步和状态同步区别等一系列问题

帧同步要注意的问题

TCP与UDP

其他

1小时燃烧完的绳子,任意根,衡量出15分钟

随机数如何保证同步