UE4 HISM实战:从Draw Call爆炸到大规模植被性能优化 📅 发布时间:2026/9/16 10:19:53 👁 浏览次数: 说实话干 UE4 这行最怕听到的一句话就是这片区域我要放三千棵树再加五百块石头密度要真实跑起来还得稳。 早两年我听到这话第一反应是先去算 Draw Call算完基本就想跑路。直到后来把 Hierarchical Instanced Static MeshHISM吃透了才敢说一句行能搞。 这篇就想把我的实操经验完整倒出来聊聊为什么 HISM 在铺量大、重复度高的静态物体时速度能吊打传统 Spawn Actor 的路子以及你真上手做的时候最容易在哪几个环节翻车。这个组件适合谁场景地编、TA、想做开放世界或大世界原型的程序甚至是被草地卡成幻灯片折磨的独立开发者都值得花十分钟看完这篇。你会明白 HISM 不是简单的ISMC 换个名字它背后那棵剔除树才是真正的性能命脉。1. 从一次场景搭建崩溃说起Spawn 的实际痛点与 HISM 的设计初衷1.1 为什么直接 Spawn 会卡Draw Call 和 CPU 端的双重暴击先看一个最常见的反面教材。初学者在蓝图里写个循环for 循环跑一千次每次都 SpawnActor 生成一棵树的 Static Mesh Actor。点下 Play 的瞬间编辑器帧率直接崩到个位数GPU 线程没吃满CPU 那边倒是排了一条长龙。拆开看这里到底发生了什么。每生成一个 Actor引擎都要为它分配 UObject、注册组件、计算包围盒、加入场景。每个 StaticMeshComponent 又单独向 RHI 提交一次绘制命令也就是一个 Draw Call。一千个树 一千个 Actor 上千次场景遍历开销 上千个 Draw Call这还没算大量 Actor 挂着的 Tick 和碰撞查询。CPU 单帧构建渲染命令的时间一长帧率自然崩。GPU 根本没忙起来瓶颈全在 CPU 提交命令的速度上。这就像你让一百个人分别搬一百块砖每搬一块都要单独跑一趟流程哪怕每个人都不累人力资源也全耗在来回跑路上。更隐蔽的问题是内存。每个 StaticMeshComponent 都要维护自己的一份 Transform、边界信息、LOD 状态、碰撞体数据。数量一上来内存浪费非常可观。做移动端项目的时候这种浪费尤其致命官方性能建议里反复强调移动端 Draw Call 要压到极低Spawn 一箩筐 Actor 的做法基本等于自杀。1.2 HISM 究竟做了什么批次合并、遮挡剔除与分层树HISM 的思路完全是另一个维度。它不生成一堆 Actor而是用一个组件管理上千个实例。这上千个实例共享同一个 Static Mesh 资源所有 Transform 打包存在 GPU 缓冲区里渲染时引擎一次性提交一个批次把一千棵树全画出来。这一步直接砍掉了两个大开销CPU 构建绘制命令的次数从一千次变成一次内存里也不用攒一千份组件数据。但这只是 ISMInstanced Static Mesh就能做到的事HISM 多出来的H才是重头戏。HISM 在引擎内部维护了一棵分层包围体树Hierarchical Tree把场景里的实例按空间位置分组成簇簇再分成更大的簇。视锥剔除的时候引擎不是逐个实例去裁剪而是从树根开始往下遍历一大片实例如果整体在视锥外直接丢弃这一支如果部分可见再往下细分到子树。这样做剔除的 CPU 开销不再是 O(N) 级别而是接近 O(log N)。实例数越多、场景越大这棵树的收益越明显。一万株草的场景树可能在十几轮遍历里就完成绝大多数的剔除判定。此外HISM 还内建了距离剔除Distance Culling和淡入淡出Fading支持这两个特性对大面积植被覆盖场景尤其实用。引擎会根据组件上设置的剔除距离在远处直接跳过整批绘制或者用透明度渐变让实例在进入视野时平滑出现避免远处突然冒出来造成的视觉跳变。1.3 为什么Hierarchical这个设计是大规模实例化的关键有人可能觉得ISM 不也是批量绘制吗为什么大世界不用 ISM 而用 HISM原因就是剔除粒度。ISM 只有一个统一包围盒要么整体可见、要么整体不可见做不到子区域级别的剔除。一旦相机转向整批实例无论屏幕内外全都参与绘制GPU 压力依然爆炸。HISM 的剔除粒度是簇级树的分支让渲染线程可以非常快地丢掉不可见的成片实例。打个比方你在十层楼里找人ISM 的做法是一层一层地挨个敲门HISM 则是先看这栋楼哪半边亮着灯再直奔那几层。楼房越高、灯光越少这种层级策略的收益就越大。实测里我做过一片两千个草堆的丘陵地HISM 在大多数视角下每帧只需要提交几百个可见实例的绘制渲染线程几乎不打嗝。2. 蓝图也能跑HISM 的快速接入与参数逐项解析2.1 最简流程从一个盆地的草地说起先走一遍纯蓝图流程。你不需要写一行 C项目工程里新建一个蓝图 Actor添加一个 Hierarchical Instanced Static Mesh Component 组件。在细节面板里指定 Static Mesh 为你想要的草模型或石头模型然后在细节面板里找到 Instances 分类点添加按钮在视图里拖动绿色箭头调整位置和旋转或者直接在细节面板里手填 Transform。如果你有大量实例坐标数据可以用蓝图节点 Add Instances 批量塞进一个 Transform 数组里一次调用就能完成几百个实例的添加。注意一个细节当你在编辑器里手动摆放 HISM 实例时默认会生成在关卡子关卡文件.umap里。如果这批数据是不需要美术手工微调的纯程序化生成内容建议通过代码在 BeginPlay 时动态生成节省关卡体积和加载时间。我在项目里通常把地形、道路旁的路灯、围栏等量产物体全部交给 HISM 运行时生成绕开编辑器数据膨胀的问题。2.2 核心参数说明从 MinInstanceDistance 到碰撞开关HISM 的参数面板很多人打开后一头雾水我把最关键的几项拆开讲Instance Start Cull Distance实例开始被剔除的距离。设置成 0 表示不启用距离剔除设置成 5000 表示超出 5000 世界单位的实例会被直接丢弃。适合用来砍远景配合场景深度做层次感。MinInstanceDistance两个实例之间的最小距离判定值主要在 LOD 切换和淡入淡出时用于衡量间距不影响剔除逻辑但在做远景低密度草时会影响过渡效果。Opacity / Fading控制实例淡入淡出。这个和 StaticMesh 材质是否支持半透明相关如果材质关闭了透明混合模式这个参数不会生效。Collision Enabled默认 HISM 实例不启用碰撞也就是射线能穿过。如果你需要角色踩在石头上或者子弹打到草上需要打开碰撞响应但这里有个大坑碰撞开启后引擎要维护碰撞数据开销会明显上涨千万不能大规模全开。后面我会专门讲碰撞如何取舍。Can Ever Affect Navigation控制实例是否参与 NavMesh 生成。如果你放的是石头、树桩这类的阻挡物需要打开如果是草务必关掉不然 NavMesh 构建时间和运行时导航查询会指数级变慢。Use Custom Instance Data开启后允许每个实例携带四个 float 类型的自定义数据材质里可以通过 Vertex Factory 的 CustomPrimitiveData 节点读取。这是实现大规模随机颜色、随机缩放、风吹摆动的关键开关。2.3 一个容易被忽略的属性材质兼容性HISM 不是任何材质都能跑得高效的。先看一个基础事实HISM 底层走的是Instanced Static Mesh Vertex Factory这个特殊的顶点工厂要求材质必须支持实例化。大多数标准材质在编译时会自动生成实例化版本但部分复杂的材质节点尤其是在 World Position Offset 中直接使用 World Position 计算的可能会被编译器退回到非实例化路径导致实例化失效。具体操作建议是材质从 World Position Offset 里驱动风吹草动时尽量使用 Local Position / Vertex Color 做扰动不要依赖 Object Position。如果你需要每个草单独做随机的相位偏移就开启 Custom Instance Data传入随机相位值而不是在材质里按 Instance Transform 计算哈希。后者在实例数多时会有明显的性能损耗。UE4 材质节点系统里和实例渲染直接相关的常用节点我列一下PerInstanceRandom实例内嵌的随机值单个 float适合做随机缩放因子或颜色混合权重。PerInstanceCustomData读取自定义数据数组里的值可以塞 0~3 索引的 float做颜色变体、相位偏移都靠它。Vertex Color部分建模软件导出的顶点色适合做贴花颜色变化和点击特效遮罩。World Position Offset顶点偏移管道植被摆动、飘动效果都在这里实现。材质节点再多再花哨先确认实例化路径没被破坏再做艺术效果。这是 HISM 优化的前置条件。3. C 接入与运行时动态更新的正确姿势3.1 C 创建 HISM 组件的基操蓝图做原型验证没问题但要在运行时高效地批量添加几千个实例还是 C 更可控。头文件里声明UCLASS() class MYGAME_API AMyFoliageActor : public AActor { GENERATED_BODY() public: AMyFoliageActor(); UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category Foliage) class UHierarchicalInstancedStaticMeshComponent* HISMComponent; UFUNCTION(BlueprintCallable, Category Foliage) void GenerateFoliage(); };构造函数里创建组件并挂到根上AMyFoliageActor::AMyFoliageActor() { PrimaryActorTick.bCanEverTick false; HISMComponent CreateDefaultSubobjectUHierarchicalInstancedStaticMeshComponent( TEXT(HISMComponent)); RootComponent HISMComponent; // 关闭运行时更新性能更好如果只做静态摆放就保持默认 HISMComponent-SetMobility(EComponentMobility::Static); // 碰撞按需开启默认关掉 HISMComponent-SetCollisionEnabled(ECollisionEnabled::NoCollision); // 允许自定义实例数据 HISMComponent-SetUseCustomInstanceData(true); }GenerateFoliage 里批量生成void AMyFoliageActor::GenerateFoliage() { UStaticMesh* Mesh LoadObjectUStaticMesh(nullptr, TEXT(/Game/Assets/Props/SM_Grass.SM_Grass)); if (!Mesh) { return; } HISMComponent-SetStaticMesh(Mesh); TArrayFTransform Transforms; Transforms.Reserve(5000); for (int32 i 0; i 5000; i) { FVector Location( FMath::RandRange(-2000.f, 2000.f), FMath::RandRange(-2000.f, 2000.f), 0.f ); FRotator Rotation(0.f, FMath::RandRange(0.f, 360.f), 0.f); FVector Scale(1.f); // 这里可以加高度贴合地形的逻辑省略 Transforms.Add(FTransform(Rotation, Location, Scale)); } HISMComponent-AddInstances(Transforms, /*bSpaceToWorldSpace*/true); }代码里我特意把 AddInstances 的第二个参数说明一下bSpaceToWorldSpace为 true 时传入的 Transform 按世界坐标解释为 false 时按组件局部坐标解释。编辑器里摆放的实例默认是局部空间运行时从数据表读世界坐标生成时一定记得把世界坐标当世界坐标传否则移动了 Actor 后实例位置全部错乱。3.2 何时用 AddInstance 何时用 AddInstanceWorldSpaceHISMComponent 提供了几个增加实例的接口AddInstance(const FTransform InstanceTransform)按组件局部坐标添加单个实例。AddInstanceWorldSpace(const FTransform WorldTransform)按世界坐标添加单个实例。AddInstances(const TArray v, bool bSpaceToWorldSpace)批量添加返回一个 booltrue 表示添加成功。BatchUpdateInstanceTransform(int32 FirstInstance, const TArray Transforms, bool bSpaceToWorldSpace, bool bMarkRenderStateDirty, bool bTeleport)批量更新已有实例的 Transform。实际项目中运行时从文件或网络加载来的坐标数据基本都是世界坐标直接用 AddInstanceWorldSpace 或者批量接口里设bSpaceToWorldSpacetrue。如果你在编辑器里摆放了一组 HISM 实例然后代码里又用局部坐标 AddInstance一旦 Actor 位于原点附近不容易出问题但 Actor 位置一旦偏移实例就全漂了。我的习惯是项目里所有动态生成的 HISM 实例统一使用世界坐标接口Actor 自身位置固定在世界原点或只作为容器不参与移动。这样调试时少很多为什么生成在奇怪位置的灵异问题。3.3 动态更新时的批量操作与性能陷阱HISM 最大的性能优势在于静态但需求不可能永远是静态的。树会被砍掉、石头会被爆破、草会被烧毁。删除实例有 RemoveInstance 和 RemoveInstances更新位置有 BatchUpdateInstanceTransform但真正的性能和逻辑陷阱在于调用频率。千万不要在 Tick 里逐个调用 UpdateInstanceTransform。每个单独更新实例的调用都可能触发局部树和渲染缓冲区的重建几百个实例逐个更新相当于几百次重建CPU 开销直接拉满。正确的做法是把所有需要更新的 Transform 收集到一个数组然后一次性调用批量更新接口。另外注意 BatchUpdateInstanceTransform 的bMarkRenderStateDirty参数。如果你只是微调位置可以传 false 跳过渲染状态标记避免不必要的渲染线程重提交但如果实例位置变化明显包围盒会变化传 false 会导致剔除树数据不准确可能出现该显示的没显示、不该显示的显示了。我建议动态更新场景下这个参数保持 true牺牲一点点性能换准确性。还有一个隐藏得很深的坑HISM 内置的树结构是增量更新的频繁 AddInstance RemoveInstance 交错进行会让树的层级不平衡剔除效率逐渐劣化。如果你知道自己要在短时间大量增删实例比如玩家正在砍一整片树林建议每帧操作完调用一次BuildTreeIfOutdated()强制引擎在合适时机重建内部树保证剔除效率回满。3.4 运行时换 Mesh、改 Material 和 LOD 参数的正确时机有时候一整批实例需要整体替换材质比如到了秋天草从绿变黄。HISM 的材质更新直接走 SetMaterial单个实例不能定制材质这是组件设计决定的。如果需要单实例独立材质只能拆分多个 HISM 组件每个组件对应一种材质。LOD 方面HISM 从 Mesh 资源里读取 LOD 列表自动按距离切换。你可以在细节面板里给 HISM 设置一个独立的LOD 覆盖比如让远处实例强制使用 LOD1进一步减少 GPU 压力。经验值供参考一棵中等复杂度的树LOD0 三角面 3 万LOD1 降到 8000LOD2 降到 2000远处 100 米外基本看不出 2000 三角面和 8000 三角面的区别。植被类 HISM 项目的 LOD0 不用做太高因为大量同屏实例的总体像素负载本来就不小高模实例收益递减得很厉害。4. HISM 与 Spawn、ISMC、HISM 的横向对比与选型4.1 不同实例化方案对比表很多新人会把 Spawn Actor、ISM、HISM 混为一谈其实它们在 CPU 渲染开销、内存占用、适用场景上差异很大。我拿实际项目里常见的四种方案做个对比表方案Draw Call 数量内存占用剔除粒度适用场景Spawn 多个 StaticMeshActor极高每个 Actor 一次提交极高每个组件独立数据每 Actor 独立数量极少几十、需要独立逻辑的物品ISMInstanced Static Mesh Component极低整批一次提交低一份网格资源 实例数据整批统一剔除静态、集群集中、数量几百内且不会拆散HISMHierarchical 实例化极低分簇提交低实例数据 树结构簇级剔除大量同类静态物体、大范围分布HISM Custom Instance Data极低中额外存自定义数据簇级剔除需要每实例颜色/相位变化的大规模植被表里可以看出 HISM 在数量多、范围广这个维度上是压倒性优势。但它也不是万能的接下来看反面场景。4.2 选型经验什么时候别用 HISM先说一个最容易踩的坑千万别把需要独立逻辑的对象塞进 HISM。比如路灯里有一盏灯需要单独开关、树木里有一棵需要被砍倒后播放掉落动画这些个体差异需求意味着你必须记录哪个树墩对应哪个实例索引管理游戏逻辑的索引映射复杂度上升一个量级。如果这批物体的数量只有几十个老老实实 Spawn Actor 更划算。还有HISM 不适合频繁大幅度变形的物体。HISM 虽支持运行时 SetupAttachment 和移动整个组件但实例之间相对位置的变化如果每帧都在发生比如一群逃跑的鸡树的更新成本会抵消实例化收益。这种情况应该用 Niagara 粒子系统的 Ribbon 或者直接上 Actor而不是硬套 HISM。另一个容易被忽略的坑是物理交互。HISM 实例带碰撞时物理引擎对实例的碰撞处理并不像 StaticMeshActor 那样完整。射线检测对实例的处理是支持的但是物理模拟——比如把一块石头推下悬崖——对 HISM 实例就非常难做。正文开头提过的查询和物理模拟器的区别在这里体现得最清楚扫描查询Query可以对 HISM 做 HitTest但模拟Simulation需要独立的物理对象HISM 实例自身不能像 Actor 那样被物理引擎推进运动只能靠代码整体搬移或单独拿出一个实例单独做物理模拟。4.3 和物理模拟器、查询接口配合时的注意点官方文档里一直强调HISM 实例适合做查询型碰撞不适合做模拟型碰撞。查询型碰撞的典型用法是玩家开枪打草子弹的射线能检测到草的碰撞体然后草被二次处理播放一个偏移动画、删除实例。这个流程完全没问题。但是如果你想让一个车内座舱里放的一排矿泉水瓶既能被 HISM 实例化又能被车辆碰撞推得满天飞那基本是给自己挖坑。在 C 或蓝图里对 HISM 打射线时返回的 HitResult 里 Actor 是挂载 HISM 所在的容器 Actor而不像普通 StaticMeshActor 那样能拿到具体那个实例。此时要拿到具体实例索引可以通过FHitResult::Item字段它记录的是命中实例的索引。想要精确到单实例做逻辑比如砍树掉落树枝需要在蓝图里把这个 Item 索引传给自己维护的游戏数据结构。所以选型时需要想清楚这批物体是视觉填充物还是可交互物体。前者果断走 HISM查询碰撞只做开枪射线反馈就够后者就要考虑拆分成少量特殊 Actor 或者使用 HISM 加索引管理双轨方案。没有银弹只有取舍。4.4 用 HISM 做外接设备数据可视化的一个扩展思路聊点题外话网上总能看到HISM 只能做草和树的印象其实它作为数据可视化工具也相当好用。前阵子我收到一批外接设备映射过来的坐标点数据大概几万个采样点要在场景里实时刷新位置把这些点全做成 Actor 性能肯定撑不住我直接建了一个 HISM 组件用球体 Mesh 布满所有采样点每次外接设备返回新数据后就批量更新所有实例的 Transform。由于 HISM 实例可以携带自定义 float 数据我把设备映射的信号强度塞进 Custom Instance Data在材质里根据这个值把球体从蓝变红。远看就是一整片会呼吸的动态热力图帧率还稳得惊人。这个思路对做数字孪生、传感器点位可视化、甚至 UI 特效反馈都挺实用算是 HISM 在植被之外的一个潜在应用点——只要你的数据是位置 颜色/大小的海量小物件HISM 就是那个被低估的得力干将。5. 常见问题与排查技巧实录5.1 实例不显示先查 Transform 再查碰撞HISM 最常被问的问题就是我明明 Add 了几百个实例运行后一株都看不见。排查顺序我一般固定在三条线上第一Transform 空间不对。最经典的操作是代码里用世界坐标的数据传给了以局部坐标解释的接口或者反过来。如果你 Add 的实例坐标是随机生成的几百万单位之外相机自然看不见。先在代码里 Log 出前几个 Transform 的位置看是不是集中在原点附近能快速排除这类问题。第二Mesh 或材质没设置。HISM 组件本身不指定 Static Mesh 和 Material就没有东西可画。编辑器里手动添加实例前必须先指 Mesh否则实例的三角面数据是空的视图里甚至没有模型显示。第三变体数据或碰撞类型问题。如果 Static Mesh 的碰撞体为无碰撞而且项目开启了仅碰撞可见之类的调试模式会出现物体看不到但射线检测不准的诡异现象。另外注意 HISM 的 CollisionEnabled 不要设成 QueryOnly 同时对材质里的 DepthFade 做过头偶尔会造成半透明混合下实例淡出后不重新淡入看起来像丢了。5.2 动态增删实例后帧率瞬间爆炸代码逻辑正确但运行一段时间后帧率突然掉一半先看是不是每帧都在 AddInstance 同一个循环里加了又删、删了又加或者玩家走过某片区域时瞬间生成上千个实例且全部带碰撞。叶片类的碰撞开销在大量实例时是成指数上涨的。我的排查习惯是打开stat GPU、stat SceneRendering、stat ini这几个指令观察 Draw Call 数量和 HISM 内部的实例数是否正常。如果 Draw Call 没涨但帧率掉了问题多半在剔除树重建去代码里搜 BuildTreeIfOutdated 是否被频繁调用或者在合理时机手动 ClearInstances 再重新生成而不是一点一点累加。还有一个非常基础但容易翻车的地方Instances 数量上限。HISM 默认对实例数量没有硬上限但实例数据数组本身是存在 GPU 缓冲区的超出显存会出现渲染闪烁或者局部实例消失。这时候可以把一个 HISM 组件拆成多个区域组件或降低单体实例数量别硬扛。5.3 关于 Spawn 一词在不同工程语境下的乌龙很多次在技术群里看到有人问UE4 里 Spawn 报错 EPERM 怎么办我第一反应是这多半不是 UE4 的问题。注意spawn eperm这种报错经常出现在 Node.js 的 child_process、Python 的子进程或者系统级工具里它表示缺少创建子进程的权限和 UE4 的 SpawnActor 完全是两码事。如果你恰好在用引擎工具链、命令行批处理或者类似 GUI 工具调外部进程时撞上这个单词先去看运行环境权限和被杀毒软件拦截而不是去改 HISM 代码。这也是我在项目协作中反复跟新人强调的查报错先认清楚是哪个引擎/工具抛出来的。UE4 的 Output Log 里如果出现 SpawnActor 相关的错误会明确写着 Attempt to spawn actor outside of gameplay 或者 SpawnActor failed。出现spawn eperm时先看是不是在跑外部脚本、启动子进程、或者调用了平台权限接口大概率排错方向完全不在一个维度上。5.4 材质节点大全里的 HISM 兼容性挑选UE4 材质节点多到可以写一本字典但不是所有节点都能在实例化顶点工厂里高效运行。和 HISM 搭配时我的材质要点是能用 PerInstance 系列节点就优先用。PerInstanceRandom、PerInstanceCustomData、Vertex Color 这几类能保证实例化路径不被破坏。谨慎使用 Object Position / Actor Position 节点做逐实例偏移。这类节点在 ISM/HISM 里会丢失实例的唯一性所有实例往往会拿到同一个值导致视觉上的全屏同步闪动或者偏移错误。World Position Offset 做风吹草动时务必基于顶点局部坐标做乘法。先乘以一个扰动值再加到 WPO 输出上别直接对世界坐标加减否则远处草抖动的幅度会被相机视角放大到离谱。透明材质在 HISM 里要谨慎。半透明实例的排序开销仍然不小大量草使用半透明材质会有性能退步。能改用遮罩Masked就改用遮罩如果必须做渐变半透明把 Opacity Mask 和 Fading 组合使用别直接开 Translucent。5.5 性能分析工具怎么验证 HISM 真的生效了最后讲怎么看数据很多人跑完项目只看肉眼帧率不够严谨。我这个流程跑了很多项目屡试不爽运行游戏后打开控制台输入stat RHI看 Draw Call 数量。场景里有一万粒草时如果 HISM 生效Draw Call 应该只增加 1~2 个按簇分级提交会有少量增加。如果发现 Draw Call 跟着实例数量线性增长大概率是材质实例化路径被破坏或者 HISM 组件没生效。再开stat SceneRendering看 Instance Culling 和 Pass 渲染耗时。HISM 的剔除效率高的话这个阶段的耗时应该远低于普通 MergedMesh 的耗时。最后用FreezeRendering冻结场景渲染在视口框选不同的相机位置逐一确认可见实例数量的变化趋势符合预期。数据比直觉可靠这套组合拳能让你在项目早期就发现 HISM 失效的问题而不是等打包上线后玩家卡成 PPT 才来后悔。我个人在实际项目里的体会是HISM 这个组件能把很多听起来很夸张的场景规模目标变成现实但前提是你得理解它的限制。它像一把非常锋利的刀切草切树切石头都是神速但你要是拿它去砍铁链需要动态逻辑的个体、去搅汤频繁更新的物理模拟那不是在用刀是在自残。最后再分享一个小技巧做调试或原型的时候我经常把临时占位用的球体/箭头模型全部挂在一个 HISM 组件里配合自定义数据做颜色区分比生成几百个可视化 Actor 干净太多了。需要显示的时候 MoveInstance 到跟丢位置不需要直接隐藏整个组件比逐个销毁省心得多。整个系统用得越熟练你越会觉得 Spawn Actor 那种粗放路线在批量场景里真的已经是上上个时代的做法了。