unity 面试题

UI优化策略

  • 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阶段(应用阶段)

    1. 剔除:视锥体剔除、层级剔除、遮挡剔除
    2. 设置渲染顺序:渲染顺序主要由渲染队的值决定、不透明物体从前往后、透明从后往前
    3. 打包数据:模型信息(顶点坐标、法线、UV、切线、顶点颜色、索引)、变换矩阵、灯光、shader
    4. SetPass Call:设置GPU渲染状态(ZWrite、ZTest、Cull、Blend)
    5. DrawCall:向GPU发送渲染命令
  • GPU阶段

      1. 顶点处理
        1. 顶点着色器(顶点变换, 模型->裁剪空间)
        2. 曲面细分着色器(可选)
        3. 几何着色器(可选)
      2. 图元装配
        1. 剪裁
        2. 透视除法(剪裁空间->NDC空间)
        3. 屏幕映射
      3. 光栅化
        1. 三角形设置
        2. 三角形遍历:找出三角形覆盖的片元、并通过差值获取该片元信息(坐标、颜色、深度、法线、uv等)
    • 片元着色

      1. 纹理采样、光照
    • 输出合并

      1. Alpha测试、模版测试、深度测试、混合

      2. 输出帧缓冲(颜色缓存、深度缓冲)

Draw Call优化

  1. 什么是DrawCall:CPU调用底层图形API向GPU发送一次绘制命令的行为
  2. 优化手段
    1. 静态和批:只适用于静态、材质相同的物体。同一物体过多会导致顶点内存暴涨
    2. 动态和批:顶点数量限制大,CPU消耗大
    3. GPU Instancing:一份网格+多个变换矩阵参数一次性发送给GPU,需要GPU支持

Canvas的作用是什么?

  • UI画布,UI系统的根容器,可以设置渲染模式、缩放模式以及渲染顺序
  • UI元素负责使用Canvas Renderer组件向Canvas发送网格、纹理、材质等数据。Canvas负责将相同材质/纹理的UI元素的网格数据合并向GPU发送DrawCall
  • 发起willRenderCanvases事件,调用CanvasUpdateRegistryPerformUpdate方法

PerformUpdate执行流程

  • CanvasUpdateRegistry维护两个List,

    • m_LayoutRebuildQueue:用于维护需要执行布局重建的ICanvasElement元素
    • m_GraphicRebuildQueue用于维护需要执行图形重建的ICanvasElement元素
  • Layout重建阶段:首先遍历CanvasUpdate.PrelayoutCanvasUpdate.LayoutCanvasUpdate.PostLayout,然后遍历m_LayoutRebuildQueue中所有元素,将其作为参数传递给这些ICanvasElementRebuild方法,主要由LayoutRebuilder实现了该方法,在Layout阶段执行Rebuild方法。

    • LayoutRebuilderRebuid方法实现

      • 自底向上的计算阶段(PerformLayoutCalculation:先递归处理所有子ILayoutElementCalculateLayoutInputHorizontal/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

      1. 调用OnPopulateMesh方法,默认将矩形四个顶点位置、颜色、uv加入到VertexHelper中。
      2. 获取自身所有IMeshModifier组件,如ShadowOutline组件的ModifyMesh方法,将修改后的顶点信息写入VertexHelper中,内部会将顶点数据拷贝多份。
      3. VertexHelper将获取的顶点数据填充全局共享的workerMesh中,然后调用canvasRenderer.SetMesh方法将网格数据传递给
    • 更新材质UpdateMaterial

      • 将自身materialForRendering材质以及mainTexture纹理发送给自身canvasRenderer组件

      • 注意:materialForRendering属性可被自身IMaterialModifier修改

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      public 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

  • 分为三个步骤:

    1. 重新计算自动布局元素的布局

    2. 为所有启用的元素重新生成网格

    3. 重新生成材质(为了batch meshes)

  • Canvas会对网格排序并生成batch

片元着色

  1. 深度测试,不通过则直接结束
  2. 执行frag shader计算颜色,与颜色缓冲区混合,默认直接替换缓冲区颜色
  3. 写入深度信息

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资产文件中,并在游戏中动态加载并生成游戏对象。
  • 导出原理
    1. 依次遍历当前场景下各个节点挂载的描述性组件,读取组件上的信息并保存到一个SceneExportInfo对象实例中。
    2. 使用Newtonsoft.Json.JsonConvert.SerializeObject方法将该对象序列化为json文件并保存到本地。
    3. 获取场景导出目录下的所有json文件,针对每个json文件,创建一个ScriptableObject资产并将json内容写入,最后将ScriptableObject资产保存至Resource目录下。
    4. 游戏运行时,当进入关卡初始化时根据关卡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),需要处理依赖
  • 资源加载流程
    1. 首先尝试从缓存中加载(缓存中保存资产的path以及弱引用),若存在则返回
    2. 若ResourceManager还没有初始化完成,则使用Resource方式加载
    3. 初始化完成则选择使用AssetDataBase或AssetBundle方式加载
  • AssetBundle加载流程
    1. 尝试从缓存中获取AB
    2. 获取失败则注册一个加载现场,若已存在相同的加载现场则等待
    3. 加载依赖AB,递归调用加载函数
    4. 若无依赖则加载AB,首先尝试从本地加载,若本地不存在或版本号发生变动这需要从远程下载AB,否则从本地使用加载AB。加载完成后将AB存入缓存。
    5. 从AB中加载目标资源,调用LoadAssetWithSubAssets方法从AssetBundle中异步加载资源及其子资源。
    6. 将资源以及子资源存放到缓存当中,key为资源路径,value为资源弱引用
    7. 资源加载完成执行回调,将自身返回。

