Context: I’m working on a dialogue system in Unreal Engine where each dialogue branch has a condition evaluated via a dynamically created blueprint. Each branch has a condition (UDialogueBranchCondition
) which contains a pointer to a UBlueprint
. Initially, I had the blueprint creation logic inside my CreateAutomaticConversionNodeAndConnections
overridden function.
Problem: I encountered an issue where multiple branches seemed to share the same generated class for their conditions, leading to unexpected behavior when evaluating conditions. Specifically, when running GetCompiledBranchCondition
on multiple branches, only one dialogue condition was evaluated correctly, as if different branches were using the same blueprint / generated class for their conditions. I confirmed it was the generated class being used by multiple conditions (logged their memory addresses).
Solution: I fixed the issue by moving the blueprint creation logic to my FAssetSchemaAction::PerformAction
function. After deleting and recreating the branches and their corresponding conditions, the issue was resolved.
Remaining Question: While the issue is fixed, I’m trying to understand why this bug occurred. The only thing I can think of is that the execution order of the logic in respect to the branch node creation caused the issue. Or a naming issue.
I hope I’ve explained things as clearly as I can. Any insights or suggestions to help understand why this issue happened would be greatly appreciated.
Environment:
- Unreal Engine Version: 5.4
CreateAutomaticConversionNodeAndConnections()
bool UAssetGraphSchema_DialogueTree::CreateAutomaticConversionNodeAndConnections(UEdGraphPin* A, UEdGraphPin* B) const
{
UEdNode_DialogueNode* NodeA = Cast<UEdNode_DialogueNode>(A->GetOwningNode());
UEdNode_DialogueNode* NodeB = Cast<UEdNode_DialogueNode>(B->GetOwningNode());
// Are nodes and pins all valid?
if (!NodeA || !NodeA->GetOutputPin() || !NodeB || !NodeB->GetInputPin())
return false;
UDialogueTree* Tree = NodeA->DialogueNode->GetTree();
FVector2D InitPos((NodeA->NodePosX + NodeB->NodePosX) / 2, (NodeA->NodePosY + NodeB->NodePosY) / 2);
FAssetSchemaAction_DialogueTree_NewBranch Action;
Action.NodeTemplate = NewObject<UEdNode_DialogueTreeBranch>(NodeA->GetGraph());
Action.NodeTemplate->SetBranch(NewObject<UDialogueBranch>(Action.NodeTemplate, Tree->BranchType));
UPackage* BlueprintOuter = GetTransientPackage();
const FString DesiredName = FString::Printf(TEXT("Condition %s_%s"), *Action.NodeTemplate->DialogueBranch->GetName(), *UDialogueCondition::StaticClass()->GetName());
const FName BlueprintName = MakeUniqueObjectName(BlueprintOuter, UDialogueCondition::StaticClass(), FName(*DesiredName));
if (UDialogueCondition* DialogueCondition = NewObject<UDialogueCondition>(Action.NodeTemplate->DialogueBranch, UDialogueCondition::StaticClass()))
{
if (UBlueprint* Blueprint = FKismetEditorUtilities::CreateBlueprint(UDialogueCondition::StaticClass(), DialogueCondition, BlueprintName, BPTYPE_Normal, UBlueprint::StaticClass(), UBlueprintGeneratedClass::StaticClass()))
{
Blueprint->Modify();
FBlueprintEditorUtils::ConformImplementedInterfaces(Blueprint);
UFunction* OverrideFunc = nullptr;
UClass* const OverrideFuncClass = FBlueprintEditorUtils::GetOverrideFunctionClass(Blueprint, "EvaluateCondition", &OverrideFunc);
// Implement the function graph
UEdGraph* NewGraph = FBlueprintEditorUtils::CreateNewGraph(Blueprint, "EvaluateCondition", UEdGraph::StaticClass(), UEdGraphSchema_K2::StaticClass());
FBlueprintEditorUtils::AddFunctionGraph(Blueprint, NewGraph, false, OverrideFuncClass);
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(Blueprint);
FKismetEditorUtilities::CompileBlueprint(Blueprint);
DialogueCondition->Blueprint = Blueprint;
Action.NodeTemplate->DialogueBranch->Condition = DialogueCondition;
}
}
if (NodeA->DialogueNode && NodeB->DialogueNode)
{
Action.NodeTemplate->DialogueBranch->ParentNode = NodeA->DialogueNode;
Action.NodeTemplate->DialogueBranch->Node = NodeB->DialogueNode;
}
NodeA->DialogueNode->Branches.Add(Action.NodeTemplate->DialogueBranch);
UEdNode_DialogueTreeBranch* EdgeNode = Cast<UEdNode_DialogueTreeBranch>(Action.PerformAction(NodeA->GetGraph(), nullptr, InitPos, false));
// Always create connections from node A to B, don't allow adding in reverse
EdgeNode->CreateConnections(NodeA, NodeB);
return true;
}
PerformAction
UEdGraphNode* FAssetSchemaAction_DialogueTree_NewBranch::PerformAction(class UEdGraph* ParentGraph, UEdGraphPin* FromPin, const FVector2D Location, bool bSelectNewNode /*= true*/)
{
UEdGraphNode* ResultNode = nullptr;
if (NodeTemplate != nullptr)
{
const FScopedTransaction Transaction(LOCTEXT("DialogueEditorNewBranch", "Dialogue Editor: New Branch"));
ParentGraph->Modify();
if (FromPin != nullptr)
FromPin->Modify();
NodeTemplate->Rename(nullptr, ParentGraph);
ParentGraph->AddNode(NodeTemplate, true, bSelectNewNode);
NodeTemplate->CreateNewGuid();
NodeTemplate->PostPlacedNewNode();
NodeTemplate->AllocateDefaultPins();
NodeTemplate->AutowireNewNode(FromPin);
if (NodeTemplate && NodeTemplate->DialogueBranch)
{
UPackage* BlueprintOuter = GetTransientPackage();
const FString DesiredName = FString::Printf(TEXT("Condition %s_%s"), *NodeTemplate->DialogueBranch->GetName(), *UDialogueCondition::StaticClass()->GetName());
const FName BlueprintName = MakeUniqueObjectName(BlueprintOuter, UDialogueCondition::StaticClass(), FName(*DesiredName));
if (UDialogueCondition* DialogueCondition = NewObject<UDialogueCondition>(NodeTemplate->DialogueBranch, UDialogueCondition::StaticClass()))
{
if (UBlueprint* Blueprint = FKismetEditorUtilities::CreateBlueprint(UDialogueCondition::StaticClass(), DialogueCondition, BlueprintName, BPTYPE_Normal, UBlueprint::StaticClass(), UBlueprintGeneratedClass::StaticClass()))
{
Blueprint->Modify();
FBlueprintEditorUtils::ConformImplementedInterfaces(Blueprint);
UFunction* OverrideFunc = nullptr;
UClass* const OverrideFuncClass = FBlueprintEditorUtils::GetOverrideFunctionClass(Blueprint, "EvaluateCondition", &OverrideFunc);
// Implement the function graph
UEdGraph* NewGraph = FBlueprintEditorUtils::CreateNewGraph(Blueprint, "EvaluateCondition", UEdGraph::StaticClass(), UEdGraphSchema_K2::StaticClass());
FBlueprintEditorUtils::AddFunctionGraph(Blueprint, NewGraph, false, OverrideFuncClass);
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(Blueprint);
FKismetEditorUtilities::CompileBlueprint(Blueprint);
DialogueCondition->Blueprint = Blueprint;
NodeTemplate->DialogueBranch->Condition = DialogueCondition;
}
}
}
NodeTemplate->NodePosX = Location.X;
NodeTemplate->NodePosY = Location.Y;
NodeTemplate->DialogueBranch->SetFlags(RF_Transactional);
NodeTemplate->SetFlags(RF_Transactional);
ResultNode = NodeTemplate;
}
return ResultNode;
}
GetCompiledBranchCondition
bool UDialogueManager::GetCompiledBranchCondition(UDialogueBranch* Branch)
{
if(Branch && Branch->Condition)
{
const UDialogueCondition* const Condition = Branch->Condition;
if(Condition->Blueprint && Condition->Blueprint->GeneratedClass)
{
const UBlueprint* const Blueprint = Condition->Blueprint;
UE_LOG(LogTemp, Warning, TEXT("Condition Logic: Found Blueprint at address %p"), Blueprint);
if (const UBlueprintGeneratedClass* const GeneratedClass = Cast<UBlueprintGeneratedClass>(Branch->Condition->Blueprint->GeneratedClass))
{
UE_LOG(LogTemp, Warning, TEXT("Condition Logic: Dound Generated Class at address %p"), GeneratedClass);
if (UDialogueCondition* const BranchCondition = NewObject<UDialogueCondition>(Branch, GeneratedClass))
{
UE_LOG(LogTemp, Warning, TEXT("Found Generated Branch Condition at addess: %p"), BranchCondition);
if (BranchCondition->Implements<UDialogueSystemInterface>())
{
bool OutResult = IDialogueSystemInterface::Execute_EvaluateCondition(BranchCondition);
UE_LOG(LogTemp, Warning, TEXT("EvaluateCondition result: %s"), OutResult ? TEXT("True") : TEXT("False"));
return OutResult;
}
}
}
}
}
return false;
}