unity 面试题

unity 面试题
NyxXUI优化策略
- Canvas每次重绘都会重新绘制所有UI元素,一个Canvas下所有UI元素都是合在一个Mesh中的
- 在UI界面很复杂时,划分子Canvas
- 动静分离
- UI的顶点位置好、颜色、材质、纹理发生变化都会导致rebuild
- 把一个面板的UI资源放到一个图集里,减少Draw Call
- 避免Overdraw,减少UI元素重叠,避免文字-图片-文字重叠,打断合批
- 对于矩形的mask,用RectMask2D代替Mask,避免额外drawcall,但会打断合批
- 隐藏Canvas可以给Canvas添加一个CanvasGroup组件,将Alpha值设置为0
- 背景大图不要和小图放在一个图集里
- 不会接收用户输入的UI关闭Raycast Target选项,减少Graphic Raycaster工作量
- 避免对UI元素使用动画,否则每帧都会dirty,使用tweening system
Unity 内置渲染管线
CPU阶段(应用阶段)
- 剔除:视锥体剔除、层级剔除、遮挡剔除
- 设置渲染顺序:渲染顺序主要由渲染队的值决定、不透明物体从前往后、透明从后往前
- 打包数据:模型信息(顶点坐标、法线、UV、切线、顶点颜色、索引)、变换矩阵、灯光、shader
- SetPass Call:设置GPU渲染状态(ZWrite、ZTest、Cull、Blend)
- DrawCall:向GPU发送渲染命令
GPU阶段
- 顶点处理
- 顶点着色器(顶点变换, 模型->裁剪空间)
- 曲面细分着色器(可选)
- 几何着色器(可选)
- 图元装配
- 剪裁
- 透视除法(剪裁空间->NDC空间)
- 屏幕映射
- 光栅化
- 三角形设置
- 三角形遍历:找出三角形覆盖的片元、并通过差值获取该片元信息(坐标、颜色、深度、法线、uv等)
- 顶点处理
片元着色
- 纹理采样、光照
输出合并
Alpha测试、模版测试、深度测试、混合
输出帧缓冲(颜色缓存、深度缓冲)
Draw Call优化
- 什么是DrawCall:CPU调用底层图形API向GPU发送一次绘制命令的行为
- 优化手段
- 静态和批:只适用于静态、材质相同的物体。同一物体过多会导致顶点内存暴涨
- 动态和批:顶点数量限制大,CPU消耗大
- GPU Instancing:一份网格+多个变换矩阵参数一次性发送给GPU,需要GPU支持
Canvas的作用是什么?
- UI画布,UI系统的根容器,可以设置渲染模式、缩放模式以及渲染顺序
- UI元素负责使用
Canvas Renderer组件向Canvas发送网格、纹理、材质等数据。Canvas负责将相同材质/纹理的UI元素的网格数据合并向GPU发送DrawCall - 发起
willRenderCanvases事件,调用CanvasUpdateRegistry的PerformUpdate方法
PerformUpdate执行流程
CanvasUpdateRegistry维护两个List,m_LayoutRebuildQueue:用于维护需要执行布局重建的ICanvasElement元素m_GraphicRebuildQueue用于维护需要执行图形重建的ICanvasElement元素
Layout重建阶段:首先遍历
CanvasUpdate.Prelayout、CanvasUpdate.Layout、CanvasUpdate.PostLayout,然后遍历m_LayoutRebuildQueue中所有元素,将其作为参数传递给这些ICanvasElement的Rebuild方法,主要由LayoutRebuilder实现了该方法,在Layout阶段执行Rebuild方法。LayoutRebuilder中Rebuid方法实现自底向上的计算阶段(
PerformLayoutCalculation):先递归处理所有子ILayoutElement(CalculateLayoutInputHorizontal/Vertical),再处理父节点
1
2
3
4
5// 递归处理子节点,再处理当前节点
for (int i = 0; i < rect.childCount; i++)
PerformLayoutCalculation(rect.GetChild(i) as RectTransform, action);
for (int i = 0; i < components.Count; i++)
action(components[i]); // 执行当前节点的计算自顶向下的控制阶段(
PerformLayoutControl):先处理父节点的布局控制(SetLayoutHorizontal/Vertical),再递归处理子节点。
1
2
3
4
5// 先处理当前节点的控制器,再递归处理子节点
for (int i = 0; i < components.Count; i++)
action(components[i]); // 执行当前节点的控制逻辑
for (int i = 0; i < rect.childCount; i++)
PerformLayoutControl(rect.GetChild(i) as RectTransform, action);
剔除阶段:调用
ClipperRegistry.Cull方法,ClipperRegistry内部维护了一个m_Clippers列表存储IClipper实例如RectMask2D,一次调用所有IClipper的PerformClipping方法,对其管辖的UI元素进行裁剪图形重建阶段:首先更新几何
UpdateGeometry,然后更新材质UpdateMaterial更新几何
UpdateGeometry:- 调用
OnPopulateMesh方法,默认将矩形四个顶点位置、颜色、uv加入到VertexHelper中。 - 获取自身所有
IMeshModifier组件,如Shadow、Outline组件的ModifyMesh方法,将修改后的顶点信息写入VertexHelper中,内部会将顶点数据拷贝多份。 - VertexHelper将获取的顶点数据填充全局共享的
workerMesh中,然后调用canvasRenderer.SetMesh方法将网格数据传递给
- 调用
更新材质
UpdateMaterial:将自身
materialForRendering材质以及mainTexture纹理发送给自身canvasRenderer组件注意:
materialForRendering属性可被自身IMaterialModifier修改
1
2
3
4
5
6
7
8
9
10
11
12
13
14public virtual Material materialForRendering
{
get
{
var components = ListPool<Component>.Get();
GetComponents(typeof(IMaterialModifier), components);
var currentMat = material;
for (var i = 0; i < components.Count; i++)
currentMat = (components[i] as IMaterialModifier).GetModifiedMaterial(currentMat);
ListPool<Component>.Release(components);
return currentMat;
}
}Mask组件
GetModifiedMaterial方法实现:修改覆盖区域的模版缓冲区,被遮罩的子元素只在模板缓冲区匹配的区域渲染mainTexture属性可被子类重写,默认返回白色纹理,Image组件返回自身精灵贴图的纹理.
什么是rebuild?
任何一个元素脏(颜色、位置等发送改变),会发生一次rebuild
分为三个步骤:
重新计算自动布局元素的布局
为所有启用的元素重新生成网格
重新生成材质(为了batch meshes)
Canvas会对网格排序并生成batch
片元着色
- 深度测试,不通过则直接结束
- 执行frag shader计算颜色,与颜色缓冲区混合,默认直接替换缓冲区颜色
- 写入深度信息
TextMeshPro对比Text
- TextMeshPro采用有向距离场的方式进行渲染,清晰度高、无OverDraw,Text清晰度低、会产生OverDraw
什么是anchoredPosition
- UI元素轴心(Pivot)相对于锚点(Anchors)矩形中心的距离
MVC
用于隔离游戏业务逻辑与显示逻辑的开发框架
M代表逻辑层,是客户端中逻辑业务、数据的载体。负责维护和更新从服务器传来的玩家数据
V代表显示层,用于控制场景中GameObject的显示逻辑,以及监听玩家输入事件
C代表控制层,负责处理游戏中各种事务的整个流程,如开启一个UI界面、一个业务流程成为一个Task,每个Task都有Init、Running、Paused、Stoped生产周期状态,以及更新管线流程。控制层从M层获取业务逻辑所需的数据,然后驱动V层的刷新显示。比如逻辑层生成一个NPC飞船,向控制层Task发送这个事件,控制层则会负责创建飞船现实层实例并动态绑定组件。
组件化
- 传统面向对象要实现一个功能通常会将其功能抽象层一个类,类内部实现各个功能模块的方法,通过继承一个类来进行拓展。
- 组件化可以降低类型的复杂度,提高扩展性。将各个功能模块拆分成各个组件,逻辑实体持有各个组件句柄,通过继承一个组件实现功能的拓展。
战斗初始化:clickStartLevelButton -> PlayerGameObject.LevelBattleStart(levelId)
-> var battle = new Battle(initInfo) -> battle.Initialize()
-> 创建逻辑实体 -> 创建显示实体(BattleSceneTask)
一键场景配置导出工具
- 为策划提供编辑器工具,一键读取关卡场景中各个节点下物体的描述信息,如玩家出生点信息、母舰信息、收集金币信息、NPC波次信息、每个波次NPC编队信息、每个编队中NPC信息、静态物品信息如陨石的位置大小旋转缩放碰撞体大小,并将该信息序列化为json字符串,并将字符串保存到Unity ScriptableObject资产文件中,并在游戏中动态加载并生成游戏对象。
- 导出原理
- 依次遍历当前场景下各个节点挂载的描述性组件,读取组件上的信息并保存到一个SceneExportInfo对象实例中。
- 使用Newtonsoft.Json.JsonConvert.SerializeObject方法将该对象序列化为json文件并保存到本地。
- 获取场景导出目录下的所有json文件,针对每个json文件,创建一个ScriptableObject资产并将json内容写入,最后将ScriptableObject资产保存至Resource目录下。
- 游戏运行时,当进入关卡初始化时根据关卡id加载对应关卡资产,动态加载资源并实例化游戏逻辑层与现实层对象并初始化,注册事件监听者。
UI框架
通过UIManager统一管理游戏中的UI界面。UI界面支持动态加载、缓存、链式打开返回、动态调整UI层级关系。
每个UI界面对应一个Controller层的对象,都有着自己的生命周期OnStart、OnPause、OnResume、OnStop。以及更新管线,管线内部负责:加载资源、数据获取、时间绑定、界面刷新。
游戏中的UI界面以预制件的形式储存在本地,动态加载并实例化放到对应的canvas节点下。
UI叠加原理:多个画布,渲染模式为camera,相机ClearFlags设置为DepthOnly,渲染时只清除Framebuffer的深度信息,从而形成UI叠加效果,相机Depth值越大,UI越靠前
UI链式打开返回原理:每个UI都会维护一个现场信息,是一个链表结构,保存了前一个UI现场以及目标UI现场,若从界面A进入界面B,之后想返回界面A,则启动界面B时需要将A的现场信息传递给B,返回是会做一个链表的遍历操作,若链表中存在目标现场则可以返回并恢复现场数据。
UI排序原理:SceneManager维护一个栈结构,越靠近栈顶越靠前,每push一个layer会设置一个脏标记,每次tick时若脏,则对根据layer描述信息(优先级)对其进行排序,然后只渲染在全屏不透明上面的layer。
管线劫持原理:当主界面关系更新流程中启动子界面时将自身作为宿主(接口)传递给子界面,当子界面管线更新流程中完成资源加载会通知宿主,当宿主资源加载完成子界面也加载完成后会通知所有子界面,最后完成管线后续流程。
管线更新流程:阻止用户输入->加载子界面->恢复现场快照->加载资源->管线劫持->界面更新->结束
资源加载
- 三种资源加载方式
- 通过AssetDataBase加载
- 优点:方便使用,无需对资源做前置处理(去除后缀)
- 缺点:仅编辑器下使用,仅支持同步加载,路径以“Assets/”开头
- 通过Resource加载
- 优点:方便使用,支持同步、异步加载
- 缺点:只能加载Resource目录下的资源,打包后无法修改只能读取,不支持热更
- 通过AssetBundle加载
- 优点:支持同步/异步加载、支持热更
- 缺点:有前置处理流程(将资源分配给AssetBundle),需要处理依赖
- 通过AssetDataBase加载
- 资源加载流程
- 首先尝试从缓存中加载(缓存中保存资产的path以及弱引用),若存在则返回
- 若ResourceManager还没有初始化完成,则使用Resource方式加载
- 初始化完成则选择使用AssetDataBase或AssetBundle方式加载
- AssetBundle加载流程
- 尝试从缓存中获取AB
- 获取失败则注册一个加载现场,若已存在相同的加载现场则等待
- 加载依赖AB,递归调用加载函数
- 若无依赖则加载AB,首先尝试从本地加载,若本地不存在或版本号发生变动这需要从远程下载AB,否则从本地使用加载AB。加载完成后将AB存入缓存。
- 从AB中加载目标资源,调用LoadAssetWithSubAssets方法从AssetBundle中异步加载资源及其子资源。
- 将资源以及子资源存放到缓存当中,key为资源路径,value为资源弱引用
- 资源加载完成执行回调,将自身返回。
群体寻路算法
已知当前速度方向向量,
重点是转向速度方向v的计算,其值为对齐方向(平均朝向)、聚集方向(平均位置)、分离方向(posA - posB)、目标方向(pTarget - posA)加权和
目标位置 = pos0 + (v - v0) * detatime
为什么Draw Call多了会影响性能
- Draw Call前的Set Pass Call(切换渲染状态)十分耗时,需要切换GPU渲染状态(如混合模式、开启深度测试、模版测试等)以及切换Shader程序
GPU Instancing
- 原理
- 如果有100个mesh要绘制,但是这些mesh是相同的,只有坐标/旋转/缩放不一样,那么就可以只传输1个mesh的数据过去,加上100个坐标/旋转/缩放信息就行,这样就可以节省掉99个mesh的数据传输
- 限制
- opengl es 3.2以上才支持
- mesh相同,材质球相同(材质球的数据可以有一些不一样,不过要用PropertyBlock封装)
- 不支持SkinedMeshRenderer
配置文件生成流程
- 确定Excel表结构,填写配置数据
- 使用工具生成对应的c#类型以及对应的protobuf二进制文件
- 根据生成的c#文件和Velocity引擎自动配置加载类,包括资源路径、序列化、反序列化方法
配置文件加载流程
- 在游戏初始化,配置文件加载器初始化中获取所有配置文件路径
- 加载配置文件到内存中,并调用对应对应反序列化函数生成对应类型实例,并缓存到字典中key为ID,value为对应类型实例
- 调用加载器对应Load方法加载配置数据
AssetBundle 打包流程
- 方法一:指定每个资源AssetBundle名称
- 方法二:
- 打包前生成BundleDescriptionList:
- 为每个文件夹创建对应Bundle描述文件,该文件描述了该文件夹下资产要打成多少个AB包,以及每个AB包名称,包含资产的路径。以及不同平台AB发布策略,如AB包存储在热更远程的服务器或者随安装包发布、在游戏启动时更新还是AB中资产使用到了再更新。
- 分两次打包:第一次收集所有要打包的AB包名以及包含的资源。第二次创建一个BundleData清单文件收集所有打包好的AB包的版本号、hash以及crc校验码、大小等信息并保存至BundleData文件中。
- 打包前生成BundleDescriptionList:
AssetBundle 框架流程
- 切换平台
- 读取打包配置文件(xml、so),文件记录了ab包生成路径、所有需要打包成AB包的信息,其中AB包信息包括:打包路径、资源颗粒度、文件后缀等
- 获取所有需要打包的文件以及文件之间的依赖关系(字典存一下),将依赖的文件也加入需要打包的文件集合中
- 根据需要打包的文件生成所有ab包的名称,并用字典记录ab包与ab包中的文件的映射关系
AssetBundle 热更新流程
- 游戏启动时会先从配置文件中获取bundle热更地址
- 下载远程Bundle清单文件,获取服务器AB列表、资源版本号信息,并记录需要预下载的AB
- 将本地Bundle清单中每个bundle和远程bundle对比,若发生缺失或版本号或哈希值发生变化则bundle加入到更新列表中并记录要下载的AB文件大小
- 提示玩家是否更新,若更新则遍历更新列表并调用UnityWebRequestAssetBundle.GetAssetBundle方法从远程异步下载AB并统计下载进度
- 下载完成后加载BundleManifest,结束
什么是有限状态机
- 管理游戏物体在有限个状态之间切换的一种数据结构,每个状态都有Enter、Update、Exit三种行为,状态之间的切换由FSM统一管理,同一时间只能执行一种状态。
- 应用:初始玩家状态机时候创建所有状态,设置初始状态。在Update中先处理玩家输入,然后根据输入设置状态切换。
分层状态机
- 为什么要分层:状态多了转换条件复杂,不好管理,分层后内层状态只需要考虑内层之间的切换,外部只用管理外层状态的切换。可以将状态根据一些场景分类,比如说战斗内、战斗外;或者当前在水中、在陆地或者在天上分层。
行为树
叶子节点作为行为节点,用于执行具体的行为。非叶子节点为控制节点或修饰节点控制节点:用来决定其子节点以顺序(依次执行,失败返回)、选择(依次执行,成功返回)、随机、并行(执行所有)等方式执行。修饰节点:取反器(对子节点取反);重复执行器(重复执行一定次数)
- 执行顺序:前序遍历,每次Tick都会遍历一遍树。
- 优点:1. 将复杂的AI行为直观、有条理的展现出来,好维护,符合人类思维模式。2. 节点复用性高
- 缺点:遍历树结构对CPU的消耗比状态机大。
平衡二叉树和红黑树的区别
- AVL平衡条件更加严格,要求每个节点的左右子树高度差不超过1;红黑树相对宽泛。
- 查找、插入和删除操作的时间复杂度都是O(log n),AVL查找更快,红黑树插入和删除更快。
ILRuntime执行原理
使用 Mono.Cecil 库读取热更程序集中的类型信息,并通过自己实现的一套解释器解释方法中的IL代码。
执行流程
- 读取热更程序集dll,以流的形式保存
- 使用 Mono.Cecil 库提供的ReadModule方法读取热更程序集并将其转换为Mono.Cecil类型系统,并将程序集中所有类型转换为
ThreadSafeDictionary<int, IMethod> mapMethod字典保存到appDomain中 - 执行方法时获取方法体的IL代码并逐个解释,一种是通过ILRuntime自己实现的栈来执行,一种通过将IL转换为自定义的寄存器指令来执行。
栈内存布局
ILRuntime中的所有对象都是以StackObject类来表示:
1
2
3
4
5
6struct StackObject
{
public ObjectTypes ObjectType;
public int Value; //高32位
public int ValueLow; //低32位
}方法执行时依次压入返回地址、参数、局部变量,执行完后清理栈并压入返回值
方法重定向原理
appDomain中包含一个字典结构
Dictionary<MethodBase, CLRRedirectionDelegate> RedirectMap用来保存重定向方法,可以通过RegisterCLRMethodRedirection向appDomain注册重定向方法。1
public void RegisterCLRMethodRedirection(MethodBase mi, CLRRedirectionDelegate func)
当执行方法遇到
call或callvirtIL指令时,说明为方法调用若RedirectMap中有该方法,说明需要重定向,则调用该重定向方法:
var redirect = cm.Redirection; if (redirect != null) esp = redirect(this, esp, mStack, cm, false); ...1
2
3
4
5
6
7
8
9
10
11
* 重定向方法需要满足该委托结构`CLRRedirectionDelegate`
*
```c#
public unsafe delegate StackObject* CLRRedirectionDelegate(
ILIntepreter intp,
StackObject* esp, // 栈顶指针
List<object> mStack,
CLRMethod method,
bool isNewObj);重定向方法执行流程
- 通过解释器获取appDomain
- 根据参数个数获取返回指针地址,类型为StackObject*,同时也是返回值存放的地址,esp减去参数个数
- 获取参数地址,类型为StackObject*
- 从栈顶到栈尾依次通过地址获取实参,获取后通过解释器的Free方法释放实参栈内存,通过
ILRuntime.CLR.Utils.Extensions.CheckCLRTypes扩展方法获取 - 处理重定向逻辑
- 若方法有返回值,则将返回值赋值,并设置返回类型,若返回值不为简单类型,则调用ILIntepreter.PushObject方法设置返回值
通过自动分析热更DLL生成CLR绑定原理
读取热更程序集初始化appDomain
获取appDomain中所有的LoadedTypes,生成各个类型的Binder类,以及各个方法的重定向方法
XIL
热更初始化流程
初始化首先获取热更程序集并初始化ILRuntime的appDomain
注册跨域继承适配器、注册值类型Binder、注册自动生成的CLR绑定、注册委托转换器
实例化热更程序集中hot.hotApp类型实例,并调用Init实例方法
AutoReplace获取热更程序集中要替换的方法或属性
调用热更程序集中含有AutoInitAndRelease属性的类型的Init静态方法
为什么需要生成热更程序集CLR绑定
- 默认情况下,从热更DLL里调用Unity主工程的方法,是通过反射的方式调用的,这个过程中会产生GC Alloc,并且执行效率会偏低。
- CLRa绑定其实就是生成主工程某些类型某些方法的重定向方法,并向ILRuntime注册方法重定向
Hotfix Inject In Editor注入原理
- 使用MonoXIL.Cecil读取Assembly-CSharp.dll程序集
- 获取程序集中所有满足条件的类型
- 获取类型中所有满足DelegateBridge中__Gen_Delegate_Imp方法签名匹配的方法
- 向方法的Body添加一个类型为DelegateBridge的局部变量
- 在方法体的开头注入代码,加载委托实例并调用它。
七大设计原则
单一职责
开放封闭原则
- 对扩展开放、对修改关闭
里式替换原则
- 用父类或子类句柄调用子类方法,结果不会发生改变
接口隔离原则
- 接口尽量细化,同时接口中的方法尽量少。
依赖倒转原则
- 使用接口或抽象类
迪米特法则
- 一个类对自己依赖的类知道的越少越好(降低类之间的耦合)
合成复用原则
- 使用聚合代替继承
设计模式
理解
- 一系列成熟的代码设计方式,用来提高代码质量
- 三类设计模式:创建型、结构型、行为型
- 常用的设计模式:单例、工厂、观察者
工厂模式
简单工厂 - 集中式创建
- 实现简单,使用方便
- 缺点:违背开闭原则,扩展需要修改工厂类
工厂方法 - 多态创建
- 优点:符合开闭原则、易于扩展
- 缺点:需要创建多个工厂类
抽象工厂 - 产品族创建
- 优点:可以创建相关产品族,保证产品兼容性,如:游戏有人族、精灵族等种族,每个种族都有战士、法师等职业。
- 缺点:扩展产品族比较困难
- 在具体工厂上加了一层抽象层,就是一个抽象工厂接口,该接口声明了一些生产一些抽象产品的方法。
- 具体工厂继承抽象工厂接口
- 新增一个具体工厂需要继承抽象工厂,并新增具体产品
- 新增一个产品需要先新增一个抽象产品,并向抽象工厂新增生产该抽象产品的方法,以及每个具体工厂生产的具体产品。
工厂模式优势
- 解耦:把对象创建和使用分开,方便维护
TCP UDP
- TCP
- 面向连接
- 可靠传送、有流量控制、拥塞控制
- 首部20-60字节
- 只能一对一通信
- UDP
- 无连接
- 不可靠、无流量控制、拥塞控制
- 首部小,8字节
- 支持一对一、一对多、多对一、多对多
字典Get原理
- 一个bucket数组、一个entry数组
- 插入首先计算key的hash值,找到对应的bucket下标
- 该下标的值对应entry数组下标
- 获取entry的hashCode,若不一致则访问next知道找到
c#委托是什么?有什么用?
- 是一个特殊的类,默认继承MulticastDelegate,内部保存了一个方法的指针。
- 用来保存回调方法,如协程方法参数传递委托,如UI事件。
接口用处
- 用于表示一个类能做什么 —— can do
- 通过依赖倒置,降低耦合性
- 方法隔离,使用接口类型的成员变量,只能调用接口方法。
如何避免资源冗余
- 提前制定好资源目录结构,一个UI预制件一个文件夹、一个角色、场景一个文件夹
- 共用纹理一个包
如何避免GC
- 使用值类型,避免值类型装箱(使用泛型)
- 避免大量字符串字面值的拼接
- 避免匿名方法(避免产生闭包对象)
- 避免容器类型扩容,如List、Stack、Queue、Dictionary,底层都是数组
- 避免协程,yield return 会返回一个IEnumerator对象
Unity GC算法
- Unity采用贝母垃垃圾回收算法
- GC会暂停程序,因此要避免GC时间过长 -> 避免一次回收大量垃圾
- 通过Root指针开始,遍历所有被引用的对象并标记,最后释放未被标记的对象。
- Unity 2019后采用渐进式GC、分成多个片GC
- 32为最小申请8字节、64最小申请16字节
无限滚动列表
初始化content的高度,大小为Item总数/每行数量
通过content的位置获取要显示的Item的下标min和max
- minIndex = anchoredPos.y / (itemHeight + space) * 每行item数量
- maxIndex = (anchoredPos.y + viewPortHeight) / (itemHeight + space + 1) * 每行item数量 - 1
- maxIndex = min(maxIndex, Item总数-1)
显示minIndex-maxIndex下标的Item,从对象池中获取,设置初始位置并初始化
- 位置x = (index % 每行item数量) * (itemWidth + space)
- 位置y = -(index / 每行item数量) * (itemHeight + space)
使用一个容器(Dictionary<int, Item>)保存当前显示Item
移除显示区域外的Item
- 维护一个preMinIndex, preMaxIndex
- 在第3步前移除[preMin, min),(max, preMax]之间的Item,返回pool
一个完整的生命周期里有哪些协程,在哪些阶段
Physics
- yield WaitForFixedUpdate
GameLogic
- yield null
- yield WaitForSeconds
- yield WWW
- yield StartCoroutine
End of frame
- yield WaitForEndOfFrame
Unity 协程原理
- 使用MonoBehaviour.StartCoroutine方法,传递一个IEnumerator参数
- Unity在生命周期中调用这些IEnumerator的MoveNext方法,并设置Current
- 协程会在gameobject active false或Monobehaviour被摧毁或调用Stopcoroutine时停止
Canvas作用
- 合并UI元素并绘制
- 合并规则:同层级同材质合并
Graphic Raycaster作用
- 获取鼠标在屏幕点击坐标,并使用射线检测与Canvas下左右UI元素的TectTransform矩形求交。返回RaycastResult数组
EventSystem作用
- 驱动输入模块(
BaseInputModule)更新 - 为输入模块提供射线检测方法(
RaycastAll)检测鼠标与场景物体相交,内部调用场景中所有BaseRaycaster组件(PhysicsRaycaster、GraphicRaycaster)中Raycast方法。
BaseInputModule作用
默认为
StandaloneInputModule,其继承关系:StandaloneInputModule->PointerInputModule->BaseInputModule
内部维护鼠标数据,如:位置、位移delta、与场景射线检测碰撞数据
RaycastResult由EventSystem驱动,每帧调用其
Process方法Process方法内部:获取鼠标按键数据(左中右三个按键)
GetMousePointerEventData:主要做两件事方法内部通过
Input中API获取鼠标数据,内部调用引擎C++层代码调用
eventSystem.RaycastAll获取鼠标点击与场景UI或3D物体求交结果
发送鼠标相关事件,由
ExecuteEvents.Execute方法实现,事件接受者需包含继承IEventSystemHandler接口的组件。
两种Raycaster
PhysicsRaycaster内部从相机发出射线与场景物体,内部调用Physics.RaycastAll并由近到远排序射线检测结果,结果为一个List<RaycastResult>数组GraphicRaycaster- 首先获取所有 RaycastableGraphics(勾选RaycastTarget)的物体。
- 若相机的
RenderMode不是ScreenSpaceOverlay并且GraphicRaycaster的BlockObject为2D或3D会调用 Physics 中的raycast3D或 Physics2D中的GetRayIntersectionAll方法第一个相交的物体,并记录距离hitDistance,之后后面调用真正的GraphicRaycaster.Raycast方法与所有Graphics求交,内部调用RectTransformUtility.RectangleContainsScreenPoint方法判断鼠标点击位置是否在图形矩形内部,若在内部则继续调用Graphic中Raycast成员方法 Graphic Raycast方法实现:从当前graphic transform出发,不断向上查找,直到找到包含ICanvasRaycastFilter组件的GO,如Mask、RectMask2D、Image、CanvasGroup,调用其IsRaycastLocationValid方法,若任何一个filter返回false则直接返回false,若全部通过,则返回true。Image组件中IsRaycastLocationValid方法实现:- 若图片没有精灵贴图,则返回true。
- 将点击点精灵图片的透明度与
alphaHitTestMinimumThreshold比较,若大于或等于则返回tue
- 将所有与射线相交的graphic的相交距离与
hitDistance相比,若大于,说明被2D或3D物体挡住了,若小于则加入resultAppendList返回给输入模块
UI开发中的GC问题
频繁
Instantiate或DestroyUI元素- 使用对象池
UI事件回调使用Lambda表达式,会产生闭包临时对象
- 手动定义回调方法
Update中拼接字符串
*
1
2
3
4void Update()
{
scoreText.text = "Score: " + currentScore; // 每帧产生新字符串
}缓存字符串:仅在数值变化时更新文本。
*
1
2
3
4
5
6
7
8
9private int cachedScore = -1;
void Update()
{
if (currentScore != cachedScore)
{
scoreText.text = $"Score: {currentScore}";
cachedScore = currentScore;
}
}或者使用StringBuilder
协程中yield return new XXX()
- 缓存XXX
频繁加载图片资源
- 缓存
Buf系统
- 配置:Buf功能、功能参数、触发条件(配置)、持续时间、能否叠加、叠加层数、能否驱散、UI、特效资源、描述信息
- Buf工厂,用于创建buf
- Buf生命周期:OnAttach、OnDetach、OnHit、OnHert、OnTick…
TCP可靠传输原理 —— 基于以字节为单位的滑动窗口
- 窗口维护三个指针:
- –已发送并已确认收到–P1–已发送未确认–P2–尚未发送–P3–不允许发送–
HybirdCLR对比Lua
- Lua语言是很精简,主要是运行时和宿主语言的交互有很大的损耗。
- HybridCLR扩充了Il2cpp运行时代码,使它由纯AOT Runtime变成AOT+Interpreter混合Runtime,进而原生支持动态加载程序集,从底层彻底支持了热更新。在所有il2cpp支持的平台上高效运行。
红点树
- 底层采用前缀树
- 初始化通过前缀字符串数组初始化前缀树
- 修改叶子节点值会触发UI事件回调,并且触发Parent重写计算红点数量
死锁必要条件
死锁的发生必须同时满足以下四个条件,缺一不可:
- 互斥条件
- 资源是独占的,一次只能被一个进程使用。
- 持有并等待
- 进程已经持有了至少一个资源,同时在等待获取其他进程持有的资源,且在等待期间不会释放已持有的资源。
- 不可剥夺
- 资源不能被强制从持有它的进程中抢占,只能由持有资源的进程主动释放。
- 循环等待
- 存在一个进程链,每个进程都在等待下一个进程所持有的资源。
解决死锁的思路
- 要避免或解决死锁,通常需要破坏上述四个条件中的至少一个:
- 破坏互斥条件:允许资源共享(如只读资源)。
- 破坏持有并等待:要求进程一次性申请所有需要的资源(如原子性申请)。
- 允许资源抢占:强制剥夺某些进程的资源(可能导致任务失败)。
- 破坏循环等待:对资源类型进行排序,按固定顺序申请资源(如总是先申请锁A再申请锁B)。
Protobuf好处
- 是什么:谷歌开发的一种数据描述语言,非常适合做数据存储以及RPC中数据的载体
- 优势:
- 序列化后的二进制数据体积相比Json和XML很小,适合网络传输
- 序列化反序列化速度很快
- 支持跨平台,将.proto文件生成.cs文件
- 序列化算法
- 采取 Tag-Length-Value 方式存储数据,冗余数据小
- 可变长存储Varint类型数据
Transform和RectTransform的区别
- Transform是所有游戏对象的基础组件,适用于所有游戏对象,包括3D模型、灯光、摄像机等。
- RectTransform是Transform的扩展,专门用于处理UI元素在UI系统中的布局和定位。
StringBuilder内部原理
- 使用一个char[]数组保存字符串中的字符
- 追加字符串若数组超出容量会创建一个新的StringBuilder对象,容量翻倍,并将超出容量的字符保存在新的Sb对象char数组中,新对象会用一个指针指向当前Sb对象。
GPU Instanceing
- 一次性为多个具有相同网格的对象发出单个DrawCall
- CPU 收集所有每个对象的Transform和Material Properties,并将它们放入数组中,这些数组被发送到 GPU。然后 GPU 遍历所有条目,并按照提供的顺序渲染它们。
SRP Batching
将材质属性缓存在GPU特定的常量缓冲区中,因此不必在每次绘制调用时发送。
物体材质需使用来自相同着色器变体
1 | CBUFFER_START(UnityPerMaterial) |
- SRP 批处理器无法处理Per-object的材质属性,可使用GPUInstancing
Lua
什么是闭包?
- 当一个函数内嵌另一个函数,内部函数访问外部函数中的局部变量,则称内部函数闭包,访问的外部局部变量为
upvalue。 - 一个典型的闭包的结构包含两个函数:一个是闭包自己;另一个是工厂(创建闭包的函数)。
闭包典型应用
实现迭代器
*
1
2
3
4
5
6
7
8
9
10
11
12function list_iter(list)
local i = 0
local n = table.getn(list)
return function()
i = i + 1
if (i <= n) then
return list[i]
else
return nil
end
end
end*
1
2
3
4
5
6
7
8
9
10
11
12
13
14list = {10, 20, 30}
iter = list_iter(list)
while true do
local val = iter()
if (val == nil) then
break;
end
print(val)
end
// 迭代器写法
for val in list_iter(list) do
print(val)
end
面向对象编程原则
- S.O.L.I.D原则
- S:单一职责设计:类,接口,对象等等,都只有一个单独的职责
- 好处:降低类的复杂度,提高代码的可读性和可维护性。
- O:开闭原则:对修改关闭,对扩展开放
- 好处:在不改动已有代码的情况下,通过扩展实现新功能,降低出错风险。
- L:里氏替换原则:子类可以扩展父类的功能,但不能修改父类的功能
- 好处:保证继承关系的正确性,避免多态中的意外错误。
- I:接口隔离原则:接口应当精小、单一
- 将一个庞大的、总揽一切的接口,拆分成多个更小、更具体的接口。
- D:依赖倒置原则:高层不应该依赖于底层,高层和底层都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。
- 好处:降低模块间耦合度,使系统更易于扩展和维护。
迪米特法则:又叫最少知道原则,一个对象应该 尽可能少地了解其他对象的细节,避免和不必要的对象发生直接耦合。“低耦合、高内聚”,不要随意和不相关的类发生联系。
合成复用法则:组合优于继承,能用组合的地方不要用继承。
前向渲染vs延迟渲染
- 光照时间复杂度:N^2
- 直接画在ColorRT上
- 光照模型随意
- 多光源支持不好
- 对带宽(内存)要求低
- 光照模型灵活
- 后处理仅有深度图,其他图需要额外画(Normal)
Early-Z
GPU硬件技术
将深度测试放到PixelShader前面
- 如果通过才跑PixelShader,否者直接丢弃该像素
只对实心物体有效
实心物体从前往后画,需要写入深度
半透明物体从后往前画,不需要写入深度
Ealy-Z失效的情况:
手动修改了深度值(情况较少)
丢弃像素(discard clip) AlphaTest(颜色混合)
优化不稳定
Z-Prepass
- 时间复杂度:场景物体数量*灯光数量
延迟渲染
- 时间复杂度:屏幕像素*灯光数量
- 需要硬件支持MRT(Multi Render Target)
- 以下的GBuffer设计会造成浪费带宽
- GBuffer优化
触发Canvas BuildBatch 触发条件
- UI调用SetDirty,包含
SetLayoutDirty、SetMaterialDirty、SetVerticesDirty - UI元素Enable、Disable、OnValid或触发SetDirty
- Image顶点属性(颜色、纹理)、maskable、层级、发生改变,会
SetVerticesDirty RectTransform发生改变,会SetLayoutDirty、SetVerticesDirty
UGUI 优化相关术语
- 降低CPU消耗方面
- 不需要接收射线检测的
Graphic禁用m_RaycastTarget(默认开启),减少Raycast消耗 - 降低Rebuild消耗,Rebuild分为
布局重建、图形重建- 降低布局重建消耗:在UI 布局位置不会动态调整的情况下,不要偷懒用Layout组件实现自动布局,直接Rectranform写死坐标位置即可,Layout 动态计算有开销。
- 降低图形重建消耗:
- 不需要接收射线检测的
延迟渲染流程
- 第一个pass(几何):渲染场景中所有物体,保留离相机最近的片元几何信息至G-Buffer中
- 第二个pass(光照):遍历G-Buffer中各个片元信息,执行光照计算。
- 优势:
- 大量光源drawcall线性开销
- 光照paas只计算可见像素,节省性能











