unity和cocos2dx的一些优化点

Cocos2dx+lua优化

1.纹理

将原始图片png转化成对应的平台支持的格式

los 使用PVRTC格式

Android 使用etc格式

优点:

硬件支持:

当使用 PNG 格式的纹理时,通常需要在运行时将这些纹理解码成 GPU 可处理的格式。这一过程包括读取 PNG 文件、解码像素数据、上传到 GPU 以及必要时的格式转换

压缩比高:

PVRTC 格式提供了较高的压缩比,可以显著减少纹理占用的内存和带宽

渲染性能:

使用压缩纹理可以减少 GPU 的纹理处理负载,从而提高渲染性能。特别是对于大型纹理,压缩格式可以显著减少内存访问次数,从而提高渲染速度

合并大图

优点:

1.减少 Draw Calls

合并大图可以显著减少 Draw Calls 的数量。这是因为每次绘制一个纹理都需要一个 DrawCall,而合并大图后,多

个纹理被整合到一个大图中,只需一个 Draw Call 即可绘制。这减少了 GPU 的状态切换次数,提高了渲染效率。

2.提高缓存效率

合并大图可以提高缓存的命中率。这是因为纹理数据通常存储在显存中,当多个纹理被合并到一个大图中时,GPU可

以更高效地访问这些纹理数据。此外,纹理图集中的数据通常连续存储,有助于提高缓存的局部性,从而减少内存访问次数。

3:减少内存带宽需求

合并大图可以减少内存带宽的需求。这是因为合并后的纹理图集减少了纹理数据的加载次数,从而降低了内存带宽的

使用。此外,通过使用纹理图集,可以减少纹理数据的重复加载,进一步降低带宽需求。

4.减少l/0

纹理图集还可以减少磁盘 I/O 操作,提高加载速度。

5.方便管理

合并大图可以使纹理资源的管理变得更加方便。将多个纹理合并到一个大图中,可以减少纹理文件的数量,便于组织和维护。

图集管理

1.尽可能的把同一texture的ui放一起,防止被其他texture打断

2.拆一个节点到多个层级(但是要考虑移动,缩放等问题)

3.引用了另一个大图的某一张资源,直接拷贝过来,不要重复生存大图

4.合理利用9宫格

5.纯色图片用程序实现

6.不要一整个功能模块公用一个大图,按这个功能不同的界面打各自的图集

7.控制大图的大小,合理缩小碎图大小,在代码中手动放大。切割碎图或拆分成2份图集

2.卡顿优化

使用异步加载资源

一幅未经压缩的 1024x1024 像素的 RGBA8888 图片在内存中大约占用 4 MB,资源过多且同步加载的话会阻塞主线程,当打开一个界面的时候,可以通过多线程异步加载,等所有资源加载完毕再执行界面打开操作。一般等待加载的

过程可以加个loading提示,并阻止点击响应,防止等待过程又打开了其他界面

使用缓存池

通过重用对象减少内存的分配,减少创建的成本(生命周期不需要重新触发一遍,重新注册各种事件等等),快速的

响应(能够做成缓存一般都是有类似的ui结构,每次只是改变某一个或几个ui),降低GC压力。当整个界面删除的时候,清楚缓存。

优化代码逻辑

1.多个全屏界面叠加的时候,只显示最上层的界面

2.spine如果隐藏,虽然没有参与渲染,但是整个spine的update还是一直在跑(计算骨骼位置,效果等等),需要手动调用pause把update停掉

3.音效如果设置成静音,直接不要调用播放接口,而不是设置音量位0

4.一些非常驻ui不要创建后立即隐藏,等逻辑触发了再创建并显示

5.节点数过多的话,可以分帧按批创建

6.刷新不要直接删除所有子节点并重新创建,而是通过reload函数来实现

7.一个image ui有多个显示规则(例如按钮的3态),不要创建多个节点通过不同状态来控制某种显示状态,而是只用1个节点通过变更image来改变显示规则

8.减少事件监听,改成同一通过父节点监听

lua性能优化

1.减少lua和c的交互(在lua保留一个值)

2.for循环和排序算法减少重复引用

3.string的拼接性能问题,用table.concat代替.字符串拼接

配置优化

1.生成增量包,减少玩家更新大小

2.字典保存配置,减少for循环查找

3.精简字段名称,默认字段不要导出

