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;