Système de pooling C++ pour Unreal Engine 5
Langage :
C++
Blueprint-Friendly (Utilisable entièrement sans coder)
Durée :
Projet sur 5 jours
Equipe :
Projet entièrement seul (développement, design technique, optimisation, outils éditeur).
Concept :
Universal Pooling Optimized est un système de pooling haute-performance conçu pour remplacer le cycle Spawn / Destroy dans Unreal Engine.
Il réduit drastiquement les sauts de frame, le Garbage Collection et les allocations dynamiques répétées, même dans des projets exigeants, où plusieurs centaines d’acteurs (ennemis, projectiles, VFX) sont actifs en continu.
Pourquoi j’ai créé ce plugin ?
Un besoin dans Boomville
Dans plusieurs projets UE5, nous devions gérer des centaines d’ennemis, de projectiles et de VFX en même temps.
C’était notamment le cas sur Boomville, où le gameplay repose sur :
Avec ce volume, le Spawn/Destroy classique causait rapidement :
Nous avions besoin d’un système stable, simple et prévisible… sans setup compliqué.
C’est ainsi qu’est né Universal Pooling Optimized :
Un système de pooling C++ data-driven, facile à intégrer, entièrement Blueprint-friendly, et doté d’outils de debug clairs pour comprendre à tout moment ce qu’il se passe dans les pools
Principes du système :
Un Pool par classe
Chaque type d’acteur possède son propre pool.
C’est la manière la plus fiable d’éviter la fragmentation, de garder un usage mémoire clair et d’offrir à l’utilisateur une vue immédiate de ce qui se passe dans l’éditeur.
Chaque classe devient la clé d’un <em>TMap<TSubclassOf<AActor>, FPoolContainer></em>.
Ce choix garantit que la récupération d’un pool est instantanée (O(1)), et que chaque acteur retourne toujours dans le conteneur qui lui appartient.
TMap<TSubclassOf<AActor>, FPoolContainer> Pools;
FPoolContainer& UUniversalPoolingSubsystem::GetOrCreatePoolContainer(TSubclassOf<AActor> ActorClass)
{
check(ActorClass);
if (FPoolContainer* Existing = Pools.Find(ActorClass))
return *Existing;
FPoolContainer NewContainer(ActorClass);
NewContainer.MaxSize = 256;
return Pools.Add(ActorClass, NewContainer);
}
Pooling déterministe
Chaque acteur passé par le pool reçoit un petit FPoolHandle qui contient :
À l'acquire le subsystem crée ce handle et le stocke dans une table de lookup :
FPoolHandle Handle(ActorClass, Index, ++Entry.Generation);
ReverseLookup.Add(TObjectKey>AActor>(Instance), Handle);
Au return, on ne parcourt jamais les tableaux :
on récupère le handle en une opération de map (O(1)), on vérifie la génération, puis on libère le bon slot :
const FPoolHandle* Handle = ReverseLookup.Find(TObjectKey>AActor>(Actor));
Grâce au FPoolHandle(classe, index, génération), le système sait exactement quel slot libérer et détecte automatiquement les handles périmés.
Résultat : toutes les opérations Acquire/Return restent en O(1), même avec des centaines d’acteurs.
Data-Driven (Config Assets & Developper Settings)
Universal Pooling Optimized est entièrement data-driven : chaque type d’acteur peut avoir son PoolConfig DataAsset, dans lequel on définit la taille initiale, l’expansion automatique, la taille minimale après shrink, ou même un groupe logique de pools.

L’utilisateur n’a rien à coder : il ajuste ses paramètres dans l’éditeur, et le subsystem applique tout automatiquement au runtime.
Un Developer Settings global permet aussi de définir un config par défaut pour toutes les classes non configurées, offrant un onboarding simple et évitant les surprises dans les projets plus larges.

Subsystem global
Universal Pooling Optimized repose sur un UGameInstanceSubsystem, ce qui garantit un système unique, global et persistant pour toute la durée du jeu.
Peu importe la map ou le contexte, le pooling reste disponible et cohérent, sans avoir à instancier quoi que ce soit.
Le subsystem fournit trois fonctions simples et stablesn celles utilisées 99% du temps :



Acquire (Spawn depuis le pool)
Renvoie immédiatement un acteur prêt à l’emploi : soit recyclé depuis le pool (O(1)), soit créé si nécessaire.
Return (Rendre au pool)
Cache et désactive proprement l’acteur, tout en le rendant réutilisable sans coût de création.
Prewarm (Précharger la pool)
Crée à l’avance un nombre donné d’instances pour éliminer les hics de performance au début d’un combat ou d’une scène.
Component + Interface
Pour utiliser le plugin, je rajoute simplement un Universal Pooled Component sur mes Blueprints.
Depuis le Details panel, je peux configurer comment l’acteur retourne au pool :

Pour gérer le gameplay, le Blueprint implémente l’interface Universal Poolable et expose deux events :

Résultat : la logique de pooling reste côté C++, et je ne fais que brancher mes comportements de jeu dans quelques events Blueprint lisibles.
Editor Debug Panel
Onglet Personnalisé :
Le plugin inclut un panneau de debug dédié dans l’éditeur, accessible depuis la barre de menu d’Unreal.
Il s’agit d’un onglet personnalisé construit en Slate, capable d’afficher en temps réel l’état complet de chaque pool : nombre total d’instances, acteurs actifs, instances libres et historique des créations.

Le panel rafraîchit automatiquement les données et propose des actions immédiates, comme vider un pool spécifique ou réinitialiser l’ensemble du système. Cette interface permet de comprendre en un coup d'œil le comportement du pooling pendant l’exécution.
L’objectif est d’offrir un outil de supervision simple, fiable et intégré nativement à l’éditeur, afin de rendre le système totalement transparent lors du développement et du débogage.
Essayez la demo : (Bientôt)
Mot de la fin :
Ce projet m’a permis de consolider des compétences essentielles en architecture système et en optimisation sur Unreal Engine.
J’ai notamment appris à concevoir un outil réutilisable, robuste et indépendant du gameplay, capable de s’intégrer proprement à l’écosystème de l’éditeur.
La création d’un pooling déterministe, la gestion fine des allocations, l’utilisation d’un Subsystem global et l'exposition Blueprint-friendly m’ont obligé à repenser chaque étape pour garantir la performance, la lisibilité et la stabilité.
Le développement du panneau de debug m’a également familiarisé avec Slate, la logique d’onglets nomades et les workflows d’outillage côté éditeur.
Enfin, travailler sur un système pensé pour être utilisé par d’autres développeurs m’a fait progresser sur la documentation, la simplicité d’utilisation et l’importance d’un design prévisible.
En espérant que ce projet vous ait plu !