Unity资源优化和管理

目的

1.提高性能:优化资源可以减少CPU和GPU的负担,避免帧率下降和卡顿现象,确保游戏运行流畅,提高用户体验

2.减少内存占用:优化后的资源占用更少的内存,防止内存溢出(O0M)和频繁的垃圾回收(GC)。提高应用的稳定性。

3.控制包体大小:减小应用的安装包体积,使其符合应用商店的上传要求,减少用户下载时间和流量消耗,提升下载意愿。

4.提升加载速度:通过优化资源的加载方式(如异步加载、按需加载),可以显著缩短游戏启动和场景切换的时间,改善用户体验。

5延长设备电池寿命、适应多样化设备、提升用户体验、还有其它

资源类型

从来源来看,可以分为两类:

1.来自第三方工具生成的文件:如模型网格、纹理、音乐音效、字体、动画、视频等

2.Unity编辑器下创建的资源:如Prefab、Animation Controller、Material、Timeline、RenderTexture、ParticleSystem、VFX等

资源设置检查,两个工具:

1.Unity UPR AssetChecker工具

2.UWA本地资源检测

纹理类型(八种)

Defaut: 默认纹理类型格式,绝大多数的纹理资源都会采用此类型模式

Normal Map:法线贴图,它可将颜色通道转换为适合实时法线贴图的格式

Editor GUl and Legacy GUI: 编辑器GUI控件上使用的纹理类型

Sprite(2D and UI): 一般用在2D游戏中或UGUI上使用的纹理

Cursor: 鼠标光标自定义纹理类型

Cookie:光照剪影类型的纹理,在光照剪影功能中会使用到

LightMap:光照贴图类型纹理,它的编码格式会取决于不同的平台而不同,如果编码格式选择不对或该平台不支持此类型编码,生成的光照贴图可能造成精度丢失

SingleChanne: 原始文件中只有一个颜色通道,那请选择此类型,它可以节省纹理的内存开销

纹理压缩

纹理压缩是指图像压缩算法,保持贴图视觉质量的同时,尽量减小纹理数据的大小。默认情况下纹理原始格式采用PNG或TGA这类通用文件格式,但与专用图像格式相比,它们访问和采样速度都比较慢,无法通用GPU硬件加速,同时纹理数据量大,占用内存较高。所以在渲染中会采用一些硬件支持的纹理压缩格式,如ASTC、ETC、ETC2、DXT等。

纹理压缩格式的内存计算方式(以一张1024X1024的贴图为例)

公式:内存=width*height*每像素对应的字节大小 Astc压缩每个色块大小固定为16byte

RGBA32 Bit:表示每个像素占用32bit 4byte,内存大小=1024X1024X4=4M

RGBA16 Bit:表示每个像素占用16bit 2byte,内存大小=1024X1024X2=2M

RGB ETC1 4Bit:表示每个像素占用4bit 0.5byte,内存大小=1024X1024X0.5=0.5M

RGBA ETC2 8Bit:表示每个像素占用8bit 1byte,内存大小=1024X1024X1=1M

RGBA PVRTC 4Bit:表示每个像素占用4bit 0.5byte,内存大小=1024X1024X0.5=0.5M

RGBAASTC 4X4 block:表示每个像素占用16byte/4x4,内存大小=1024X1024X1= 1M

RGBAASTC 6X6 block:表示每个像素占用16byte/6x6,内存大小=1024X1024X0.45=0.45M

RGBAASTC8X8 block:表示每个像素占用16byte/8x8,内存大小=1024X1024X0.25=0.25M

图片尺寸要求:

ETC1(不支持透明通道)、ETC2(支持透明通道)要求图片宽和高可以不相等但是必须被4整除

PVRTC压缩格式要求图片的宽高必须相等并且是2的整数次幂,例如512X512,如果是512X1024那么就无法压缩了

硬件限制:

ETC2只支持OpenGL ES 3.0以上的Android手机(大概2013年以后的手机都支持,不用使用ETC1-不用通道分离)

ASTC只支持苹果A8以后的设备,iPhone6及以上的手机(大概2014年以后的手机都支持)

IOS平台使用ASTC6*6(不支持苹果5,iphone6以后都支持 支持透明通道)压缩率比PVRT4 bit 好,硬件限制弱

最佳实践:

一般情况下:美术要求清晰 使用:RGBAASTC 4X4 block;无要求:RGBAASTC 6X6 block

