What we are going for:
If you’re only interested in code go to the “How” section or visit our GitHub repo.
What is Blueprint Compiler Extension (BCE), and why would you use it?
What?
BCE class allows you to add conditions to the blueprint compilation process. It does it by giving access to everything you can see in BPAssetView.
Example:
I created actor A, and it won’t work properly if someone adds a StaticMeshComponent(SMC) to it. How to let the user know that he can’t use SMCs in actor A?
The first idea I could come up with would be to use the constructor. If there is any SMC in the components list then log an error message and everything will be good (btw I hope you don’t like this idea as much as I do). Let’s go through drawbacks of this solution:
- Constructor gets called an incredible amount of times when you work with BPs, in the engine, or when running the game. When you open BP one gets called, when you place an actor in level via editor it gets called again, and most importantly it is called every time you spawn an actor during the game (this last alone makes the idea a “no go” for me).
NOTE: You could mark code with#if WITH_EDITOR
macro this would delete the runtime problem(build only), but solving it this way kind of violates my way of understanding the CleanCode concept by leaving me with non-easily-reusable code and without knowledge on how to force Users to create variables in BPs from C++. - Users might not notice the message in the log or even worse, ignore it…
- A project can be built and packaged with actor A not working properly.
The second idea I came across was to use Interfaces as an additional part of my system. How would it work?
Well basically for every component I created, there would be a corresponding interface that had to be implemented by the owner. By overriding the OnComponentCreated method we can look for the component we’re interested in. If it’s found, log a message. As it seems to be a better idea than the previous one because I can mostly get rid of the first problem. Unfortunately, the second and third ones stay untouched (Here is a forum thread about it).
Okay, I covered “What” let’s briefly touch “Why” and then go to the best part “How”.
Why? (Solving our issues)
Issue number 1:
Component check is called in the editor and on runtime multiple times.
With BCE we need to perform checks only once, in the best place for it, during BP compilation.
Cool, checked.
Issue number 2:
Users can ignore our warnings/errors.
BCE has the option to return BP compilation Warning or Error so if BP won’t compile, the user will have to notice it and make corrections.
And yet another issue has fallen.
Issue number 3:
A project can be packaged and you won’t even get a warning/error.
Here the solution is the same as in number 2 – BCE gives us tools to entirely remove the problem.
How to use BCE
Firstly let’s bullet out what I’ll do to implement BCE so it will be easier to follow.
- Setting up the environment
- Converting base Module to EditorModule
- Including necessary dependencies to EditorModule
- Regenerating Project’s files
- Creating classes we’ll use (ActorWithoutSM, CompilerExtension)
- UBlueprintCompilerExtension derived class overview
- Overview of method to override
- How to get data we’re interested in
- Registering BCE
Adding dependencies to our module
We will create our BCE inside a new EditorModule.
I won’t write here how to create a module or what it is because it’s already on the internet, here to be specific 🙂
ProTip: If you really don’t want to learn now how to create modules on your own check out this plugin.
You might want to add to your module name “Editor” postfix just as I did:
Once your module was created go to <projectName>Editor.Target.cs
and add it in there. Thanks to this, <YourModule> will be loaded only when opening the editor.
Including dependencies to the module
Now you need to add some dependencies to use BCE. Go to YourModule.Build.cs
file and add this line:
PrivateDependencyModuleNames.AddRange
(
new string[]
{"UnrealEd", "Kismet", "KismetCompiler", "YourProjectName"}
);
C++WARNING: The YourProject part is necessary as without it you won’t be able to easily use classes from your project. If you don’t like this approach please check out the “Advanced” BCE section at the end of this post.
Creating classes
Now, if you set up everything correctly a folder with your module’s name should appear in the UE editor.
If you can’t see your module just check if the module you created is listed in the dropdown menu in the “Add C++ Class” window.
Change the view to see all available classes and find BluepintCompilerExtension
class and create a class deriving from BCE.
Class I created:
Here is the class I want to prevent from having an SMC. Notice it’s located in my project, not in the module.
If you have your ClassToCheck and a child of BCE, regenerate the project’s files and we’ll move to the more juicy stuff. 🙂
BCE class overview
Overview of the method to override
The only virtual method in our BCE class parent is the one we’re interested in.
Its description in the parent is decent so let’s see the parameters it accepts:
- CompilationContext – This contains a C++ representation of our blueprint and some compilation-related data like old CDO (Class default object).
- Data – This struct contains UberGraph for our BP. What is it? You would ask. Well, as stated in the UE bible, this is a graph created from all of the EventGraphs located in our blueprint (UE basically copies stuff from all graphs to one big), it contains references to all Events located in our BP.
Finding data we’re interested in.
Basically, we want to know if our BP has any components. If so, does any of them is an SMC or its child? This array of Blueprint components is stored in USimpleConstructionScript
. This construction script is stored in C++ representation of our BP, so digging out this array will look like that:
// Samurais 2023.
#include "BPCompilerExtension_BaseSimple.h"
// Class we want to check.
#include "ActorWithoutSMC.h"
// includes for digging out array with components.
#include "KismetCompiler/Public/KismetCompiler.h"
#include "Engine/SimpleConstructionScript.h"
#include "Engine/SCS_Node.h"
// includes for logging out message/error.
#include "Kismet2/CompilerResultsLog.h"
#include "Logging/TokenizedMessage.h"
void UBPCompilerExtension_BaseSimple::ProcessBlueprintCompiled(const FKismetCompilerContext& CompilationContext, const FBlueprintCompiledData& Data)
{
// Check if compiled BP is child of class we want to check.
if(CompilationContext.Blueprint->ParentClass->IsChildOf(AActorWithoutSMC::StaticClass()))
{
if(const TObjectPtr<USimpleConstructionScript> SimpleConstructionScript = CompilationContext.Blueprint->SimpleConstructionScript)
{
// SCS stores data needed to recreate components in BP in so called Nodes (one node per component).
TArray<USCS_Node*> SCS_Nodes = SimpleConstructionScript->GetAllNodes();
for(const USCS_Node* SCS_Node : SCS_Nodes)
{
// Acquiring component from SCS_Node and casting it to StaticMeshComponent (if success fail compilation).
if(SCS_Node)
if(Cast<UStaticMeshComponent> (SCS_Node->ComponentTemplate))
{
/* Fail BP compilation and print message on PIE but when packaging game DON'T FAIL.
*
* CompilationContext.MessageLog.AddTokenizedMessage(FTokenizedMessage::Create(
* EMessageSeverity::Error,
* FText::FromString("Don't use StaticMeshComponent with this actor.")));
*/
// Fail BP compilation, print message on PIE and fail when trying to package game.
CompilationContext.MessageLog.Error(TEXT("Don't use StaticMeshComponent with this actor."));
}
}
}
}
}
C++Ok, let’s go through the code:
- Lines 6 and 21: The first thing you might notice is the “include” of my actor that won’t work with SMC. BCE will be called for every blueprint so I need to restrict this code only to classes I care about (more about this I’ll write in the part where I’ll register my BCE).
- Lines 23 and 26: In the next lines I get
SimpleConstructionScript
(SCS). Then I get an array ofSCS_Nodes
(data required to recreate components in BP). - Lines 30-42: Nodes store a template for the component they represent so casting it to a component I don’t want to have is enough for a check. If the cast succeeds I create an error message.
Registering BCE to BlueprintCompilationManager
To register a BCE we need to find a suitable place for it. I recommend using the StartupModule method from your module class(the one inheriting from IModuleInterface
). That’s because we want to do registration as early as possible and do it only once.
So as you can see all I’m doing here is creating a BCE object and passing it to BlueprintCompilationManager
. With that, every time any blueprint is compiled my implementation of ProcessBlueprintCompiled
is called. Thanks to the IsChildOf()
check at the beginning, it will process BPs derived from AActorWihtoutSMC
only.
Final result:
About why my BCE will be called during every blueprint compilation
RegisterCompilerExtension accepts TSubclassOf<UBlueprint>
. The decision whether to execute that extension or not for a specific BlueprintAsset is based on if that BlueprintAsset derives from the subclass that was passed.
The Hierarchy looks like that:
- UBlueprint:
- UAnimInstanceBlueprint
- UUserWidgetBlueprint
- UControlRigBlueprint
- etc…
All of the above are Engine C++ classes.
What I mean by this is? When you create a BP from AActor you end up with a BlueprintAsset (UBlueprint instance). It can be seen as an AActor child but is not derived from AActor in a C++ meaning of it. UBlueprint only stores a pointer to that AActor.
UBlueprint concept is important to understand so I’ll give you some more examples:
- AActor – Class that can be represented by BlueprintAsset
- BP_MyActor – BlueprintAsset Class that stores a pointer to AActor.
- UUserWidgetBlueprint – BlueprintAsset Class
- UActorComponent – Class that can be represented by BlueprintAsset(by storing a pointer to UActorComponent).
- UAnimBlueprint – BlueprintAsset Class.
“Advanced” BCE
The module without the “Simple” postfix has additional functionalities. I’m listing them below.
Picking what Blueprint should be checked by which BCE when compiling it was moved to ProjectSettings -> Editor -> BPCompilerExtension
Module with BCE doesn’t have to know about any of my classes from the project (no includes or additional dependencies, and user-friendliness).
More complicated logic is separated from the Module class (Readability).
The separated logic is mainly responsible for checking nulls, reading settings, setup, and adding an extension to the manager.