// Copyright Epic Games, Inc. All Rights Reserved. #pragma once #include "Containers/Map.h" #include "Containers/SparseArray.h" #include "CoreTypes.h" #include "EntitySystem/BuiltInComponentTypes.h" #include "EntitySystem/MovieSceneComponentTypeInfo.h" #include "EntitySystem/MovieSceneEntityIDs.h" #include "EntitySystem/MovieSceneEntitySystem.h" #include "EntitySystem/MovieSceneEntitySystemTask.h" #include "EntitySystem/MovieSceneEntitySystemTypes.h" #include "Templates/PointerIsConvertibleFromTo.h" #include "UObject/ObjectMacros.h" #include "UObject/UObjectGlobals.h" #include "MovieSceneEntityGroupingSystem.generated.h" #define UE_API MOVIESCENE_API class UMovieSceneEntityGroupingSystem; namespace UE::MovieScene { struct FAddGroupMutation; struct FUpdateGroupsTask; /** Concept that checks whether a grouping policy supports batch operations based on the presence of a InitializeGroupKeys function */ struct CBatchGroupingPolicy { template auto Requires(T& In) -> decltype(&T::InitializeGroupKeys); }; /** * Utility class used by the grouping system's policies (see below) to manage groups. */ struct FEntityGroupBuilder { UE_API FEntityGroupBuilder(UMovieSceneEntityGroupingSystem* InOwner, FEntityGroupingPolicyKey InPolicyKey); /** Make a full group ID from an existing group index */ FEntityGroupID MakeGroupID(int32 GroupIndex) const { return FEntityGroupID(PolicyKey, GroupIndex); } /** Make an invalid group ID that is associated with the policy key */ FEntityGroupID MakeInvalidGroupID() const { return FEntityGroupID(PolicyKey, INDEX_NONE); } /** Add the entity to the given group. The entity must already have the group ID component. */ UE_API void AddEntityToGroup(const FMovieSceneEntityID& InEntity, const FEntityGroupID& InNewGroupID); /** Remove the entity from the given group. The entity must already have the group ID component. */ UE_API void RemoveEntityFromGroup(const FMovieSceneEntityID& InEntity, const FEntityGroupID& InPreviousGroupID); UE_API int32 AllocateGroupIndex(); UE_API void ReportUsedGroupIndex(int32 GroupIndex); private: UMovieSceneEntityGroupingSystem* Owner; FEntityGroupingPolicyKey PolicyKey; }; /** * Base class for grouping handlers, used by the grouping system (see below). */ struct IEntityGroupingHandler { virtual ~IEntityGroupingHandler() {} virtual void ProcessAllocation(FEntityAllocationIteratorItem Item, FReadEntityIDs EntityIDs, TWrite GroupIDs, FEntityGroupBuilder* Builder) = 0; virtual void OnGroupIndexFreed(int32 InGroupIndex) {} #if WITH_EDITOR virtual void OnObjectsReplaced(const TMap& ReplacementMap) = 0; #endif }; /** * Strongly-typed grouping handler class, which knows about the exact components to look for, and how * to use them to group entities. */ template struct TEntityGroupingHandlerImpl; template struct TEntityGroupingHandler : TEntityGroupingHandlerImpl, ComponentTypes...> { TEntityGroupingHandler(GroupingPolicy&& InPolicy, TComponentTypeID... InComponents) : TEntityGroupingHandlerImpl, ComponentTypes...>( MoveTemp(InPolicy), InComponents...) { } }; template struct TEntityGroupingHandlerBase { using GroupKeyType = typename GroupingPolicy::GroupKeyType; int32 GetOrAllocateGroupIndex(typename TCallTraits::ParamType InGroupKey, FEntityGroupBuilder* Builder) { int32& GroupIndex = GroupKeyToIndex.FindOrAdd(InGroupKey, INDEX_NONE); if (GroupIndex == INDEX_NONE) { // This group key isn't known to us... let's allocate a new group index for it. // Try to find an available index first. Otherwise use a new high index. GroupIndex = Builder->AllocateGroupIndex(); IndexToGroupKey.FindOrAdd(GroupIndex, InGroupKey); } else { // We know this group key, so we'll return the group index we already have // associated with it. We just need to "revive" it in case it was scheduled // for being freed. Builder->ReportUsedGroupIndex(GroupIndex); } return GroupIndex; } protected: /** The group keys that we know about, mapped to their corresponding group index */ TMap GroupKeyToIndex; TMap IndexToGroupKey; }; template struct TEntityGroupingHandlerImpl, ComponentTypes...> : TEntityGroupingHandlerBase, IEntityGroupingHandler { using GroupKeyType = typename GroupingPolicy::GroupKeyType; /** The grouping policy */ GroupingPolicy Policy; /** The components that are required for making up a group key */ TTuple...> Components; TEntityGroupingHandlerImpl(GroupingPolicy&& InPolicy, TComponentTypeID... InComponents) : Policy(MoveTemp(InPolicy)) , Components(InComponents...) { } /** Process an allocation and group the entities found therein */ virtual void ProcessAllocation(FEntityAllocationIteratorItem Item, FReadEntityIDs EntityIDs, TWrite GroupIDs, FEntityGroupBuilder* Builder) override { const FBuiltInComponentTypes* BuiltInComponents = FBuiltInComponentTypes::Get(); const FComponentMask& AllocationType = Item.GetAllocationType(); const bool bNeedsLink = AllocationType.Contains(BuiltInComponents->Tags.NeedsLink); const bool bNeedsUnlink = AllocationType.Contains(BuiltInComponents->Tags.NeedsUnlink); ensure(bNeedsLink || bNeedsUnlink); if (bNeedsLink) { VisitLinkedEntities(Item, EntityIDs, GroupIDs, Builder); } else if (bNeedsUnlink) { VisitUnlinkedEntities(Item, EntityIDs, GroupIDs, Builder); } } void VisitLinkedEntities(FEntityAllocationIteratorItem Item, FReadEntityIDs EntityIDs, TWrite GroupIDs, FEntityGroupBuilder* Builder) { if constexpr (TModels_V) { const FEntityAllocation* Allocation = Item.GetAllocation(); Policy.InitializeGroupKeys(*this, Builder, Item, EntityIDs, GroupIDs, Allocation->ReadComponents(Components.template Get())...); } else { const FEntityAllocation* Allocation = Item.GetAllocation(); const int32 Num = Allocation->Num(); const FEntityGroupID InvalidGroupID = Builder->MakeInvalidGroupID(); TTuple...> ComponentReaders( Allocation->ReadComponents(Components.template Get())...); for (int32 Index = 0; Index < Num; ++Index) { GroupKeyType GroupKey; const bool bValidGroupKey = Policy.GetGroupKey(ComponentReaders.template Get()[Index]..., GroupKey); const FMovieSceneEntityID EntityID(EntityIDs[Index]); FEntityGroupID& GroupID(GroupIDs[Index]); if (bValidGroupKey) { // Find or create the appropriate group and put the entity in it. int32 NewGroupIndex = this->GetOrAllocateGroupIndex(GroupKey, Builder); FEntityGroupID NewGroupID = Builder->MakeGroupID(NewGroupIndex); Builder->AddEntityToGroup(EntityID, NewGroupID); GroupID = NewGroupID; } else { // This entity doesn't belong to any group. // Let's assign an invalid group ID that nonetheless has a valid policy key // pointing to this grouping. GroupID = InvalidGroupID; } } } } void VisitUnlinkedEntities(FEntityAllocationIteratorItem Item, FReadEntityIDs EntityIDs, TWrite GroupIDs, FEntityGroupBuilder* Builder) { const FEntityAllocation* Allocation = Item.GetAllocation(); const int32 Num = Allocation->Num(); for (int32 Index = 0; Index < Num; ++Index) { const FMovieSceneEntityID EntityID(EntityIDs[Index]); FEntityGroupID& GroupID(GroupIDs[Index]); if (GroupID.HasGroup()) { Builder->RemoveEntityFromGroup(EntityID, GroupID); // Leave the GroupID on the entity so that downstream systems can use it to track // that this entity is leaving its group, but flag it so we don't re-free it. ensure(!EnumHasAllFlags(GroupID.Flags , EEntityGroupFlags::RemovedFromGroup)); GroupID.Flags |= EEntityGroupFlags::RemovedFromGroup; } } } virtual void OnGroupIndexFreed(int32 GroupIndex) override { GroupKeyType Key = this->IndexToGroupKey.FindChecked(GroupIndex); ensure(this->IndexToGroupKey.Remove(GroupIndex) == 1); ensure(this->GroupKeyToIndex.Remove(Key) == 1); } #if WITH_EDITOR virtual void OnObjectsReplaced(const TMap& ReplacementMap) override { // Get a list of keys that contain replaced objects. TMap ReplacedKeys; for (const TPair& Pair : this->GroupKeyToIndex) { GroupKeyType NewKey = Pair.Key; if (Policy.OnObjectsReplaced(NewKey, ReplacementMap)) { ReplacedKeys.Add(Pair.Key, NewKey); } } // Replace the keys but keep the group indices. for (const TPair& Pair : ReplacedKeys) { int32 GroupIndex; const bool bRemoved = this->GroupKeyToIndex.RemoveAndCopyValue(Pair.Key, GroupIndex); if (ensure(bRemoved)) { this->GroupKeyToIndex.Add(Pair.Value, GroupIndex); this->IndexToGroupKey.Add(GroupIndex, Pair.Value); } } } #endif }; namespace Private { template bool ReplaceGroupKeyObjectElement(T&& InElem, const TMap& ReplacementMap) { return false; } template typename TEnableIf::Value, bool>::Type ReplaceGroupKeyObjectElement(T& InOutElem, const TMap& ReplacementMap) { UObject* CurrentObject = InOutElem; if (UObject* const* NewObject = ReplacementMap.Find(CurrentObject)) { InOutElem = NewObject; return true; } return false; } } /** * A simple grouping policy that uses a tuple of component values as the group key. */ template struct TTupleGroupingPolicy { using GroupKeyType = TTuple; bool GetGroupKey(ComponentTypes... InComponents, GroupKeyType& OutGroupKey) { OutGroupKey = MakeTuple(InComponents...); return true; } bool OnObjectsReplaced(GroupKeyType& InOutKey, const TMap& ReplacementMap) { bool bReplaced = false; VisitTupleElements([&ReplacementMap, &bReplaced](auto& InElem) { bReplaced |= Private::ReplaceGroupKeyObjectElement(InElem, ReplacementMap); }, InOutKey); return bReplaced; } }; } // namespace UE::MovieScene UCLASS(MinimalAPI) class UMovieSceneEntityGroupingSystem : public UMovieSceneEntitySystem { public: using FEntityGroupID = UE::MovieScene::FEntityGroupID; using FEntityGroupingPolicyKey = UE::MovieScene::FEntityGroupingPolicyKey; using IEntityGroupingHandler = UE::MovieScene::IEntityGroupingHandler; using FMovieSceneEntityID = UE::MovieScene::FMovieSceneEntityID; GENERATED_BODY() UE_API UMovieSceneEntityGroupingSystem(const FObjectInitializer& ObjInit); /** * Add a new grouping policy that will use the given components to make up a group key. * * Grouping policies must be simple structs that can be copied and owned by the grouping system, and * that implement the following members: * * - GroupKeyType [mandatory] * A typedef or alias to the group key type to use to group entities together. * * - GetGroupKey(ComponentTypes... InComponents, GroupKeyType& OutGroupKey) [mandatory] * A function that creates a group key used to group entities together. * Returns true to indicate the key is valid, false otherwise. If false, the entity * corresponding to the given components will not be grouped. * * - OnObjectsReplaced(GroupKeyType& InOut, const TMap&) [mandatory, sadly] * Potentially changes a key if it contains a pointer to a replaced object. Should * return true if any replacement occured. * * - PreTask() [optional] * A function called before any grouping is done during an instantiation phase. * * - PostTask() [optional] * A function called after any grouping is done dunring an instantiation phase. * * - PreProcessGroups() [optional] * * - PostProcessGroups() [optional] */ template FEntityGroupingPolicyKey AddGrouping(GroupingPolicy&& InPolicy, TComponentTypeID... InComponents) { return AddGrouping(Forward(InPolicy), UE::MovieScene::FEntityComponentFilter(), InComponents...); } template FEntityGroupingPolicyKey AddGrouping(GroupingPolicy&& InPolicy, UE::MovieScene::FEntityComponentFilter&& InComponentFilter, TComponentTypeID... InComponents) { using namespace UE::MovieScene; using NewGroupHandlerType = TEntityGroupingHandler; static_assert(sizeof(NewGroupHandlerType) <= 256, "Handler type too big! Please increase the TInlineValue size."); const int32 HandlerIndex = GroupHandlers.Emplace(); FEntityGroupingHandlerInfo& HandlerInfo = GroupHandlers[HandlerIndex]; HandlerInfo.Handler.Emplace(MoveTemp(InPolicy), InComponents...); HandlerInfo.ComponentFilter = MoveTemp(InComponentFilter); HandlerInfo.ComponentFilter.All({ InComponents... }); FEntityGroupingPolicyKey NewPolicyKey{ HandlerIndex }; return NewPolicyKey; } /** * Add a new grouping policy that will make a key that is a tuple of the given components' values. */ template FEntityGroupingPolicyKey AddGrouping(TComponentTypeID... InComponents) { UE::MovieScene::TTupleGroupingPolicy TuplePolicy; return AddGrouping(MoveTemp(TuplePolicy), InComponents...); } /** * Remove a previously added grouping policy. */ UE_API void RemoveGrouping(FEntityGroupingPolicyKey InPolicyKey); /** * Allocate a new group index used to uniquely identify a collection of entities that animate the same target. * Group indices are globally unique within this system, regardless of the 'type' of the target. */ UE_API int32 AllocateGroupIndex(FEntityGroupingPolicyKey InPolicy); /** * Return the maximum number of groups currently allocated */ int32 NumGroups() const { return AllocatedGroupIndices.GetMaxIndex(); } void FreeEmptyGroups(); private: virtual bool IsRelevantImpl(UMovieSceneEntitySystemLinker* InLinker) const override; virtual void OnRun(FSystemTaskPrerequisites& InPrerequisites, FSystemSubsequentTasks& Subsequents) override; virtual void OnLink() override; virtual void OnUnlink() override; virtual void OnCleanTaggedGarbage() override; void ProcessModifiedGroups(); #if WITH_EDITOR void OnObjectsReplaced(const TMap& ReplacementMap); #endif private: /** The list of group indices in use */ TSparseArray AllocatedGroupIndices; struct FEntityGroupInfo { int32 NumEntities = 0; }; TMap Groups; TSparseArray EntityIDToGroup; /** The transient list of groups freed this frame */ TBitArray<> EmptyGroupIndices; struct FEntityGroupingHandlerInfo { TInlineValue Handler; UE::MovieScene::FEntityComponentFilter ComponentFilter; }; TSparseArray GroupHandlers; friend struct UE::MovieScene::FAddGroupMutation; friend struct UE::MovieScene::FUpdateGroupsTask; friend struct UE::MovieScene::FEntityGroupBuilder; }; #undef UE_API