diff --git a/Source/Editor/Surface/Archetypes/Animation.cs b/Source/Editor/Surface/Archetypes/Animation.cs
index 15096e696..0f73d67d6 100644
--- a/Source/Editor/Surface/Archetypes/Animation.cs
+++ b/Source/Editor/Surface/Archetypes/Animation.cs
@@ -953,6 +953,25 @@ namespace FlaxEditor.Surface.Archetypes
NodeElementArchetype.Factory.Text(0, Surface.Constants.LayoutOffsetY * 6, "Node:"),
}
},
+ new NodeArchetype
+ {
+ TypeID = 32,
+ Title = "Animation Slot",
+ Description = "Plays the animation from code with blending (eg. hit reaction).",
+ Flags = NodeFlags.AnimGraph,
+ Size = new Vector2(200, 40),
+ DefaultValues = new object[]
+ {
+ "Default",
+ },
+ Elements = new[]
+ {
+ NodeElementArchetype.Factory.Output(0, string.Empty, typeof(void), 0),
+ NodeElementArchetype.Factory.Input(0, string.Empty, true, typeof(void), 1),
+ NodeElementArchetype.Factory.Text(0, Surface.Constants.LayoutOffsetY, "Slot:"),
+ NodeElementArchetype.Factory.TextBox(30, Surface.Constants.LayoutOffsetY, 140, TextBox.DefaultHeight, 0, false),
+ }
+ },
};
}
}
diff --git a/Source/Engine/Animations/Graph/AnimGraph.Base.cpp b/Source/Engine/Animations/Graph/AnimGraph.Base.cpp
index facf78b43..5dd7eaa3b 100644
--- a/Source/Engine/Animations/Graph/AnimGraph.Base.cpp
+++ b/Source/Engine/Animations/Graph/AnimGraph.Base.cpp
@@ -105,6 +105,14 @@ void StateMachineBucketInit(AnimGraphInstanceData::Bucket& bucket)
bucket.StateMachine.TransitionPosition = 0.0f;
}
+void SlotBucketInit(AnimGraphInstanceData::Bucket& bucket)
+{
+ bucket.Slot.Index = -1;
+ bucket.Slot.TimePosition = 0.0f;
+ bucket.Slot.BlendInPosition = 0.0f;
+ bucket.Slot.BlendOutPosition = 0.0f;
+}
+
bool SortMultiBlend1D(const byte& a, const byte& b, AnimGraphNode* n)
{
// Sort items by X location from the lowest to the highest
@@ -424,6 +432,12 @@ bool AnimGraphBase::onNodeLoaded(Node* n)
}
break;
}
+ // Animation Slot
+ case 32:
+ {
+ ADD_BUCKET(SlotBucketInit);
+ break;
+ }
}
break;
// Custom
diff --git a/Source/Engine/Animations/Graph/AnimGraph.cpp b/Source/Engine/Animations/Graph/AnimGraph.cpp
index 53af88ba5..6d702350d 100644
--- a/Source/Engine/Animations/Graph/AnimGraph.cpp
+++ b/Source/Engine/Animations/Graph/AnimGraph.cpp
@@ -90,6 +90,7 @@ void AnimGraphInstanceData::Clear()
Parameters.Resize(0);
State.Resize(0);
NodesPose.Resize(0);
+ Slots.Resize(0);
}
void AnimGraphInstanceData::ClearState()
@@ -101,6 +102,7 @@ void AnimGraphInstanceData::ClearState()
RootMotion = RootMotionData::Identity;
State.Resize(0);
NodesPose.Resize(0);
+ Slots.Clear();
}
void AnimGraphInstanceData::Invalidate()
diff --git a/Source/Engine/Animations/Graph/AnimGraph.h b/Source/Engine/Animations/Graph/AnimGraph.h
index 0136e35df..88c7a2eec 100644
--- a/Source/Engine/Animations/Graph/AnimGraph.h
+++ b/Source/Engine/Animations/Graph/AnimGraph.h
@@ -249,6 +249,19 @@ public:
}
};
+///
+/// The animation graph slot-based animation.
+///
+struct FLAXENGINE_API AnimGraphSlot
+{
+ String Name;
+ AssetReference Animation;
+ float Speed = 1.0f;
+ float BlendInTime = 0.0f;
+ float BlendOutTime = 0.0f;
+ bool Pause = false;
+};
+
///
/// The animation graph instance data storage. Required to update the animation graph.
///
@@ -293,6 +306,14 @@ public:
float TransitionPosition;
};
+ struct SlotBucket
+ {
+ int32 Index;
+ float TimePosition;
+ float BlendInPosition;
+ float BlendOutPosition;
+ };
+
///
/// The single data storage bucket for the instanced animation graph node. Used to store the node state (playback position, state, transition data).
///
@@ -304,6 +325,7 @@ public:
MultiBlendBucket MultiBlend;
BlendPoseBucket BlendPose;
StateMachineBucket StateMachine;
+ SlotBucket Slot;
};
};
@@ -359,6 +381,11 @@ public:
///
Delegate LocalPoseOverride;
+ ///
+ /// The slots animations.
+ ///
+ Array Slots;
+
public:
///
diff --git a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp
index e02b3eb4a..080109a12 100644
--- a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp
+++ b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp
@@ -1817,6 +1817,80 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
value = nodes;
break;
}
+ // Animation Slot
+ case 32:
+ {
+ auto& slots = context.Data->Slots;
+ if (slots.Count() == 0)
+ {
+ value = tryGetValue(node->GetBox(1), Value::Null);
+ return;
+ }
+ const StringView slotName(node->Values[0]);
+ auto& bucket = context.Data->State[node->BucketIndex].Slot;
+ if (bucket.Index != -1 && (slots.Count() <= bucket.Index || slots[bucket.Index].Animation == nullptr))
+ {
+ // Current slot animation ended
+ bucket.Index = -1;
+ }
+ if (bucket.Index == -1)
+ {
+ // Pick the animation to play
+ for (int32 i = 0; i < slots.Count(); i++)
+ {
+ auto& slot = slots[i];
+ if (slot.Animation && slot.Name == slotName)
+ {
+ // Start playing animation
+ bucket.Index = i;
+ bucket.TimePosition = 0.0f;
+ bucket.BlendInPosition = 0.0f;
+ bucket.BlendOutPosition = 0.0f;
+ break;
+ }
+ }
+ if (bucket.Index == -1 || !slots[bucket.Index].Animation->IsLoaded())
+ {
+ value = tryGetValue(node->GetBox(1), Value::Null);
+ return;
+ }
+ }
+
+ // Play the animation
+ auto& slot = slots[bucket.Index];
+ Animation* anim = slot.Animation;
+ ASSERT(slot.Animation && slot.Animation->IsLoaded());
+ const float deltaTime = slot.Pause ? 0.0f : context.DeltaTime * slot.Speed;
+ const float length = anim->GetLength();
+ float newTimePos = bucket.TimePosition + deltaTime;
+ if (newTimePos >= length)
+ {
+ // End playing animation
+ value = tryGetValue(node->GetBox(1), Value::Null);
+ bucket.Index = -1;
+ slot.Animation = nullptr;
+ return;
+ }
+ value = SampleAnimation(node, false, length, 0.0f, bucket.TimePosition, newTimePos, anim, slot.Speed);
+ bucket.TimePosition = newTimePos;
+ if (slot.BlendOutTime > 0.0f && length - slot.BlendOutTime < bucket.TimePosition)
+ {
+ // Blend out
+ auto input = tryGetValue(node->GetBox(1), Value::Null);
+ bucket.BlendOutPosition += deltaTime;
+ const float alpha = Math::Saturate(bucket.BlendOutPosition / slot.BlendOutTime);
+ value = Blend(node, value, input, alpha, AlphaBlendMode::HermiteCubic);
+ }
+ else if (slot.BlendInTime > 0.0f && bucket.BlendInPosition < slot.BlendInTime)
+ {
+ // Blend in
+ auto input = tryGetValue(node->GetBox(1), Value::Null);
+ bucket.BlendInPosition += deltaTime;
+ const float alpha = Math::Saturate(bucket.BlendInPosition / slot.BlendInTime);
+ value = Blend(node, input, value, alpha, AlphaBlendMode::HermiteCubic);
+ }
+ break;
+ }
default:
break;
}
diff --git a/Source/Engine/Level/Actors/AnimatedModel.cpp b/Source/Engine/Level/Actors/AnimatedModel.cpp
index 39496156f..9fc6672d1 100644
--- a/Source/Engine/Level/Actors/AnimatedModel.cpp
+++ b/Source/Engine/Level/Actors/AnimatedModel.cpp
@@ -324,6 +324,89 @@ void AnimatedModel::ClearBlendShapeWeights()
_blendShapes.Clear();
}
+void AnimatedModel::PlaySlotAnimation(const StringView& slotName, Animation* anim, float speed, float blendInTime, float blendOutTime)
+{
+ CHECK(anim);
+ for (auto& slot : GraphInstance.Slots)
+ {
+ if (slot.Animation == anim && slot.Name == slotName)
+ {
+ slot.Pause = false;
+ slot.BlendInTime = blendInTime;
+ return;
+ }
+ }
+ int32 index = 0;
+ for (; index < GraphInstance.Slots.Count(); index++)
+ {
+ if (GraphInstance.Slots[index].Animation == nullptr)
+ break;
+ }
+ if (index == GraphInstance.Slots.Count())
+ GraphInstance.Slots.AddOne();
+ auto& slot = GraphInstance.Slots[index];
+ slot.Name = slotName;
+ slot.Animation = anim;
+ slot.Speed = speed;
+ slot.BlendInTime = blendInTime;
+ slot.BlendOutTime = blendOutTime;
+}
+
+void AnimatedModel::StopSlotAnimation()
+{
+ GraphInstance.Slots.Clear();
+}
+
+void AnimatedModel::StopSlotAnimation(const StringView& slotName, Animation* anim)
+{
+ for (auto& slot : GraphInstance.Slots)
+ {
+ if (slot.Animation == anim && slot.Name == slotName)
+ {
+ slot.Animation = nullptr;
+ break;
+ }
+ }
+}
+
+void AnimatedModel::PauseSlotAnimation()
+{
+ for (auto& slot : GraphInstance.Slots)
+ slot.Pause = true;
+}
+
+void AnimatedModel::PauseSlotAnimation(const StringView& slotName, Animation* anim)
+{
+ for (auto& slot : GraphInstance.Slots)
+ {
+ if (slot.Animation == anim && slot.Name == slotName)
+ {
+ slot.Pause = true;
+ break;
+ }
+ }
+}
+
+bool AnimatedModel::IsPlayingSlotAnimation()
+{
+ for (auto& slot : GraphInstance.Slots)
+ {
+ if (slot.Animation && !slot.Pause)
+ return true;
+ }
+ return false;
+}
+
+bool AnimatedModel::IsPlayingSlotAnimation(const StringView& slotName, Animation* anim)
+{
+ for (auto& slot : GraphInstance.Slots)
+ {
+ if (slot.Animation == anim && slot.Name == slotName && !slot.Pause)
+ return true;
+ }
+ return false;
+}
+
void AnimatedModel::ApplyRootMotion(const RootMotionData& rootMotionDelta)
{
// Skip if no motion
diff --git a/Source/Engine/Level/Actors/AnimatedModel.h b/Source/Engine/Level/Actors/AnimatedModel.h
index bb42913b8..f3eb2500b 100644
--- a/Source/Engine/Level/Actors/AnimatedModel.h
+++ b/Source/Engine/Level/Actors/AnimatedModel.h
@@ -303,6 +303,54 @@ public:
///
API_FUNCTION() void ClearBlendShapeWeights();
+public:
+
+ ///
+ /// Plays the animation on the slot in Anim Graph.
+ ///
+ /// The name of the slot.
+ /// The animation to play.
+ /// The playback speed.
+ /// The animation blending in time (in seconds). Cam be used to smooth the slot animation playback with the input pose when starting the animation.
+ /// The animation blending out time (in seconds). Cam be used to smooth the slot animation playback with the input pose when ending animation.
+ API_FUNCTION() void PlaySlotAnimation(const StringView& slotName, Animation* anim, float speed = 1.0f, float blendInTime = 0.2f, float blendOutTime = 0.2f);
+
+ ///
+ /// Stops all the animations playback on the all slots in Anim Graph.
+ ///
+ API_FUNCTION() void StopSlotAnimation();
+
+ ///
+ /// Stops the animation playback on the slot in Anim Graph.
+ ///
+ /// The name of the slot.
+ /// The animation to stop.
+ API_FUNCTION() void StopSlotAnimation(const StringView& slotName, Animation* anim);
+
+ ///
+ /// Pauses all the animations playback on the all slots in Anim Graph.
+ ///
+ API_FUNCTION() void PauseSlotAnimation();
+
+ ///
+ /// Pauses the animation playback on the slot in Anim Graph.
+ ///
+ /// The name of the slot.
+ /// The animation to pause.
+ API_FUNCTION() void PauseSlotAnimation(const StringView& slotName, Animation* anim);
+
+ ///
+ /// Checks if the any animation playback is active on the any slot in Anim Graph (not paused).
+ ///
+ API_FUNCTION() bool IsPlayingSlotAnimation();
+
+ ///
+ /// Checks if the animation playback is active on the slot in Anim Graph (not paused).
+ ///
+ /// The name of the slot.
+ /// The animation to check.
+ API_FUNCTION() bool IsPlayingSlotAnimation(const StringView& slotName, Animation* anim);
+
private:
void ApplyRootMotion(const RootMotionData& rootMotionDelta);