Blueprint Compiler Extension

What we are going for:

Pic 1. I tried to compile my BP but it says I can’t use StaticMeshComponent in it.

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.

Pic 2. Almost everything you see on this screenshot can be accessed from BCE

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:

  1. 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++.
  2. Users might not notice the message in the log or even worse, ignore it…
  3. 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.

  1. 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)
  2. UBlueprintCompilerExtension derived class overview
    • Overview of method to override
    • How to get data we’re interested in
  3. 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:

Pic 3. I’m assuming your module is in the same state as it would be after creating it when following the link I provided.

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.

Pic 4. Adding Editor Module to the project

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:

YourModule.Build.cs
PrivateDependencyModuleNames.AddRange
(
    new string[]
    {"UnrealEd", "Kismet", "KismetCompiler", "YourProjectName"}
);
C++
Pic 5. Adding dependencies to the Editor Module

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.

Pic 6. Editor module folder

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.

Pic 7. Adding class to the module

Change the view to see all available classes and find BluepintCompilerExtension class and create a class deriving from BCE.

Pic 8. Choosing parent class

Class I created:

Pic 9. BCE class

Here is the class I want to prevent from having an SMC. Notice it’s located in my project, not in the module.

Pic 10. An actor without Static Mesh Component

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.

Pic 11. The virtual method we are 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:

BPCompilerExtension_BaseSimple.cpp
// 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 of SCS_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.

Pic 12. Editor module header
Pic 13. Editor module source

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:

Pic 14. BP compiler error stating that SMC cannot be used in this Actor

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.

Pic 15. All these 4 BlueprintAssets are instances of the UBlueprint class and inherit at some point from it.

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.

Pic 16. UBlueprint stores pointer to UObject or AActor
Pic 17. Blueprint Editor displays the name of UObject stored in pointer

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

Pic 18. BCEs and BPs are connected in Project Settings

Module with BCE doesn’t have to know about any of my classes from the project (no includes or additional dependencies, and user-friendliness).

Pic 19. BCE module is encapsulated

More complicated logic is separated from the Module class (Readability).

Pic 20. Complicated logic is separated

The separated logic is mainly responsible for checking nulls, reading settings, setup, and adding an extension to the manager.

Pic 21. Additional checks in the BCE

Always eager to learn new things.
2 years of experience in Unreal Engine.

Leave a Reply

Your email address will not be published. Required fields are marked *