You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
o3de/Gems/Maestro/Code/Source/Components/EditorSequenceComponent.cpp

454 lines
22 KiB
C++

/*
* Copyright (c) Contributors to the Open 3D Engine Project. For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include "Maestro_precompiled.h"
#include "EditorSequenceComponent.h"
#include "EditorSequenceAgentComponent.h"
#include "Objects/EntityObject.h"
#include "TrackView/TrackViewSequenceManager.h"
#include <Maestro/Types/AnimValueType.h>
#include <Maestro/Types/SequenceType.h>
#include <Maestro/Types/AnimNodeType.h>
#include <AzCore/Math/Uuid.h>
#include <AzToolsFramework/API/ToolsApplicationAPI.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/Serialization/EditContext.h>
#include <AzCore/RTTI/BehaviorContext.h>
#include <AzCore/Component/ComponentApplicationBus.h>
#include <AzToolsFramework/API/ComponentEntityObjectBus.h>
#include <Maestro/Bus/SequenceAgentComponentBus.h>
#include <AzToolsFramework/API/EntityCompositionRequestBus.h>
#include <AzToolsFramework/Entity/EditorEntityHelpers.h>
namespace Maestro
{
/*static*/ AZ::ScriptTimePoint EditorSequenceComponent::s_lastPropertyRefreshTime;
/*static*/ const double EditorSequenceComponent::s_refreshPeriodMilliseconds = 200.0; // 5 Hz refresh rate
/*static*/ const int EditorSequenceComponent::s_invalidSequenceId = -1;
namespace ClassConverters
{
static bool UpVersionAnimationData(AZ::SerializeContext&, AZ::SerializeContext::DataElementNode&);
} // namespace ClassConverters
///////////////////////////////////////////////////////////////////////////////////////////////////////
EditorSequenceComponent::EditorSequenceComponent()
: m_sequenceId(s_invalidSequenceId)
{
}
///////////////////////////////////////////////////////////////////////////////////////////////////////
EditorSequenceComponent::~EditorSequenceComponent()
{
bool isDuringUndo = false;
AzToolsFramework::ToolsApplicationRequests::Bus::BroadcastResult(isDuringUndo, &AzToolsFramework::ToolsApplicationRequests::Bus::Events::IsDuringUndoRedo);
// Don't RemoveEntityToAnimate if we are in the middle of an Undo event.
// Doing so will create will mark this entity dirty and break the undo system.
if (!isDuringUndo && m_sequence)
{
for (int i = m_sequence->GetNodeCount(); --i >= 0;)
{
IAnimNode* animNode = m_sequence->GetNode(i);
if (animNode->GetType() == AnimNodeType::AzEntity)
{
RemoveEntityToAnimate(animNode->GetAzEntityId());
}
}
}
IEditor* editor = nullptr;
EBUS_EVENT_RESULT(editor, AzToolsFramework::EditorRequests::Bus, GetEditor);
if (editor)
{
IAnimSequence* sequence = editor->GetMovieSystem()->FindSequenceById(m_sequenceId);
ITrackViewSequenceManager* pSequenceManager = editor->GetSequenceManager();
if (sequence && pSequenceManager && pSequenceManager->GetSequenceByEntityId(sequence->GetSequenceEntityId()))
{
pSequenceManager->OnDeleteSequenceEntity(sequence->GetSequenceEntityId());
}
}
if (m_sequence)
{
m_sequence = nullptr;
m_sequenceId = s_invalidSequenceId;
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////////
/*static*/ void EditorSequenceComponent::Reflect(AZ::ReflectContext* context)
{
AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context);
if (serializeContext)
{
serializeContext->Class<AnimSerialize::AnimationData>()
->Field("SerializedString", &AnimSerialize::AnimationData::m_serializedData)
->Version(1, &ClassConverters::UpVersionAnimationData);
serializeContext->Class<EditorSequenceComponent, EditorComponentBase>()
->Field("Sequence", &EditorSequenceComponent::m_sequence)
->Version(4);
AZ::EditContext* editContext = serializeContext->GetEditContext();
if (editContext)
{
editContext->Class<EditorSequenceComponent>(
"Sequence", "Plays Cinematic Animations")
->ClassElement(AZ::Edit::ClassElements::EditorData, "")
->Attribute(AZ::Edit::Attributes::Category, "Cinematics")
->Attribute(AZ::Edit::Attributes::Icon, "Icons/Components/Sequence.png")
->Attribute(AZ::Edit::Attributes::ViewportIcon, "Icons/Components/Viewport/Sequence.png")
//->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC("Game"))
->Attribute(AZ::Edit::Attributes::AddableByUser, false) // SequenceAgents are only added by TrackView
->Attribute(AZ::Edit::Attributes::AutoExpand, true)
;
}
}
AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context);
if (behaviorContext)
{
behaviorContext->Class<EditorSequenceComponent>()->RequestBus("SequenceComponentRequestBus");
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////////
void EditorSequenceComponent::Init()
{
EditorComponentBase::Init();
m_sequenceId = s_invalidSequenceId;
IEditor* editor = nullptr;
EBUS_EVENT_RESULT(editor, AzToolsFramework::EditorRequests::Bus, GetEditor);
if (editor)
{
bool sequenceWasDeserialized = false;
if (m_sequence)
{
// m_sequence is already filled if the component it was deserialized - register it with Track View
sequenceWasDeserialized = true;
editor->GetSequenceManager()->OnCreateSequenceComponent(m_sequence);
}
else
{
// if m_sequence is NULL, we're creating a new sequence - request the creation from the Track view
m_sequence = static_cast<CAnimSequence*>(editor->GetSequenceManager()->OnCreateSequenceObject(m_entity->GetName().c_str(), false, GetEntityId()));
}
if (m_sequence)
{
m_sequenceId = m_sequence->GetId();
}
if (sequenceWasDeserialized)
{
// Notify Trackview of the load
ITrackViewSequence* trackViewSequence = editor->GetSequenceManager()->GetSequenceByEntityId(GetEntityId());
if (trackViewSequence)
{
trackViewSequence->Load();
}
}
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////////
void EditorSequenceComponent::Activate()
{
EditorComponentBase::Activate();
Maestro::EditorSequenceComponentRequestBus::Handler::BusConnect(GetEntityId());
Maestro::SequenceComponentRequestBus::Handler::BusConnect(GetEntityId());
IEditor* editor = nullptr;
EBUS_EVENT_RESULT(editor, AzToolsFramework::EditorRequests::Bus, GetEditor);
if (editor)
{
editor->GetSequenceManager()->OnSequenceActivated(GetEntityId());
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////////
void EditorSequenceComponent::Deactivate()
{
Maestro::EditorSequenceComponentRequestBus::Handler::BusDisconnect();
Maestro::SequenceComponentRequestBus::Handler::BusDisconnect();
// disconnect from TickBus if we're connected (which would only happen if we deactivated during a pending property refresh)
AZ::TickBus::Handler::BusDisconnect();
EditorComponentBase::Deactivate();
}
///////////////////////////////////////////////////////////////////////////////////////////////////////
void EditorSequenceComponent::AddEntityToAnimate(AZ::EntityId entityToAnimate)
{
Maestro::EditorSequenceAgentComponent* agentComponent = nullptr;
auto component = AzToolsFramework::FindComponent<EditorSequenceAgentComponent>::OnEntity(entityToAnimate);
if (component)
{
agentComponent = static_cast<Maestro::EditorSequenceAgentComponent*>(component);
}
else
{
// #TODO LY-21846: Use "SequenceAgentComponentService" to find component, rather than specific component-type.
auto addComponentResult = AzToolsFramework::AddComponents<EditorSequenceAgentComponent>::ToEntities(entityToAnimate);
if (addComponentResult.IsSuccess())
{
if (!addComponentResult.GetValue()[entityToAnimate].m_componentsAdded.empty())
{
// We need to register our Entity and Component Ids with the SequenceAgentComponent so we can communicate over EBuses
// with it. We can't do this registration over an EBus because we haven't registered with it yet - do it with pointers?
// Is this safe?
agentComponent = static_cast<Maestro::EditorSequenceAgentComponent*>(
addComponentResult.GetValue()[entityToAnimate].m_componentsAdded[0]);
}
else
{
AZ_Assert(
!addComponentResult.GetValue()[entityToAnimate].m_componentsAdded.empty(),
"Add component result was successful, but the EditorSequenceAgentComponent wasn't added. "
"This can happen if the entity id isn't found for some reason: entity id = %s",
entityToAnimate.ToString().c_str());
}
}
}
AZ_Assert(agentComponent, "EditorSequenceComponent::AddEntityToAnimate unable to create or find sequenceAgentComponent.");
// Notify the SequenceAgentComponent that we're connected to it - after this call, all communication with the Agent is over an EBus
if (agentComponent)
{
agentComponent->ConnectSequence(GetEntityId());
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////////
void EditorSequenceComponent::RemoveEntityToAnimate(AZ::EntityId removedEntityId)
{
const Maestro::SequenceAgentEventBusId ebusId(GetEntityId(), removedEntityId);
// Notify the SequenceAgentComponent that we're disconnecting from it
EBUS_EVENT_ID(ebusId, Maestro::SequenceAgentComponentRequestBus, DisconnectSequence);
}
///////////////////////////////////////////////////////////////////////////////////////////////////////
void EditorSequenceComponent::GetAllAnimatablePropertiesForComponent(IAnimNode::AnimParamInfos& properties, AZ::EntityId animatedEntityId, AZ::ComponentId componentId)
{
const Maestro::SequenceAgentEventBusId ebusId(GetEntityId(), animatedEntityId);
Maestro::EditorSequenceAgentComponentRequestBus::Event(ebusId, &Maestro::EditorSequenceAgentComponentRequestBus::Events::GetAllAnimatableProperties, properties, componentId);
}
///////////////////////////////////////////////////////////////////////////////////////////////////////
void EditorSequenceComponent::GetAnimatableComponents(AZStd::vector<AZ::ComponentId>& componentIds, AZ::EntityId animatedEntityId)
{
const Maestro::SequenceAgentEventBusId ebusId(GetEntityId(), animatedEntityId);
EBUS_EVENT_ID(ebusId, Maestro::EditorSequenceAgentComponentRequestBus, GetAnimatableComponents, componentIds);
}
///////////////////////////////////////////////////////////////////////////////////////////////////////
AZ::Uuid EditorSequenceComponent::GetAnimatedAddressTypeId(const AZ::EntityId& animatedEntityId, const Maestro::SequenceComponentRequests::AnimatablePropertyAddress& animatableAddress)
{
AZ::Uuid typeId = AZ::Uuid::CreateNull();
const Maestro::SequenceAgentEventBusId ebusId(GetEntityId(), animatedEntityId);
Maestro::SequenceAgentComponentRequestBus::EventResult(typeId, ebusId, &Maestro::SequenceAgentComponentRequestBus::Events::GetAnimatedAddressTypeId, animatableAddress);
return typeId;
}
///////////////////////////////////////////////////////////////////////////////////////////////////////
void EditorSequenceComponent::GetAssetDuration(AnimatedValue& returnValue, const AZ::EntityId& animatedEntityId, AZ::ComponentId componentId, const AZ::Data::AssetId& assetId)
{
const Maestro::SequenceAgentEventBusId ebusId(GetEntityId(), animatedEntityId);
EBUS_EVENT_ID(ebusId, Maestro::SequenceAgentComponentRequestBus, GetAssetDuration, returnValue, componentId, assetId);
}
///////////////////////////////////////////////////////////////////////////////////////////////////////
void EditorSequenceComponent::BuildGameEntity(AZ::Entity* gameEntity)
{
SequenceComponent *gameSequenceComponent = gameEntity->CreateComponent<SequenceComponent>();
gameSequenceComponent->m_sequence = m_sequence;
}
///////////////////////////////////////////////////////////////////////////////////////////////////////
AnimValueType EditorSequenceComponent::GetValueType([[maybe_unused]] const AZStd::string& animatableAddress)
{
// TODO: look up type from BehaviorContext Property
return AnimValueType::Float;
}
///////////////////////////////////////////////////////////////////////////////////////////////////////
bool EditorSequenceComponent::SetAnimatedPropertyValue(const AZ::EntityId& animatedEntityId, const Maestro::SequenceComponentRequests::AnimatablePropertyAddress& animatableAddress, const AnimatedValue& value)
{
const Maestro::SequenceAgentEventBusId ebusId(GetEntityId(), animatedEntityId);
bool changed = false;
bool animatedEntityIsSelected = false;
// put this component on the TickBus to refresh propertyGrids if it is Selected (and hence it's values will be shown in the EntityInspector)
AzToolsFramework::ToolsApplicationRequests::Bus::BroadcastResult(animatedEntityIsSelected, &AzToolsFramework::ToolsApplicationRequests::Bus::Events::IsSelected, animatedEntityId);
if (animatedEntityIsSelected && !AZ::TickBus::Handler::BusIsConnected())
{
AZ::TickBus::Handler::BusConnect();
}
EBUS_EVENT_ID_RESULT(changed, ebusId, Maestro::SequenceAgentComponentRequestBus, SetAnimatedPropertyValue, animatableAddress, value);
return changed;
}
void EditorSequenceComponent::OnTick([[maybe_unused]] float deltaTime, AZ::ScriptTimePoint time)
{
// refresh the property displays at a lower refresh rate
if ((time.GetMilliseconds() - s_lastPropertyRefreshTime.GetMilliseconds()) > s_refreshPeriodMilliseconds)
{
s_lastPropertyRefreshTime = time;
// refresh
AzToolsFramework::ToolsApplicationEvents::Bus::Broadcast(&AzToolsFramework::ToolsApplicationEvents::Bus::Events::InvalidatePropertyDisplay, AzToolsFramework::Refresh_Values);
// disconnect from tick bus now that we've refreshed
AZ::TickBus::Handler::BusDisconnect();
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////////
void EditorSequenceComponent::GetAnimatedPropertyValue(AnimatedValue& returnValue, const AZ::EntityId& animatedEntityId, const Maestro::SequenceComponentRequests::AnimatablePropertyAddress& animatableAddress)
{
const Maestro::SequenceAgentEventBusId ebusId(GetEntityId(), animatedEntityId);
EBUS_EVENT_ID(ebusId, Maestro::SequenceAgentComponentRequestBus, GetAnimatedPropertyValue, returnValue, animatableAddress);
}
///////////////////////////////////////////////////////////////////////////////////////////////////////
bool EditorSequenceComponent::MarkEntityAsDirty() const
{
bool retSuccess = false;
AZ::Entity* entity = nullptr;
EBUS_EVENT_RESULT(entity, AZ::ComponentApplicationBus, FindEntity, GetEntityId());
if (entity)
{
CEntityObject* entityObject = nullptr;
EBUS_EVENT_ID_RESULT(entityObject, GetEntityId(), AzToolsFramework::ComponentEntityEditorRequestBus, GetSandboxObject);
if (entityObject)
{
entityObject->SetModified(false);
retSuccess = true;
}
}
return retSuccess;
}
//=========================================================================
namespace ClassConverters
{
// recursively traverses XML tree rooted at node converting transform nodes. Returns true if any node was converted.
static bool convertTransformXMLNodes(XmlNodeRef node)
{
bool nodeConverted = false;
// recurse through children
for (int i = node->getChildCount(); --i >= 0;)
{
if (convertTransformXMLNodes(node->getChild(i)))
{
nodeConverted = true;
}
}
XmlString nodeType;
if (node->isTag("Node") && node->getAttr("Type", nodeType) && nodeType == "Component")
{
XmlString componentTypeId;
if (node->getAttr("ComponentTypeId", componentTypeId) && componentTypeId == "{27F1E1A1-8D9D-4C3B-BD3A-AFB9762449C0}") // Type Uuid AZ::EditorTransformComponentTypeId
{
static const char* paramTypeName = "paramType";
static const char* paramUserValueName = "paramUserValue";
static const char* virtualPropertyName = "virtualPropertyName";
// go through child nodes. Convert previous Position, Rotation or Scale tracks ByString to enumerated param types
for (const XmlNodeRef& childNode : node)
{
XmlString paramType;
if (childNode->isTag("Track") && childNode->getAttr(paramTypeName, paramType) && paramType == "ByString")
{
XmlString paramUserValue;
if (childNode->getAttr(paramUserValueName, paramUserValue) && paramUserValue == "Position")
{
childNode->setAttr(paramTypeName, "Position");
childNode->setAttr(virtualPropertyName, "Position");
childNode->delAttr(paramUserValueName);
nodeConverted = true;
}
else if (childNode->getAttr(paramUserValueName, paramUserValue) && paramUserValue == "Rotation")
{
childNode->setAttr(paramTypeName, "Rotation");
childNode->setAttr(virtualPropertyName, "Rotation");
childNode->delAttr(paramUserValueName);
nodeConverted = true;
}
else if (childNode->getAttr(paramUserValueName, paramUserValue) && paramUserValue == "Scale")
{
childNode->setAttr(paramTypeName, "Scale");
childNode->setAttr(virtualPropertyName, "Scale");
childNode->delAttr(paramUserValueName);
nodeConverted = true;
}
}
}
}
}
return nodeConverted;
}
static bool UpVersionAnimationData(AZ::SerializeContext& context, AZ::SerializeContext::DataElementNode& classElement)
{
if (classElement.GetVersion() == 0)
{
// upgrade V0 to V1 - change "Position", "Rotation", "Scale" anim params in Transform Component Nodes from AnimParamType::ByString to
// AnimParamType::Position, AnimParamType::Rotation, AnimParamType::Scale respectively
int serializedAnimStringIdx = classElement.FindElement(AZ::Crc32("SerializedString"));
if (serializedAnimStringIdx == -1)
{
AZ_Error("Serialization", false, "Failed to find 'SerializedString' element.");
return false;
}
AZStd::string serializedAnimString;
classElement.GetSubElement(serializedAnimStringIdx).GetData(serializedAnimString);
const char* buffer = serializedAnimString.c_str();
size_t size = serializedAnimString.length();
if (size > 0)
{
XmlNodeRef xmlArchive = gEnv->pSystem->LoadXmlFromBuffer(buffer, size);
// recursively traverse and convert through all nodes
if (convertTransformXMLNodes(xmlArchive))
{
// if a node was converted, replace the classElement Data with the converted XML
serializedAnimString = xmlArchive->getXML();
classElement.GetSubElement(serializedAnimStringIdx).SetData(context, serializedAnimString);
}
}
}
return true;
}
} // namespace ClassConverters
} // namespace Maestro