纹理Mipmap

Mipmap纹理是逐级减少分辨率来保存纹理副本,可以理解为纹理的LOD层级,当纹理染时会根据像素在屏幕中占据的纹理空间大小选择合适的mipmap级别来进行采样渲染。

优点:

GPU不需要在远距离上对对象进行全分辨率纹理采样,因此可以提高纹理采样性能。同时也解决了远距离下的过采样导致的噪点问题,提高纹理渲染质量。

缺点:

由于Mipmap纹理要生成低分辨率副本,会造成额外的内存开销(多3分之1的内存)

什么时候不需要生成MipMap

1.2D场景

2.固定视角,摄像机无法缩放远近

纹理Read/Write

开启此选项会导致纹理内存使用量增加一倍;默认不开启,除非你的脚本逻辑中需要动态读写该纹理时,需要打开此选项

其它纹理问题

1.纹理图集大小设置不合理,纹理图集利用率偏低,浪费内存;同一纹理图集中,纹理资源生命周期不一样,也会造成纹理内存长时间占用,难以释放,应尽量合理的设置图集大小,并尽可能将类似生命周期的小纹理打到同一图集中,方便运行时销毁释放内存

2.不合理的半透明纹理,占据屏幕超大区域,造成Overdraw与内存开销,应尽量从UI设计上避免,这种情况也会出现在粒子特效中

3.过多的2D序列帧动画,并且不将这些动画打成图集

模型的导入设置

模型导入基本流程

模型来源:

大多数模型来自第三方DCC工具(如3D Max、Maya)

推荐使用FBX文件格式,避免使用特有工程文件格式。

DCC工具导出设置:

统一单位,确保与Unity一致。

导出网格应为多边形拓扑模式,不支持贝塞尔曲线等

烘焙变形体到网格,避免导出纹理和材质,减少导入效率问题,

避免导出摄像机、灯光等场景信息。

原始模型性能影响

模型面数:尽量减少面数,避免使用微三角面(仅含个位数像素)

拓扑结构:确保模型闭合,合理化平滑组结构。

材质数量:尽量减少材质数量,避免增加Shader和贴图开销

蒙皮与骨骼:使用相同的蒙皮网格,减少骨骼数量,避免性能瓶颈。

IK与FK分离:确保IK骨骼节点分离,以便导出时删除。

模型导入Unity后的设置

导入设置窗口:根据模型格式,Unity提供不同的选项。

骨骼和动画:判断是否使用人形骨骼或通用骨骼,选择合适的导入选项。

材质与贴图:设置材质、贴图,并拖拽到场景中测试。

关注的选项

Scene信息导入:一般情况下可选择不开启。

Mesh设置:

Mesh Compression:可开启,确保网格准确性。

Read/Write:仅在需要动态修改时开启,通常保持关闭以节省内存,

Optimize Mesh & Generate Colliders:保持默认设置。

几何体信息设置:

Index Format:确认网格顶点数不超过65535时可用16位索引

关闭不必要的法线、切线等生成,减少资源占用。

模型资源优化设置

Project Settings:

Vertex Compression:为模型设置顶点压缩。

Optimize Mesh Data:删除不需要的数据,减少文件大小。

动画文件的导入设置

AnimationType

无动画(None)

旧版动画(Legacy):不推荐使用。

通用骨骼框架(Generic):用于非人形动画。

人形骨骼框架(Humanoid):用于人形动画,需启用Kinematics或Animation Retargeting。

选择原则

无动画选择None。

非人形动画选择Generic。

人形动画:需要Kinematices或Animation Retargeting功能,或者自定义骨骼对象时选择HumanoidRig;其它都选择Generic Rig,在骨骼数差不多的情况下,Generic Rig会比Humanoid Rig节省30%甚至更多的CPU时间。

骨骼框架优化

Skin Weights: 默认4根骨头,对于一些不重要的动画对象可以减少到1根,减少计算量。另外在untiy工程设置的Quality选项中,也可以对高中低平台做不同设置

Optimize Bones:建议开启,在导入时自动剔除没有蒙皮顶点的骨骼

Optimize Game Objects:在Avatar和Animator组件中删除导入游戏角色对象的变换层级结构,而使用unity动画内部结构骨骼,消减骨骼transform带来的性能开销可以提高动画角色性能,但有些情况下会造成角色动画错误,这个选项可以尝试开启但要看表现效果而定。

