// 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& 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 >& NodeToWidgetLookup) const { UAnimStateTransitionNode* TransNode = CastChecked(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* pPrevNodeWidget = NodeToWidgetLookup.Find(PrevState); const TSharedRef* pNextNodeWidget = NodeToWidgetLookup.Find(NextState); if ((pPrevNodeWidget != NULL) && (pNextNodeWidget != NULL)) { const TSharedRef& PrevNodeWidget = *pPrevNodeWidget; const TSharedRef& 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 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 SGraphNodeAnimTransition::GenerateRichTooltip() { UAnimStateTransitionNode* TransNode = CastChecked(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(); UEdGraphPin* CanExecPin = NULL; if (UAnimationTransitionGraph* TransGraph = Cast(TransNode->BoundGraph)) { if (UAnimGraphNode_TransitionResult* ResultNode = TransGraph->GetResultNode()) { CanExecPin = ResultNode->FindPin(TEXT("bCanEnterTransition")); } } TSharedRef 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 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(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 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(FBlueprintEditorUtils::FindBlueprintForNodeChecked(TransNode)); check(AnimBlueprint); UAnimInstance* ActiveObject = Cast(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(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(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 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(&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(GraphNode); return StaticGetTransitionColor(TransNode, (IsHovered() || (PrevStateNodeWidgetPtr.IsValid() && PrevStateNodeWidgetPtr.Pin()->IsHovered()))); } const FSlateBrush* SGraphNodeAnimTransition::GetTransitionIconImage() const { UAnimStateTransitionNode* TransNode = CastChecked(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(GraphNode); return FString::Printf(TEXT("%.2f seconds"), TransNode->CrossfadeDuration); } void SGraphNodeAnimTransition::OnMouseEnter(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) { UAnimStateTransitionNode* TransNode = CastChecked(GraphNode); if (UEdGraphPin* Pin = TransNode->GetInputPin()) { GetOwnerPanel()->AddPinToHoverSet(Pin); } SGraphNode::OnMouseEnter(MyGeometry, MouseEvent); } void SGraphNodeAnimTransition::OnMouseLeave(const FPointerEvent& MouseEvent) { UAnimStateTransitionNode* TransNode = CastChecked(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 // towards the PrevStateNode and +1 is 1 towards the NextStateNode. const float MutliNodeSpace = 0.2f; // Space between multiple transition nodes (in units of ) 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(NewCorner.X); GraphNode->NodePosY = static_cast(NewCorner.Y); } ///////////////////////////////////////////////////// #undef LOCTEXT_NAMESPACE