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 UndoObjects => Values; + + /// + public override void Initialize(LayoutElementsContainer layout) + { + base.Initialize(layout); + + var instance = (AnimEvent)Values[0]; + var scriptType = TypeUtils.GetObjectType(instance); + var editor = CustomEditorsUtil.CreateEditor(scriptType, false); + layout.Object(Values, editor); + } + } + + private sealed class Proxy : ProxyBase + { + [EditorDisplay("General", EditorDisplayAttribute.InlineStyle), CustomEditor(typeof(ProxyEditor))] + public AnimEvent Event + { + get => Media.Instance; + set => Media.Instance = value; + } + + public Proxy(AnimationEventTrack track, AnimationEventMedia media) + : base(track, media) + { + } + } + + private bool _isRegisteredForScriptsReload; + private string _instanceTypeName; + private byte[] _instanceData; + + /// + /// The event type. + /// + public ScriptType Type; + + /// + /// The event instance. + /// + public AnimEvent Instance; + + /// + /// True if event is continuous (with duration), not a single frame. + /// + public bool IsContinuous; + + /// + /// Initializes a new instance of the class. + /// + public AnimationEventMedia() + { + PropertiesEditObject = new Proxy(null, this); + } + + private void OnScriptsReloadBegin() + { + if (Instance) + { + _instanceTypeName = Type.TypeName; + Type = ScriptType.Null; + _instanceData = FlaxEngine.Json.JsonSerializer.SaveToBytes(Instance); + Object.Destroy(ref Instance); + ScriptsBuilder.ScriptsReloadEnd += OnScriptsReloadEnd; + } + } + + private void OnScriptsReloadEnd() + { + ScriptsBuilder.ScriptsReloadEnd -= OnScriptsReloadEnd; + Type = TypeUtils.GetType(_instanceTypeName); + if (Type == ScriptType.Null) + { + Editor.LogError("Missing anim event type " + _instanceTypeName); + return; + } + Instance = (AnimEvent)Type.CreateInstance(); + FlaxEngine.Json.JsonSerializer.LoadFromBytes(Instance, _instanceData, Globals.EngineBuildNumber); + _instanceData = null; + } + + /// + /// Initializes track for the specified type. + /// + /// The type. + public void Init(ScriptType type) + { + Type = type; + IsContinuous = new ScriptType(typeof(AnimContinuousEvent)).IsAssignableFrom(type); + CanDelete = true; + CanSplit = IsContinuous; + CanResize = IsContinuous; + TooltipText = Surface.SurfaceUtils.GetVisualScriptTypeDescription(type); + Instance = (AnimEvent)type.CreateInstance(); + BackgroundColor = Instance.Color; + if (!_isRegisteredForScriptsReload) + { + _isRegisteredForScriptsReload = true; + ScriptsBuilder.ScriptsReloadBegin += OnScriptsReloadBegin; + } + } + + /// + protected override void OnDurationFramesChanged() + { + if (Type != ScriptType.Null) + DurationFrames = IsContinuous ? Mathf.Max(DurationFrames, 2) : 1; + + base.OnDurationFramesChanged(); + } + + /// + public override void OnDestroy() + { + Type = ScriptType.Null; + Object.Destroy(ref Instance); + if (_isRegisteredForScriptsReload) + { + _isRegisteredForScriptsReload = false; + ScriptsBuilder.ScriptsReloadBegin -= OnScriptsReloadBegin; + } + + base.OnDestroy(); + } + } + + /// + /// The timeline track for and . + /// + public sealed class AnimationEventTrack : Track + { + /// + /// Gets the archetype. + /// + /// The archetype. + public static TrackArchetype GetArchetype() + { + return new TrackArchetype + { + TypeId = 19, + Name = "Animation Event", + Create = options => new AnimationEventTrack(ref options), + Load = LoadTrack, + Save = SaveTrack, + }; + } + + private static void LoadTrack(int version, Track track, BinaryReader stream) + { + var e = (AnimationEventTrack)track; + var count = stream.ReadInt32(); + while (e.Media.Count > count) + e.RemoveMedia(e.Media.Last()); + while (e.Media.Count < count) + e.AddMedia(new AnimationEventMedia()); + for (int i = 0; i < count; i++) + { + var m = (AnimationEventMedia)e.Media[i]; + m.StartFrame = (int)stream.ReadSingle(); + m.DurationFrames = (int)stream.ReadSingle(); + var typeName = stream.ReadStrAnsi(13); + var type = TypeUtils.GetType(typeName); + if (type == ScriptType.Null) + throw new Exception($"Unknown type {typeName}."); + m.Init(type); + stream.ReadJson(m.Instance); + } + } + + private static void SaveTrack(Track track, BinaryWriter stream) + { + var e = (AnimationEventTrack)track; + var count = e.Media.Count; + stream.Write(count); + for (int i = 0; i < count; i++) + { + var m = (AnimationEventMedia)e.Media[i]; + stream.Write((float)m.StartFrame); + stream.Write((float)m.DurationFrames); + stream.WriteStrAnsi(m.Type.TypeName, 13); + stream.WriteJson(m.Instance); + } + } + + /// + public AnimationEventTrack(ref TrackCreateOptions options) + : base(ref options) + { + // Add button + const float buttonSize = 14; + var addButton = new Button + { + Text = "+", + TooltipText = "Add events", + AutoFocus = true, + AnchorPreset = AnchorPresets.MiddleRight, + IsScrollable = false, + Offsets = new Margin(-buttonSize - 2 + _muteCheckbox.Offsets.Left, buttonSize, buttonSize * -0.5f, buttonSize), + Parent = this, + }; + addButton.ButtonClicked += OnAddButtonClicked; + } + + private void OnAddButtonClicked(Button button) + { + var cm = new ContextMenu.ContextMenu(); + OnContextMenu(cm); + cm.Show(button.Parent, button.BottomLeft); + } + + /// + public override void OnTimelineContextMenu(ContextMenu.ContextMenu menu, float time) + { + base.OnTimelineContextMenu(menu, time); + + AddContextMenu(menu, time); + } + + /// + protected override void OnContextMenu(ContextMenu.ContextMenu menu) + { + base.OnContextMenu(menu); + + menu.AddSeparator(); + AddContextMenu(menu, 0.0f); + } + + private void AddContextMenu(ContextMenu.ContextMenu menu, float time) + { + var addEvent = menu.AddChildMenu("Add Anim Event"); + var addContinuousEvent = menu.AddChildMenu("Add Anim Continuous Event"); + var animEventTypes = Editor.Instance.CodeEditing.All.Get().Where(x => new ScriptType(typeof(AnimEvent)).IsAssignableFrom(x)); + foreach (var type in animEventTypes) + { + if (type.IsAbstract || !type.CanCreateInstance) + continue; + var add = new ScriptType(typeof(AnimContinuousEvent)).IsAssignableFrom(type) ? addContinuousEvent : addEvent; + var b = add.ContextMenu.AddButton(type.Name); + b.TooltipText = Surface.SurfaceUtils.GetVisualScriptTypeDescription(type); + b.Tag = type; + b.Parent.Tag = time; + b.ButtonClicked += OnAddAnimEvent; + } + } + + + private void OnAddAnimEvent(ContextMenuButton button) + { + var type = (ScriptType)button.Tag; + var time = (float)button.Parent.Tag; + var media = new AnimationEventMedia(); + media.Init(type); + media.StartFrame = (int)(time * Timeline.FramesPerSecond); + media.DurationFrames = media.IsContinuous ? Mathf.Max(Timeline.DurationFrames / 10, 10) : 1; + Timeline.AddMedia(this, media); + } + } +} diff --git a/Source/Editor/Viewport/Previews/AnimatedModelPreview.cs b/Source/Editor/Viewport/Previews/AnimatedModelPreview.cs index 1eabd1977..da1083d6e 100644 --- a/Source/Editor/Viewport/Previews/AnimatedModelPreview.cs +++ b/Source/Editor/Viewport/Previews/AnimatedModelPreview.cs @@ -163,6 +163,7 @@ namespace FlaxEditor.Viewport.Previews : base(useWidgets) { Task.Begin += OnBegin; + ScriptsBuilder.ScriptsReloadBegin += OnScriptsReloadBegin; // Setup preview scene _previewModel = new AnimatedModel @@ -275,6 +276,12 @@ namespace FlaxEditor.Viewport.Previews } } + private void OnScriptsReloadBegin() + { + // Prevent any crashes due to dangling references to anim events + _previewModel.ResetAnimation(); + } + private int ComputeLODIndex(SkinnedModel model) { if (PreviewActor.ForcedLOD != -1) @@ -428,6 +435,7 @@ namespace FlaxEditor.Viewport.Previews /// public override void OnDestroy() { + ScriptsBuilder.ScriptsReloadBegin -= OnScriptsReloadBegin; Object.Destroy(ref _floorModel); Object.Destroy(ref _previewModel); NodesMask = null; diff --git a/Source/Engine/Animations/AnimEvent.cpp b/Source/Engine/Animations/AnimEvent.cpp new file mode 100644 index 000000000..87c2e9b3b --- /dev/null +++ b/Source/Engine/Animations/AnimEvent.cpp @@ -0,0 +1,70 @@ +// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved. + +#include "AnimEvent.h" +#include "Engine/Scripting/BinaryModule.h" +#include "Engine/Scripting/ManagedSerialization.h" +#include "Engine/Serialization/SerializationFwd.h" +#include "Engine/Serialization/Serialization.h" + +AnimEvent::AnimEvent(const SpawnParams& params) + : ScriptingObject(params) +{ +} + +void AnimEvent::Serialize(SerializeStream& stream, const void* otherObj) +{ + SERIALIZE_GET_OTHER_OBJ(AnimEvent); + +#if !COMPILE_WITHOUT_CSHARP + // Handle C# objects data serialization + if (Flags & ObjectFlags::IsManagedType) + { + stream.JKEY("V"); + if (other) + { + ManagedSerialization::SerializeDiff(stream, GetOrCreateManagedInstance(), other->GetOrCreateManagedInstance()); + } + else + { + ManagedSerialization::Serialize(stream, GetOrCreateManagedInstance()); + } + } +#endif + + // Handle custom scripting objects data serialization + if (Flags & ObjectFlags::IsCustomScriptingType) + { + stream.JKEY("D"); + _type.Module->SerializeObject(stream, this, other); + } +} + +void AnimEvent::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) +{ +#if !COMPILE_WITHOUT_CSHARP + // Handle C# objects data serialization + if (Flags & ObjectFlags::IsManagedType) + { + auto* const v = SERIALIZE_FIND_MEMBER(stream, "V"); + if (v != stream.MemberEnd() && v->value.IsObject() && v->value.MemberCount() != 0) + { + ManagedSerialization::Deserialize(v->value, GetOrCreateManagedInstance()); + } + } +#endif + + // Handle custom scripting objects data serialization + if (Flags & ObjectFlags::IsCustomScriptingType) + { + auto* const v = SERIALIZE_FIND_MEMBER(stream, "D"); + if (v != stream.MemberEnd() && v->value.IsObject() && v->value.MemberCount() != 0) + { + _type.Module->DeserializeObject(v->value, this, modifier); + } + } +} + +AnimContinuousEvent::AnimContinuousEvent(const SpawnParams& params) + : AnimEvent(params) +{ +} diff --git a/Source/Engine/Animations/AnimEvent.h b/Source/Engine/Animations/AnimEvent.h new file mode 100644 index 000000000..f5afc275f --- /dev/null +++ b/Source/Engine/Animations/AnimEvent.h @@ -0,0 +1,72 @@ +// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved. + +#pragma once + +#include "Engine/Scripting/ScriptingObject.h" +#include "Engine/Core/ISerializable.h" +#if USE_EDITOR +#include "Engine/Core/Math/Color.h" +#endif + +class AnimatedModel; +class Animation; + +/// +/// The animation notification event triggered during animation playback. +/// +API_CLASS(Abstract) class FLAXENGINE_API AnimEvent : public ScriptingObject, public ISerializable +{ + DECLARE_SCRIPTING_TYPE(AnimEvent); + +#if USE_EDITOR + /// + /// Event display color in the Editor. + /// + API_FIELD(Attributes="HideInEditor, NoSerialize") Color Color = Color::White; +#endif + + /// + /// Animation event notification. + /// + /// The animated model actor instance. + /// The source animation. + /// The current animation time (in seconds). + /// The current animation tick delta time (in seconds). + API_FUNCTION() virtual void OnEvent(AnimatedModel* actor, Animation* anim, float time, float deltaTime) + { + } + + // [ISerializable] + void Serialize(SerializeStream& stream, const void* otherObj) override; + void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) override; +}; + +/// +/// The animation notification event (with duration) triggered during animation playback that contains begin and end (event notification is received as a tick). +/// +API_CLASS(Abstract) class FLAXENGINE_API AnimContinuousEvent : public AnimEvent +{ + DECLARE_SCRIPTING_TYPE(AnimContinuousEvent); + + /// + /// Animation notification called before the first event. + /// + /// The animated model actor instance. + /// The source animation. + /// The current animation time (in seconds). + /// The current animation tick delta time (in seconds). + API_FUNCTION() virtual void OnBegin(AnimatedModel* actor, Animation* anim, float time, float deltaTime) + { + } + + /// + /// Animation notification called after the last event (guaranteed to be always called). + /// + /// The animated model actor instance. + /// The source animation. + /// The current animation time (in seconds). + /// The current animation tick delta time (in seconds). + API_FUNCTION() virtual void OnEnd(AnimatedModel* actor, Animation* anim, float time, float deltaTime) + { + } +}; diff --git a/Source/Engine/Animations/Graph/AnimGraph.cpp b/Source/Engine/Animations/Graph/AnimGraph.cpp index 6d702350d..118430c54 100644 --- a/Source/Engine/Animations/Graph/AnimGraph.cpp +++ b/Source/Engine/Animations/Graph/AnimGraph.cpp @@ -2,6 +2,7 @@ #include "AnimGraph.h" #include "Engine/Animations/Animations.h" +#include "Engine/Animations/AnimEvent.h" #include "Engine/Content/Assets/SkinnedModel.h" #include "Engine/Graphics/Models/SkeletonData.h" #include "Engine/Scripting/Scripting.h" @@ -91,6 +92,9 @@ void AnimGraphInstanceData::Clear() State.Resize(0); NodesPose.Resize(0); Slots.Resize(0); + for (const auto& e : Events) + ((AnimContinuousEvent*)e.Instance)->OnEnd((AnimatedModel*)Object, e.Anim, 0.0f, 0.0f); + Events.Resize(0); } void AnimGraphInstanceData::ClearState() @@ -103,6 +107,9 @@ void AnimGraphInstanceData::ClearState() State.Resize(0); NodesPose.Resize(0); Slots.Clear(); + for (const auto& e : Events) + ((AnimContinuousEvent*)e.Instance)->OnEnd((AnimatedModel*)Object, e.Anim, 0.0f, 0.0f); + Events.Clear(); } void AnimGraphInstanceData::Invalidate() @@ -246,6 +253,8 @@ void AnimGraphExecutor::Update(AnimGraphInstanceData& data, float dt) // Initialize buckets ResetBuckets(context, &_graph); } + for (auto& e : data.Events) + e.Hit = false; // Init empty nodes data context.EmptyNodes.RootMotion = RootMotionData::Identity; @@ -279,6 +288,19 @@ void AnimGraphExecutor::Update(AnimGraphInstanceData& data, float dt) if (animResult == nullptr) animResult = GetEmptyNodes(); } + if (data.Events.Count() != 0) + { + ANIM_GRAPH_PROFILE_EVENT("Events"); + for (int32 i = data.Events.Count() - 1; i >= 0; i--) + { + const auto& e = data.Events[i]; + if (!e.Hit) + { + ((AnimContinuousEvent*)e.Instance)->OnEnd((AnimatedModel*)context.Data->Object, e.Anim, 0.0f, 0.0f); + data.Events.RemoveAt(i); + } + } + } // Allow for external override of the local pose (eg. by the ragdoll) data.LocalPoseOverride(animResult); diff --git a/Source/Engine/Animations/Graph/AnimGraph.h b/Source/Engine/Animations/Graph/AnimGraph.h index f7dd36b10..c38eacee9 100644 --- a/Source/Engine/Animations/Graph/AnimGraph.h +++ b/Source/Engine/Animations/Graph/AnimGraph.h @@ -16,6 +16,7 @@ #define ANIM_GRAPH_MULTI_BLEND_2D_MAX_TRIS 32 #define ANIM_GRAPH_MAX_STATE_TRANSITIONS 64 #define ANIM_GRAPH_MAX_CALL_STACK 100 +#define ANIM_GRAPH_MAX_EVENTS 64 class AnimGraph; class AnimSubGraph; @@ -267,6 +268,7 @@ struct FLAXENGINE_API AnimGraphSlot /// 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).