自用备忘录
通用
网络相关
帧同步和状态同步
https://zhuanlan.zhihu.com/p/357973435
帧同步
帧同步的战斗逻辑在客户端
在帧同步下,通信就比较简单了,服务端只转发操作,不做任何逻辑处理。
客户端按照一定的帧速率(理解为逻辑帧,而不是客户端的渲染帧)去上传当前的操作指令,服务端将操作指令广播给所有客户端,
当客户端收到指令后执行本地代码,如果输入的指令一致,计算的过程一致,那么计算的结果肯定是一致的,这样就能保证所有客户端的同步,这就是帧同步。
由于帧同步战斗逻辑都在客户端,服务器没有验证,带来的问题就是外挂的产生(加速、透视、自动瞄准、数据修改等)
网络条件较差的客户端会影响其他玩家的游戏体验。(优化方案:乐观帧锁定、渲染与逻辑帧分离、客户端预执行、指令流水线化、操作回滚等)
不同机器浮点数精度问题、容器排序不确定性、RPC时序、随机数值计算不统一
状态同步
状态同步的战斗逻辑在服务端
在状态同步下,客户端更像是一个服务端数据的表现层
一般的流程是
客户端上传操作到服务器,
服务器收到后计算游戏行为的结果,然后以广播的方式下发游戏中各种状态,
客户端收到状态后再根据状态显示内容。
状态同步做回放系统的话会是个灾难。
延迟过大、客户端性能浪费、服务端压力大
对带宽的浪费。对于对象少的游戏,可以用快照保存整个游戏的状态发送,但一旦数量多起来,数量的占用就会直线上升。(优化:增量快照同步,协议同步指定数据)
两者的区别
最大的区别就是战斗核心逻辑写在哪?状态同步的战斗逻辑在服务端,帧同步的战斗逻辑在客户端。
状态同步比帧同步流量消耗大,例如一个复杂游戏的英雄属性可能有100多条,每次改变都要同步一次属性,这个消耗是巨大的,而帧同步不需要同步属性;
帧同步的回放&观战比状态同步好做得多,因为只需要保存每局所有人的操作就好了,而状态同步的回放&观战,需要有一个回放&观战服务器,当一局战斗打响,战斗服务器在给客户端发送消息的同时,还需要把这些消息发给放&观战服务器,回放&观战服务器做储存,如果有其他客户端请求回放或者观战,则回放&观战服务器把储存起来的消息按时间发给客户端。
状态同步的安全性比帧同步高很多,因为状态同步的所有逻辑和数值都是在服务端的,如果想作弊,就必须攻击服务器,而攻击服务器的难度比更改自己客户端数据的难度高得多,而且更容易被追踪,被追踪到了还会有极高的法律风险。而帧同步因为所有数据全部在客户端,所以解析客户端的数据之后,就可以轻松达到自己想要的效果。
FishnetRPC
RPC就是远程调用,
- 给方法标记特性,编译阶段会通过反射
- 调用时,首先将函数的参数序列化,再然后把函数信息,函数调用者信息一同打包成网络消息
- 序列化后的消息通过网络协议被发送到目标客户端或服务器。FishNet 会处理消息的传输,保证消息的可靠性(如丢包重试机制)和时序控制
- 接收方反序列化接收,通过解析出来的网络消息,调用函数
MipMap是什么
MipMap(多级渐远纹理)是计算机图形学中一种优化纹理渲染的技术,通过预生成多分辨率的纹理层级,并根据物体距离相机的远近选择不同分辨率的纹理图像进行渲染,以达到优化渲染效果和减少锯齿状图形的目的。
优化带宽使用:在读取贴图时,MipMap技术可以提高缓存命中率,从而减少带宽的消耗。
提高渲染速度:由于MipMap贴图需要被读取的像素少于普通贴图,因此渲染速度得到了提升。
抗锯齿:MipMap通过在不同距离下使用不同分辨率的纹理图像,避免了锯齿状图形的出现使得渲染结果更加平滑。
会增大1/3的存储大小
unity协程
原理
Unity 协程的原理是基于 C# 的 IEnumerator 和 yield return 机制,由 Unity 引擎的主循环(Game Loop)调度器驱动执行的。
它本质上不是线程,而是 一种由 Unity 主线程“每帧推进”的状态机。
口述PK
Unity的协程,首先它是一个底层类似状态机的实现,它基于时间片轮转的机制去做伪并发。
就是说它并不是在真正意义上的多线程上去并发运行,而是让你在使用上感觉它像并发。
比如在程序里,你可以通过协程做一些延时操作,或者把某些逻辑和主逻辑分开单独放到一个协程里,主逻辑就不会被阻塞。
线程是操作系统层面的概念,进程之下就是线程。
操作系统能够调度线程,但是它并不知道协程的存在。
协程不是由操作系统来管理的,所以也不是真正的并发。
减少drawcall
官方翻译
DrawCall批处理介绍
Draw Call 批处理(Draw Call Batching)
Draw Call 批处理是一种优化 Draw Call(绘制调用)的方法,它通过合并网格来减少 Unity 所需的绘制调用次数。Unity 提供了两种内置的 Draw Call 批处理方式:
静态批处理(Static Batching):用于静态 GameObject,Unity 会将它们合并并一起渲染。
动态批处理(Dynamic Batching):适用于足够小的网格,它会在 CPU 上变换顶点,合并相似顶点,并在一个绘制调用中渲染。
Unity 的内置 Draw Call 批处理相较于手动合并网格有一些优势,最显著的是 Unity 仍然可以对网格进行单独剔除(culling)。不过,它也有一些缺点:静态批处理会带来内存和存储开销,而动态批处理则会带来 CPU 开销。
要求与兼容性
本节介绍 Unity 内置 Draw Call 批处理方法在不同渲染管线中的兼容性。
功能 内置渲染管线 通用渲染管线 (URP) 高清渲染管线 (HDRP) 自定义可编程渲染管线 (SRP)
静态批处理 是 是 是 是
动态批处理 是 是 否 是
使用 Draw Call 批处理
以下是有关静态与动态批处理通用的使用信息。若需了解每种批处理方法的具体启用方式和使用方法,请参阅相关章节:Static batching 与 Dynamic batching。
Mesh Renderer、Trail Renderer、Line Renderer、Particle System 和 Sprite Renderer 支持批处理。
不支持的组件包括 Skinned Mesh Renderer 和 Cloth。
Unity 仅对相同类型的 Renderer 进行批处理,例如 Mesh Renderer 只能与 Mesh Renderer 批处理。
Unity 仅会批处理使用相同材质的 GameObject。因此,为了获得最佳批处理效果,应尽量在多个 GameObject 之间共享材质。
如果两个材质仅贴图不同,可通过贴图图集(Texture Atlasing)的方式将多个贴图合并为一个较大的贴图,然后使用同一个材质。
相关技术参考:Texture Atlasing – Wikipedia
在 内置渲染管线 中,你可以使用 MaterialPropertyBlock 来更改材质属性而不打破批处理。虽然 CPU 仍需进行部分渲染状态变更,但这比使用多个材质更高效。
注意: 在使用 Scriptable Render Pipeline (SRP) 的项目中不要使用 MaterialPropertyBlock,否则会破坏 SRP Batcher 的兼容性。
透明着色器(Transparent Shader) 通常要求 Unity 按照从后到前的顺序渲染网格。为了批处理透明网格,Unity 会先对它们进行排序,再尝试进行批处理。但由于透明物体需要排序渲染,所以其批处理效果通常不如不透明物体。
如果无法使用批处理机制,也可以手动合并靠得较近的网格作为备选方案。有关更多信息,请参阅 Unity 文档中的 “Combining meshes”。
警告:
当你从 C# 脚本中访问共享材质属性时,务必使用 Renderer.sharedMaterial,而不是 Renderer.material。因为 Renderer.material 会创建材质的副本并重新赋值给 Renderer,从而会导致 Unity 停止对该 Renderer 进行批处理。
如何优化
优化 Draw Call(绘制调用)
在将几何体绘制到屏幕上时,Unity 会向图形 API 发出 Draw Call(绘制调用)。每个绘制调用都告诉图形 API 要绘制什么 和 如何绘制。它包含绘制所需的全部信息,例如纹理、着色器和缓冲区等。尽管绘制本身可能开销不大,但准备绘制调用的过程通常对性能影响更大。
为什么要优化绘制调用
在准备一个绘制调用的过程中,CPU 会设置各种资源,并更改 GPU 的内部设置,这些设置统称为 渲染状态(Render State)。例如切换材质(Material)就是一种渲染状态的更改,而这类更改是图形 API 最消耗资源的操作之一。
因此,优化绘制调用的核心目标是:减少渲染状态更改的次数。主要有两种方式:
减少总的绘制调用数量(Draw Calls):绘制调用少了,渲染状态更改的机会也自然少。
组织绘制调用的顺序,减少状态切换:如果多个绘制调用能使用相同的渲染状态,图形 API 就可以“合并”它们,从而避免多次切换。
优化绘制调用和状态切换的好处:
提高每帧的渲染效率(降低帧时间);
降低设备耗电,延长电池续航;
减少设备发热;
提高项目可维护性:在早期就优化好绘制调用,可以后期添加更多对象而不会带来过大性能损耗。
Unity 中可用的绘制调用优化方式:
-
GPU 实例化(GPU Instancing)
一次性渲染多个相同的 Mesh(如多棵树、灌木)。这些对象使用相同的材质,但可以拥有不同的变换矩阵(位置、旋转、缩放)。 -
Draw Call 批处理(Batching)
Unity 提供以下几种内置批处理:
静态批处理(Static Batching):
将静态 GameObject 的网格预先合并;
Unity 会将合并后的数据发送到 GPU,但渲染时仍能单独剔除每个子网格;
适合位置不变的物体。
动态批处理(Dynamic Batching):
CPU 在运行时对符合条件的动态物体进行顶点变换和合并;
前提是这些顶点结构一致(例如都只有位置和法线);
小网格(顶点数 < 900)更容易被批处理。
手动合并网格(Mesh.CombineMeshes):
开发者在代码中手动将多个网格合并为一个;
渲染时只用一个绘制调用,适合对性能要求高的场景。
- SRP Batcher(仅限 SRP 项目)
如果你使用的是 Scriptable Render Pipeline(如 URP/HDRP),SRP Batcher 可以减少 CPU 设置绘制调用所需的时间,适用于使用相同 Shader Variant 的材质。
优化方式的优先级(Unity 优先使用哪种)
当一个 GameObject 同时满足多个优化方式时,Unity 会按以下优先级选择最高级别的方式:
- SRP Batcher + 静态批处理(SRP 项目中两者可共存)
- GPU Instancing
- 动态批处理(Dynamic Batching)
例如:
如果一个 GameObject 被标记为静态批处理,并且也启用了 GPU Instancing,Unity 会选择静态批处理,并禁用 GPU 实例化;
类似地,如果一个物体可以用 GPU 实例化,Unity 会禁用动态批处理。
在 Unity Inspector 中,如果批处理方式被替代了,通常会显示警告,提示你可以考虑关闭冲突的方式以启用另一个。
静态批处理
🔧 静态合批(Static Batching)
静态合批是一种合并不移动的网格(Meshes)以减少 Draw Call(绘制调用)的方法。Unity 会将被合并的网格转换到世界空间中,并为它们构建一个共享的顶点缓冲区和索引缓冲区。接着,对于可见的网格,Unity 会执行一系列简单的 Draw Call,几乎不需要状态切换。
静态合批 不会减少 Draw Call 的数量,而是通过减少每次 Draw Call 之间的渲染状态变化来优化性能。
性能优势
静态合批比动态合批更高效,因为静态合批 不会在 CPU 上变换顶点坐标。想了解更多性能影响,请参阅 性能影响。
要求与兼容性
渲染管线兼容性
功能 内置渲染管线 URP(通用管线) HDRP(高清管线) 自定义 SRP
静态合批(Static Batching) ✅ 支持 ✅ 支持 ✅ 支持 ✅ 支持
如何使用静态合批
Unity 可在构建时(Build Time)和运行时(Runtime)执行静态合批:
如果 GameObject 在构建应用前就存在于场景中,推荐在 编辑器中启用静态合批。
如果是在运行时创建的 GameObject 或网格,请使用 运行时 API。
使用运行时 API 合批后,你可以变换静态合批根节点的 Transform(移动、旋转、缩放),但不能变换合批内部的单个对象。
静态合批要求
为了让 GameObject 被静态合批,它们需要符合以下条件:
GameObject 是激活状态;
有启用的 Mesh Filter 组件,并且引用了一个 Mesh;
Mesh 设为可读写(Read/Write Enabled);
Mesh 顶点数大于 0;
Mesh 未被其他合并处理过;
GameObject 有启用的 Mesh Renderer;
材质的 Shader 没有设置 DisableBatching = true;
构建时的静态合批(Build Time)
在 Unity 编辑器中启用静态合批:
进入菜单:Edit > Project Settings > Player;
在 Other Settings 中勾选 Static Batching;
在 Scene 或 Hierarchy 中选择要合批的 GameObject;
在 Inspector 中,勾选 Static Editor Flags 中的 Batching Static 选项;
Unity 会自动将满足条件的静态网格合并为同一个 Draw Call。
提示:你可以一次性选择多个 GameObject,批量启用静态合批。
注意:如果在构建时启用静态合批,Unity 不会在运行时占用 CPU 来生成合并后的网格数据,效率更高。
运行时静态合批(Runtime)
Unity 提供了 StaticBatchingUtility 类用于运行时静态合批。
你可以使用静态方法 StaticBatchingUtility.Combine() 来将一组 GameObject 合并,适用于程序化生成的网格。
不同于构建时,运行时静态合批不需要在 Player Settings 中启用 Static Batching。
性能影响(Performance Implications)
静态合批会占用更多的 CPU 内存 来存储合并后的几何数据。
如果多个 GameObject 使用同一个 Mesh,Unity 会为每个对象复制 Mesh 并插入合并网格中;
也就是说,相同的几何数据在合批网格中会出现多次;
这适用于编辑器和运行时两种方式;
如果你希望节省内存空间,可能需要牺牲渲染性能,避免对某些 GameObject 使用静态合批。
例如:在密集森林场景中把所有树都标记为静态对象,可能会严重占用内存。
限制:单个静态合批的最大顶点数为 64,000,超出后 Unity 会创建新的合批。
动态合批
动态合批 (Dynamic Batching)
动态合批是一种 Draw Call 合批方法,用于将移动的 GameObject 合并,从而减少 Draw Call 数量。
它在普通网格(meshes)和运行时动态生成的几何体(如粒子系统)之间的工作方式有所不同。关于这两种几何体之间的内部差异,请参见 Unity 官方文档中的《用于网格的动态合批》和《用于动态生成几何体的动态合批》。
注意:用于网格的动态合批最初是为了优化旧款低端设备上的性能而设计的。
在现代消费级硬件上,动态合批所需的 CPU 运算开销可能比一次 Draw Call 的开销还大,从而导致性能下降。
更多信息请见《用于网格的动态合批》章节。
要求与兼容性
渲染管线兼容性
功能 内置渲染管线 (Built-in RP) 通用渲染管线 (URP) 高清渲染管线 (HDRP) 自定义 SRP
动态合批 ✔ 支持 ✔ 支持 ❌ 不支持 ✔ 支持
如何使用动态合批
Unity 始终对动态几何体(如粒子系统)使用动态合批。
对于普通网格,要启用动态合批,请按以下步骤操作:
打开菜单:Edit > Project Settings > Player
在“Other Settings”中勾选 Dynamic Batching 选项
若满足合批条件(见下文“限制”),Unity 会自动将移动网格合并成同一个 Draw Call
用于网格的动态合批
Unity 会在 CPU 上将所有顶点转换到世界空间,再统一传送到 GPU。
因此,仅当 CPU 上的变换运算比一次 Draw Call 的成本低时,动态合批才会提高性能。
Draw Call 的资源开销取决于多个因素,主要是图形 API。
例如在主机平台或使用 Apple Metal 这类现代图形 API 时,Draw Call 的开销通常很低,启用动态合批反而可能降低性能。
是否启用动态合批,应通过性能分析工具,分别测试启用与未启用时的表现。
Unity 在阴影渲染阶段可对材质不同但阴影参数一致的物体进行合批。
例如多个箱子使用了不同贴图的材质,但在阴影投射时这些材质的参数一致,Unity 可以将它们在阴影渲染阶段合批。
限制条件
以下情况下,Unity 无法使用动态合批或只能部分使用:
❌ 网格顶点过多:
Unity 对网格的顶点数量有限制。若顶点属性总数超过 900,或顶点数量超过 300,则不能合批。
例如:Shader 仅使用 position、normal、一个 UV,则可合批最多 300 个顶点
若使用了 UV0、UV1 和 tangent,则最多只能合批 180 个顶点
❌ 物体包含镜像变换:
如果 GameObject 的 Transform 包含镜像(例如 scale 为 -1),就无法与正常物体合批
❌ 不同材质实例:
使用不同实例的材质不能合批(即使材质内容看起来一样)。
✅ 例外:阴影投射阶段可以合批(见上文)
❌ 使用 Lightmap 的物体:
含 Lightmap 的物体具有额外渲染参数。只有使用相同 Lightmap 位置的物体才能合批
❌ 多 Pass Shader:
Unity 只能合批首个 Pass,之后每个光源的额外 Pass 都会单独 Draw Call
❌ 延迟渲染(Legacy Deferred):
这种渲染路径分为光照预处理和物体绘制两个 Pass,无法使用动态合批
用于动态生成几何体的动态合批
以下组件会动态生成几何体,Unity 可通过动态合批优化它们:
内置粒子系统(Particle System)
线渲染器(Line Renderer)
拖尾渲染器(Trail Renderer)
这些组件的合批方式不同于网格:
对于每个渲染器,Unity 会将可合批内容构建成一个大的顶点缓冲区(Vertex Buffer)
渲染器设置好材质状态
Unity 绑定顶点缓冲区到 GPU
对于每个渲染器,Unity 更新顶点偏移并提交一个新的 Draw Call
GPU实例化
GPU 实例化(GPU Instancing)
GPU 实例化是一种绘制调用优化方法,它可以在一次绘制调用中渲染多个使用相同材质的网格副本。每个网格副本称为一个实例(Instance)。这种方法适用于场景中重复出现的物体,比如树木或灌木。
GPU 实例化在一次绘制调用中渲染完全相同的网格。为了增加变化、避免视觉上的重复感,每个实例可以具有不同的属性,例如颜色或缩放比例。在 Frame Debugger 中,这种绘制调用会显示为 Draw Mesh (instanced)。
要求与兼容性
平台兼容性
GPU 实例化可用于所有平台,除了 WebGL 1.0。
渲染管线兼容性
功能 内置管线 通用渲染管线 (URP) 高清渲染管线 (HDRP) 自定义 SRP
GPU 实例化 ✅ ✅ (1) ✅ (1) ✅ (1)
注意:
(1) 仅当所使用的 Shader 不兼容 SRP Batcher 时可用。
与 SRP Batcher 的关系
GPU 实例化与 SRP Batcher 不兼容。如果一个 GameObject 与 SRP Batcher 兼容,Unity 会优先使用 SRP Batcher 渲染它,而不是使用 GPU 实例化。详情见“优化优先级”。
若项目使用 SRP Batcher,但你希望某个 GameObject 使用 GPU 实例化,可以:
- 使用 Graphics.DrawMeshInstanced:此 API 绕过 GameObject,直接使用指定参数在屏幕上绘制网格;
- 手动移除 SRP Batcher 兼容性(见 Unity 文档“移除兼容性”)。
使用 GPU 实例化
Unity 会对使用相同网格和材质的 GameObject 应用 GPU 实例化。
使用条件:
-
Shader 必须支持 GPU 实例化
Unity 的 Standard Shader 以及所有 Surface Shader 默认支持;
要为其他 Shader 添加支持,参阅:创建支持 GPU Instancing 的 Shader。 -
网格来源
MeshRenderer 组件或 Graphics.DrawMesh 调用;
Unity 会收集这些网格并尝试合批;
如果 GameObject 与 SRP Batcher 兼容,则不会实例化此网格;
Unity 不支持对 SkinnedMeshRenderer 使用 GPU 实例化;
Graphics.DrawMeshInstanced 或 Graphics.DrawMeshInstancedIndirect:
这些 API 每次调用单独处理,不能跨调用实例化。 -
启用 GPU Instancing
在材质的 Inspector 面板中勾选 “Enable GPU Instancing” 选项。
光照支持
GPU 实例化支持 Unity 的 Baked Global Illumination(预计算全局光照)系统。Unity 的 Standard Shader 和 Surface Shader 默认支持。
每个实例可使用以下光照源之一:
任意数量的 光照探针(Light Probes);
一个 光照贴图(Lightmap)(注意可使用多个贴图区域);
一个 Light Probe Proxy Volume (LPPV) 组件;
LPPV 空间体积必须提前烘焙,包含所有实例。
特殊情况支持:
动态 MeshRenderer + Light Probes:支持;
静态 MeshRenderer + 同一个 Lightmap:支持;
需在 Static Editor Flags 中启用 Contribute GI。
使用 API 实现光照支持:
使用 Graphics.DrawMeshInstanced:
设置 LightProbeUsage 为 CustomProvided;
提供带有光照探针数据的 MaterialPropertyBlock;
性能影响
对于顶点数较少的网格,GPU 实例化可能效果不佳——因为 GPU 无法充分利用资源,导致性能下降。
一般建议:不要对顶点数低于 256 的网格使用 GPU 实例化;
更佳做法:如果需要重复绘制这类网格,建议使用 单个缓冲区(Buffer) 储存所有网格信息,再绘制
GPU实例化和批处理的不同
- GPU 实例化通过在一个DrawCall中渲染多个相同网格和材质的物体,每个实例可以有不同的属性(如位置、颜色和缩放),从而减少绘制调用数量,提高性能。与此不同,批处理通过将多个物体的网格合并在一起来减少DrawCall,但每个物体依然是单独渲染的。GPU 实例化适合渲染大量相同的物体,而批处理适用于静态或少量变动的物体。
- GPU实例化通过一个单一的draw call渲染多个相同的物体实例,并将变换和其他属性(如颜色、缩放)传递给GPU,允许GPU在硬件层面并行处理这些实例的渲染,而不需要CPU为每个实例单独发出draw call。与此不同,批处理技术则是将多个静态或动态物体的几何数据合并为一个批次,通过一个draw call渲染,但每个物体的变换和其他属性仍需要CPU处理,且这些物体在GPU中通常会被分开渲染。因此,GPU实例化能够显著减少CPU负担并提高渲染性能,尤其在场景中有大量相同物体时。
UGUI
Unity UGUI 合批的开销分析
UGUI 是基于Canvas来进行合并计算的。这样会导致以下几个问题:
1: 不同Cavans的UI元素,是无法合批渲染,无法使用同一个drawcall;
2: 每次合并的时候,会合并计算Canvas下所有的UI元素, 具体的算法流程为:
Step1: 一开始计算合并Cavans下所有的UI元素;
Step2: 每帧提交合并后的结果给GPU渲染;
Step3: 当某个UI元素改变以后,先计算某个UI元素改变后的数据,再结合其它UI元素,重新合并到一起。
3: 每次UI元素的位置等相关信息改变,都会引发合并计算;
4: “不动物体”的合并计算开销是最小的,如果Cavans下所有的UI元素一旦创建都不再改变,那么合并计算这块只要计算一次,性能最好。
5: 当Cavans下有不断变化的物体时,每次都会有合并计算,此时不动的物体少,那么最后合并的时候物体的数据就少。
优化方法
使用UI元素的Atlas:将UI元素的纹理打包到一个Atlas中,这样可以减少纹理切换和Draw Call的数量。
UI面板元素Z轴都应该为0
使用UI画布的Sort Order:根据UI元素的层次结构和渲染顺序,调整UI画布的Sort Order,以便尽量减少重叠和渲染次序的变化,从而减少Draw Call的数量。
不同的 Canvas 无法合批,但变化较多的 UI 元素可以单独分一个 Canvas
修改图片颜色
会导致网格顶点里的颜色发生变化,进而导致网格rebuild
优化方案是创建自定义材质球,然后提前通知 UGUI 使用这个材质球进行渲染。
在动画中改变颜色和 Alpha 时,直接修改这个自定义材质球的参数,避免 Mesh 的重建
动静分离
如果 Canvas 没有标记为 dirty(即内容、位置、大小、颜色等都没变),
➜ Unity 就跳过 UI 更新流程,直接复用上一帧的批次(DrawCall 数据),合批“结果”是缓存的。
如果某个 UI 元素变化了(哪怕只有一个字、一个像素、一个颜色),
➜ 对应的 Canvas 会标记为 dirty,触发 Canvas Rebuild,
➜ 重新生成网格、重建合批顺序,可能打断已有合批,造成更多 DrawCall。
哪些操作会打断合批?
- 材质或者图集不同,
- UI元素发生遮挡关系,导致渲染顺序发生变化时
- UGUI底层:UI面板元素Z轴都应该为0
- UI元素发生变化时(会触发rebuild)
const —— 常量限定符
用于定义不可修改的值,或者保证某个对象不会被意外修改。
常量变量
const int MAX_HP = 100;
表示 MAX_HP 是一个常量,不能再赋值修改。
常量指针 / 指向常量的指针
const int* ptr; // 指向常量的指针(不能通过 ptr 修改 ptr)
int const ptr; // 常量指针(ptr 不能改变指向)
const int* const ptr; // 两者都是常量
函数参数(防止副作用)
void PrintName(const std::string& name);
保证函数内部不会修改 name。
类中的常量成员
class Player {
public:
const int id;
Player(int i) : id(i) {} // 只能通过构造函数初始化
};
常量成员函数
class Player {
public:
int GetHP() const; // 保证函数不修改成员变量
};
static —— 静态作用限定符
static 表示 “静态存储”,含义随使用位置不同而变化:
🔹 1. 函数内部的 static 变量
void foo() {
static int count = 0;
count++;
std::cout << count << std::endl;
}
该变量在多次调用中保留值,只初始化一次。
🔹 2. 类的静态成员
class Game {
public:
static int PlayerCount;
};
int Game::PlayerCount = 0;
类的静态成员属于类本身,不属于实例。
🔹 3. 静态函数成员
class Utils {
public:
static void Log();
};
可通过类直接访问:Utils::Log();
🔹 4. 文件作用域 static(全局变量或函数)
static int enemyCount = 0;
static void helperFunction() {}
表示此变量/函数只在当前 .cpp 文件中可见,防止命名冲突。
✅ 总结对比表:
特性 const static
是否可变 ❌ 不可变(常量) ✅ 可变(但共享或持久)
作用 防止值修改,传参安全 控制作用域、生命周期、共享
常用于 常量定义、参数修饰、成员函数等 静态变量、类成员、函数内部计数、全局限制可见性
生命周期 常规变量生命周期 程序运行期间持久存在(全局或静态区域)
内存泄漏
内存泄漏的分类:
显式泄漏 new 分配内存后忘记 delete
隐式泄漏 指针指向新对象前未释放旧对象:p = new A();(旧对象泄漏)
容器类未清理 std::vector<T> 存储的指针未手动释放
循环引用(智能指针) shared_ptr 循环引用导致对象无法销毁
如何避免内存泄漏?
🔹 1. 成对使用 new / delete
int p = new int(5);
delete p;
🔹 2. 使用智能指针(推荐)
std::unique_ptr
std::shared_ptr
unique_ptr:独占所有权。
shared_ptr:共享所有权(注意避免循环引用)。
🔹 3. 避免裸指针 + 手动管理
尽量避免 new 和 delete,改用 RAII 风格管理资源。
🔹 4. 容器内释放资源
std::vector<int*> vec;
for (auto p : vec) delete p;
vec.clear();
智能指针
- std::unique_ptr —— 独占式智能指针
特点:
拥有 唯一所有权,不可复制,只能移动。
离开作用域时会自动 delete。
用法示例:
#include <memory>
std::unique_ptr<int> ptr = std::make_unique<int>(10);
优势:
不会造成资源共享混乱。
性能好,适合表现明确的“所有权”。
- std::shared_ptr —— 共享式智能指针
特点:
使用 引用计数机制 管理资源。
最后一个 shared_ptr 被销毁时,资源才会被释放。
用法示例:
#include <memory>
std::shared_ptr<int> p1 = std::make_shared<int>(20);
std::shared_ptr<int> p2 = p1; // 引用计数+1
注意:
可能发生 循环引用 问题,导致内存泄漏(见下文 weak_ptr)。
- std::weak_ptr —— 观察者式智能指针
特点:
不拥有资源,只是观察 shared_ptr 的资源。
不会增加引用计数,避免 循环引用。
用法示例:
std::shared_ptr<int> sp = std::make_shared<int>(30);
std::weak_ptr<int> wp = sp; // 不增加引用计数
访问前需要判断资源是否有效:
if (auto spt = wp.lock()) {
std::cout << *spt << std::endl;
}
常用于:
树结构、图结构中避免 shared_ptr 互相引用导致的 内存泄漏。
总结对比:
类型 拥有资源 可复制 引用计数 是否释放资源
unique_ptr ✅ ❌ ❌ ✅ 自动释放
shared_ptr ✅ ✅ ✅ ✅ 自动释放
weak_ptr ❌ ✅ ❌ ❌ 不释放资源
C++11新特性
- auto 自动类型推导
- based for 循环遍历
- 智能指针
- 右值引用(&&)+ 移动语义 https://blog.csdn.net/lirendada/article/details/146970801
- Lambda 表达式(匿名函数)
- nullptr空指针,类型安全
- std::function 和 std::bind
https://blog.csdn.net/swaggyelite/article/details/147280296 - 枚举类(enum class)
红黑树相关
红黑树原则
节点非黑即红
根节点一定是黑色
叶子节点(NIL)一定是黑色
(红色属性)每个红色节点的两个子节点,都为黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
(黑色属性)从任一节点到其每个叶子的所有路径,都包含相同数目的黑色节点。
红黑树与AVL树的区别
红黑树与AVL树区别
1、调整平衡的实现机制不同
红黑树根据路径上黑色节点数目一致,来确定是否失衡,如果失衡,就通过变色和旋转来恢复
AVL根据树的平衡因子(所有节点的左右子树高度差的绝对值不超过1),来确定是否失衡,如果失衡,就通过旋转来恢复
2、红黑树的插入效率更高
红黑树是用非严格的平衡来换取增删节点时候旋转次数的降低,任何不平衡都会在三次旋转之内解决,
红黑树并不追求“完全平衡”,它只要求部分地达到平衡要求,降低了对旋转的要求,从而提高了性能
而AVL是严格平衡树(高度平衡的二叉搜索树),因此在增加或者删除节点的时候,根据不同情况,旋转的次数比红黑树要多。
所以红黑树的插入效率更高
3、红黑树统计性能比AVL树更高
红黑树的算法时间复杂度和AVL相同,但统计性能比AVL树更高,
4、适用性:AVL查找效率高
如果你的应用中,查询的次数远远大于插入和删除,那么选择AVL树,如果查询和插入删除次数几乎差不多,应选择红黑树。
浙公网安备 33010602011771号