Animation标签

这里的选项会随模型动画文件类型有所不同,比如说Bake Animations,这个选项只对应于Maya,3DMax等DCC工具的原始文件格式,并且文件中使用了布料、流体等烘焙动画时才有用,如果我们采用fbx格式,该选项会呈现禁用状态。

Resmple Curves

将动画曲线重新采样为四元数值,并为动画每帧生成一个新的四元数关键帧,仅当导入动画文件包含欧拉曲线时才会显示此选项。

Anim.Compression

Off 不压缩,质量最高,内存消耗最大

Keyframe Reduction 减少冗余关键帧,减小动画文件大小和内存大小

Optimal,仅适用于Generic与Humanoid动画类型,Unity决定如何进行压缩

RotationError、PositionError、ScaleError

代表transform在千分之5的区间内都可以认为两帧数据一致,从而进行keyframe reduction来减少冗余关键帧。

Animation Custom Properties

导入用户自定义属性,一般对应DCC工具中的extraUserProperties字段中定义的数据

动画曲线数据信息

Curves Pos: 位置曲线

Quaternion:四元数曲线 Resample Curves开启会有

Euler: 欧拉曲线

Scale:缩放曲线

Muscles:肌肉曲线,Humanoid类型下会有

Generic:一般属性动画曲线,如颜色、材质等

PPtr:精灵动画曲线,一般2D系统下会有

Curves Total:曲线总数

Constant: 优化为常数的曲线

Dense: 使用了密集数据(线性插值后的离散值)存储

Stream:使用了流式数据(插值的时间和切线数据)存储

动画文件导入设置优化后信息查看原则

1.看效果差异(与原始制作动画差异是否明显)

2.看曲线数量(总曲线数量与各种曲线数量,常量曲线比重大更好)

