Add **Animation Events**
This commit is contained in:
@@ -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>
|
||||
|
||||
283
Source/Editor/GUI/Timeline/Tracks/AnimationEventTrack.cs
Normal file
283
Source/Editor/GUI/Timeline/Tracks/AnimationEventTrack.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
70
Source/Engine/Animations/AnimEvent.cpp
Normal file
70
Source/Engine/Animations/AnimEvent.cpp
Normal 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)
|
||||
{
|
||||
}
|
||||
72
Source/Engine/Animations/AnimEvent.h
Normal file
72
Source/Engine/Animations/AnimEvent.h
Normal 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)
|
||||
{
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ public:
|
||||
CameraCut = 16,
|
||||
//AnimationChannel = 17,
|
||||
//AnimationChannelData = 18,
|
||||
//AnimationEvent = 19,
|
||||
};
|
||||
|
||||
enum class Flags
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user