群体寻路算法

  • 已知当前速度方向向量,

  • 重点是转向速度方向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

配置文件生成流程

  1. 确定Excel表结构,填写配置数据
  2. 使用工具生成对应的c#类型以及对应的protobuf二进制文件
  3. 根据生成的c#文件和Velocity引擎自动配置加载类,包括资源路径、序列化、反序列化方法

配置文件加载流程

  1. 在游戏初始化,配置文件加载器初始化中获取所有配置文件路径
  2. 加载配置文件到内存中,并调用对应对应反序列化函数生成对应类型实例,并缓存到字典中key为ID,value为对应类型实例
  3. 调用加载器对应Load方法加载配置数据

AssetBundle 打包流程

  • 方法一:指定每个资源AssetBundle名称
  • 方法二:
    1. 打包前生成BundleDescriptionList:
      • 为每个文件夹创建对应Bundle描述文件,该文件描述了该文件夹下资产要打成多少个AB包,以及每个AB包名称,包含资产的路径。以及不同平台AB发布策略,如AB包存储在热更远程的服务器或者随安装包发布、在游戏启动时更新还是AB中资产使用到了再更新。
    2. 分两次打包:第一次收集所有要打包的AB包名以及包含的资源。第二次创建一个BundleData清单文件收集所有打包好的AB包的版本号、hash以及crc校验码、大小等信息并保存至BundleData文件中。

AssetBundle 框架流程

  1. 切换平台
  2. 读取打包配置文件(xml、so),文件记录了ab包生成路径、所有需要打包成AB包的信息,其中AB包信息包括:打包路径、资源颗粒度、文件后缀等
  3. 获取所有需要打包的文件以及文件之间的依赖关系(字典存一下),将依赖的文件也加入需要打包的文件集合中
  4. 根据需要打包的文件生成所有ab包的名称,并用字典记录ab包与ab包中的文件的映射关系

AssetBundle 热更新流程

  1. 游戏启动时会先从配置文件中获取bundle热更地址
  2. 下载远程Bundle清单文件,获取服务器AB列表、资源版本号信息,并记录需要预下载的AB
  3. 将本地Bundle清单中每个bundle和远程bundle对比,若发生缺失或版本号或哈希值发生变化则bundle加入到更新列表中并记录要下载的AB文件大小
  4. 提示玩家是否更新,若更新则遍历更新列表并调用UnityWebRequestAssetBundle.GetAssetBundle方法从远程异步下载AB并统计下载进度
  5. 下载完成后加载BundleManifest,结束

