Add **Animation Events**

This commit is contained in:
Wojtek Figat
2022-01-08 15:06:02 +01:00
parent 79200a784b
commit 82a43dea28
11 changed files with 720 additions and 3 deletions

View File

@@ -52,7 +52,7 @@ namespace FlaxEditor.GUI.Timeline
/// </summary>
/// <param name="undo">The undo/redo to use for the history actions recording. Optional, can be null to disable undo support.</param>
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());
}
/// <summary>

View File

@@ -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
{
/// <summary>
/// The timeline media for <see cref="AnimEvent"/> and <see cref="AnimContinuousEvent"/>.
/// </summary>
public sealed class AnimationEventMedia : Media
{
private sealed class ProxyEditor : SyncPointEditor
{
/// <inheritdoc />
public override IEnumerable<object> UndoObjects => Values;
/// <inheritdoc />
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<AnimationEventTrack, AnimationEventMedia>
{
[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;
/// <summary>
/// The event type.
/// </summary>
public ScriptType Type;
/// <summary>
/// The event instance.
/// </summary>
public AnimEvent Instance;
/// <summary>
/// True if event is continuous (with duration), not a single frame.
/// </summary>
public bool IsContinuous;
/// <summary>
/// Initializes a new instance of the <see cref="AnimationEventMedia"/> class.
/// </summary>
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;
}
/// <summary>
/// Initializes track for the specified type.
/// </summary>
/// <param name="type">The type.</param>
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;
}
}
/// <inheritdoc />
protected override void OnDurationFramesChanged()
{
if (Type != ScriptType.Null)
DurationFrames = IsContinuous ? Mathf.Max(DurationFrames, 2) : 1;
base.OnDurationFramesChanged();
}
/// <inheritdoc />
public override void OnDestroy()
{
Type = ScriptType.Null;
Object.Destroy(ref Instance);
if (_isRegisteredForScriptsReload)
{
_isRegisteredForScriptsReload = false;
ScriptsBuilder.ScriptsReloadBegin -= OnScriptsReloadBegin;
}
base.OnDestroy();
}
}
/// <summary>
/// The timeline track for <see cref="AnimEvent"/> and <see cref="AnimContinuousEvent"/>.
/// </summary>
public sealed class AnimationEventTrack : Track
{
/// <summary>
/// Gets the archetype.
/// </summary>
/// <returns>The archetype.</returns>
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);
}
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
public override void OnTimelineContextMenu(ContextMenu.ContextMenu menu, float time)
{
base.OnTimelineContextMenu(menu, time);
AddContextMenu(menu, time);
}
/// <inheritdoc />
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);
}
}
}

View File

@@ -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
/// <inheritdoc />
public override void OnDestroy()
{
ScriptsBuilder.ScriptsReloadBegin -= OnScriptsReloadBegin;
Object.Destroy(ref _floorModel);
Object.Destroy(ref _previewModel);
NodesMask = null;

View File

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

View File

@@ -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;
/// <summary>
/// The animation notification event triggered during animation playback.
/// </summary>
API_CLASS(Abstract) class FLAXENGINE_API AnimEvent : public ScriptingObject, public ISerializable
{
DECLARE_SCRIPTING_TYPE(AnimEvent);
#if USE_EDITOR
/// <summary>
/// Event display color in the Editor.
/// </summary>
API_FIELD(Attributes="HideInEditor, NoSerialize") Color Color = Color::White;
#endif
/// <summary>
/// Animation event notification.
/// </summary>
/// <param name="actor">The animated model actor instance.</param>
/// <param name="anim">The source animation.</param>
/// <param name="time">The current animation time (in seconds).</param>
/// <param name="deltaTime">The current animation tick delta time (in seconds).</param>
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;
};
/// <summary>
/// The animation notification event (with duration) triggered during animation playback that contains begin and end (event notification is received as a tick).
/// </summary>
API_CLASS(Abstract) class FLAXENGINE_API AnimContinuousEvent : public AnimEvent
{
DECLARE_SCRIPTING_TYPE(AnimContinuousEvent);
/// <summary>
/// Animation notification called before the first event.
/// </summary>
/// <param name="actor">The animated model actor instance.</param>
/// <param name="anim">The source animation.</param>
/// <param name="time">The current animation time (in seconds).</param>
/// <param name="deltaTime">The current animation tick delta time (in seconds).</param>
API_FUNCTION() virtual void OnBegin(AnimatedModel* actor, Animation* anim, float time, float deltaTime)
{
}
/// <summary>
/// Animation notification called after the last event (guaranteed to be always called).
/// </summary>
/// <param name="actor">The animated model actor instance.</param>
/// <param name="anim">The source animation.</param>
/// <param name="time">The current animation time (in seconds).</param>
/// <param name="deltaTime">The current animation tick delta time (in seconds).</param>
API_FUNCTION() virtual void OnEnd(AnimatedModel* actor, Animation* anim, float time, float deltaTime)
{
}
};

View File

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

View File

@@ -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
/// </summary>
class FLAXENGINE_API AnimGraphInstanceData
{
friend AnimGraphExecutor;
public:
// ---- Quick documentation ----
@@ -402,6 +404,18 @@ public:
/// Invalidates the update timer.
/// </summary>
void Invalidate();
private:
struct Event
{
AnimEvent* Instance;
Animation* Anim;
AnimGraphNode* Node;
bool Hit;
};
Array<Event, InlinedAllocation<8>> Events;
};
/// <summary>

View File

@@ -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<float>(anim->Data.FramesPerSecond);
const float eventDeltaTime = (animPos - animPrevPos) / static_cast<float>(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>());
((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;
}

View File

@@ -42,6 +42,7 @@ public:
CameraCut = 16,
//AnimationChannel = 17,
//AnimationChannelData = 18,
//AnimationEvent = 19,
};
enum class Flags

View File

@@ -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<int32, int32> 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<AnimEvent>(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<Animation, &Animation::OnScriptsReloadStart>(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<AnimEvent>(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<Animation, &Animation::OnScriptsReloadStart>(this);
}
#endif
}
}
}
return LoadResult::Ok;
}
void Animation::unload(bool isReloading)
{
#if USE_EDITOR
if (_registeredForScriptingReload)
{
_registeredForScriptingReload = false;
Level::ScriptsReloadStart.Unbind<Animation, &Animation::OnScriptsReloadStart>(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

View File

@@ -7,6 +7,7 @@
#include "Engine/Animations/AnimationData.h"
class SkinnedModel;
class AnimEvent;
/// <summary>
/// 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;
};
/// <summary>
/// Contains <see cref="AnimEvent"/> instance.
/// </summary>
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:
/// <summary>
@@ -55,6 +75,11 @@ public:
/// </summary>
AnimationData Data;
/// <summary>
/// The animation events (keyframes per named track).
/// </summary>
Array<Pair<String, StepCurve<AnimEventData>>> Events;
/// <summary>
/// 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).