Files
UnrealEngine/Engine/Source/Editor/AnimationBlueprintEditor/Private/AnimationStateNodes/SGraphNodeAnimTransition.cpp
2025-05-18 13:04:45 +08:00

483 lines
16 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "AnimationStateNodes/SGraphNodeAnimTransition.h"
#include "AnimGraphNode_Base.h"
#include "AnimGraphNode_StateMachineBase.h"
#include "AnimGraphNode_TransitionResult.h"
#include "AnimStateNodeBase.h"
#include "AnimStateTransitionNode.h"
#include "Animation/AnimBlueprint.h"
#include "Animation/AnimBlueprintGeneratedClass.h"
#include "Animation/AnimInstance.h"
#include "Animation/AnimNode_StateMachine.h"
#include "Animation/AnimStateMachineTypes.h"
#include "AnimationStateMachineGraph.h"
#include "AnimationTransitionGraph.h"
#include "ConnectionDrawingPolicy.h"
#include "Containers/EnumAsByte.h"
#include "Delegates/Delegate.h"
#include "EdGraph/EdGraphNode.h"
#include "EdGraphSchema_K2.h"
#include "Engine/Blueprint.h"
#include "HAL/PlatformCrt.h"
#include "IDocumentation.h"
#include "Internationalization/Internationalization.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "Layout/Geometry.h"
#include "Misc/AssertionMacros.h"
#include "Misc/Attribute.h"
#include "SGraphPanel.h"
#include "SKismetLinearExpression.h"
#include "SlotBase.h"
#include "Styling/AppStyle.h"
#include "Templates/Casts.h"
#include "Types/SlateEnums.h"
#include "UObject/ObjectPtr.h"
#include "UObject/UObjectGlobals.h"
#include "UObject/WeakObjectPtr.h"
#include "UObject/WeakObjectPtrTemplates.h"
#include "Widgets/Images/SImage.h"
#include "Widgets/SBoxPanel.h"
#include "Widgets/SOverlay.h"
#include "Widgets/SToolTip.h"
#include "Widgets/Text/STextBlock.h"
class SWidget;
class UEdGraphPin;
class UObject;
struct FPointerEvent;
struct FSlateBrush;
#define LOCTEXT_NAMESPACE "TransitionNodes"
/////////////////////////////////////////////////////
// SGraphNodeAnimTransition
void SGraphNodeAnimTransition::Construct(const FArguments& InArgs, UAnimStateTransitionNode* InNode)
{
this->GraphNode = InNode;
this->UpdateGraphNode();
}
void SGraphNodeAnimTransition::GetNodeInfoPopups(FNodeInfoContext* Context, TArray<FGraphInformationPopupInfo>& Popups) const
{
}
void SGraphNodeAnimTransition::MoveTo(const FVector2f& NewPosition, FNodeSet& NodeFilter, bool bMarkDirty)
{
// Ignored; position is set by the location of the attached state nodes
}
bool SGraphNodeAnimTransition::RequiresSecondPassLayout() const
{
return true;
}
void SGraphNodeAnimTransition::PerformSecondPassLayout(const TMap< UObject*, TSharedRef<SNode> >& NodeToWidgetLookup) const
{
UAnimStateTransitionNode* TransNode = CastChecked<UAnimStateTransitionNode>(GraphNode);
// Find the geometry of the state nodes we're connecting
FGeometry StartGeom;
FGeometry EndGeom;
int32 TransIndex = 0;
int32 NumOfTrans = 1;
UAnimStateNodeBase* PrevState = TransNode->GetPreviousState();
UAnimStateNodeBase* NextState = TransNode->GetNextState();
if ((PrevState != NULL) && (NextState != NULL))
{
const TSharedRef<SNode>* pPrevNodeWidget = NodeToWidgetLookup.Find(PrevState);
const TSharedRef<SNode>* pNextNodeWidget = NodeToWidgetLookup.Find(NextState);
if ((pPrevNodeWidget != NULL) && (pNextNodeWidget != NULL))
{
const TSharedRef<SNode>& PrevNodeWidget = *pPrevNodeWidget;
const TSharedRef<SNode>& NextNodeWidget = *pNextNodeWidget;
StartGeom = FGeometry(FVector2D(PrevState->NodePosX, PrevState->NodePosY), FVector2D::ZeroVector, PrevNodeWidget->GetDesiredSize(), 1.0f);
EndGeom = FGeometry(FVector2D(NextState->NodePosX, NextState->NodePosY), FVector2D::ZeroVector, NextNodeWidget->GetDesiredSize(), 1.0f);
TArray<UAnimStateTransitionNode*> Transitions;
PrevState->GetTransitionList(Transitions);
Transitions = Transitions.FilterByPredicate([NextState](const UAnimStateTransitionNode* InTransition) -> bool
{
return InTransition->GetNextState() == NextState;
});
TransIndex = Transitions.IndexOfByKey(TransNode);
NumOfTrans = Transitions.Num();
PrevStateNodeWidgetPtr = PrevNodeWidget;
}
}
//Position Node
PositionBetweenTwoNodesWithOffset(StartGeom, EndGeom, TransIndex, NumOfTrans);
}
TSharedRef<SWidget> SGraphNodeAnimTransition::GenerateRichTooltip()
{
UAnimStateTransitionNode* TransNode = CastChecked<UAnimStateTransitionNode>(GraphNode);
if (TransNode->BoundGraph == NULL)
{
return SNew(STextBlock).Text(LOCTEXT("NoAnimGraphBoundToNodeMessage", "Error: No graph"));
}
// Find the expression hooked up to the can execute pin of the transition node
const UEdGraphSchema_K2* Schema = GetDefault<UEdGraphSchema_K2>();
UEdGraphPin* CanExecPin = NULL;
if (UAnimationTransitionGraph* TransGraph = Cast<UAnimationTransitionGraph>(TransNode->BoundGraph))
{
if (UAnimGraphNode_TransitionResult* ResultNode = TransGraph->GetResultNode())
{
CanExecPin = ResultNode->FindPin(TEXT("bCanEnterTransition"));
}
}
TSharedRef<SVerticalBox> Widget = SNew(SVerticalBox);
const FText TooltipDesc = GetPreviewCornerText(false);
// Transition rule linearized
Widget->AddSlot()
.AutoHeight()
.Padding( 2.0f )
[
SNew(STextBlock)
.TextStyle( FAppStyle::Get(), TEXT("Graph.TransitionNode.TooltipName") )
.Text(TooltipDesc)
];
if(TransNode->bAutomaticRuleBasedOnSequencePlayerInState)
{
if (CanExecPin != nullptr && CanExecPin->LinkedTo.Num() > 0)
{
Widget->AddSlot()
.AutoHeight()
.Padding(2.0f)
[
SNew(STextBlock)
.TextStyle(FAppStyle::Get(), TEXT("Graph.TransitionNode.TooltipRule"))
.Text(LOCTEXT("AnimGraphNodeAutomaticRuleWarning_ToolTip", "Warning : Automatic Rule Based Transition will override graph exit rule."))
.ColorAndOpacity(FCoreStyle::Get().GetColor("ErrorReporting.WarningBackgroundColor"))
];
}
else
{
Widget->AddSlot()
.AutoHeight()
.Padding(2.0f)
[
SNew(STextBlock)
.TextStyle(FAppStyle::Get(), TEXT("Graph.TransitionNode.TooltipRule"))
.Text(LOCTEXT("AnimGraphNodeAutomaticRule_ToolTip", "Automatic Rule"))
];
}
}
else
{
Widget->AddSlot()
.AutoHeight()
.Padding( 2.0f )
[
SNew(STextBlock)
.TextStyle( FAppStyle::Get(), TEXT("Graph.TransitionNode.TooltipRule") )
.Text(LOCTEXT("AnimGraphNodeTransitionRule_ToolTip", "Transition Rule (in words)"))
];
Widget->AddSlot()
.AutoHeight()
.Padding( 2.0f )
[
SNew(SKismetLinearExpression, CanExecPin)
];
}
if (TransNode->bDisabled)
{
Widget->AddSlot()
.AutoHeight()
.Padding(2.0f)
[
SNew(STextBlock)
.Text(LOCTEXT("TransitionRuleDisabled", "Disabled"))
];
}
Widget->AddSlot()
.AutoHeight()
.Padding( 2.0f )
[
IDocumentation::Get()->CreateToolTip(FText::FromString("Documentation"), NULL, TransNode->GetDocumentationLink(), TransNode->GetDocumentationExcerptName())
];
return Widget;
}
TSharedPtr<SToolTip> SGraphNodeAnimTransition::GetComplexTooltip()
{
return SNew(SToolTip)
[
GenerateRichTooltip()
];
}
void SGraphNodeAnimTransition::UpdateGraphNode()
{
InputPins.Empty();
OutputPins.Empty();
// Reset variables that are going to be exposed, in case we are refreshing an already setup node.
RightNodeBox.Reset();
LeftNodeBox.Reset();
this->ContentScale.Bind( this, &SGraphNode::GetContentScale );
this->GetOrAddSlot( ENodeZone::Center )
.HAlign(HAlign_Center)
.VAlign(VAlign_Center)
[
SNew(SOverlay)
+SOverlay::Slot()
[
SNew(SImage)
.Image( FAppStyle::GetBrush("Graph.TransitionNode.ColorSpill") )
.ColorAndOpacity( this, &SGraphNodeAnimTransition::GetTransitionColor )
]
+SOverlay::Slot()
[
SNew(SImage)
.Image( this, &SGraphNodeAnimTransition::GetTransitionIconImage )
]
];
}
FText SGraphNodeAnimTransition::GetPreviewCornerText(bool bReverse) const
{
UAnimStateTransitionNode* TransNode = CastChecked<UAnimStateTransitionNode>(GraphNode);
UAnimStateNodeBase* PrevState = (bReverse ? TransNode->GetNextState() : TransNode->GetPreviousState());
UAnimStateNodeBase* NextState = (bReverse ? TransNode->GetPreviousState() : TransNode->GetNextState());
FText Result = LOCTEXT("BadTransition", "Bad transition (missing source or target)");
// Show the priority if there is any ambiguity
if (PrevState != NULL)
{
if (NextState != NULL)
{
TArray<UAnimStateTransitionNode*> TransitionFromSource;
PrevState->GetTransitionList(/*out*/ TransitionFromSource);
bool bMultiplePriorities = false;
if (TransitionFromSource.Num() > 1)
{
// See if the priorities differ
for (int32 Index = 0; (Index < TransitionFromSource.Num()) && !bMultiplePriorities; ++Index)
{
const bool bDifferentPriority = (TransitionFromSource[Index]->PriorityOrder != TransNode->PriorityOrder);
bMultiplePriorities |= bDifferentPriority;
}
}
if (bMultiplePriorities)
{
Result = FText::Format(LOCTEXT("TransitionXToYWithPriority", "{0} to {1} (Priority {2})"), FText::FromString(PrevState->GetStateName()), FText::FromString(NextState->GetStateName()), FText::AsNumber(TransNode->PriorityOrder));
}
else
{
Result = FText::Format(LOCTEXT("TransitionXToY", "{0} to {1}"), FText::FromString(PrevState->GetStateName()), FText::FromString(NextState->GetStateName()));
}
}
}
return Result;
}
FLinearColor SGraphNodeAnimTransition::StaticGetTransitionColor(UAnimStateTransitionNode* TransNode, bool bIsHovered)
{
//@TODO: Make configurable by styling
const FLinearColor ActiveColor(1.0f, 0.4f, 0.3f, 1.0f);
const FLinearColor HoverColor(0.724f, 0.256f, 0.0f, 1.0f);
FLinearColor BaseColor(0.9f, 0.9f, 0.9f, 1.0f);
// Display various types of debug data
UAnimBlueprint* AnimBlueprint = Cast<UAnimBlueprint>(FBlueprintEditorUtils::FindBlueprintForNodeChecked(TransNode));
check(AnimBlueprint);
UAnimInstance* ActiveObject = Cast<UAnimInstance>(AnimBlueprint->GetObjectBeingDebugged());
UAnimBlueprintGeneratedClass* Class = AnimBlueprint->GetAnimBlueprintGeneratedClass();
//@TODO: WIP fast path / slow path coloring
if (AnimBlueprint->bWarnAboutBlueprintUsage || ((ActiveObject != nullptr) && (ActiveObject->PCV_ShouldNotifyAboutNodesNotUsingFastPath() || ActiveObject->PCV_ShouldWarnAboutNodesNotUsingFastPath())))
{
if (UAnimationTransitionGraph* TransGraph = Cast<UAnimationTransitionGraph>(TransNode->GetBoundGraph()))
{
if (UAnimGraphNode_TransitionResult* ResultNode = TransGraph->GetResultNode())
{
if (ResultNode->BlueprintUsage == EBlueprintUsage::UsesBlueprint)
{
BaseColor = FLinearColor(0.4f, 0.4f, 1.0f);
}
}
}
}
if ((ActiveObject != NULL) && (Class != NULL))
{
UAnimationStateMachineGraph* StateMachineGraph = CastChecked<UAnimationStateMachineGraph>(TransNode->GetGraph());
if (FStateMachineDebugData* DebugInfo = Class->GetAnimBlueprintDebugData().StateMachineDebugData.Find(StateMachineGraph))
{
// A transition node could be associated with multiple transitions indicies when coming from an alias. Check all of them
TArray<int32> TransitionIndices;
DebugInfo->NodeToTransitionIndex.MultiFind(TransNode, TransitionIndices);
const int32 TransNum = TransitionIndices.Num();
for (int32 Index = 0; Index < TransNum; ++Index)
{
if(IsTransitionActive(TransitionIndices[Index], *Class, *StateMachineGraph, *ActiveObject))
{
// We're active!
return ActiveColor;
}
}
}
}
//@TODO: ANIMATION: Sort out how to display this
// if (TransNode->SharedCrossfadeIdx != INDEX_NONE)
// {
// WireColor.R = (TransNode->SharedCrossfadeIdx & 1 ? 1.0f : 0.15f);
// WireColor.G = (TransNode->SharedCrossfadeIdx & 2 ? 1.0f : 0.15f);
// WireColor.B = (TransNode->SharedCrossfadeIdx & 4 ? 1.0f : 0.15f);
// }
// If shared transition, show different color
if (TransNode->bSharedRules)
{
BaseColor = TransNode->SharedColor;
}
if (TransNode->bDisabled)
{
BaseColor = FLinearColor(0.125, 0.125, 0.125, 0.5);
}
return bIsHovered ? HoverColor : BaseColor;
}
bool SGraphNodeAnimTransition::IsTransitionActive(int32 TransitionIndex, UAnimBlueprintGeneratedClass& AnimClass, UAnimationStateMachineGraph& StateMachineGraph, UAnimInstance& AnimInstance)
{
if (AnimClass.GetAnimNodeProperties().Num())
{
if (FAnimNode_StateMachine* CurrentInstance = AnimClass.GetPropertyInstance<FAnimNode_StateMachine>(&AnimInstance, StateMachineGraph.OwnerAnimGraphNode))
{
if (CurrentInstance->IsTransitionActive(TransitionIndex))
{
return true;
}
}
}
return false;
}
FSlateColor SGraphNodeAnimTransition::GetTransitionColor() const
{
// Highlight the transition node when the node is hovered or when the previous state is hovered
UAnimStateTransitionNode* TransNode = CastChecked<UAnimStateTransitionNode>(GraphNode);
return StaticGetTransitionColor(TransNode, (IsHovered() || (PrevStateNodeWidgetPtr.IsValid() && PrevStateNodeWidgetPtr.Pin()->IsHovered())));
}
const FSlateBrush* SGraphNodeAnimTransition::GetTransitionIconImage() const
{
UAnimStateTransitionNode* TransNode = CastChecked<UAnimStateTransitionNode>(GraphNode);
return (TransNode->LogicType == ETransitionLogicType::TLT_Inertialization)
? FAppStyle::GetBrush("Graph.TransitionNode.Icon_Inertialization")
: FAppStyle::GetBrush("Graph.TransitionNode.Icon");
}
FString SGraphNodeAnimTransition::GetCurrentDuration() const
{
UAnimStateTransitionNode* TransNode = CastChecked<UAnimStateTransitionNode>(GraphNode);
return FString::Printf(TEXT("%.2f seconds"), TransNode->CrossfadeDuration);
}
void SGraphNodeAnimTransition::OnMouseEnter(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent)
{
UAnimStateTransitionNode* TransNode = CastChecked<UAnimStateTransitionNode>(GraphNode);
if (UEdGraphPin* Pin = TransNode->GetInputPin())
{
GetOwnerPanel()->AddPinToHoverSet(Pin);
}
SGraphNode::OnMouseEnter(MyGeometry, MouseEvent);
}
void SGraphNodeAnimTransition::OnMouseLeave(const FPointerEvent& MouseEvent)
{
UAnimStateTransitionNode* TransNode = CastChecked<UAnimStateTransitionNode>(GraphNode);
if (UEdGraphPin* Pin = TransNode->GetInputPin())
{
GetOwnerPanel()->RemovePinFromHoverSet(Pin);
}
SGraphNode::OnMouseLeave(MouseEvent);
}
void SGraphNodeAnimTransition::PositionBetweenTwoNodesWithOffset(const FGeometry& StartGeom, const FGeometry& EndGeom, int32 NodeIndex, int32 MaxNodes) const
{
// Get a reasonable seed point (halfway between the boxes)
const FVector2D StartCenter = FGeometryHelper::CenterOf(StartGeom);
const FVector2D EndCenter = FGeometryHelper::CenterOf(EndGeom);
const FVector2D SeedPoint = (StartCenter + EndCenter) * 0.5f;
// Find the (approximate) closest points between the two boxes
const FVector2D StartAnchorPoint = FGeometryHelper::FindClosestPointOnGeom(StartGeom, SeedPoint);
const FVector2D EndAnchorPoint = FGeometryHelper::FindClosestPointOnGeom(EndGeom, SeedPoint);
// Position ourselves halfway along the connecting line between the nodes, elevated away perpendicular to the direction of the line
const float Height = 30.0f;
const FVector2D DesiredNodeSize = GetDesiredSize();
FVector2D DeltaPos(EndAnchorPoint - StartAnchorPoint);
if (DeltaPos.IsNearlyZero())
{
DeltaPos = FVector2D(10.0f, 0.0f);
}
const FVector2D Normal = FVector2D(DeltaPos.Y, -DeltaPos.X).GetSafeNormal();
const FVector2D NewCenter = StartAnchorPoint + (0.5f * DeltaPos) + (Height * Normal);
FVector2D DeltaNormal = DeltaPos.GetSafeNormal();
// Calculate node offset in the case of multiple transitions between the same two nodes
// MultiNodeOffset: the offset where 0 is the centre of the transition, -1 is 1 <size of node>
// towards the PrevStateNode and +1 is 1 <size of node> towards the NextStateNode.
const float MutliNodeSpace = 0.2f; // Space between multiple transition nodes (in units of <size of node> )
const float MultiNodeStep = (1.f + MutliNodeSpace); //Step between node centres (Size of node + size of node spacer)
const float MultiNodeStart = -((MaxNodes - 1) * MultiNodeStep) / 2.f;
const float MultiNodeOffset = MultiNodeStart + (NodeIndex * MultiNodeStep);
// Now we need to adjust the new center by the node size, zoom factor and multi node offset
const FVector2D NewCorner = NewCenter - (0.5f * DesiredNodeSize) + (DeltaNormal * MultiNodeOffset * DesiredNodeSize.Size());
GraphNode->NodePosX = static_cast<int32>(NewCorner.X);
GraphNode->NodePosY = static_cast<int32>(NewCorner.Y);
}
/////////////////////////////////////////////////////
#undef LOCTEXT_NAMESPACE