什么是有限状态机

  • 管理游戏物体在有限个状态之间切换的一种数据结构,每个状态都有Enter、Update、Exit三种行为,状态之间的切换由FSM统一管理,同一时间只能执行一种状态。
  • 应用:初始玩家状态机时候创建所有状态,设置初始状态。在Update中先处理玩家输入,然后根据输入设置状态切换。

分层状态机

  • 为什么要分层:状态多了转换条件复杂,不好管理,分层后内层状态只需要考虑内层之间的切换,外部只用管理外层状态的切换。可以将状态根据一些场景分类,比如说战斗内、战斗外;或者当前在水中、在陆地或者在天上分层。

行为树

  • 叶子节点作为行为节点,用于执行具体的行为。非叶子节点为控制节点修饰节点
    • 控制节点:用来决定其子节点以顺序(依次执行,失败返回)、选择(依次执行,成功返回)、随机并行(执行所有)等方式执行。
    • 修饰节点:取反器(对子节点取反);重复执行器(重复执行一定次数)
  • 执行顺序:前序遍历,每次Tick都会遍历一遍树。
  • 优点:1. 将复杂的AI行为直观、有条理的展现出来,好维护,符合人类思维模式。2. 节点复用性高
  • 缺点:遍历树结构对CPU的消耗比状态机大。

平衡二叉树和红黑树的区别

  1. AVL平衡条件更加严格,要求每个节点的左右子树高度差不超过1;红黑树相对宽泛。
  2. 查找、插入和删除操作的时间复杂度都是O(log n),AVL查找更快,红黑树插入和删除更快。

ILRuntime执行原理

  • 使用 Mono.Cecil 库读取热更程序集中的类型信息,并通过自己实现的一套解释器解释方法中的IL代码。

  • 执行流程

    1. 读取热更程序集dll,以流的形式保存
    2. 使用 Mono.Cecil 库提供的ReadModule方法读取热更程序集并将其转换为Mono.Cecil类型系统,并将程序集中所有类型转换为ThreadSafeDictionary<int, IMethod> mapMethod字典保存到appDomain中
    3. 执行方法时获取方法体的IL代码并逐个解释,一种是通过ILRuntime自己实现的栈来执行,一种通过将IL转换为自定义的寄存器指令来执行。
  • 栈内存布局

    • ILRuntime中的所有对象都是以StackObject类来表示:

      1
      2
      3
      4
      5
      6
      struct 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)
    • 当执行方法遇到callcallvirtIL指令时,说明为方法调用

    • 若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);
      • 重定向方法执行流程

        1. 通过解释器获取appDomain
        2. 根据参数个数获取返回指针地址,类型为StackObject*,同时也是返回值存放的地址,esp减去参数个数
        3. 获取参数地址,类型为StackObject*
        4. 从栈顶到栈尾依次通过地址获取实参,获取后通过解释器的Free方法释放实参栈内存,通过ILRuntime.CLR.Utils.Extensions.CheckCLRTypes扩展方法获取
        5. 处理重定向逻辑
        6. 若方法有返回值,则将返回值赋值,并设置返回类型,若返回值不为简单类型,则调用ILIntepreter.PushObject方法设置返回值
  • 通过自动分析热更DLL生成CLR绑定原理

    1. 读取热更程序集初始化appDomain

    2. 获取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注入原理

    1. 使用MonoXIL.Cecil读取Assembly-CSharp.dll程序集
    2. 获取程序集中所有满足条件的类型
    3. 获取类型中所有满足DelegateBridge中__Gen_Delegate_Imp方法签名匹配的方法
    4. 向方法的Body添加一个类型为DelegateBridge的局部变量
    5. 在方法体的开头注入代码,加载委托实例并调用它。

