From 8c2a156e1f124479d0aaed5e26ab22e58577b1e0 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sun, 7 Jan 2024 18:34:15 +0100 Subject: [PATCH] Add SourceState and DestinationState modes to State Machine interruption modes in Anim Graph #1735 --- .../Archetypes/Animation.StateMachine.cs | 24 ++- .../Editor/Surface/Undo/ConnectBoxesAction.cs | 2 + .../Animations/Graph/AnimGraph.Base.cpp | 5 +- Source/Engine/Animations/Graph/AnimGraph.h | 9 +- .../Animations/Graph/AnimGroup.Animation.cpp | 180 ++++++++++++------ 5 files changed, 153 insertions(+), 67 deletions(-) diff --git a/Source/Editor/Surface/Archetypes/Animation.StateMachine.cs b/Source/Editor/Surface/Archetypes/Animation.StateMachine.cs index 9d4367303..9ec1bab19 100644 --- a/Source/Editor/Surface/Archetypes/Animation.StateMachine.cs +++ b/Source/Editor/Surface/Archetypes/Animation.StateMachine.cs @@ -1583,14 +1583,24 @@ namespace FlaxEditor.Surface.Archetypes None = 0, /// - /// Transition rule will be rechecked during active transition with option to interrupt transition. + /// Transition rule will be rechecked during active transition with option to interrupt transition (to go back to the source state). /// RuleRechecking = 1, /// - /// Interrupted transition is immediately stopped without blending out. + /// Interrupted transition is immediately stopped without blending out (back to the source/destination state). /// Instant = 2, + + /// + /// Enables checking other transitions in the source state that might interrupt this one. + /// + SourceState = 4, + + /// + /// Enables checking transitions in the destination state that might interrupt this one. + /// + DestinationState = 8, } /// @@ -1613,6 +1623,8 @@ namespace FlaxEditor.Surface.Archetypes UseDefaultRule = 4, InterruptionRuleRechecking = 8, InterruptionInstant = 16, + InterruptionSourceState = 32, + InterruptionDestinationState = 64, } /// @@ -1773,7 +1785,7 @@ namespace FlaxEditor.Surface.Archetypes } /// - /// Transition interruption options. + /// Transition interruption options (flags, can select multiple values). /// [EditorOrder(70), DefaultValue(InterruptionFlags.None)] public InterruptionFlags Interruption @@ -1785,12 +1797,18 @@ namespace FlaxEditor.Surface.Archetypes flags |= InterruptionFlags.RuleRechecking; if (_data.HasFlag(Data.FlagTypes.InterruptionInstant)) flags |= InterruptionFlags.Instant; + if (_data.HasFlag(Data.FlagTypes.InterruptionSourceState)) + flags |= InterruptionFlags.SourceState; + if (_data.HasFlag(Data.FlagTypes.InterruptionDestinationState)) + flags |= InterruptionFlags.DestinationState; return flags; } set { _data.SetFlag(Data.FlagTypes.InterruptionRuleRechecking, value.HasFlag(InterruptionFlags.RuleRechecking)); _data.SetFlag(Data.FlagTypes.InterruptionInstant, value.HasFlag(InterruptionFlags.Instant)); + _data.SetFlag(Data.FlagTypes.InterruptionSourceState, value.HasFlag(InterruptionFlags.SourceState)); + _data.SetFlag(Data.FlagTypes.InterruptionDestinationState, value.HasFlag(InterruptionFlags.DestinationState)); SourceState.SaveTransitions(true); } } diff --git a/Source/Editor/Surface/Undo/ConnectBoxesAction.cs b/Source/Editor/Surface/Undo/ConnectBoxesAction.cs index 72b9242a4..07118228e 100644 --- a/Source/Editor/Surface/Undo/ConnectBoxesAction.cs +++ b/Source/Editor/Surface/Undo/ConnectBoxesAction.cs @@ -24,6 +24,8 @@ namespace FlaxEditor.Surface.Undo public ConnectBoxesAction(InputBox iB, OutputBox oB, bool connect) { + if (iB == null || oB == null || iB.ParentNode == null || oB.ParentNode == null) + throw new System.ArgumentNullException(); _surface = iB.Surface; _context = new ContextHandle(iB.ParentNode.Context); _connect = connect; diff --git a/Source/Engine/Animations/Graph/AnimGraph.Base.cpp b/Source/Engine/Animations/Graph/AnimGraph.Base.cpp index 48fa7fed1..c05b82b70 100644 --- a/Source/Engine/Animations/Graph/AnimGraph.Base.cpp +++ b/Source/Engine/Animations/Graph/AnimGraph.Base.cpp @@ -99,10 +99,7 @@ void BlendPoseBucketInit(AnimGraphInstanceData::Bucket& bucket) void StateMachineBucketInit(AnimGraphInstanceData::Bucket& bucket) { - bucket.StateMachine.LastUpdateFrame = 0; - bucket.StateMachine.CurrentState = nullptr; - bucket.StateMachine.ActiveTransition = nullptr; - bucket.StateMachine.TransitionPosition = 0.0f; + Platform::MemoryClear(&bucket.StateMachine, sizeof(bucket.StateMachine)); } void SlotBucketInit(AnimGraphInstanceData::Bucket& bucket) diff --git a/Source/Engine/Animations/Graph/AnimGraph.h b/Source/Engine/Animations/Graph/AnimGraph.h index 2a8a7f7ab..c160b671c 100644 --- a/Source/Engine/Animations/Graph/AnimGraph.h +++ b/Source/Engine/Animations/Graph/AnimGraph.h @@ -129,6 +129,8 @@ public: UseDefaultRule = 4, InterruptionRuleRechecking = 8, InterruptionInstant = 16, + InterruptionSourceState = 32, + InterruptionDestinationState = 64, }; public: @@ -256,7 +258,10 @@ public: uint64 LastUpdateFrame; AnimGraphNode* CurrentState; AnimGraphStateTransition* ActiveTransition; + AnimGraphStateTransition* BaseTransition; + AnimGraphNode* BaseTransitionState; float TransitionPosition; + float BaseTransitionPosition; }; struct SlotBucket @@ -858,7 +863,7 @@ public: } /// - /// Resets all the state bucket used by the given graph including sub-graphs (total). Can eb used to reset the animation state of the nested graph (including children). + /// Resets all the state bucket used by the given graph including sub-graphs (total). Can be used to reset the animation state of the nested graph (including children). /// void ResetBuckets(AnimGraphContext& context, AnimGraphBase* graph); @@ -887,5 +892,7 @@ private: Variant SampleAnimationsWithBlend(AnimGraphNode* node, bool loop, float length, float startTimePos, float prevTimePos, float& newTimePos, Animation* animA, Animation* animB, Animation* animC, float speedA, float speedB, float speedC, float alphaA, float alphaB, float alphaC); Variant Blend(AnimGraphNode* node, const Value& poseA, const Value& poseB, float alpha, AlphaBlendMode alphaMode); Variant SampleState(AnimGraphNode* state); + void InitStateTransition(AnimGraphContext& context, AnimGraphInstanceData::StateMachineBucket& stateMachineBucket, AnimGraphStateTransition* transition = nullptr); + AnimGraphStateTransition* UpdateStateTransitions(AnimGraphContext& context, const AnimGraphNode::StateMachineData& stateMachineData, AnimGraphNode* state, AnimGraphNode* ignoreState = nullptr); void UpdateStateTransitions(AnimGraphContext& context, const AnimGraphNode::StateMachineData& stateMachineData, AnimGraphInstanceData::StateMachineBucket& stateMachineBucket, const AnimGraphNode::StateBaseData& stateData); }; diff --git a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp index 598e55da1..cdf1ebd84 100644 --- a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp +++ b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp @@ -470,10 +470,12 @@ Variant AnimGraphExecutor::Blend(AnimGraphNode* node, const Value& poseA, const { ANIM_GRAPH_PROFILE_EVENT("Blend Pose"); + if (isnan(alpha) || isinf(alpha)) + alpha = 0; + alpha = Math::Saturate(alpha); alpha = AlphaBlend::Process(alpha, alphaMode); const auto nodes = node->GetNodes(this); - auto nodesA = static_cast(poseA.AsPointer); auto nodesB = static_cast(poseB.AsPointer); if (!ANIM_GRAPH_IS_VALID_PTR(poseA)) @@ -494,32 +496,40 @@ Variant AnimGraphExecutor::Blend(AnimGraphNode* node, const Value& poseA, const Variant AnimGraphExecutor::SampleState(AnimGraphNode* state) { - // Prepare auto& data = state->Data.State; if (data.Graph == nullptr || data.Graph->GetRootNode() == nullptr) - { - // Invalid state graph return Value::Null; - } - ANIM_GRAPH_PROFILE_EVENT("Evaluate State"); - - // Evaluate state auto rootNode = data.Graph->GetRootNode(); - auto result = eatBox((Node*)rootNode, &rootNode->Boxes[0]); - - return result; + return eatBox((Node*)rootNode, &rootNode->Boxes[0]); } -void AnimGraphExecutor::UpdateStateTransitions(AnimGraphContext& context, const AnimGraphNode::StateMachineData& stateMachineData, AnimGraphInstanceData::StateMachineBucket& stateMachineBucket, const AnimGraphNode::StateBaseData& stateData) +void AnimGraphExecutor::InitStateTransition(AnimGraphContext& context, AnimGraphInstanceData::StateMachineBucket& stateMachineBucket, AnimGraphStateTransition* transition) +{ + // Reset transiton + stateMachineBucket.ActiveTransition = transition; + stateMachineBucket.TransitionPosition = 0.0f; + + // End base transition + if (stateMachineBucket.BaseTransition) + { + ResetBuckets(context, stateMachineBucket.BaseTransitionState->Data.State.Graph); + stateMachineBucket.BaseTransition = nullptr; + stateMachineBucket.BaseTransitionState = nullptr; + stateMachineBucket.BaseTransitionPosition = 0.0f; + } +} + +AnimGraphStateTransition* AnimGraphExecutor::UpdateStateTransitions(AnimGraphContext& context, const AnimGraphNode::StateMachineData& stateMachineData, AnimGraphNode* state, AnimGraphNode* ignoreState) { int32 transitionIndex = 0; + const AnimGraphNode::StateBaseData& stateData = state->Data.State; while (transitionIndex < ANIM_GRAPH_MAX_STATE_TRANSITIONS && stateData.Transitions[transitionIndex] != AnimGraphNode::StateData::InvalidTransitionIndex) { const uint16 idx = stateData.Transitions[transitionIndex]; ASSERT(idx < stateMachineData.Graph->StateTransitions.Count()); auto& transition = stateMachineData.Graph->StateTransitions[idx]; - if (transition.Destination == stateMachineBucket.CurrentState) + if (transition.Destination == state || transition.Destination == ignoreState) { // Ignore transition to the current state transitionIndex++; @@ -527,7 +537,7 @@ void AnimGraphExecutor::UpdateStateTransitions(AnimGraphContext& context, const } // Evaluate source state transition data (position, length, etc.) - const Value sourceStatePtr = SampleState(stateMachineBucket.CurrentState); + const Value sourceStatePtr = SampleState(state); auto& transitionData = context.TransitionData; // Note: this could support nested transitions but who uses state machine inside transition rule? if (ANIM_GRAPH_IS_VALID_PTR(sourceStatePtr)) { @@ -548,6 +558,7 @@ void AnimGraphExecutor::UpdateStateTransitions(AnimGraphContext& context, const if (transition.RuleGraph && !useDefaultRule) { // Execute transition rule + ANIM_GRAPH_PROFILE_EVENT("Rule"); auto rootNode = transition.RuleGraph->GetRootNode(); ASSERT(rootNode); if (!(bool)eatBox((Node*)rootNode, &rootNode->Boxes[0])) @@ -570,10 +581,7 @@ void AnimGraphExecutor::UpdateStateTransitions(AnimGraphContext& context, const canEnter = true; if (canEnter) { - // Start transition - stateMachineBucket.ActiveTransition = &transition; - stateMachineBucket.TransitionPosition = 0.0f; - break; + return &transition; } // Skip after Solo transition @@ -583,6 +591,18 @@ void AnimGraphExecutor::UpdateStateTransitions(AnimGraphContext& context, const transitionIndex++; } + + // No transition + return nullptr; +} + +void AnimGraphExecutor::UpdateStateTransitions(AnimGraphContext& context, const AnimGraphNode::StateMachineData& stateMachineData, AnimGraphInstanceData::StateMachineBucket& stateMachineBucket, const AnimGraphNode::StateBaseData& stateData) +{ + AnimGraphStateTransition* transition = UpdateStateTransitions(context, stateMachineData, stateMachineBucket.CurrentState); + if (transition) + { + InitStateTransition(context, stateMachineBucket, transition); + } } void ComputeMultiBlendLength(float& length, AnimGraphNode* node) @@ -1511,10 +1531,9 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu // Blend two animations { - const float alpha = Math::Saturate(bucket.TransitionPosition / blendDuration); + const float alpha = bucket.TransitionPosition / blendDuration; const auto valueA = tryGetValue(node->GetBox(FirstBlendPoseBoxIndex + bucket.PreviousBlendPoseIndex), Value::Null); const auto valueB = tryGetValue(node->GetBox(FirstBlendPoseBoxIndex + poseIndex), Value::Null); - value = Blend(node, valueA, valueB, alpha, mode); } @@ -1620,22 +1639,21 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu // Enter to the first state pointed by the Entry node (without transitions) bucket.CurrentState = data.Graph->GetRootNode(); - bucket.ActiveTransition = nullptr; - bucket.TransitionPosition = 0.0f; + InitStateTransition(context, bucket); - // Reset all state buckets pof the graphs and nodes included inside the state machine + // Reset all state buckets of the graphs and nodes included inside the state machine ResetBuckets(context, data.Graph); } #define END_TRANSITION() \ ResetBuckets(context, bucket.CurrentState->Data.State.Graph); \ bucket.CurrentState = bucket.ActiveTransition->Destination; \ - bucket.ActiveTransition = nullptr; \ - bucket.TransitionPosition = 0.0f + InitStateTransition(context, bucket) // Update the active transition if (bucket.ActiveTransition) { bucket.TransitionPosition += context.DeltaTime; + ASSERT(bucket.CurrentState); // Check for transition end if (bucket.TransitionPosition >= bucket.ActiveTransition->BlendDuration) @@ -1643,38 +1661,70 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu END_TRANSITION(); } // Check for transition interruption - else if (EnumHasAnyFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::InterruptionRuleRechecking)) + else if (EnumHasAnyFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::InterruptionRuleRechecking) && + EnumHasNoneFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::UseDefaultRule) && + bucket.ActiveTransition->RuleGraph) { - const bool useDefaultRule = EnumHasAnyFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::UseDefaultRule); - if (bucket.ActiveTransition->RuleGraph && !useDefaultRule) + // Execute transition rule + auto rootNode = bucket.ActiveTransition->RuleGraph->GetRootNode(); + if (!(bool)eatBox((Node*)rootNode, &rootNode->Boxes[0])) { - // Execute transition rule - auto rootNode = bucket.ActiveTransition->RuleGraph->GetRootNode(); - if (!(bool)eatBox((Node*)rootNode, &rootNode->Boxes[0])) + bool cancelTransition = false; + if (EnumHasAnyFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::InterruptionInstant)) { - bool cancelTransition = false; - if (EnumHasAnyFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::InterruptionInstant)) + cancelTransition = true; + } + else + { + // Blend back to the source state (remove currently applied delta and rewind transition) + bucket.TransitionPosition -= context.DeltaTime; + bucket.TransitionPosition -= context.DeltaTime; + if (bucket.TransitionPosition <= ZeroTolerance) { cancelTransition = true; } - else - { - // Blend back to the source state (remove currently applied delta and rewind transition) - bucket.TransitionPosition -= context.DeltaTime; - bucket.TransitionPosition -= context.DeltaTime; - if (bucket.TransitionPosition <= ZeroTolerance) - { - cancelTransition = true; - } - } - if (cancelTransition) - { - // Go back to the source state - ResetBuckets(context, bucket.CurrentState->Data.State.Graph); - bucket.ActiveTransition = nullptr; - bucket.TransitionPosition = 0.0f; - } } + if (cancelTransition) + { + // Go back to the source state + ResetBuckets(context, bucket.CurrentState->Data.State.Graph); + InitStateTransition(context, bucket); + } + } + } + if (bucket.ActiveTransition && !bucket.BaseTransition && EnumHasAnyFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::InterruptionSourceState)) + { + // Try to interrupt with any other transition in the source state (except the current transition) + if (AnimGraphStateTransition* transition = UpdateStateTransitions(context, data, bucket.CurrentState, bucket.ActiveTransition->Destination)) + { + // Change active transition to the interrupted one + if (EnumHasNoneFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::InterruptionInstant)) + { + // Cache the current blending state to be used as a base when blending towards new destination state (seamless blending after interruption) + bucket.BaseTransition = bucket.ActiveTransition; + bucket.BaseTransitionState = bucket.CurrentState; + bucket.BaseTransitionPosition = bucket.TransitionPosition; + } + bucket.ActiveTransition = transition; + bucket.TransitionPosition = 0.0f; + } + } + if (bucket.ActiveTransition && !bucket.BaseTransition && EnumHasAnyFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::InterruptionDestinationState)) + { + // Try to interrupt with any other transition in the destination state (except the transition back to the current state if exists) + if (AnimGraphStateTransition* transition = UpdateStateTransitions(context, data, bucket.ActiveTransition->Destination, bucket.CurrentState)) + { + // Change active transition to the interrupted one + if (EnumHasNoneFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::InterruptionInstant)) + { + // Cache the current blending state to be used as a base when blending towards new destination state (seamless blending after interruption) + bucket.BaseTransition = bucket.ActiveTransition; + bucket.BaseTransitionState = bucket.CurrentState; + bucket.BaseTransitionPosition = bucket.TransitionPosition; + } + bucket.CurrentState = bucket.ActiveTransition->Destination; + bucket.ActiveTransition = transition; + bucket.TransitionPosition = 0.0f; } } } @@ -1703,9 +1753,23 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu } } - // Sample the current state - const auto currentState = SampleState(bucket.CurrentState); - value = currentState; + if (bucket.BaseTransitionState) + { + // Sample the other state (eg. when blending from interrupted state to the another state from the old destination) + value = SampleState(bucket.BaseTransitionState); + if (bucket.BaseTransition) + { + // Evaluate the base pose from the time when transition was interrupted + const auto destinationState = SampleState(bucket.BaseTransition->Destination); + const float alpha = bucket.BaseTransitionPosition / bucket.BaseTransition->BlendDuration; + value = Blend(node, value, destinationState, alpha, bucket.BaseTransition->BlendMode); + } + } + else + { + // Sample the current state + value = SampleState(bucket.CurrentState); + } // Handle active transition blending if (bucket.ActiveTransition) @@ -1714,14 +1778,12 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu const auto destinationState = SampleState(bucket.ActiveTransition->Destination); // Perform blending - const float alpha = Math::Saturate(bucket.TransitionPosition / bucket.ActiveTransition->BlendDuration); - value = Blend(node, currentState, destinationState, alpha, bucket.ActiveTransition->BlendMode); + const float alpha = bucket.TransitionPosition / bucket.ActiveTransition->BlendDuration; + value = Blend(node, value, destinationState, alpha, bucket.ActiveTransition->BlendMode); } - // Update bucket bucket.LastUpdateFrame = context.CurrentFrameIndex; #undef END_TRANSITION - break; } // Entry @@ -2142,7 +2204,7 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu // Blend out auto input = tryGetValue(node->GetBox(1), Value::Null); bucket.BlendOutPosition += deltaTime; - const float alpha = Math::Saturate(bucket.BlendOutPosition / slot.BlendOutTime); + const float alpha = bucket.BlendOutPosition / slot.BlendOutTime; value = Blend(node, value, input, alpha, AlphaBlendMode::HermiteCubic); } else if (bucket.LoopsDone == 0 && slot.BlendInTime > 0.0f && bucket.BlendInPosition < slot.BlendInTime) @@ -2150,7 +2212,7 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu // Blend in auto input = tryGetValue(node->GetBox(1), Value::Null); bucket.BlendInPosition += deltaTime; - const float alpha = Math::Saturate(bucket.BlendInPosition / slot.BlendInTime); + const float alpha = bucket.BlendInPosition / slot.BlendInTime; value = Blend(node, input, value, alpha, AlphaBlendMode::HermiteCubic); } break;