Improving Generated Code - protospatial/NodeToCode GitHub Wiki
Although Node to Code tries to provide ample output steering via detailed system instructions, sometimes a less capable model may output code that doesn't meet expectations or needs some cleanup. There are several factors to consider and steps you can take to improve results.
// Poor quality example:
UFUNCTION(BlueprintCallable)
float DoComplexMath(float input_value,FString operation_type,bool should_clamp){
float result=input_value;if(operation_type=="square"){result*=result;}
return should_clamp?FMath::Clamp(result,0.f,1.f):result;
}
// Better quality example:
UFUNCTION(BlueprintCallable, Category = "Math|Operations")
float DoComplexMath(const float InputValue, const FString& OperationType, const bool bShouldClamp)
{
float Result = InputValue;
if (OperationType == TEXT("Square"))
{
Result *= Result;
}
return bShouldClamp ? FMath::Clamp(Result, 0.0f, 1.0f) : Result;
}
// Poor quality example:
FString* StringPtr = new FString("Unsafe"); // Manual memory management
TArray<int> Numbers; // Missing size hints
FVector Location(0); // Incomplete initialization
// Better quality example:
FString SafeString = TEXT("Safe"); // Automatic memory management
TArray<int32> Numbers; // Explicit integer type
FVector Location(0.0f, 0.0f, 0.0f); // Full initialization
// Poor quality example:
void ProcessData(); // Missing UFUNCTION
FString PlayerName; // Missing UPROPERTY
// Better quality example:
UFUNCTION(BlueprintCallable, Category = "Data Processing")
void ProcessData();
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Player Info")
FString PlayerName;
This problem is typically more common in smaller local models but can also (less commonly) happen with larger, more capable, cloud-based LLMs.
- The context window was completely filled, leaving out critical sections of the serialized blueprint or reference code used for translation. This will leave the LLM to fill in blanks without relevant context.
- There are either too many reference source files added, or they are too large, filling up a too much of the context window and making it more difficult for the LLM to accurately retrieve relevant context.
- The model being used is one that is simply not capable enough for this task.
Note
When a translation is finished, you can see the input and output token usage through the UE Output Log. Input usage is the most relevant statistic to > keep an eye on.
Note
It's generally recommended to stay under or within 65% - 85% utilization of an LLM's advertised context window to retain the majority of model's capabilities and reduce hallucinations.
Note
If you're using Ollama for your LLM provider, ensure that you adjust the Context Window paramater accordingly in the Node to Code Plugin settings. It's recommended to keep this value at or above 16,000 for up to medium sized blueprint graph translations. As you translate larger blueprint graphs, increase Translation Depth, and/or add reference source code files, the context window will need to increase dramatically.
Node to Code follows specific patterns when translating Blueprint elements:
// Blueprint Event Graph typically becomes:
UCLASS()
class MYGAME_API AMyActor : public AActor
{
GENERATED_BODY()
// Blueprint variables become properties
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MyCategory")
float MyBlueprintVariable;
// Blueprint events become functions
UFUNCTION(BlueprintImplementableEvent, Category = "MyCategory")
void MyBlueprintEvent();
// Blueprint custom events become UFUNCTIONs
UFUNCTION(BlueprintCallable, Category = "MyCategory")
void MyCustomEvent();
};
- Event Handling
// Blueprint BeginPlay event becomes:
virtual void BeginPlay() override
{
Super::BeginPlay();
// Translated logic here
}
// Blueprint Tick event becomes:
virtual void Tick(float DeltaTime) override
{
Super::Tick(DeltaTime);
// Translated logic here
}
- Variable Access
// Blueprint "Get" node becomes direct property access
float Value = MyVariable;
// Blueprint "Set" node becomes assignment
MyVariable = NewValue;
// Blueprint "Get" with validity check becomes:
if (UObject* Object = MyObjectVariable)
{
// Safe usage here
}
- Flow Control
// Blueprint branch node becomes if-else
if (Condition)
{
// True path
}
else
{
// False path
}
// Blueprint sequence node becomes sequential statements
FirstFunction();
SecondFunction();
ThirdFunction();
// Blueprint for-each loop becomes range-based for
for (AActor* Actor : TActorRange<AActor>(GetWorld()))
{
// Loop body
}
Check out Choosing an LLM Provider for a more detailed breakdown on model strengths.
// Example header to include as reference:
UCLASS()
class MYGAME_API AMyBaseActor : public AActor
{
GENERATED_BODY()
public:
// Document your patterns
UFUNCTION(BlueprintCallable, Category = "MyGame|Utilities")
void StandardUtilityFunction();
// Show proper property usage
UPROPERTY(EditDefaultsOnly, Category = "MyGame|Configuration")
float ConfigValue;
};
Key reference file characteristics:
- Clean, well-documented code
- Proper UE patterns and macros
- Relevant to immediate translation context
- Demonstrates project conventions
- Add comments directly to nodes (eg "IMPORTANT: ") to help steer the output in relation to that node
-
Clean Up Nodes
- Remove unused variables and nodes
- Delete deprecated nodes
- Organize node layout
- Group related functionality
-
Add Documentation
- Document node purpose
- Explain reasoning for any complex logic
- Note performance considerations
- Specify expected behavior
Note
Node comments can improve translation quality by providing additional context to the LLM.
-
Simplify Complex Operations
- Break down large functions
- Extract reusable logic
- Use function libraries
- Implement clear interfaces
Problem: Missing includes or forward declarations.
// Generated code with missing dependencies
class UMyComponent; // Undefined class error
Fix: Add necessary includes or forward declarations.
// In header (.h)
#include "GameFramework/Actor.h"
class UMyComponent; // Forward declaration for pointers/references
// In source (.cpp)
#include "MyComponent.h" // Full include for implementation
Problem: Incorrect or missing UE macros.
// Generated code with incorrect macros
UCLASS()
class MyActor : public AActor // Missing API macro
Fix: Add appropriate module API and other required macros.
UCLASS()
class MYGAME_API AMyActor : public AActor // Added API macro
{
GENERATED_BODY() // Don't forget this!
Problem: Incorrect function specifiers or implementation.
// Generated code with incorrect function specification
void BlueprintFunction() // Missing UFUNCTION
Fix: Add appropriate UFUNCTION macro and implementation.
UFUNCTION(BlueprintCallable, Category = "MyFunctions")
void BlueprintFunction()
{
// Implementation
}
Problem: Direct translation of latent Blueprint nodes.
// Generated code attempting direct latent operation
void MyLatentOperation()
{
Delay(2.0f); // Won't work as expected
}
Fix: Use proper latent function mechanisms.
UFUNCTION(BlueprintCallable, meta = (Latent, LatentInfo = "LatentInfo"))
void MyLatentOperation(FLatentActionInfo LatentInfo)
{
if (UWorld* World = GetWorld())
{
FLatentActionManager& LatentManager = World->GetLatentActionManager();
FMyLatentAction* Action = new FMyLatentAction(LatentInfo);
LatentManager.AddNewAction(LatentInfo.CallbackTarget, LatentInfo.UUID, Action);
}
}
Problem: Unsafe pointer handling or memory leaks.
// Generated code with potential issues
UObject* NewObject = CreateObject(); // No null check
NewObject->DoSomething(); // Potential crash
Fix: Add proper checks and use smart pointers where appropriate.
if (UObject* NewObject = CreateObject())
{
// Safe to use
NewObject->DoSomething();
}
Note
Keep the original Blueprint accessible while debugging the translated code.
Steps for comparison:
- Set breakpoints in C++ code
- Enable Blueprint debug visualization
- Compare execution flow
- Verify variable values at key points
// Add debug logging at key points
void MyFunction()
{
UE_LOG(LogTemp, Log, TEXT("Starting MyFunction"));
// Check variable values
UE_LOG(LogTemp, Log, TEXT("MyVar = %f"), MyVar);
// Verify object validity
if (IsValid(TargetActor))
{
UE_LOG(LogTemp, Log, TEXT("TargetActor is valid"));
}
}
Steps to verify integration:
- Test in isolation first
- Verify Blueprint interface exposure
- Check performance compared to Blueprint
- Validate network replication if applicable
Always document significant changes:
/**
* Modified from Blueprint translation to handle edge cases
* @param Input - Description of input parameter
* @return Description of return value
*/
UFUNCTION(BlueprintCallable, Category = "MyFunctions")
float ModifiedFunction(float Input);
Testing checklist:
- Compile successfully
- Runtime behavior matches Blueprint
- Edge cases handled
- Performance impact acceptable
- No new errors or warnings introduced
Note
Test each fix individually before moving on to the next issue.
Look for these opportunities:
// Before optimization
for (int32 i = 0; i < MyArray.Num(); ++i)
{
// Array accessed multiple times
ProcessItem(MyArray[i]);
}
// After optimization
const int32 Count = MyArray.Num();
for (int32 i = 0; i < Count; ++i)
{
// Array size cached
ProcessItem(MyArray[i]);
}
Add defensive programming:
// Add parameter validation
void ProcessData(const TArray<FString>& Data)
{
if (Data.Num() == 0)
{
UE_LOG(LogTemp, Warning, TEXT("ProcessData called with empty array"));
return;
}
// Process valid data
}
Remember that fixing generated code is an iterative process. Take time to understand the translation, verify behavior, and implement improvements systematically. The goal is to maintain the Blueprint's functionality while leveraging C++'s performance and safety features. For a thorough approach to validating your changes, see Review and Validation.