七大设计原则

  • 单一职责

  • 开放封闭原则

    • 对扩展开放、对修改关闭
  • 里式替换原则

    • 用父类或子类句柄调用子类方法,结果不会发生改变
  • 接口隔离原则

    • 接口尽量细化,同时接口中的方法尽量少。
  • 依赖倒转原则

    • 使用接口或抽象类
  • 迪米特法则

    • 一个类对自己依赖的类知道的越少越好(降低类之间的耦合)
  • 合成复用原则

    • 使用聚合代替继承

设计模式

  • 理解

    • 一系列成熟的代码设计方式,用来提高代码质量
    • 三类设计模式:创建型、结构型、行为型
    • 常用的设计模式:单例、工厂、观察者
  • 工厂模式

    • 简单工厂 - 集中式创建

      • 实现简单,使用方便
      • 缺点:违背开闭原则,扩展需要修改工厂类
    • 工厂方法 - 多态创建

      • 优点:符合开闭原则、易于扩展
      • 缺点:需要创建多个工厂类
    • 抽象工厂 - 产品族创建

      • 优点:可以创建相关产品族,保证产品兼容性,如:游戏有人族、精灵族等种族,每个种族都有战士、法师等职业。
      • 缺点:扩展产品族比较困难
      • 在具体工厂上加了一层抽象层,就是一个抽象工厂接口,该接口声明了一些生产一些抽象产品的方法。
      • 具体工厂继承抽象工厂接口
      • 新增一个具体工厂需要继承抽象工厂,并新增具体产品
      • 新增一个产品需要先新增一个抽象产品,并向抽象工厂新增生产该抽象产品的方法,以及每个具体工厂生产的具体产品。
  • 工厂模式优势

    • 解耦:把对象创建和使用分开,方便维护

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字节

无限滚动列表

  1. 初始化content的高度,大小为Item总数/每行数量

  2. 通过content的位置获取要显示的Item的下标min和max

    • minIndex = anchoredPos.y / (itemHeight + space) * 每行item数量
    • maxIndex = (anchoredPos.y + viewPortHeight) / (itemHeight + space + 1) * 每行item数量 - 1
    • maxIndex = min(maxIndex, Item总数-1)
  3. 显示minIndex-maxIndex下标的Item,从对象池中获取,设置初始位置并初始化

    • 位置x = (index % 每行item数量) * (itemWidth + space)
    • 位置y = -(index / 每行item数量) * (itemHeight + space)
  4. 使用一个容器(Dictionary<int, Item>)保存当前显示Item

  5. 移除显示区域外的Item

    • 维护一个preMinIndex, preMaxIndex
    • 在第3步前移除[preMin, min),(max, preMax]之间的Item,返回pool

一个完整的生命周期里有哪些协程,在哪些阶段

  1. Physics

    • yield WaitForFixedUpdate
  2. GameLogic

    • yield null
    • yield WaitForSeconds
    • yield WWW
    • yield StartCoroutine
  3. 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组件(PhysicsRaycasterGraphicRaycaster)中Raycast方法。

BaseInputModule作用

  • 默认为StandaloneInputModule,其继承关系:

    • StandaloneInputModule->PointerInputModule->BaseInputModule
  • 内部维护鼠标数据,如:位置、位移delta、与场景射线检测碰撞数据RaycastResult

  • 由EventSystem驱动,每帧调用其Process方法

  • Process方法内部:

    1. 获取鼠标按键数据(左中右三个按键)GetMousePointerEventData:主要做两件事

      1. 方法内部通过Input中API获取鼠标数据,内部调用引擎C++层代码

      2. 调用eventSystem.RaycastAll获取鼠标点击与场景UI或3D物体求交结果

    2. 发送鼠标相关事件,由ExecuteEvents.Execute方法实现,事件接受者需包含继承IEventSystemHandler接口的组件。

