
1. 项目概述为什么我们需要一个蓝图友好的对象池如果你在UE5里做过稍微复杂点的项目尤其是那种需要频繁生成和销毁Actor的比如弹幕射击、RPG技能特效、开放世界动态刷怪肯定对性能波动不陌生。每次SpawnActor引擎背后都在干一堆重活分配内存、调用构造函数、初始化组件、注册到世界……销毁时再来一遍反向操作。短时间内搞个几十上百次帧率掉给你看是分分钟的事。对象池Object Pool这个老概念就是来解决这个痛点的与其反复生成销毁不如提前创建好一批对象放在池子里用的时候取出来不用的时候还回去只是改变其可见性和状态。这能极大减少CPU的卡顿和内存分配器的压力。但UE5自带的UActorPoolableComponent用起来有点别扭它更偏向于C端在蓝图里想优雅、批量地管理多种不同类型的对象池比如子弹、特效、敌人并不直观。而且我们往往希望有一个全局的、统一的管理入口而不是在每个关卡蓝图或者GameMode里写一堆重复的逻辑。这就是为什么我们要基于Subsystem来打造一个蓝图友好的通用对象池系统。Subsystem是UE5里一个非常棒的设计它提供了具有明确生命周期的全局单例对象。用GameInstanceSubsystem你的对象池可以伴随整个游戏进程存在跨关卡持久化并且在蓝图里能像访问一个普通的蓝图库节点一样方便地调用。这个项目的核心目标就是构建一个这样的系统通过UE5的Subsystem架构实现一个在蓝图中易于配置、调用和管理能显著优化频繁生成销毁性能的通用对象池。它不只是个代码模块更是一套从设计思想到具体实现的完整解决方案让你告别Actor的反复生成销毁拥抱丝滑的性能体验。2. 系统核心设计与架构拆解2.1 为什么选择GameInstanceSubsystem在UE5中Subsystem有几种类型EngineSubsystem,EditorSubsystem,GameInstanceSubsystem,WorldSubsystem,LocalPlayerSubsystem。为对象池选择GameInstanceSubsystem是基于以下几个关键考量生命周期匹配GameInstance在游戏启动时创建直到游戏进程结束才销毁。这意味着我们的对象池生命周期覆盖了整个游戏会话。无论是主菜单、过场动画还是多个游戏关卡池子里的对象都可以被复用避免了关卡切换时重建池子的开销。这对于有大量可复用对象的游戏如Roguelike地牢、大型MMO至关重要。全局访问性GameInstanceSubsystem可以通过UGameInstance::GetSubsystem模板方法在C中轻松获取在蓝图中也有对应的节点。这为我们在游戏逻辑的任何地方角色、控制器、Widget、甚至其他子系统访问对象池提供了极大的便利。自动管理Subsystem由引擎自动实例化和销毁我们无需手动处理单例模式的创建和析构减少了出错的可能代码更干净。相比之下WorldSubsystem的生命周期与一个UWorld通常是一个关卡绑定关卡卸载它就被销毁了不适合做跨关卡的持久化资源池。而EngineSubsystem生命周期又太长且与编辑器耦合更深不适合纯运行时逻辑。2.2 对象池系统的核心类设计我们的系统主要包含三个核心类它们共同协作职责清晰UObjectPoolSubsystem (继承自 UGameInstanceSubsystem)角色系统的总管家和对外接口。职责维护一个TMap用于存储多个不同的对象池键Key可以是池子的名字FName或类路径值Value是具体的池子对象。提供蓝图可调用的函数如CreatePool创建池、GetObjectFromPool从池中取对象、ReturnObjectToPool归还对象到池。处理池子的预热Pre-fill、清空等全局管理逻辑。UObjectPoolBase (继承自 UObject)角色单个对象池的抽象基类定义了池子的基本操作接口。职责管理一个可用对象队列TArrayUObject*或TQueue和一个已用对象映射TMapUObject*, FPoolObjectData。实现SpawnFromPool和DespawnToPool的核心逻辑。提供虚函数供子类重写以处理特定类型对象如AActor的激活、禁用等行为。UActorObjectPool (继承自 UObjectPoolBase)角色专门用于管理AActor及其派生类的具体池子实现。职责重写父类的虚函数实现Actor特有的逻辑从池中取出时可能需要设置位置、旋转、显示Actor、启用Tick等归还时则需要隐藏Actor、取消Tick、解除所有附加等。处理Actor模板的加载和缓存。为什么池子管理者UObjectPoolBase本身是UObject而不是简单的结构体这主要是为了利用UE的反射系统和垃圾回收机制。作为UObject它可以被蓝图识别方便未来可能的扩展比如在编辑器中配置池子参数。同时它也能安全地被GameInstanceSubsystem引用和管理生命周期清晰。2.3 数据流转与状态管理一个对象在池子中的生命周期通常包含以下几个状态理解这个状态机对正确使用池子至关重要非活跃Inactive对象被创建后或从游戏世界归还后处于池子的空闲队列中。对于Actor此时它通常被设置为隐藏SetActorHiddenInGame(true)且可能被移到远离摄像机的地方如FVector(0,0,-10000)并禁用TickSetActorTickEnabled(false)。预激活Pre-Activating当有请求从池中获取对象时系统从空闲队列取出一个对象准备将其状态恢复到可用。活跃Active对象被成功取出设置到指定的位置和状态并投入到游戏世界中使用。对于Actor此时是可见的并执行正常的游戏逻辑。等待归还Pending Return对象使用完毕逻辑层调用归还函数。系统开始执行清理操作。回收中Recycling执行具体的禁用逻辑如隐藏Actor、取消物理模拟等然后将其重新放入空闲队列回到“非活跃”状态。这个状态循环的核心是避免真正的构造和析构。我们只是在这几个状态间切换对象所有内存分配只在初始创建池子预热时发生一次。注意一个常见的误区是只隐藏Actor而不重置其状态。比如一个敌人Actor被“击杀”后归还它的血量、身上的Buff、动画状态机可能还保留着死亡时的状态。下次取出时如果不重置就会出现一个“满血但播放死亡动画”的敌人。因此归还逻辑必须包含完整的对象状态重置这通常需要你在对象的类里实现一个Reset()或Initialize()函数并在池子归还时调用。3. 核心实现细节与蓝图节点暴露3.1 Subsystem的C实现要点首先创建我们的UObjectPoolSubsystem头文件。关键是要重写Initialize和Deinitialize方法并在其中管理池子资源的创建和释放。// ObjectPoolSubsystem.h #pragma once #include Subsystems/GameInstanceSubsystem.h #include ObjectPoolSubsystem.generated.h class UObjectPoolBase; UCLASS() class YOURMODULE_API UObjectPoolSubsystem : public UGameInstanceSubsystem { GENERATED_BODY() public: // 重写初始化可以在这里进行一些默认池的预热 virtual void Initialize(FSubsystemCollectionBase Collection) override; virtual void Deinitialize() override; // 蓝图可调用函数创建指定类和初始大小的池 UFUNCTION(BlueprintCallable, Category Object Pool, meta (DeterminesOutputType ObjectClass)) UObjectPoolBase* CreatePool(TSubclassOfUObjectPoolBase PoolClass, FName PoolName, TSubclassOfAActor ActorClass, int32 InitialSize 10); // 蓝图可调用函数从指定池获取一个Actor UFUNCTION(BlueprintCallable, Category Object Pool) AActor* GetActorFromPool(FName PoolName, const FTransform SpawnTransform, bool bUseTransform true); // 蓝图可调用函数归还一个Actor到指定池 UFUNCTION(BlueprintCallable, Category Object Pool) bool ReturnActorToPool(AActor* ActorToReturn); // ... 其他函数如检查池是否存在、获取池状态等 private: UPROPERTY() TMapFName, UObjectPoolBase* ObjectPools; };在.cpp文件中CreatePool函数是核心。它需要动态创建指定类型的池子对象并用传入的Actor类进行初始化。// ObjectPoolSubsystem.cpp UObjectPoolBase* UObjectPoolSubsystem::CreatePool(TSubclassOfUObjectPoolBase PoolClass, FName PoolName, TSubclassOfAActor ActorClass, int32 InitialSize) { if (ObjectPools.Contains(PoolName)) { UE_LOG(LogTemp, Warning, TEXT(Pool with name %s already exists!), *PoolName.ToString()); return ObjectPools[PoolName]; } if (!PoolClass || !ActorClass) { UE_LOG(LogTemp, Error, TEXT(Invalid PoolClass or ActorClass provided!)); return nullptr; } // 创建池子实例 UObjectPoolBase* NewPool NewObjectUObjectPoolBase(this, PoolClass); if (NewPool) { // 初始化池子 if (NewPool-InitializePool(ActorClass, InitialSize)) { ObjectPools.Add(PoolName, NewPool); return NewPool; } else { UE_LOG(LogTemp, Error, TEXT(Failed to initialize pool %s), *PoolName.ToString()); } } return nullptr; }这里的NewObject使用了this作为Outer意味着池子对象的生命周期将由Subsystem管理当Subsystem被销毁时这些池子对象也会被自动清理。3.2 Actor对象池的具体实现UActorObjectPool需要处理Actor的生成和回收细节。关键的SpawnFromPoolInternal和DespawnToPoolInternal虚函数重写如下// ActorObjectPool.cpp UObject* UActorObjectPool::SpawnFromPoolInternal(const FTransform* DesiredTransform) { AActor* SpawnedActor nullptr; // 1. 从空闲队列获取 if (!AvailableObjects.IsEmpty()) { AvailableObjects.Dequeue(SpawnedActor); } // 2. 如果池子空了且允许动态扩容则新建一个 else if (bCanGrow) { SpawnedActor CreateNewPooledObject(); } if (SpawnedActor) { // 3. 激活Actor ActivatePooledActor(SpawnedActor, DesiredTransform); // 4. 记录到已用映射 ActiveObjects.Add(SpawnedActor, FPoolObjectData(SpawnedActor)); return SpawnedActor; } UE_LOG(LogTemp, Warning, TEXT(ActorObjectPool is empty and cannot grow!)); return nullptr; } bool UActorObjectPool::DespawnToPoolInternal(UObject* ObjectToReturn) { AActor* ActorToReturn CastAActor(ObjectToReturn); if (!ActorToReturn || !ActiveObjects.Contains(ActorToReturn)) { return false; } // 1. 从已用映射中移除 ActiveObjects.Remove(ActorToReturn); // 2. 停用Actor DeactivatePooledActor(ActorToReturn); // 3. 放回空闲队列 AvailableObjects.Enqueue(ActorToReturn); return true; } AActor* UActorObjectPool::CreateNewPooledObject() { UWorld* World GetWorld(); if (!World || !ActorTemplateClass) { return nullptr; } // 使用SpawnActor但注意这里生成后立即会被我们“回收”到非活跃状态 FActorSpawnParameters SpawnParams; SpawnParams.SpawnCollisionHandlingOverride ESpawnActorCollisionHandlingMethod::AlwaysSpawn; SpawnParams.ObjectFlags | RF_Transient; // 标记为临时对象在某些情况下有助于管理 SpawnParams.bDeferConstruction false; // 立即完成构造 AActor* NewActor World-SpawnActorAActor(ActorTemplateClass, FVector::ZeroVector, FRotator::ZeroRotator, SpawnParams); if (NewActor) { // 立即将其置为非活跃状态相当于“预热”了一个新对象到池中 DeactivatePooledActor(NewActor); TotalPoolSize; } return NewActor; } void UActorObjectPool::ActivatePooledActor(AActor* Actor, const FTransform* DesiredTransform) { if (DesiredTransform) { Actor-SetActorTransform(*DesiredTransform, false, nullptr, ETeleportType::ResetPhysics); } Actor-SetActorHiddenInGame(false); Actor-SetActorTickEnabled(true); Actor-SetActorEnableCollision(true); // 调用Actor自定义的复位函数如果有的话 if (Actor-ImplementsUPoolableObject()) { IPoolableObject::Execute_OnPooledObjectActivate(Actor); } } void UActorObjectPool::DeactivatePooledActor(AActor* Actor) { Actor-SetActorHiddenInGame(true); Actor-SetActorTickEnabled(false); Actor-SetActorEnableCollision(false); // 可选将Actor移到很远的地方避免其组件仍参与任何计算 Actor-SetActorLocation(FVector(0, 0, -10000), false, nullptr, ETeleportType::ResetPhysics); // 取消所有附加 Actor-DetachFromActor(FDetachmentTransformRules::KeepWorldTransform); // 调用Actor自定义的清理函数 if (Actor-ImplementsUPoolableObject()) { IPoolableObject::Execute_OnPooledObjectDeactivate(Actor); } }3.3 蓝图接口设计让设计师也能轻松使用为了让策划和特效设计师能在蓝图中无痛使用我们需要设计直观的蓝图节点。主要暴露以下几个创建对象池 (Create Object Pool)输入池子名称FName如“BulletPool”、“EffectPool”、要管理的Actor类TSubclassOfAActor、初始大小int32。输出执行引脚以及可选的创建成功的池子引用用于后续高级配置。节点位置放在ObjectPoolSubsystem下通过Get Game Instance-Get Subsystem (Object Pool)链访问。从对象池生成 (Spawn Actor from Pool)输入池子名称FName、生成变换FTransform可选如果不提供则使用Actor自身的默认位置。输出生成的AActor引用。如果池子为空且不允许动态扩容应返回nullptr蓝图逻辑需要处理这种情况。设计要点这个节点应该尽可能模仿原生的Spawn Actor from Class节点的使用体验降低学习成本。归还对象到池 (Return Actor to Pool)输入要归还的AActor引用。输出执行引脚和布尔值表示归还是否成功。关键逻辑这个节点内部需要根据Actor的类型反向查找它属于哪个池子。我们可以在Actor生成时给它附加一个自定义组件或设置一个Tag来记录其来源池的ID。获取池子信息 (Get Pool Info)输入池子名称。输出当前池子大小、空闲对象数、活跃对象数等。用于监控和调试。在蓝图中一个典型的生成-归还流程看起来会非常简洁事件BeginPlay - [创建对象池 “Bullet” 类子弹BP 大小20] 事件玩家开火 - [从对象池“Bullet”生成] - 成功 - 设置子弹速度 - [设置定时器 3秒后] - [归还Actor到池 (子弹引用)]4. 实战在游戏项目中集成与使用对象池4.1 集成步骤与初始化时机将对象池系统集成到你的UE5项目中建议遵循以下步骤模块配置在项目的.Build.cs文件中添加必要的模块依赖通常是GameplayTags如果你用Tag来标识池子对象和你的自定义模块。子系统注册确保你的UObjectPoolSubsystem类在正确的模块启动函数中被正确注册。通常这在你创建类时由UE的宏自动处理但需要检查模块的.cpp文件是否包含了该子类的头文件。游戏初始化时预热对象池的创建和预热Pre-fill最好在游戏加载初期进行避免在游戏运行时因首次生成对象导致卡顿。理想的时机包括游戏实例初始化后在UGameInstance::Init()中或你的GameInstanceSubsystem::Initialize()中创建核心池子如玩家子弹、常见特效。关卡加载流中对于特定关卡的资源如某种敌人可以在关卡BeginPlay时或使用Streaming Level的加载完成事件中创建对应的池子。异步预热对于大型池子如超过100个对象可以考虑使用异步任务AsyncTask分批进行预热防止主线程卡住。一个常见的做法是在游戏主菜单或加载界面后台预热最常用的几个对象池。4.2 针对不同对象类型的池化策略不是所有对象都适合用同一个池子或者用同一种方式池化。我们需要根据对象类型制定策略对象类型池化策略注意事项简单视觉特效 (Niagara/Particle)必须池化收益最高。特效的NiagaraSystem或ParticleSystem组件是重头。归还时除了隐藏一定要重置粒子系统Reset()否则下次播放会从上次停止的位置继续。子弹/投射物强烈推荐池化。需要重置物理状态速度、角速度归零、碰撞启用、以及任何自定义的Hit事件标记。敌人/NPC复杂但可池化。这是最复杂的。需要完整重置AI状态停止行为树、清空黑板、生命值、装备、动画状态机。建议为敌人基类实现一个强力的ResetToPoolState()函数。可交互物品/武器视情况而定。如果频繁拾取丢弃可以池化。需要重置物品状态是否被拾取、物理模拟等。UI控件/Widget非常适合池化。UE的Widget本身创建开销不大但复杂Widget的构造和初始化可能耗时。池化可以复用已构建的Widget树。归还时需清除所有数据绑定和动画。声音组件可以考虑。UAudioComponent的生成销毁也有开销。池化可以复用组件只需更换播放的Sound Wave。对于Actor池化的一个关键技巧使用“休眠”而非“销毁”组件。对于复杂的Actor与其在归还时销毁所有组件再在下一次生成时重建不如在归还时让组件进入一种低消耗的“休眠”状态。例如将SkeletalMeshComponent的Visibility和Tick关闭但保留其引用。这需要组件本身支持这种状态切换但能进一步减少开销。4.3 在蓝图中编写池化友好的Actor逻辑要让一个Actor能被对象池完美管理它的蓝图或C类需要做一些配合实现一个初始化/重置函数创建一个自定义事件比如ResetForPool。在这个事件里把所有需要重置的变量恢复到默认状态血量、弹药、技能冷却、动画实例状态、时间轴、动态材质参数等。使用接口进行通信创建一个蓝图接口Blueprint Interface例如BPI_Poolable。里面定义两个函数OnPooledObjectActivate当从池中取出时调用和OnPooledObjectDeactivate当归还到池时调用。让你的Actor实现这个接口。这样对象池系统可以在不感知具体Actor类型的情况下调用这些通用的重置函数。谨慎使用BeginPlay和EndPlayBeginPlay在Actor每次被放入世界时都会调用EndPlay在每次被移除时调用。如果你的初始化逻辑只在第一次创建时运行一次如加载静态数据可以放在构造函数或一个单独的Initialize函数中并通过一个bInitialized布尔变量来防止在池化复用中被重复调用。同样清理逻辑也要考虑是否会在池化时被意外触发。处理组件和定时器在归还到池之前确保停止所有活动的定时器ClearTimer、延迟节点虽然蓝图延迟节点会自动取消但显式处理更好、以及粒子/音效播放。解除所有附件Detach。下面是一个敌人Actor蓝图的简化配合示例事件 BeginPlay检查bPoolInitialized变量。如果为假则执行一次性的数据加载如从DataTable读取属性然后设置bPoolInitialized为真。无论真假都执行每局游戏开始的通用初始化如设置初始血量、播放出生动画。自定义事件 ResetForPool (来自 BPI_Poolable)将血量设为最大值停止行为树清空仇恨列表停止所有蒙太奇动画重置所有技能冷却时间将Mesh隐藏对象池系统也会做但这里双重保险取消所有定时器。事件 OnPooledObjectActivate (来自 BPI_Poolable)显示Mesh启动行为树播放待机动画。事件 OnPooledObjectDeactivate (来自 BPI_Poolable)直接调用ResetForPool事件。5. 性能优化、调试与常见问题排查5.1 性能分析与优化点对象池引入后性能瓶颈会从“生成销毁”转移到“池子管理”和“对象状态重置”。我们需要关注以下几点池子大小与动态扩容预热大小设置一个合理的初始大小。太小会导致运行时频繁动态扩容依然有Spawn开销太大则浪费内存。可以通过游戏数据分析如一场战斗平均同时存在的子弹数来设定。动态扩容策略bCanGrow标志位控制是否允许池子不够时新建对象。对于不确定上限的对象如特效可以开启。对于有严格上限的如同屏敌人数则应关闭并在获取对象失败时处理如播放一个“创建失败”的提示特效。收缩策略通常对象池只增不减。但在某些内存敏感的场景如从战斗关卡回到大地图可以实现一个ShrinkPool函数将多余的空闲对象真正销毁掉。查找效率使用TMapFName, UObjectPoolBase*来管理池子查找是O(1)的没问题。但在一个池子内部从“已用对象映射”中通过Actor反向查找其数据也应该是O(1)的。确保你的ActiveObjects映射使用高效的键如Actor的UniqueID或直接指针。状态重置的成本这是容易被忽略的隐形开销。如果一个Actor的Reset函数里做了非常重的操作如清理一个巨大的数组、重新加载纹理那池化的收益会被抵消。优化重置逻辑只重置必要的运行时状态。内存占用监控对象池占用了“备用”内存。你需要知道池子总共占用了多少内存。可以在UObjectPoolSubsystem中实现一个GetTotalMemoryUsage函数遍历所有池子累加其中每个对象通过GetResourceSize()或近似估算得到的内存大小并在开发时输出到屏幕或日志。5.2 调试与可视化工具为了方便开发和调试建议为对象池系统添加一些可视化工具控制台命令在C中注册一些Exec命令方便在运行时调试。// 在Subsystem或某个Manager中 bool UObjectPoolSubsystem::Exec(UWorld* InWorld, const TCHAR* Cmd, FOutputDevice Ar) { if (FParse::Command(Cmd, TEXT(ShowObjectPools))) { Ar.Logf(TEXT( Object Pool Status )); for (const auto Pair : ObjectPools) { Ar.Logf(TEXT(Pool [%s]: Total%d, Active%d, Available%d), *Pair.Key.ToString(), Pair.Value-GetTotalSize(), Pair.Value-GetActiveCount(), Pair.Value-GetAvailableCount()); } return true; } // ... 其他命令 return false; }在游戏中按“~”打开控制台输入ShowObjectPools就能看到所有池子的实时状态。屏幕绘制信息在游戏HUD或调试绘制中显示关键池子的使用情况如“子弹池45/50”这对平衡性调整和性能监控很有帮助。蓝图调试节点创建一些蓝图节点如Print Pool Stats to Screen方便策划和美术在编辑器中测试时查看。5.3 常见问题与解决方案速查表在实际使用中你肯定会遇到一些坑。下面是我踩过之后总结出来的常见问题及解决办法问题现象可能原因解决方案从池中取出的Actor位置/旋转不对归还时没有正确重置变换或者取出时设置的变换被某些逻辑覆盖。1. 确保在DeactivatePooledActor中不重置Actor的世界变换我们只是移动它到远处。2. 在ActivatePooledActor中使用SetActorTransform并确保Teleport参数为true以忽略物理插值。特效播放不完整或从中间开始粒子系统或Niagara系统在归还时没有完全重置。在归还逻辑中除了隐藏必须调用粒子组件的Reset()函数对于Niagara是ResetSystem()。对于复杂的级联特效可能需要遍历所有子粒子组件进行重置。Actor的Tick还在运行归还时没有禁用Actor或组件的Tick。在DeactivatePooledActor中调用SetActorTickEnabled(false)。对于可能有独立Tick的组件如某些移动组件也需要单独禁用。碰撞异常对象看不见但能撞到归还时只隐藏了Mesh但没有禁用碰撞。在归还逻辑中调用SetActorEnableCollision(false)。对于复杂碰撞体可能需要更精细的控制。内存泄漏池子里的对象没有被垃圾回收池子持有对象的强引用UPROPERTY()或TStrongObjectPtr导致即使游戏逻辑不再使用GC也无法回收。这是最危险的错误。确保池子对对象的引用是“非持有”的。空闲队列可以用TArrayTWeakObjectPtr来存储弱引用当对象被外部逻辑意外销毁时池子能感知到。或者在Subsystem的Deinitialize中手动清空所有池子并释放引用。多人游戏网络复制中对象池异常对象池只在服务器端管理客户端生成的Actor没有正确复制或者归还逻辑没有在客户端执行。网络游戏的对象池需要专门设计。通常池化只应在服务器端进行。服务器从池中取出Actor然后通过网络复制生成到客户端。归还时服务器通知客户端销毁。客户端不应有自己的本地池化逻辑除非是纯视觉特效。蓝图接口函数没有被调用Actor没有正确实现接口或者接口函数没有被标记为BlueprintImplementableEvent/BlueprintNativeEvent。检查Actor的类设置中是否添加了接口。确保接口函数在蓝图中被重写如果使用的是BlueprintImplementableEvent或者在C中有默认实现如果使用的是BlueprintNativeEvent。一个关于“对象意外死亡”的深度处理技巧有时候池子里的Actor可能因为游戏逻辑如飞出边界销毁、被强制DestroyActor而意外被销毁。如果池子还持有它的引用就会变成野指针。为了解决这个问题可以在Actor的EndPlay事件中主动通知对象池系统“我要死了请把我从池子记录里移除”。这需要在Actor和池子之间建立一个反向通信机制比如通过一个全局的事件分发器Event Dispatcher或者让Actor在生成时注册一个到池子的委托Delegate在销毁时调用。虽然增加了复杂度但能极大提高系统的健壮性。最后对象池不是银弹。它主要优化的是频繁、批量、生命周期短的对象的生成销毁开销。对于只生成一次、长期存在的对象如玩家角色、主要建筑使用对象池反而增加了复杂度。正确评估你的使用场景让合适的工具做合适的事这才是性能优化的精髓。这套基于Subsystem的蓝图友好对象池系统为你提供了一种强大而优雅的优化手段希望能帮助你打造出更流畅的UE5游戏体验。