3.看动画文件大小(动画文件在小几百k或更少合理,超过1M以上的动画文件考虑是否合理

音频的导入设置

优化双声道音频:

若左右声道内容相同,建议使用Force to Mono将其转为单声道,减少内存和文件大小

音频原始与压缩大小:

查看音频原始资源大小及导入后压缩大小,注意整体压缩比。

推荐使用未压缩的WAV文件作为音频源,通过支持的压缩格式进行压缩

压缩格式选择:

Vorbis:适用于大多数Unity音频文件,尤其是移动平台,

MP3:适合不循环的音乐,尤其在iOS上。

ADPCM:适合简短常用音效,解码速度快,尽管压缩比一般。

音频采样率:

48000Hz采样率在移动平台上通常过高,建议设置为22050Hz,以减少文件大小和内存占用。

音频加载类型:

Decompress on Load:适合小于200KB的音效文件

Compressed in Memory:推荐用于大于200KB的音效文件

Streaming:适用于较长或背景音乐,避免加载时卡顿。

静音处理:

不仅将音量设置为零,建议销毁Audio Source组件,彻底卸载音频,节省内存。

UGUI性能优化

UI性能的四类问题

Canvas Re-batch时间过长

Canvas Over-dirty,Re-batch次数过多

生成网格顶点时间过长

Fill-rate overutilization:片元着色器利用率过高,造成GPU负担。

Canvas画布

Canvas负责管理UGUI元素,负责UI渲染网格的生成与更新,并向GPU发送DrawCal指令。这些工作全都是在引擎native层由c++负责完成的,对于每个canvas对象,在绘制之前都要进行一个合批的过程。如果canvas底下的所有UI元素,每一帧都保持不变,那么我们只需要在绘制前合批一次,并保存下结果,并在之后的每帧渲染中继续使用这个保存的结果。如果UI元素发生了变化,这时候画布需要重新匹配几何体,而画布被标记为dirty,这时被标记为dirtv的canvas会触发Re-batch,也就是需要重新进行合批。

Canvas Re-batch讨程

根据UI元素深度关系进行排序

检查UI元素的覆盖关系

检查UI元素材质并进行合批

GPU片元着色器利用率过高问题

UGUI中渲染是在Transparent半透明渲染队列中完成的,半透明队列的绘制顺序是从后往前画,由于UI元素做Alpha Blend操作,我们在做UI时很难保障每一个像素不被重画,这就造成Ul的Overdraw太高,也就是同一个位置的像素会被绘制多次,这会造成片元着色器利用率过高,造成GPU负担。

UlSpriteAtlas图集利用率不高的情况下,大量完全透明的像素被采样也会导致像素被重绘,造成片元着色器利用率过高;同时纹理采样器浪费了大量采样在无效的像素上,导致需要采样的图集像素不能尽快的被采样,造成纹理采样器的填充率过低,同样也会带来性能问题。

Re-Build过程

Re-Build是在Canvas Re-Batch过程中完成的。主要逻辑在c#层,用来重新计算Layout布局与渲染网格重建,每当Canvas组件调用WillRenderCanvases事件时都会调用PerformUpdate::CanvasUpdateRegistry接口

通过ICanvasElement.Rebuild方法重新构建Dirty的Layout组件

通过ClippingRegistry.Culf方法,任何已注册的裁剪组件Clipping Components(Such as Masks)的对象进场裁剪剔除操作

任何Dirty的Graphics Components都会被要求重新生成图形元素

Layout组件和Graphic组件什么时候被标记成dirty

Layout Rebuild:Ul元素位置、大小、颜色发生变化

Graphic Rebuild:顶点或材质数据变化时标记dirty

使用Canvas的基本准则

1.将所有可能打断合批的UI图层移到最下边,尽量避免UI元素出现重叠区域,尤其是一些很小的UI元素,比如字体是一个很小的矩形区域,这个字体很小,容易被忽略,但每一帧都会导致重新合批

2.可以拆分使用多个同级或嵌套的Canvas来减少Canvas的Rebatch复杂度,在UGUI对象层级结构中,无论使用多个同级Canvas或者嵌套的Canvas都是没有区别的,所有Re-batch和Re-Build的过程都是针对于单独canvas进行的,并不会做出跨canvas合批,使用多个canvas来管理UI层级不仅可以减少合批与重建的复杂度,还可以使UI层级在Hierarchy视图中看起来更直观

3.拆分动态和静态对象放到不同Canvas下,避免动态UI元素导致每帧都要做太复杂的Canvas Re-batch操作

4.不使用Layout组件

UGUI的输入和射线检测

默认情况下,UGUI中是通过Canvas的Graphic Raycaster组件来处理输入,触摸以及鼠标悬停事件,每个canvas都会绑定一个GrahpicRaycaster组件

射线优化

需要交互的UI组件才开启Raycast Target

开启Raycast Target的UI组件越少,层级越浅,性能越好

对于复杂的控件,尽量在根节点开启Raycast Target

对于嵌套的Canvas,OverrideSorting属性会打断射线,可以降低层级遍历的成本

Unity工程目录结构及用途

Asset文件夹:用来存储和重用的项目资产,所有我们导入的静态资源以及我们通过编辑器生成的资源都会存放在此文件夹下。

Library文件夹:用来存储项目内部资产数据信息的目录,这个文件夹较大,主要是unity编辑器内部使用,不需要将此文件夹参与到代码托管中,每台机器在项目导入时生成的也有可能有差异,一般合作开发时,编辑器使用出现问题时,清理此文件夹并重新生成,可能会解决一些由于缓存产生的问题,在以后你使用编辑器无法打开工程或打开工程出错时,不妨删除此文件夹,重新生成。

Packages文件夹:用来存储项目的包文件信息,对应的在Library下有PackageCache文件夹,用来缓存我们的包文件信息。

Project Settings文件夹:用来存储项目设置的信息。

UserSettings文件夹:用来存储用户设置信息。

Temp文件夹:用来存储使用Unity编辑器打开项目时的临时数据,一旦关闭Unity编辑器也会被删除。

Logs文件夹:用来存储项目的日志信息(不包含编辑器日志信息)。

Assets目录中的特殊文件夹及用途

Editor文件夹(可以多个)

Editor Default Resources文件夹(根目录唯一)

Gizmos文件夹(根目录唯一)

Plugins文件夹(2019后已无)

Resources文件夹

这个文件夹用来存储一些原型设计时可以从脚本中按需加载的资源,通过Resources.Load的接口加载此类资源,Asset文件夹中可以添加多个Resources文件夹,同样也可以是Editor文件夹的子文件夹,但其中的资源需要通过Editor脚本进行加载,并会从构建发布中剥离,值得注意的是Resources文件夹通常是unity项目中性能问题的主要来源,Resources文件夹使用不当,很容易造成unity项目构建出现膨胀,导致内存消耗过高,应用程序启动时间显著增加,应用程序包体过大的问题,强烈建议在正式项目中不要使用Resources目录,应尽量使用AssetBundle方式进行构建和加载资源

Standard Assets文件夹(2018.1后已无)

StreamingAssets文件夹

存放不随应用程序构建而希望独立的原始文件格式提供的资源,如单独的视频等流媒体文件。此文件夹内的文件可以按原样复制到目标计算机中,然后通过特定的文件夹单独访问该文件。

Assets目录结构设计

一级目录设计原则

目录尽可能少

区分编辑模式与运行模式

区分工程大版本 针对超大规模项目,比如说我们要按游戏的原版和资料片去划分目录,也可以按各种mod来划分目录

访问场景文件、全局配置文件便捷,尽可能将场景文件以及全局配置文件放到一级目录,这样方便在编辑器模式下快速访问

不在一级目录做资源类别区分,只有Video类视频建议直接放到StreamAssets下

二级目录设计原则

只区分资源类型

资源类型大类划分要齐全

不做子类型区分

不做功能区分

不做生命周期区分

三级目录设计原则

Audio/Texture/Models三级目录做子类型区分

audio可以使用loadType来划分文件夹

texture可以通过texture type类型来划分文件夹

models可以使用蒙皮模型还是非蒙皮模型来划分文件夹

其它类型资源做功能模块/生命周期区分

四级目录设计原则

只有Audio/Texture/Models做四级目录,按模块/生命周期划分

资源导入工作流的三种方案

1.手动编写工具

优点:根据项目特点自定义安排导入工作流,并且可以和后续资源制作与打包工作流结合

缺点:存在开发和维护成本,会让编辑器菜单界面变得复杂,对新人理解工程不友好

适合类型:大型商业游戏团队

AssetPostprocessor,这个对象会提供一系列OnPreprocessXXX接口修改资源导入设置属性,所有对象都有这个接口,它会在资源被导入时调用,这里也就是我们修改资源导入设置的最佳时机,在这个接口下,我们会拿到导入资源的import接口,修改它的导入设置属性,即可完成对导入设置的修改。

AssetsModifiedProcessor,资源被添加、删除、修改、移动时回掉该对象的OnAssetsModified接口,通过这个接口,我们可以在资源被修改时做相应的资源导入设置

2.利用Presets功能

优点:使用简单方便,只需要Assets目录结构合理规范即可

缺点:无法和后续工作流整合,只适合做资源导入设置。

适合类型:小型团队或中小规模项目

Presets是将相同属性设置跨多个组件、资源或项目设置保存和应用的资源,该资源运行时没有效果,仅能在Unity编辑器下使用。

1.创建和保存:

在 Inspector 面板中,每个组件右上角都有三个按钮。点击中间按钮可以将当前组件的属性保存为 Preset。

Presets 可以序列化组件的设置,以便后续应用。

2.应用 Presets:

通过相同的按钮可以将保存的 Preset 应用到当前游戏对象的组件上。

资源导入设置页也支持持久化成 Presets。

3.Presets Manager:

可以将 Presets 资源添加到项目设置中的 Presets Manager.

新创建的对象或导入的新资源可以快速应用相应的 Presets。

Presets Manager 支持根据资源类型和需求添加多个 Preset,从而简化资源导入设置工作流

4.高级过滤和搜索

在 Presets Manager 中可以为每个 Preset 添加高级过滤和搜索选项,支持多种匹配和通配符。

5.资源变化检测:

可以使用官方文档提供的代码检测资源变化,并在变化时重新设置导入设置。将相关代码放入 Editor 文件夹,删除 Presets Manager 中的默认预设,并将自定义Presets 放入相应资产文件夹。

3.利用AssetGraph工具

优点:功能全,覆盖Unity资源工作流全流程,节点化编辑,直观

缺点:有一定上手成本,一些自定义生成节点也需要开发,不是Unity标准包,Unity新功能支持较慢。

适合类型:任何规模项目和中大型团队

全部评论

相关推荐

2024-12-27 09:53
已编辑
电子科技大学 C++
投票
美团 后端开发 n*15.5
点赞 评论 收藏
分享
评论
点赞
2
分享

创作者周榜

更多
牛客网
牛客企业服务