两种Raycaster

  • PhysicsRaycaster内部从相机发出射线与场景物体,内部调用Physics.RaycastAll 并由近到远排序射线检测结果,结果为一个List<RaycastResult>数组
  • GraphicRaycaster
    • 首先获取所有 RaycastableGraphics(勾选RaycastTarget)的物体。
    • 若相机的RenderMode不是ScreenSpaceOverlay并且 GraphicRaycasterBlockObject 为2D或3D会调用 Physics 中的 raycast3D 或 Physics2D中的 GetRayIntersectionAll方法第一个相交的物体,并记录距离hitDistance,之后后面调用真正的GraphicRaycaster.Raycast方法与所有Graphics求交,内部调用RectTransformUtility.RectangleContainsScreenPoint方法判断鼠标点击位置是否在图形矩形内部,若在内部则继续调用Graphic中Raycast成员方法
    • Graphic Raycast方法实现:从当前graphic transform出发,不断向上查找,直到找到包含ICanvasRaycastFilter组件的GO,如MaskRectMask2DImageCanvasGroup,调用其IsRaycastLocationValid方法,若任何一个filter返回false则直接返回false,若全部通过,则返回true。
    • Image组件中IsRaycastLocationValid方法实现:
      • 若图片没有精灵贴图,则返回true。
      • 将点击点精灵图片的透明度与alphaHitTestMinimumThreshold比较,若大于或等于则返回tue
    • 将所有与射线相交的graphic的相交距离与hitDistance相比,若大于,说明被2D或3D物体挡住了,若小于则加入resultAppendList返回给输入模块

UI开发中的GC问题

  • 频繁InstantiateDestroyUI元素

    • 使用对象池
  • UI事件回调使用Lambda表达式,会产生闭包临时对象

    • 手动定义回调方法
  • Update中拼接字符串

    *

    1
    2
    3
    4
    void Update()
    {
    scoreText.text = "Score: " + currentScore; // 每帧产生新字符串
    }
    • 缓存字符串:仅在数值变化时更新文本。

      *

      1
      2
      3
      4
      5
      6
      7
      8
      9
      private 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重写计算红点数量

死锁必要条件

死锁的发生必须同时满足以下四个条件,缺一不可:

  1. 互斥条件
    • 资源是独占的,一次只能被一个进程使用。
  2. 持有并等待
    • 进程已经持有了至少一个资源,同时在等待获取其他进程持有的资源,且在等待期间不会释放已持有的资源。
  3. 不可剥夺
    • 资源不能被强制从持有它的进程中抢占,只能由持有资源的进程主动释放。
  4. 循环等待
    • 存在一个进程链,每个进程都在等待下一个进程所持有的资源。

解决死锁的思路

  • 要避免或解决死锁,通常需要破坏上述四个条件中的至少一个:
  1. 破坏互斥条件:允许资源共享(如只读资源)。
  2. 破坏持有并等待:要求进程一次性申请所有需要的资源(如原子性申请)。
  3. 允许资源抢占:强制剥夺某些进程的资源(可能导致任务失败)。
  4. 破坏循环等待:对资源类型进行排序,按固定顺序申请资源(如总是先申请锁A再申请锁B)。

Protobuf好处

  • 是什么:谷歌开发的一种数据描述语言,非常适合做数据存储以及RPC中数据的载体
  • 优势:
    1. 序列化后的二进制数据体积相比Json和XML很小,适合网络传输
    2. 序列化反序列化速度很快
    3. 支持跨平台,将.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
2
3
4
5
6
7
8
9
10
11
CBUFFER_START(UnityPerMaterial)
// 将 _BaseColor 放入特定的常量内存缓冲区
float4 _BaseColor;
CBUFFER_END

CBUFFER_START(UnityPerDraw)
float4x4 unity_ObjectToWorld;
float4x4 unity_WorldToObject;
float4 unity_LODFade;
real4 unity_WorldTransformParams;
CBUFFER_END
  • SRP 批处理器无法处理Per-object的材质属性,可使用GPUInstancing

