1. 项目概述:从静态美术到动态世界的构建核心
在Unity 2D或2.5D项目的开发中,我们常常会遇到一个瓶颈:当美术资源越来越多,场景越来越复杂时,性能问题、资源管理混乱和开发效率低下会接踵而至。一个典型的场景是,你的游戏有上百种敌人、几十种地形和复杂的动画,如果每个精灵(Sprite)都独立加载,内存会迅速膨胀,Draw Call(绘制调用)会高得吓人,游戏在移动端或WebGL平台上的表现会非常糟糕。这正是我们今天要深入探讨的核心:如何通过一套系统性的进阶方案,解决这些痛点。
这个方案围绕四个紧密相连的技术支柱展开:SpriteRenderer的动态控制、SpriteAtlas的高效资源管理、瓦片地图的自动化生成以及动画系统的深度优化。这不仅仅是四个孤立的知识点,而是一套从资源准备、场景构建到运行时性能调优的完整工作流。掌握它们,意味着你能将项目从“能运行”提升到“运行得流畅且易于维护”的工业级水准。无论你是在开发一款精致的独立游戏,还是一个需要频繁更新内容的商业项目,这套组合拳都能显著提升你的开发体验和最终产品的质量。
2. 核心思路与架构设计
2.1 为何是这“四驾马车”?
在深入细节之前,我们先理清这四项技术的内在逻辑。它们分别对应了资源生命周期的不同阶段:
SpriteAtlas(资源打包与加载阶段):这是所有2D资源的“仓库”和“调度中心”。它的核心价值在于将大量零散的小图片(精灵)打包成一张或几张大的纹理图集。这样做最直接的好处是合并Draw Call。Unity在渲染时,每切换一次材质球(Material)和纹理(Texture),就可能产生一次Draw Call。将数百个精灵打包进一个图集,它们就可以共享同一个材质球和纹理,从而将数百次Draw Call合并为几次,这是性能提升最关键的一步。同时,它简化了资源引用和内存管理。
瓦片地图(场景构建与编辑阶段):这是利用打包好的资源(尤其是图集中的精灵)快速、可视化构建游戏世界(如关卡、背景)的工具。Tilemap系统允许你像拼图一样,用“瓦片”(Tile)来绘制地图,支持多层(如地面层、装饰层、碰撞层)、规则瓦片、随机瓦片、动画瓦片等。它的“自动化”体现在通过规则瓦片(Rule Tiles)和笔刷,可以智能地根据相邻瓦片自动选择正确的精灵,极大提升了关卡设计师的工作效率。
SpriteRenderer & 动画系统(运行时表现与控制阶段):当资源被加载,场景被构建好后,就需要在运行时让它们“活”起来。
SpriteRenderer是Unity中渲染一个2D精灵最基本的组件,控制着精灵的显示。而动画系统(无论是传统的Animation/Animator,还是更高效的脚本驱动或ECS方案)则负责控制SpriteRenderer所显示精灵的序列变化,从而产生运动、特效等视觉反馈。对这一层的优化,直接关系到游戏的流畅度和响应速度。深度优化(全阶段贯穿):这是一个贯穿始终的理念,在上述每个环节中,我们都需要植入性能意识。例如,在SpriteAtlas阶段考虑纹理压缩格式和最大尺寸;在瓦片地图阶段使用
Tilemap Collider 2D的优化选项;在动画阶段避免每帧使用GetComponent,或采用对象池管理频繁创建销毁的动画对象。
因此,一个高效的开发流程应该是:美术制作精灵 -> 用SpriteAtlas打包 -> 在Tilemap中使用图集中的瓦片构建场景 -> 通过代码动态控制SpriteRenderer和动画系统来驱动游戏逻辑 -> 全程实施优化策略。
2.2 技术选型与备选方案考量
SpriteAtlas vs 传统Resources/AssetBundle:对于2D精灵资源,SpriteAtlas是Unity官方主推且最专业的解决方案。相比将图片直接放在Resources文件夹,SpriteAtlas提供了纹理打包、冗余剔除、平台差异化设置等高级功能。相比自己用AssetBundle管理2D图集,SpriteAtlas与Unity编辑器、SpriteRenderer、Tilemap的集成度更高,使用更便捷。对于Addressables系统,SpriteAtlas可以作为其一个资产条目被引用和管理,二者并不冲突,Addressables更侧重于动态加载和更新的架构。
Unity内置Tilemap vs 第三方或自研工具:对于大多数2D项目,Unity内置的Tilemap系统已经完全够用,且与Unity物理2D、光照2D等系统无缝集成。仅在需要极其特殊的地图逻辑(如无限动态地形、超大规模地图流式加载)时,才需要考虑结合第三方工具(如Tiled地图编辑器导入)或自研解决方案。
Animator状态机 vs 代码驱动动画:对于角色、敌人等拥有复杂状态(待机、行走、攻击、受伤)的动画,使用Animator状态机是标准做法,可视化且逻辑清晰。但对于UI动画、简单的特效序列帧动画,直接通过代码控制
SpriteRenderer.sprite的切换可能更轻量、更直接。在性能临界处(如大量同屏单位),可以考虑使用ECS(实体组件系统)配合Burst编译器来并行处理动画帧的更新,但这属于高阶优化范畴。
注意:不要盲目追求最“高级”的技术。评估标准应是:1. 项目规模和复杂度;2. 目标平台性能限制;3. 团队技术储备。一个中小型移动端游戏,将SpriteAtlas和内置Tilemap用好,再对Animator进行基础优化(如减少状态机层、合理使用动画融合),效果就已经非常显著。
3. SpriteAtlas高效资源管理实战
3.1 SpriteAtlas的创建与配置精髓
创建SpriteAtlas很简单,在Project窗口右键 -> Create -> 2D -> Sprite Atlas即可。但里面的配置选项,才是决定其效率的关键。
对象打包(Objects for Packing):这是核心,你把需要打包的精灵、或者包含精灵的文件夹拖到这里。一个最佳实践是按功能或场景模块化打包。例如,将“主城UI图标”打成一个图集,“森林地形瓦片”打成一个,“玩家角色动画帧”打成一个。避免把所有资源塞进一个巨型图集,否则任何微小修改都会导致整个图集重建,且内存占用不灵活。
打包设置(Pack Settings):
- Allow Rotation:允许精灵旋转90度以更好地填充空间。对于非对称精灵(如角色),通常关闭,避免运行时额外的旋转计算。
- Tight Packing:紧密打包。对于有透明通道的精灵,启用此选项可以更高效地利用纹理空间,但可能会在精灵边缘产生微小的缝隙(称为“出血”问题)。如果你的精灵在Tilemap中拼接时出现白边,可以尝试关闭此选项,或增加Padding值。
- Padding:内边距。这是防止纹理采样时“颜色渗出”的关键。通常设置为2、4或8(像素)。值越大,图集空间利用率越低,但能更有效地避免相邻精灵边缘颜色的相互干扰。
纹理设置(Texture Settings):这里需要针对不同平台进行优化。
- Format:纹理格式。对于移动端(Android/iOS),
ASTC格式在平衡画质和压缩比上表现优异,但需要设备支持。ETC2支持更广泛但可能有色块。对于PC/主机,BC7(DX11)或ASTC是不错的选择。在WebGL平台,需要注意纹理格式的兼容性和内存占用。 - Max Texture Size:最大纹理尺寸。不要无脑设为8192。需要考虑目标平台的支持上限(如一些低端移动GPU可能只支持2048)和内存占用。一个2048x2048的RGBA32纹理会占用16MB内存!通过模块化打包,控制每个图集在1024或2048以内是明智的。
- Compression Quality:压缩质量。在开发期可以设为
Fast以加快打包速度,发布时改为Best以获得最佳压缩率。
- Format:纹理格式。对于移动端(Android/iOS),
包含在构建中(Include in Build):这个选项决定了图集是否会被自动打进玩家包(Player Build)。如果你通过Resources文件夹或Addressables动态加载图集,可能需要关闭此选项,改为在代码中手动加载。对于始终需要的核心UI或基础地形图集,勾选此选项更方便。
3.2 动态加载与卸载策略
图集创建好后,如何在代码中使用?关键在于SpriteAtlas这个类。
using UnityEngine.U2D; // 需要引用此命名空间 public class SpriteAtlasManager : MonoBehaviour { public SpriteAtlas uiAtlas; // 拖拽赋值或通过Addressables加载 public string spriteName = "Icon_Coin"; void Start() { // 方式1:通过图集实例获取精灵(最常用) if (uiAtlas != null) { Sprite coinSprite = uiAtlas.GetSprite(spriteName); if (coinSprite != null) { GetComponent<SpriteRenderer>().sprite = coinSprite; } else { Debug.LogWarning($"在图集中未找到名为 {spriteName} 的精灵!"); } } // 方式2:通过精灵名称直接加载(需图集在Resources路径下且勾选“Enable Atlas V2”) // Sprite sprite = Resources.Load<Sprite>("MyAtlas/Icon_Coin"); } }动态加载的黄金法则:
- 按需加载:不要在场景初始化时加载所有图集。例如,只在进入战斗场景时加载“技能特效”图集,在打开背包时加载“物品图标”图集。
- 及时卸载:当确定一个图集不再需要时(如切换场景),手动卸载其资源,释放内存。如果图集是通过
Resources.Load加载的,使用Resources.UnloadAsset;如果是通过Addressables加载的,使用对应的Release方法。 - 引用管理:确保你的
SpriteRenderer或UI Image引用的精灵Sprite,其所属的纹理图集(Texture)也一直留在内存中。如果图集被意外卸载,引用它的精灵会变成“粉红色”(丢失材质)。这是一个常见的坑。
实操心得:我曾在一个项目中发现,即使切换了场景,上一个场景的图集内存仍未释放。排查后发现,是一些全局的、静态的脚本缓存了某个精灵的引用。因此,在管理图集生命周期时,务必检查是否有“隐藏”的引用阻止了资源卸载。使用Unity Profiler的Memory模块,查看
Texture2D的引用链,是定位这类问题的利器。
4. 瓦片地图自动化生成与高级技巧
4.1 从零构建一个智能地形
假设我们要制作一个经典的2D平台游戏地形,包含草地、泥土、悬崖和水。
准备精灵与创建瓦片:首先,将你的地形精灵导入Unity,并确保它们的
Sprite Mode设置为Multiple,然后用Sprite Editor进行切片。切片后,在Project窗口选中这些切片好的精灵,右键 -> Create -> 2D -> Tiles ->Rule Tile。Rule Tile是自动化生成的核心。配置Rule Tile(规则瓦片):在创建的Rule Tile资产中,你可以定义一系列规则。每条规则包含:
- 规则列表:定义当前瓦片在“上下左右”等相邻位置有其他瓦片时,应该显示为哪个精灵。例如,一个“草地中心”瓦片,可以规则“如果上方、下方、左方、右方都有其他草地瓦片,则显示为完整的草地精灵”。
- 默认精灵:当没有匹配任何规则时显示的精灵。
- 输出:可以输出为不同的瓦片类型(如动画瓦片、随机瓦片)。
通过精心设计规则,你只需要用笔刷简单涂抹,Unity就会自动为你选择正确的边缘、角落精灵,生成看起来非常自然、无缝连接的地形,效率提升十倍不止。
使用Tilemap绘制:在Hierarchy中右键 -> 2D Object -> Tilemap,创建一个Tilemap。在Tile Palette窗口(Window -> 2D -> Tile Palette)中,将你的Rule Tile拖入调色板。然后你就可以像画画一样,在Scene视图中绘制地图了。按住Shift拖动可以快速填充大片区域。
4.2 多层地图与碰撞生成
一个完整的关卡通常需要多个图层:
- Ground层:基础地形,通常附加
Tilemap Collider 2D组件,并勾选Used By Composite,然后它会自动添加一个Composite Collider 2D。在Composite Collider 2D上,将Geometry Type设为Polygons,Generation Type设为Synchronous或Asynchronous。这样,所有分散的瓦片碰撞体会被合并成一个或少数几个高效的碰撞体多边形,极大提升物理性能。 - Decoration层:装饰物,如花草、石头。这层通常不需要碰撞。
- Overlay层:覆盖物,如阴影、水面的高光。可以设置Tilemap的
Order in Layer来调整渲染顺序。
你可以通过代码动态修改瓦片,实现“可破坏的地形”效果:
public Tilemap groundTilemap; public TileBase grassTile; public TileBase dirtTile; void DigAtPosition(Vector3 worldPos) { Vector3Int cellPos = groundTilemap.WorldToCell(worldPos); TileBase currentTile = groundTilemap.GetTile(cellPos); if (currentTile == grassTile) { // 将草地瓦片替换为泥土瓦片 groundTilemap.SetTile(cellPos, dirtTile); // 重要:修改瓦片后,需要手动刷新Tilemap的碰撞体 groundTilemap.GetComponent<TilemapCollider2D>().ProcessTilemapChanges(); } }4.3 性能优化要点
- 分块(Chunk):Unity的Tilemap在底层是分块管理的。避免单个Tilemap过大(比如超过100x100),过大的单次更新(如
SetTile)会触发大面积的重建。如果地图很大,考虑按区域分成多个Tilemap GameObject。 - 碰撞体优化:如上所述,务必使用
Composite Collider 2D。此外,在Tilemap Collider 2D上,可以调整Max Tile Change Count和Extrusion Factor来平衡更新性能和碰撞精度。 - 剔除(Culling):对于大型静态背景Tilemap,可以考虑使用自定义的脚本,根据摄像机位置动态禁用远处Tilemap Renderer的渲染,或者使用Unity的
CullingGroupAPI。
5. SpriteRenderer动态控制与动画系统深度优化
5.1 超越Inspector:代码驱动SpriteRenderer
SpriteRenderer组件看似简单,但通过代码精细控制,可以实现很多动态效果。
public class AdvancedSpriteController : MonoBehaviour { private SpriteRenderer sr; public Sprite[] runFrames; public float frameRate = 12f; private int currentFrame; private float timer; void Start() { sr = GetComponent<SpriteRenderer>(); // 缓存引用,避免每帧GetComponent if (sr == null) sr = gameObject.AddComponent<SpriteRenderer>(); } void Update() { // 示例1:手动序列帧动画(轻量级) timer += Time.deltaTime; if (timer >= 1f / frameRate) { timer = 0; currentFrame = (currentFrame + 1) % runFrames.Length; sr.sprite = runFrames[currentFrame]; // 直接切换sprite } // 示例2:动态颜色混合(受击闪烁) // sr.color = Color.Lerp(Color.white, Color.red, Mathf.PingPong(Time.time, 0.5f)); // 示例3:材质属性覆写(高级效果) // sr.material.SetFloat("_HueShift", Time.time * 0.1f); } // 动态更换图集中的精灵 public void ChangeSpriteFromAtlas(SpriteAtlas atlas, string spriteName) { Sprite newSprite = atlas.GetSprite(spriteName); if (newSprite != null) { sr.sprite = newSprite; } } }关键技巧:
- 材质实例化:默认情况下,多个
SpriteRenderer共享同一个材质(Sprite-Default)。如果你需要为某个精灵单独修改材质属性(如设置_Color进行染色),必须先获取其材质的实例,否则会修改所有使用该默认材质的精灵。MaterialPropertyBlock mpb = new MaterialPropertyBlock(); sr.GetPropertyBlock(mpb); // 获取当前属性块 mpb.SetColor("_Color", Color.cyan); // 修改颜色 sr.SetPropertyBlock(mpb); // 应用属性块 // 这样做不会创建新的材质实例,性能更好。 - Sorting Layers & Order in Layer:这是控制2D渲染前后顺序的生命线。通过代码动态修改
sr.sortingOrder,可以实现角色走到树后时被遮挡的效果。
5.2 动画系统(Animator)的深度优化
Animator功能强大,但滥用也会成为性能杀手。
精简状态机与层:
- 移除所有未使用的状态和过渡。
- 评估是否真的需要多个动画层(Layers)。每一层都会增加更新开销。对于简单的覆盖动画(如上半身射击),可以考虑用代码混合,而不是动画层。
- 简化过渡条件。避免使用大量复杂的Bool、Trigger参数和冗长的过渡连线。复杂的逻辑判断尽量放在代码中,动画状态机只负责播放。
优化动画剪辑:
- 减少关键帧密度:对于非关键性动作(如远处NPC的待机动画),可以在动画导入设置中增加
Rotation Error和Position Error容差值,让Unity在压缩动画时删除更多不必要的关键帧。 - 启用动画压缩:在Project面板选中动画文件,在Inspector中设置
Compression为Optimal或Keyframe Reduction,并勾选Anim. Compression。务必在压缩后检查动画是否失真。 - 使用Humanoid动画:如果是人形角色,使用Humanoid动画类型可以利用Unity的肌肉系统和重定向功能,但也会增加一些开销。对于简单的2D精灵序列帧动画,使用
Generic或Legacy类型即可。
- 减少关键帧密度:对于非关键性动作(如远处NPC的待机动画),可以在动画导入设置中增加
Culling Mode(剔除模式):
- 对于屏幕外的角色或特效,将其Animator的
Culling Mode设置为Cull Update Transforms甚至Cull Completely。这样当渲染器不可见时,动画的更新和骨骼变换计算会被跳过,节省大量CPU时间。这是最容易被忽略也最有效的优化之一。
- 对于屏幕外的角色或特效,将其Animator的
避免在Update中频繁操作Animator:
// 糟糕的做法:每帧都设置参数 // void Update() { animator.SetFloat("Speed", playerVelocity.magnitude); } // 较好的做法:仅在值变化时设置 private float lastSpeed; void Update() { float currentSpeed = playerVelocity.magnitude; if (Mathf.Abs(currentSpeed - lastSpeed) > 0.01f) { animator.SetFloat("Speed", currentSpeed); lastSpeed = currentSpeed; } }
5.3 应对高并发动画:对象池与帧同步
当场景中需要同时播放大量相同动画时(如子弹爆炸、金币收集),频繁的Instantiate和Destroy会造成GC(垃圾回收)卡顿。
对象池模式是解决此问题的标准答案:
public class ExplosionEffectPool : MonoBehaviour { public GameObject explosionPrefab; public int poolSize = 20; private Queue<GameObject> pool = new Queue<GameObject>(); void Start() { for (int i = 0; i < poolSize; i++) { GameObject obj = Instantiate(explosionPrefab); obj.SetActive(false); obj.transform.SetParent(this.transform); pool.Enqueue(obj); } } public GameObject GetExplosion(Vector3 position) { GameObject obj; if (pool.Count > 0) { obj = pool.Dequeue(); } else { // 池空了,动态扩容(或回收最老的对象) obj = Instantiate(explosionPrefab); } obj.transform.position = position; obj.SetActive(true); // 获取动画组件并播放 Animator anim = obj.GetComponent<Animator>(); if (anim != null) { anim.Play("Explosion", -1, 0f); // 从第0帧开始播放 } StartCoroutine(ReturnToPoolAfterAnimation(obj, anim.GetCurrentAnimatorStateInfo(0).length)); return obj; } private IEnumerator ReturnToPoolAfterAnimation(GameObject obj, float delay) { yield return new WaitForSeconds(delay); obj.SetActive(false); pool.Enqueue(obj); } }对于成百上千个需要播放简单序列帧动画的物体(如闪烁的星星),每帧遍历所有物体并更新SpriteRenderer仍然很耗时。此时可以考虑帧同步:创建一个全局的管理器,统一管理这些动画的帧计时和Sprite切换,减少单个组件的Update调用。
6. 性能剖析与常见问题排查
6.1 使用Profiler定位性能瓶颈
当游戏出现卡顿时,Unity Profiler是你的第一站。
CPU瓶颈:
- 打开Profiler的CPU Usage模块。
- 如果
Animation.Update或Animator.Update耗时很高,说明动画系统是瓶颈。回顾第5.2节的优化点,检查Animator复杂度、Culling Mode设置。 - 如果
SpriteRenderer.Update或Canvas.SendWillRenderCanvases(对于UI)耗时高,说明渲染更新开销大。检查是否有大量动态修改的SpriteRenderer或UI元素。
渲染瓶颈(GPU/渲染线程):
- 在Profiler的Rendering区域,关注
Batches(合批数)和SetPass Calls。 - 目标:尽可能降低这两个数值。SpriteAtlas的主要贡献就是减少
SetPass Calls。 - 如果Batches仍然很高,检查是否有破坏合批的因素:
- 使用过多不同的材质球(即使是同一个Shader,参数不同也会打断)。
- SpriteRenderer的
Order in Layer排序导致无法动态合批。 - 考虑使用
Sprite Atlas V2的“Variants”功能,为不同平台或质量等级生成不同压缩格式的图集,避免运行时因格式转换而破坏合批。
- 在Profiler的Rendering区域,关注
内存瓶颈:
- 在Memory模块,查看
Texture2D的内存占用。确认你的SpriteAtlas纹理尺寸是否过大,数量是否过多。 - 检查是否存在纹理冗余(同一张图片被打包进多个图集)。
- 查看
Sprite和AnimationClip的数量和大小,确保没有不必要的资源被加载。
- 在Memory模块,查看
6.2 典型问题与解决方案速查表
| 问题现象 | 可能原因 | 排查与解决方案 |
|---|---|---|
| 精灵边缘出现白边或黑边 | 1. SpriteAtlas打包时Padding值太小。2. 精灵原始图片边缘有透明或半透明像素,且 Tight Packing启用。3. 纹理过滤模式(Filter Mode)为 Bilinear或Trilinear,导致采样到相邻精灵颜色。 | 1. 增加SpriteAtlas的Padding值(如从2改为4)。2. 在图片处理软件中确保精灵边缘是纯透明或纯底色,或关闭 Tight Packing。3. 对于像素风游戏,将纹理的 Filter Mode设为Point (no filter)。 |
| Tilemap瓦片拼接处有缝隙 | 同上,属于“纹理出血”问题。此外,也可能是Sprite的Pixels Per Unit设置不统一。 | 1. 采用上述解决白边的方法。 2. 确保所有用于同一Tilemap的精灵,其 Pixels Per Unit值完全相同。 |
| Draw Call (Batches) 异常高 | 1. 未使用SpriteAtlas,或图集打包策略不佳。 2. 使用了过多不同的Shader或Material。 3. UI(Canvas)与SpriteRenderer混合渲染,打断了合批。 | 1. 检查并优化SpriteAtlas配置,确保相关精灵在同一图集。 2. 尽量使用Unity标准Sprite Shader,并通过 MaterialPropertyBlock修改属性而非创建新材质。3. 尝试调整渲染顺序,或使用多摄像机分层渲染。 |
| 动画播放卡顿、不流畅 | 1. Animator状态机过于复杂,Update开销大。 2. 动画剪辑关键帧过多,或压缩导致数据错误。 3. 屏幕外角色未进行剔除。 | 1. 简化Animator,将逻辑判断移至代码。 2. 检查动画导入设置,在保证质量的前提下调整压缩比。 3. 将Animator的 Culling Mode设为Cull Update Transforms。 |
| 切换场景后,旧图集资源未释放 | 1. 有脚本持有了旧精灵的引用。 2. 图集被静态变量或单例缓存。 3. 通过 Resources.Load加载后未调用Resources.UnloadUnusedAssets。 | 1. 使用Profiler Memory工具查看纹理的引用链。 2. 检查并清理全局缓存。 3. 在合适的时机(如加载界面)调用 Resources.UnloadUnusedAssets()或使用Addressables的引用计数管理。 |
| WebGL平台初始化或运行缓慢 | 1. SpriteAtlas纹理尺寸过大,下载和解压耗时。 2. 未针对WebGL进行纹理压缩(如使用ETC2/ASTC)。 3. 首包资源过多。 | 1. 减小图集最大尺寸,采用更激进的模块化打包。 2. 在Player Settings -> WebGL -> Publishing Settings中启用压缩格式。 3. 考虑使用Addressables进行资源分包和按需加载。 |
6.3 一个实战优化案例:战斗场景帧率提升
我曾接手一个2D弹幕游戏项目,战斗场景同屏敌人和子弹很多时,帧率会从60fps骤降到30fps。通过Profiler分析,发现瓶颈主要在:
- CPU端:
Animation.Update和SpriteRenderer.Update开销巨大。每个子弹和敌人都有一个简单的Animator播放旋转或闪烁动画。 - 渲染端:
Batches超过200。
采取的优化措施:
- 替换Animator:对于子弹的旋转动画,改用脚本在
Update中直接修改transform.rotation。对于简单的闪烁动画,改用脚本控制SpriteRenderer.color在Alpha值间插值。移除了上百个简单的Animator组件。 - 合并材质:所有子弹和敌人使用了三种类似的Shader变体(普通、加亮、受击)。我们将其统一为一个Shader,并通过
MaterialPropertyBlock传递一个_EffectType参数来控制表现,使得所有同类对象可以合批。 - 优化图集:重新规划了图集,将战斗场景高频使用的子弹、小敌机精灵打包到一个专用的、尺寸为1024x1024的图集中。
- 对象池:为子弹和爆炸特效实现了严格的对象池。
结果:优化后,同屏单位数相同时,CPU耗时下降约60%,Batches降至80以下,帧率稳定在55-60fps。这个案例的核心启示是:对于大量、简单的动态对象,轻量级的脚本控制往往比完整的Animator状态机更高效;而渲染合批的优化,需要从Shader和材质的一致性这个根源上着手。