Standard Delay nodes in Unreal Engine don’t respect the Gameplay Ability lifecycle. In the Gameplay Ability System (GAS), abilities should use the Wait Delay task node (UAbilityTask_WaitDelay) instead of standard latent delays.
To enforce this automatically, we can create a Blueprint Compiler Extension (UBlueprintCompilerExtension). By hooking directly into the Blueprint compilation process, we can check node graphs during compilation and fail the build if a forbidden node is used.
Unreal Engine provides the UBlueprintCompilerExtension class (in the BlueprintCompiler module) to run custom validation rules during Blueprint compilation.
When a developer compiles a Blueprint:
UBlueprintCompilerExtension.ProcessBlueprintCompiled() on each extension.Because compiler extensions rely on editor-only modules like BlueprintCompiler, KismetCompiler, and UnrealEd, this code must live in an Editor Module (e.g., MyProjectEditor). Placing editor dependencies inside a primary game module will break standalone builds.
.uproject FileAdd the editor module configuration to your project’s .uproject file under "Modules":
{
"Name": "MyProjectEditor",
"Type": "Editor",
"LoadingPhase": "PostEngineInit",
"AdditionalDependencies": [
"MyProject"
]
}
In your project’s MyProjectEditor.Target.cs, add the editor module to ExtraModuleNames:
ExtraModuleNames.AddRange(new string[] { "MyProject", "MyProjectEditor" });
Create MyProjectEditor.Build.cs in your editor module directory:
using UnrealBuildTool;
public class MyProjectEditor : ModuleRules
{
public MyProjectEditor(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[] {
"Core",
"CoreUObject",
"Engine",
"MyProject"
});
PrivateDependencyModuleNames.AddRange(new string[] {
"UnrealEd",
"BlueprintGraph",
"KismetCompiler",
"Kismet",
"GameplayAbilities"
});
}
}
Initialize the editor module with main header and source files:
#pragma once
#include "CoreMinimal.h"
#include "Modules/ModuleManager.h"
class FMyProjectEditorModule : public IModuleInterface {
public:
virtual void StartupModule() override;
virtual void ShutdownModule() override;
};
#include "MyProjectEditor.h"
#include "Modules/ModuleManager.h"
#include "BlueprintCompilationManager.h"
#include "BPCE_Ability_Delay.h"
static UBPCE_Ability_Delay* AbilityDelayCheckExtension = nullptr;
void FMyProjectEditorModule::StartupModule() {
AbilityDelayCheckExtension = NewObject<UBPCE_Ability_Delay>();
AbilityDelayCheckExtension->AddToRoot();
FBlueprintCompilationManager::RegisterCompilerExtension(UBlueprint::StaticClass(), AbilityDelayCheckExtension);
}
void FMyProjectEditorModule::ShutdownModule() {
if (AbilityDelayCheckExtension) {
AbilityDelayCheckExtension->RemoveFromRoot();
AbilityDelayCheckExtension = nullptr;
}
}
IMPLEMENT_MODULE(FMyProjectEditorModule, MyProjectEditor);
Implement the validator class inside the editor module:
#pragma once
#include "CoreMinimal.h"
#include "BlueprintCompilerExtension.h"
#include "BPCE_Ability_Delay.generated.h"
UCLASS()
class MYPROJECTEDITOR_API UBPCE_Ability_Delay : public UBlueprintCompilerExtension {
GENERATED_BODY()
public:
virtual void ProcessBlueprintCompiled(const FKismetCompilerContext& CompilationContext, const FBlueprintCompiledData& Data) override;
};
#include "BPCE_Ability_Delay.h"
#include "K2Node_CallFunction.h"
#include "KismetCompiler.h"
#include "Abilities/GameplayAbility.h"
#include "EdGraph/EdGraph.h"
#include "Kismet/KismetSystemLibrary.h"
void UBPCE_Ability_Delay::ProcessBlueprintCompiled(const FKismetCompilerContext& CompilationContext, const FBlueprintCompiledData& Data) {
const UBlueprint* Blueprint = CompilationContext.Blueprint;
if (!Blueprint || !Blueprint->ParentClass) {
return;
}
// Check if the blueprint is a Gameplay Ability
if (!Blueprint->ParentClass->IsChildOf(UGameplayAbility::StaticClass())) {
return;
}
TArray<UEdGraph*> AllGraphs;
Blueprint->GetAllGraphs(AllGraphs);
// Change the Blacklisted Function here
const UClass* LibraryClass = UKismetSystemLibrary::StaticClass();
static UFunction* DelayFunc = LibraryClass->FindFunctionByName(GET_FUNCTION_NAME_CHECKED(UKismetSystemLibrary, Delay));
for (UEdGraph* Graph : AllGraphs) {
if (!Graph) {
continue;
}
for (UEdGraphNode* Node : Graph->Nodes) {
if (const UK2Node_CallFunction* FuncNode = Cast<UK2Node_CallFunction>(Node)) {
const UFunction* Func = FuncNode->GetTargetFunction();
if (Func == DelayFunc) {
UE_LOG(LogTemp, Error, TEXT("Critical: Blueprint '%s' contains a forbidden Delay node in graph '%s'!"), *Blueprint->GetName(), *Graph->GetName());
FText ErrorMessage = FText::FromString(TEXT("Delay nodes are forbidden in Gameplay Abilities. Use 'Wait Delay' node or other latent ability tasks instead (Error in @@)"));
CompilationContext.MessageLog.Error(*ErrorMessage.ToString(), Node);
}
}
}
}
}
Blueprint->GetAllGraphs(AllGraphs) collects all graphs within the Blueprint (event graph, custom functions, macro graphs, interfaces). Inside each graph, we iterate through Graph->Nodes and cast to UK2Node_CallFunction to inspect target function calls.
Instead of string comparisons on function names every compile pass, we cache the target UFunction pointer using FindFunctionByName and match against the node’s target function:
const UClass* LibraryClass = UKismetSystemLibrary::StaticClass();
static UFunction* DelayFunc = LibraryClass->FindFunctionByName(GET_FUNCTION_NAME_CHECKED(UKismetSystemLibrary, Delay));
Unreal Engine’s compiler log parser supports the @@ token. When included in the error message, the compiler results window replaces @@ with a clickable hyperlink pointing to the node passed as the second parameter:
FText ErrorMessage = FText::FromString(TEXT("... Error in @@ ..."));
When pushed with CompilationContext.MessageLog.Error(Message, Node), double-clicking the compiler error jumps directly to the problematic node in the graph editor.
To run validation on all existing Gameplay Ability assets at once:
GameplayAbility assets in the Content Browser.This executes compiler validation across every selected ability and outputs errors for any forbidden Delay nodes directly to the log.
Once UBlueprintCompilerExtension is registered, you can expand it to enforce additional node blacklists or architectural constraints across your Blueprints.