Lua

什么是闭包?

  • 当一个函数内嵌另一个函数,内部函数访问外部函数中的局部变量,则称内部函数闭包,访问的外部局部变量为upvalue
  • 一个典型的闭包的结构包含两个函数:一个是闭包自己;另一个是工厂(创建闭包的函数)。

闭包典型应用

  • 实现迭代器

    *

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function 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
    14
    list = {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

面向对象编程原则

  1. S.O.L.I.D原则
  • S:单一职责设计:类,接口,对象等等,都只有一个单独的职责
    • 好处:降低类的复杂度,提高代码的可读性和可维护性。
  • O:开闭原则:对修改关闭,对扩展开放
    • 好处:在不改动已有代码的情况下,通过扩展实现新功能,降低出错风险。
  • L:里氏替换原则:子类可以扩展父类的功能,但不能修改父类的功能
    • 好处:保证继承关系的正确性,避免多态中的意外错误。
  • I:接口隔离原则:接口应当精小、单一
    • 将一个庞大的、总揽一切的接口,拆分成多个更小、更具体的接口。
  • D:依赖倒置原则:高层不应该依赖于底层,高层和底层都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。
    • 好处:降低模块间耦合度,使系统更易于扩展和维护。
  1. 迪米特法则:又叫最少知道原则,一个对象应该 尽可能少地了解其他对象的细节,避免和不必要的对象发生直接耦合。“低耦合、高内聚”,不要随意和不相关的类发生联系。

  2. 合成复用法则:组合优于继承,能用组合的地方不要用继承。

前向渲染vs延迟渲染

  • 光照时间复杂度:N^2
  • 直接画在ColorRT上
  • 光照模型随意
  • 多光源支持不好
  • 对带宽(内存)要求低
  • 光照模型灵活
  • 后处理仅有深度图,其他图需要额外画(Normal)

Early-Z

  • GPU硬件技术

  • 将深度测试放到PixelShader前面

    • 如果通过才跑PixelShader,否者直接丢弃该像素
  • 只对实心物体有效

    • 实心物体从前往后画,需要写入深度

    • 半透明物体从后往前画,不需要写入深度

  • Ealy-Z失效的情况:

    • 手动修改了深度值(情况较少)

    • 丢弃像素(discard clip) AlphaTest(颜色混合)

    • 优化不稳定

Z-Prepass

  • 时间复杂度:场景物体数量*灯光数量
    alt text
    alt text

延迟渲染

  • 时间复杂度:屏幕像素*灯光数量
  • 需要硬件支持MRT(Multi Render Target)
  • alt text
  • 以下的GBuffer设计会造成浪费带宽
  • alt text
  • GBuffer优化
  • alt text
  • alt text
  • alt text

触发Canvas BuildBatch 触发条件

  • UI调用SetDirty,包含SetLayoutDirtySetMaterialDirtySetVerticesDirty
  • UI元素Enable、Disable、OnValid或触发SetDirty
  • Image顶点属性(颜色、纹理)、maskable、层级、发生改变,会SetVerticesDirty
  • RectTransform发生改变,会SetLayoutDirtySetVerticesDirty

UGUI 优化相关术语

  • 降低CPU消耗方面
    1. 不需要接收射线检测的Graphic禁用m_RaycastTarget(默认开启),减少Raycast消耗
    2. 降低Rebuild消耗,Rebuild分为布局重建图形重建
      • 降低布局重建消耗:在UI 布局位置不会动态调整的情况下,不要偷懒用Layout组件实现自动布局,直接Rectranform写死坐标位置即可,Layout 动态计算有开销。
      • 降低图形重建消耗:

延迟渲染流程

  • 第一个pass(几何):渲染场景中所有物体,保留离相机最近的片元几何信息至G-Buffer中
  • 第二个pass(光照):遍历G-Buffer中各个片元信息,执行光照计算。
  • 优势:
    1. 大量光源drawcall线性开销
    2. 光照paas只计算可见像素,节省性能