Add SourceState and DestinationState modes to State Machine interruption modes in Anim Graph

#1735
This commit is contained in:
Wojtek Figat
2024-01-07 18:34:15 +01:00
parent af0439f3ce
commit 8c2a156e1f
5 changed files with 153 additions and 67 deletions

View File

@@ -1583,14 +1583,24 @@ namespace FlaxEditor.Surface.Archetypes
None = 0,
/// <summary>
/// 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).
/// </summary>
RuleRechecking = 1,
/// <summary>
/// Interrupted transition is immediately stopped without blending out.
/// Interrupted transition is immediately stopped without blending out (back to the source/destination state).
/// </summary>
Instant = 2,
/// <summary>
/// Enables checking other transitions in the source state that might interrupt this one.
/// </summary>
SourceState = 4,
/// <summary>
/// Enables checking transitions in the destination state that might interrupt this one.
/// </summary>
DestinationState = 8,
}
/// <summary>
@@ -1613,6 +1623,8 @@ namespace FlaxEditor.Surface.Archetypes
UseDefaultRule = 4,
InterruptionRuleRechecking = 8,
InterruptionInstant = 16,
InterruptionSourceState = 32,
InterruptionDestinationState = 64,
}
/// <summary>
@@ -1773,7 +1785,7 @@ namespace FlaxEditor.Surface.Archetypes
}
/// <summary>
/// Transition interruption options.
/// Transition interruption options (flags, can select multiple values).
/// </summary>
[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);
}
}

View File

@@ -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;

View File

@@ -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)

View File

@@ -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:
}
/// <summary>
/// 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).
/// </summary>
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);
};

View File

@@ -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<AnimGraphImpulse*>(poseA.AsPointer);
auto nodesB = static_cast<AnimGraphImpulse*>(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;