diff --git a/Source/Editor/GUI/Timeline/AnimationTimeline.cs b/Source/Editor/GUI/Timeline/AnimationTimeline.cs
index 50e5ddfdb..19c1543e0 100644
--- a/Source/Editor/GUI/Timeline/AnimationTimeline.cs
+++ b/Source/Editor/GUI/Timeline/AnimationTimeline.cs
@@ -52,7 +52,7 @@ namespace FlaxEditor.GUI.Timeline
///
/// The undo/redo to use for the history actions recording. Optional, can be null to disable undo support.
public AnimationTimeline(FlaxEditor.Undo undo)
- : base(PlaybackButtons.Play | PlaybackButtons.Stop, undo, false, false)
+ : base(PlaybackButtons.Play | PlaybackButtons.Stop, undo, false, true)
{
PlaybackState = PlaybackStates.Seeking;
ShowPreviewValues = false;
@@ -61,6 +61,7 @@ namespace FlaxEditor.GUI.Timeline
// Setup track types
TrackArchetypes.Add(AnimationChannelTrack.GetArchetype());
TrackArchetypes.Add(AnimationChannelDataTrack.GetArchetype());
+ TrackArchetypes.Add(AnimationEventTrack.GetArchetype());
}
///
diff --git a/Source/Editor/GUI/Timeline/Tracks/AnimationEventTrack.cs b/Source/Editor/GUI/Timeline/Tracks/AnimationEventTrack.cs
new file mode 100644
index 000000000..cce09a84f
--- /dev/null
+++ b/Source/Editor/GUI/Timeline/Tracks/AnimationEventTrack.cs
@@ -0,0 +1,283 @@
+// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using FlaxEditor.CustomEditors;
+using FlaxEditor.GUI.ContextMenu;
+using FlaxEditor.Scripting;
+using FlaxEditor.Utilities;
+using FlaxEngine;
+using FlaxEngine.GUI;
+using Object = FlaxEngine.Object;
+
+namespace FlaxEditor.GUI.Timeline.Tracks
+{
+ ///
+ /// The timeline media for and .
+ ///
+ public sealed class AnimationEventMedia : Media
+ {
+ private sealed class ProxyEditor : SyncPointEditor
+ {
+ ///
+ public override IEnumerable
class FLAXENGINE_API AnimGraphInstanceData
{
+ friend AnimGraphExecutor;
public:
// ---- Quick documentation ----
@@ -402,6 +404,18 @@ public:
/// Invalidates the update timer.
///
void Invalidate();
+
+private:
+
+ struct Event
+ {
+ AnimEvent* Instance;
+ Animation* Anim;
+ AnimGraphNode* Node;
+ bool Hit;
+ };
+
+ Array> Events;
};
///
diff --git a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp
index 080109a12..ff7ce0058 100644
--- a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp
+++ b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp
@@ -5,7 +5,9 @@
#include "Engine/Content/Assets/SkeletonMask.h"
#include "Engine/Content/Assets/AnimationGraphFunction.h"
#include "Engine/Animations/AlphaBlend.h"
+#include "Engine/Animations/AnimEvent.h"
#include "Engine/Animations/InverseKinematics.h"
+#include "Engine/Level/Actors/AnimatedModel.h"
namespace
{
@@ -146,6 +148,7 @@ Variant AnimGraphExecutor::SampleAnimation(AnimGraphNode* node, bool loop, float
if (anim == nullptr || !anim->IsLoaded())
return Value::Null;
PROFILE_CPU_ASSET(anim);
+ const float oldTimePos = prevTimePos;
// Calculate actual time position within the animation node (defined by length and loop mode)
const float pos = GetAnimPos(newTimePos, startTimePos, loop, length);
@@ -180,6 +183,81 @@ Variant AnimGraphExecutor::SampleAnimation(AnimGraphNode* node, bool loop, float
ExtractRootMotion(mapping, rootNodeIndex, anim, animPos, animPrevPos, nodes->Nodes[rootNodeIndex], nodes->RootMotion);
}
+ // Collect events
+ if (anim->Events.Count() != 0)
+ {
+ ANIM_GRAPH_PROFILE_EVENT("Events");
+ auto& context = Context.Get();
+ float eventTimeMin = animPrevPos;
+ float eventTimeMax = animPos;
+ if (loop)
+ {
+ // Check if animation looped
+ const float posNotLooped = startTimePos + oldTimePos;
+ if (posNotLooped < 0.0f || posNotLooped > length)
+ {
+ if (context.DeltaTime * speed < 0)
+ {
+ // Playback backwards
+ Swap(eventTimeMin, eventTimeMax);
+ }
+ }
+ }
+ const float eventTime = animPos / static_cast(anim->Data.FramesPerSecond);
+ const float eventDeltaTime = (animPos - animPrevPos) / static_cast(anim->Data.FramesPerSecond);
+ for (const auto& track : anim->Events)
+ {
+ for (const auto& k : track.Second.GetKeyframes())
+ {
+ if (!k.Value.Instance)
+ continue;
+ const float duration = k.Value.Duration > 1 ? k.Value.Duration : 0.0f;
+ if (k.Time <= eventTimeMax && eventTimeMin <= k.Time + duration)
+ {
+ int32 stateIndex = -1;
+ if (duration > 1)
+ {
+ // Begin for continuous event
+ for (stateIndex = 0; stateIndex < context.Data->Events.Count(); stateIndex++)
+ {
+ const auto& e = context.Data->Events[stateIndex];
+ if (e.Instance == k.Value.Instance && e.Node == node)
+ break;
+ }
+ if (stateIndex == context.Data->Events.Count())
+ {
+ auto& e = context.Data->Events.AddOne();
+ e.Instance = k.Value.Instance;
+ e.Anim = anim;
+ e.Node = node;
+ ASSERT(k.Value.Instance->Is());
+ ((AnimContinuousEvent*)k.Value.Instance)->OnBegin((AnimatedModel*)context.Data->Object, anim, eventTime, eventDeltaTime);
+ }
+ }
+
+ // Event
+ k.Value.Instance->OnEvent((AnimatedModel*)context.Data->Object, anim, eventTime, eventDeltaTime);
+ if (stateIndex != -1)
+ context.Data->Events[stateIndex].Hit = true;
+ }
+ else if (duration > 1)
+ {
+ // End for continuous event
+ for (int32 i = 0; i < context.Data->Events.Count(); i++)
+ {
+ const auto& e = context.Data->Events[i];
+ if (e.Instance == k.Value.Instance && e.Node == node)
+ {
+ ((AnimContinuousEvent*)k.Value.Instance)->OnEnd((AnimatedModel*)context.Data->Object, anim, eventTime, eventDeltaTime);
+ context.Data->Events.RemoveAt(i);
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+
return nodes;
}
diff --git a/Source/Engine/Animations/SceneAnimations/SceneAnimation.h b/Source/Engine/Animations/SceneAnimations/SceneAnimation.h
index 83dd85550..98bccc0cf 100644
--- a/Source/Engine/Animations/SceneAnimations/SceneAnimation.h
+++ b/Source/Engine/Animations/SceneAnimations/SceneAnimation.h
@@ -42,6 +42,7 @@ public:
CameraCut = 16,
//AnimationChannel = 17,
//AnimationChannelData = 18,
+ //AnimationEvent = 19,
};
enum class Flags
diff --git a/Source/Engine/Content/Assets/Animation.cpp b/Source/Engine/Content/Assets/Animation.cpp
index 1ef3add14..789153c21 100644
--- a/Source/Engine/Content/Assets/Animation.cpp
+++ b/Source/Engine/Content/Assets/Animation.cpp
@@ -6,10 +6,13 @@
#include "Engine/Profiler/ProfilerCPU.h"
#include "Engine/Content/Factories/BinaryAssetFactory.h"
#include "Engine/Animations/CurveSerialization.h"
+#include "Engine/Animations/AnimEvent.h"
+#include "Engine/Scripting/Scripting.h"
#include "Engine/Threading/Threading.h"
#include "Engine/Serialization/MemoryReadStream.h"
#if USE_EDITOR
#include "Engine/Serialization/MemoryWriteStream.h"
+#include "Engine/Level/Level.h"
#endif
REGISTER_BINARY_ASSET(Animation, "FlaxEngine.Animation", false);
@@ -19,6 +22,21 @@ Animation::Animation(const SpawnParams& params, const AssetInfo* info)
{
}
+#if USE_EDITOR
+
+void Animation::OnScriptsReloadStart()
+{
+ for (auto& e : Events)
+ {
+ for (auto& k : e.Second.GetKeyframes())
+ {
+ Level::ScriptsReloadRegisterObject((ScriptingObject*&)k.Value.Instance);
+ }
+ }
+}
+
+#endif
+
Animation::InfoData Animation::GetInfo() const
{
ScopeLock lock(Locker);
@@ -127,7 +145,7 @@ void Animation::LoadTimeline(BytesContainer& result) const
const float fpsInv = 1.0f / fps;
stream.WriteFloat(fps);
stream.WriteInt32((int32)Data.Duration);
- int32 tracksCount = Data.Channels.Count();
+ int32 tracksCount = Data.Channels.Count() + Events.Count();
for (auto& channel : Data.Channels)
tracksCount +=
(channel.Position.GetKeyframes().HasItems() ? 1 : 0) +
@@ -214,6 +232,24 @@ void Animation::LoadTimeline(BytesContainer& result) const
trackIndex++;
}
}
+ for (auto& e : Events)
+ {
+ // Animation Event track
+ stream.WriteByte(19); // Track Type
+ stream.WriteByte(0); // Track Flags
+ stream.WriteInt32(-1); // Parent Index
+ stream.WriteInt32(0); // Children Count
+ stream.WriteString(e.First, -13); // Name
+ stream.Write(&Color32::White); // Color
+ stream.WriteInt32(e.Second.GetKeyframes().Count()); // Events Count
+ for (const auto& k : e.Second.GetKeyframes())
+ {
+ stream.WriteFloat(k.Time);
+ stream.WriteFloat(k.Value.Duration);
+ stream.WriteStringAnsi(k.Value.TypeName, 13);
+ stream.WriteJson(k.Value.Instance);
+ }
+ }
result.Copy(stream.GetHandle(), stream.GetPosition());
}
@@ -253,6 +289,7 @@ bool Animation::SaveTimeline(BytesContainer& data)
// Tracks
Data.Channels.Clear();
+ Events.Clear();
Dictionary animationChannelTrackIndexToChannelIndex;
animationChannelTrackIndexToChannelIndex.EnsureCapacity(tracksCount * 3);
for (int32 trackIndex = 0; trackIndex < tracksCount; trackIndex++)
@@ -325,6 +362,36 @@ bool Animation::SaveTimeline(BytesContainer& data)
}
break;
}
+ case 19:
+ {
+ // Animation Event
+ int32 count;
+ stream.ReadInt32(&count);
+ auto& eventTrack = Events.AddOne();
+ eventTrack.First = name;
+ eventTrack.Second.Resize(count);
+ for (int32 i = 0; i < count; i++)
+ {
+ auto& k = eventTrack.Second.GetKeyframes()[i];
+ stream.ReadFloat(&k.Time);
+ stream.ReadFloat(&k.Value.Duration);
+ stream.ReadStringAnsi(&k.Value.TypeName, 13);
+ const ScriptingTypeHandle typeHandle = Scripting::FindScriptingType(k.Value.TypeName);
+ k.Value.Instance = NewObject(typeHandle);
+ stream.ReadJson(k.Value.Instance);
+ if (!k.Value.Instance)
+ {
+ LOG(Error, "Failed to spawn object of type {0}.", String(k.Value.TypeName));
+ continue;
+ }
+ if (!_registeredForScriptingReload)
+ {
+ _registeredForScriptingReload = true;
+ Level::ScriptsReloadStart.Bind(this);
+ }
+ }
+ break;
+ }
default:
LOG(Error, "Unsupported track type {0} for animation.", trackType);
return true;
@@ -364,7 +431,7 @@ bool Animation::Save(const StringView& path)
MemoryWriteStream stream(4096);
// Info
- stream.WriteInt32(100);
+ stream.WriteInt32(101);
stream.WriteDouble(Data.Duration);
stream.WriteDouble(Data.FramesPerSecond);
stream.WriteBool(Data.EnableRootMotion);
@@ -381,6 +448,22 @@ bool Animation::Save(const StringView& path)
Serialization::Serialize(stream, anim.Scale);
}
+ // Animation events
+ stream.WriteInt32(Events.Count());
+ for (int32 i = 0; i < Events.Count(); i++)
+ {
+ auto& e = Events[i];
+ stream.WriteString(e.First, 172);
+ stream.WriteInt32(e.Second.GetKeyframes().Count());
+ for (const auto& k : e.Second.GetKeyframes())
+ {
+ stream.WriteFloat(k.Time);
+ stream.WriteFloat(k.Value.Duration);
+ stream.WriteStringAnsi(k.Value.TypeName, 17);
+ stream.WriteJson(k.Value.Instance);
+ }
+ }
+
// Set data to the chunk asset
auto chunk0 = GetOrCreateChunk(0);
ASSERT(chunk0 != nullptr);
@@ -432,6 +515,7 @@ Asset::LoadResult Animation::load()
switch (headerVersion)
{
case 100:
+ case 101:
{
stream.ReadInt32(&headerVersion);
stream.ReadDouble(&Data.Duration);
@@ -471,13 +555,72 @@ Asset::LoadResult Animation::load()
}
}
+ // Animation events
+ if (headerVersion >= 101)
+ {
+ int32 eventTracksCount;
+ stream.ReadInt32(&eventTracksCount);
+ Events.Resize(eventTracksCount, false);
+#if !USE_EDITOR
+ StringAnsi typeName;
+#endif
+ for (int32 i = 0; i < eventTracksCount; i++)
+ {
+ auto& e = Events[i];
+ stream.ReadString(&e.First, 172);
+ int32 eventsCount;
+ stream.ReadInt32(&eventsCount);
+ e.Second.GetKeyframes().Resize(eventsCount);
+ for (auto& k : e.Second.GetKeyframes())
+ {
+ stream.ReadFloat(&k.Time);
+ stream.ReadFloat(&k.Value.Duration);
+#if USE_EDITOR
+ StringAnsi& typeName = k.Value.TypeName;
+#endif
+ stream.ReadStringAnsi(&typeName, 17);
+ const ScriptingTypeHandle typeHandle = Scripting::FindScriptingType(typeName);
+ k.Value.Instance = NewObject(typeHandle);
+ stream.ReadJson(k.Value.Instance);
+ if (!k.Value.Instance)
+ {
+ LOG(Error, "Failed to spawn object of type {0}.", String(typeName));
+ continue;
+ }
+#if USE_EDITOR
+ if (!_registeredForScriptingReload)
+ {
+ _registeredForScriptingReload = true;
+ Level::ScriptsReloadStart.Bind(this);
+ }
+#endif
+ }
+ }
+ }
+
return LoadResult::Ok;
}
void Animation::unload(bool isReloading)
{
+#if USE_EDITOR
+ if (_registeredForScriptingReload)
+ {
+ _registeredForScriptingReload = false;
+ Level::ScriptsReloadStart.Unbind(this);
+ }
+#endif
ClearCache();
Data.Dispose();
+ for (const auto& e : Events)
+ {
+ for (const auto& k : e.Second.GetKeyframes())
+ {
+ if (k.Value.Instance)
+ Delete(k.Value.Instance);
+ }
+ }
+ Events.Clear();
}
AssetChunksFlag Animation::getChunksToPreload() const
diff --git a/Source/Engine/Content/Assets/Animation.h b/Source/Engine/Content/Assets/Animation.h
index 950b7a45a..8c9b9c095 100644
--- a/Source/Engine/Content/Assets/Animation.h
+++ b/Source/Engine/Content/Assets/Animation.h
@@ -7,6 +7,7 @@
#include "Engine/Animations/AnimationData.h"
class SkinnedModel;
+class AnimEvent;
///
/// Asset that contains an animation spline represented by a set of keyframes, each representing an endpoint of a linear curve.
@@ -48,6 +49,25 @@ DECLARE_BINARY_ASSET_HEADER(Animation, 1);
API_FIELD() int32 MemoryUsage;
};
+ ///
+ /// Contains instance.
+ ///
+ struct FLAXENGINE_API AnimEventData
+ {
+ float Duration = 0.0f;
+ AnimEvent* Instance = nullptr;
+#if USE_EDITOR
+ StringAnsi TypeName;
+#endif
+ };
+
+private:
+
+#if USE_EDITOR
+ bool _registeredForScriptingReload = false;
+ void OnScriptsReloadStart();
+#endif
+
public:
///
@@ -55,6 +75,11 @@ public:
///
AnimationData Data;
+ ///
+ /// The animation events (keyframes per named track).
+ ///
+ Array>> Events;
+
///
/// Contains the mapping for every skeleton node to the animation data channels.
/// Can be used for a simple lookup or to check if a given node is animated (unused nodes are using -1 index).