Files
UnrealEngine/Engine/Source/Runtime/MovieSceneTracks/Private/Bindings/MovieSceneSpawnableActorBinding.cpp
Brandyn / Techy fcc1b09210 init
2026-04-04 15:40:51 -05:00

636 lines
21 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "Bindings/MovieSceneSpawnableActorBinding.h"
#include "EntitySystem/MovieSceneSharedPlaybackState.h"
#include "Templates/SubclassOf.h"
#include "GameFramework/Actor.h"
#include "Engine/World.h"
#include "Engine/Level.h"
#include "IMovieSceneObjectSpawner.h"
#include "Particles/ParticleSystemComponent.h"
#include "EntitySystem/MovieSceneEntitySystemLinker.h"
#include "Misc/PackageName.h"
#include "Engine/Engine.h"
#include "Engine/LevelStreaming.h"
#include "MovieScenePossessable.h"
#include "MovieScene.h"
#include "IMovieScenePlayer.h"
#include "Engine/Blueprint.h"
#include "Engine/BlueprintGeneratedClass.h"
#include "Components/StaticMeshComponent.h"
#include "TransformData.h"
#include "MovieSceneBindingReferences.h"
#include "MovieSceneCommonHelpers.h"
#include "Serialization/ArchiveReplaceOrClearExternalReferences.h"
#include "Systems/MovieSceneDeferredComponentMovementSystem.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(MovieSceneSpawnableActorBinding)
static const FName SequencerActorTag(TEXT("SequencerActor"));
static const FName SequencerPreviewActorTag(TEXT("SequencerPreviewActor"));
#define LOCTEXT_NAMESPACE "MovieScene"
namespace UE::MovieSceneTracks
{
class FArchiveClearExternalReferences : public FArchiveReplaceOrClearExternalReferences<UObject>
{
using Super = FArchiveReplaceOrClearExternalReferences<UObject>;
TArray<UObject*> RemovedObjects;
public:
FArchiveClearExternalReferences(UObject* InSearchObject, const TMap<UObject*, UObject*>& InReplacementMap, UPackage* InDestPackage)
: Super(InSearchObject, InReplacementMap, InDestPackage, EArchiveReplaceObjectFlags::NullPrivateRefs | EArchiveReplaceObjectFlags::DelayStart )
{
SerializeSearchObject();
}
const TArray<UObject*>& GetRemovedObjects() const { return RemovedObjects; }
virtual FArchive& operator<<(UObject*& Obj) override
{
UObject* Original = Obj;
Super::operator<<(Obj);
if (Original && !Obj)
{
RemovedObjects.AddUnique(Original);
}
return *this;
}
};
static void LogRemovedExternalObjects(UObject* ReferencingObject, const TArray<UObject*>& InRemovedObjects)
{
if (InRemovedObjects.IsEmpty())
{
return;
}
const FString List = FString::JoinBy(InRemovedObjects, TEXT(", "), [](UObject* Object)
{
return ensure(Object) ? Object->GetPathName() : TEXT("null");
});
UE_LOG(LogMovieScene, Warning, TEXT("While saving %s, the following references were cleared because they were private and external: %s"),
*ReferencingObject->GetPathName(), *List
);
}
}
UObject* UMovieSceneSpawnableActorBindingBase::SpawnObjectInternal(UWorld* WorldContext, FName SpawnName, const FGuid& BindingId, int32 BindingIndex, UMovieScene& MovieScene, FMovieSceneSequenceIDRef TemplateID, TSharedRef<const UE::MovieScene::FSharedPlaybackState> SharedPlaybackState)
{
const EObjectFlags SpawnObjectFlags = RF_Transient | RF_Transactional;
TSubclassOf<AActor> ActorClass = GetActorClass();
if (!ActorClass || ActorClass->HasAllClassFlags(CLASS_NewerVersionExists))
{
return nullptr;
}
AActor* ActorTemplate = GetActorTemplate();
FString SpawnLabel;
#if WITH_EDITOR
const IConsoleVariable* AllowSetActorLabelCvar = IConsoleManager::Get().FindConsoleVariable(TEXT("LevelSequence.EnableReadableActorLabelsForSpawnables"));
const bool bAllowSetActorLabel = AllowSetActorLabelCvar && AllowSetActorLabelCvar->GetBool();
// Historically, setting the actor label has caused performance issues in some scenarios (by causing async loading flushes); however, there's no
// evidence for this anymore, so the cvar is here to turn off this behavior if needed.
if ((WorldContext->WorldType == EWorldType::Editor) || bAllowSetActorLabel)
{
if (FMovieScenePossessable* Possessable = MovieScene.FindPossessable(BindingId))
{
UMovieSceneSequence* Sequence = MovieScene.GetTypedOuter<UMovieSceneSequence>();
const FMovieSceneBindingReferences* BindingReferences = Sequence ? Sequence->GetBindingReferences() : nullptr;
if (ActorTemplate && BindingReferences && BindingReferences->GetReferences(BindingId).Num() > 1)
{
// If there are multiple bound objects, use the Object Template actor label instead of the possessable name
SpawnLabel = ActorTemplate->GetActorLabel();
}
else
{
SpawnLabel = Possessable->GetName();
}
}
else
{
SpawnLabel = GetDesiredBindingName();
if (SpawnLabel.Len() == 0)
{
SpawnLabel = SpawnName.ToString();
}
}
}
#endif
// Spawn the actor
FActorSpawnParameters SpawnInfo;
{
SpawnInfo.Name = SpawnName;
SpawnInfo.ObjectFlags = SpawnObjectFlags;
#if WITH_EDITOR
SpawnInfo.InitialActorLabel = SpawnLabel;
#endif
SpawnInfo.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
// allow pre-construction variables to be set.
SpawnInfo.bDeferConstruction = true;
SpawnInfo.Template = ActorTemplate;
SpawnInfo.OverrideLevel = WorldContext->PersistentLevel;
}
if (ActorTemplate)
{
// Disable all particle components so that they don't auto fire as soon as the actor is spawned. The particles should be triggered through the particle track.
for (UActorComponent* Component : ActorTemplate->GetComponents())
{
if (UParticleSystemComponent* ParticleComponent = Cast<UParticleSystemComponent>(Component))
{
// The particle needs to be set inactive in case its template was active.
ParticleComponent->SetActiveFlag(false);
Component->bAutoActivate = false;
}
}
}
FTransform SpawnTransformToUse = GetSpawnTransform();
AActor* SpawnedActor = WorldContext->SpawnActorAbsolute(ActorClass, SpawnTransformToUse, SpawnInfo);
if (!SpawnedActor)
{
return nullptr;
}
if (bNetAddressableName)
{
SpawnedActor->SetNetAddressable();
}
if (UMovieSceneEntitySystemLinker* Linker = SharedPlaybackState->GetLinker())
{
if (UMovieSceneDeferredComponentMovementSystem* DeferredMovementSystem = Linker->FindSystem<UMovieSceneDeferredComponentMovementSystem>())
{
for (UActorComponent* ActorComponent : SpawnedActor->GetComponents())
{
if (USceneComponent* SceneComponent = Cast<USceneComponent>(ActorComponent))
{
DeferredMovementSystem->DeferMovementUpdates(SceneComponent);
}
}
}
}
// The below block duplicates code in UMovieSceneSpawnableBinding, but this ensures that at least for this
// type of binding, these tags are added before calling FinishSpawning, as some client code may check for this tag
// in component initialization.
{
#if WITH_EDITOR
SpawnedActor->bIsEditorPreviewActor = false;
#endif
// tag this actor so we know it was spawned by sequencer
SpawnedActor->Tags.AddUnique(SequencerActorTag);
}
const bool bIsDefaultTransform = true;
SpawnedActor->FinishSpawning(SpawnTransformToUse, bIsDefaultTransform);
return SpawnedActor;
}
void UMovieSceneSpawnableActorBindingBase::DestroySpawnedObjectInternal(UObject* Object)
{
AActor* Actor = Cast<AActor>(Object);
if (!ensure(Actor))
{
return;
}
UWorld* World = Actor->GetWorld();
if (World)
{
const bool bNetForce = false;
const bool bShouldModifyLevel = false;
World->DestroyActor(Actor, bNetForce, bShouldModifyLevel);
}
}
void UMovieSceneSpawnableActorBindingBase::AutoSetNetAddressableName()
{
bNetAddressableName = false;
AActor* ActorTemplatePtr = GetActorTemplate();
if (ActorTemplatePtr && ActorTemplatePtr->FindComponentByClass<UStaticMeshComponent>() != nullptr)
{
bNetAddressableName = true;
}
}
FTransform UMovieSceneSpawnableActorBindingBase::GetSpawnTransform() const
{
FTransform ReturnSpawnTransform;
if (AActor* ActorTemplatePtr = GetActorTemplate())
{
if (USceneComponent* RootComponent = ActorTemplatePtr->GetRootComponent())
{
ReturnSpawnTransform.SetTranslation(RootComponent->GetRelativeLocation());
ReturnSpawnTransform.SetRotation(RootComponent->GetRelativeRotation().Quaternion());
ReturnSpawnTransform.SetScale3D(RootComponent->GetRelativeScale3D());
}
}
return ReturnSpawnTransform;
}
ULevelStreaming* GetLevelStreamingHelper(const FName& DesiredLevelName, const UWorld* World)
{
if (DesiredLevelName == NAME_None)
{
return nullptr;
}
const TArray<ULevelStreaming*>& StreamingLevels = World->GetStreamingLevels();
FString SafeLevelNameString = DesiredLevelName.ToString();
if (FPackageName::IsShortPackageName(SafeLevelNameString))
{
// Make sure MyMap1 and Map1 names do not resolve to a same streaming level
SafeLevelNameString.InsertAt(0, '/');
}
#if WITH_EDITOR
FWorldContext* WorldContext = GEngine->GetWorldContextFromWorld(World);
if (WorldContext && WorldContext->PIEInstance != INDEX_NONE)
{
SafeLevelNameString = UWorld::ConvertToPIEPackageName(SafeLevelNameString, WorldContext->PIEInstance);
}
#endif
for (ULevelStreaming* LevelStreaming : StreamingLevels)
{
if (LevelStreaming && LevelStreaming->GetWorldAssetPackageName().EndsWith(SafeLevelNameString, ESearchCase::IgnoreCase))
{
return LevelStreaming;
}
}
return nullptr;
}
UWorld* UMovieSceneSpawnableActorBinding::GetWorldContext(TSharedRef<const UE::MovieScene::FSharedPlaybackState> SharedPlaybackState) const
{
UObject* PlaybackContext = SharedPlaybackState->GetPlaybackContext();
UWorld* WorldContext = PlaybackContext ? PlaybackContext->GetWorld() : nullptr;
FName DesiredLevelName = LevelName;
if (DesiredLevelName != NAME_None)
{
if (WorldContext && WorldContext->GetFName() == DesiredLevelName)
{
// done, spawn into this world
}
else
{
ULevelStreaming* LevelStreaming = GetLevelStreamingHelper(DesiredLevelName, WorldContext);
if (LevelStreaming && LevelStreaming->GetWorldAsset().IsValid())
{
WorldContext = LevelStreaming->GetWorldAsset().Get();
}
else
{
UE_LOG(LogMovieScene, Warning, TEXT("Can't find sublevel '%s' to spawn into, defaulting to Persistent level"), *DesiredLevelName.ToString());
}
}
}
return WorldContext;
}
FName UMovieSceneSpawnableActorBindingBase::GetSpawnName(const FGuid& BindingId, UMovieScene& MovieScene, FMovieSceneSequenceIDRef TemplateID, TSharedRef<const UE::MovieScene::FSharedPlaybackState> SharedPlaybackState) const
{
// We use the net addressable name for spawnable actors on any non-editor, non-standalone world (ie, all clients, servers and PIE worlds)
UWorld* WorldContext = GetWorldContext(SharedPlaybackState);
const bool bUseNetAddressableName = bNetAddressableName && WorldContext && (WorldContext->WorldType != EWorldType::Editor) && (WorldContext->GetNetMode() != ENetMode::NM_Standalone);
FString DesiredBindingName = GetDesiredBindingName();
if (FMovieScenePossessable* Possessable = MovieScene.FindPossessable(BindingId))
{
if (DesiredBindingName.IsEmpty())
{
DesiredBindingName = Possessable->GetName();
}
#if WITH_EDITOR
UClass* ActorClass = GetActorClass();
if (bUseNetAddressableName)
{
return GetNetAddressableName(SharedPlaybackState, BindingId, TemplateID, DesiredBindingName);
}
else if (ensure(WorldContext != nullptr && WorldContext->PersistentLevel))
{
return MakeUniqueObjectName(WorldContext->PersistentLevel, ActorClass ? ActorClass : AActor::StaticClass(), *DesiredBindingName);
}
#else
return bUseNetAddressableName ? GetNetAddressableName(SharedPlaybackState, BindingId, TemplateID, DesiredBindingName) : NAME_None;
#endif
}
return NAME_None;
}
FName UMovieSceneSpawnableActorBindingBase::GetNetAddressableName(TSharedRef<const UE::MovieScene::FSharedPlaybackState> SharedPlaybackState, const FGuid& BindingId, FMovieSceneSequenceID SequenceID, const FString& BaseName) const
{
UObject* AddressingContext = nullptr;
if (IMovieScenePlayer* Player = UE::MovieScene::FPlayerIndexPlaybackCapability::GetPlayer(SharedPlaybackState))
{
AddressingContext = Player->AsUObject();
}
if (!AddressingContext)
{
if (UObject* PlaybackContext = SharedPlaybackState->GetPlaybackContext())
{
AddressingContext = PlaybackContext;
}
}
if (!AddressingContext)
{
return NAME_None;
}
TStringBuilder<128> AddressableName;
// Spawnable name
AddressableName.Append(*BaseName, BaseName.Len());
// SequenceID
AddressableName.Appendf(TEXT("_0x%08X"), SequenceID.GetInternalValue());
// Spawnable GUID
AddressableName.Appendf(TEXT("_%08X%08X%08X%08X"), BindingId.A, BindingId.B, BindingId.C, BindingId.D);
// Actor / player Name
if (AActor* OuterActor = AddressingContext->GetTypedOuter<AActor>())
{
AddressableName.AppendChar(TEXT('_'));
OuterActor->GetFName().AppendString(AddressableName);
}
else
{
AddressableName.AppendChar(TEXT('_'));
AddressingContext->GetFName().AppendString(AddressableName);
}
return FName(AddressableName.Len(), AddressableName.GetData());
}
TSubclassOf<AActor> UMovieSceneSpawnableActorBinding::GetActorClass() const
{
if (AActor* ActorTemplatePtr = GetActorTemplate())
{
return ActorTemplatePtr->GetClass();
}
return nullptr;
}
struct FIsSpawnable
{
FIsSpawnable() : bIsSpawnable(false) {}
explicit FIsSpawnable(bool bInIsSpawnable) : bIsSpawnable(bInIsSpawnable) {}
bool IsDefault() const { return !bIsSpawnable; }
bool bIsSpawnable;
};
void UMovieSceneSpawnableActorBinding::SetObjectTemplate(UObject* InObjectTemplate)
{
ensure(InObjectTemplate == nullptr || InObjectTemplate->IsA<AActor>());
checkf(InObjectTemplate == nullptr || !InObjectTemplate->HasAnyFlags(RF_ClassDefaultObject), TEXT("Setting CDOs as object templates is not supported. Please use the class directly."));
ActorTemplate = Cast<AActor>(InObjectTemplate);
if (ActorTemplate)
{
// TODO: We should move this out of FMovieSceneSpawnable eventually
FMovieSceneSpawnable::MarkSpawnableTemplate(*ActorTemplate);
}
AutoSetNetAddressableName();
}
void UMovieSceneSpawnableActorBinding::CopyObjectTemplate(UObject* InSourceObject, UMovieSceneSequence& MovieSceneSequence)
{
if (!InSourceObject)
{
return;
}
ensure(InSourceObject->IsA<AActor>());
if (AActor* SourceActor = Cast<AActor>(InSourceObject))
{
const FName ActorName = ActorTemplate ? ActorTemplate->GetFName() : SourceActor->GetFName();
if (ActorTemplate)
{
const FString NewName = MakeUniqueObjectName(MovieSceneSequence.GetMovieScene(), GetActorClass(), "ExpiredSpawnable").ToString();
// Without REN_DontCreateRedirectors, we'd create a redirector causing a name collision when we call MakeSpawnableTemplateFromInstance.
ActorTemplate->Rename(*NewName, nullptr, REN_DoNotDirty | REN_DontCreateRedirectors | REN_NonTransactional);
ActorTemplate->MarkAsGarbage();
ActorTemplate = nullptr;
}
ActorTemplate = Cast<AActor>(MovieSceneSequence.MakeSpawnableTemplateFromInstance(*SourceActor, ActorName));
check(ActorTemplate);
// TODO: We should move this out of FMovieSceneSpawnable eventually
FMovieSceneSpawnable::MarkSpawnableTemplate(*ActorTemplate);
AutoSetNetAddressableName();
MovieSceneSequence.MarkPackageDirty();
}
}
bool UMovieSceneSpawnableActorBinding::SupportsBindingCreationFromObject(const UObject* SourceObject) const
{
if (!SourceObject)
{
// In this case we would just make an empty binding
return true;
}
else if (SourceObject->IsA<AActor>())
{
return true;
}
else if (const UBlueprint* SourceBlueprint = Cast<const UBlueprint>(SourceObject))
{
return SourceBlueprint->GeneratedClass->IsChildOf(AActor::StaticClass());
}
#if WITH_EDITORONLY_DATA
else if (const UBlueprintGeneratedClass* SourceBlueprintGeneratedClass = Cast<const UBlueprintGeneratedClass>(SourceObject))
{
if (const UBlueprint* BlueprintGeneratedBy = Cast<const UBlueprint>(SourceBlueprintGeneratedClass->ClassGeneratedBy))
{
return BlueprintGeneratedBy->GeneratedClass->IsChildOf(AActor::StaticClass());
}
}
#endif
else if (const UClass* InClass = Cast<const UClass>(SourceObject))
{
return InClass->IsChildOf(AActor::StaticClass());
}
return false;
}
UMovieSceneCustomBinding* UMovieSceneSpawnableActorBinding::CreateNewCustomBinding(UObject* SourceObject, UMovieScene& OwnerMovieScene)
{
UMovieSceneSpawnableActorBinding* NewCustomBinding = nullptr;
const FName TemplateName = MakeUniqueObjectName(&OwnerMovieScene, UObject::StaticClass(), SourceObject ? SourceObject->GetFName() : TEXT("EmptyBinding"));
const FName InstancedBindingName = MakeUniqueObjectName(&OwnerMovieScene, UObject::StaticClass(), *FString(TemplateName.ToString() + TEXT("_CustomBinding")));
auto CreateBinding = [&]()
{
return NewObject<UMovieSceneSpawnableActorBinding>(&OwnerMovieScene, GetClass(), InstancedBindingName, RF_Transactional);
};
// Deal with creating a spawnable from an instance of an actor
if (AActor* Actor = Cast<AActor>(SourceObject))
{
// Remove any previous tags- new ones will be added as necessary during spawning
Actor->Tags.Remove(SequencerActorTag);
Actor->Tags.Remove(SequencerPreviewActorTag);
// If the source actor is not transactional, temporarily add the flag to ensure that the duplicated object is created with the transactional flag.
// This is necessary for the creation of the object to exist in the transaction buffer for multi-user workflows
const bool bWasTransactional = Actor->HasAnyFlags(RF_Transactional);
if (!bWasTransactional)
{
Actor->SetFlags(RF_Transactional);
}
NewCustomBinding = CreateBinding();
if (NewCustomBinding)
{
AActor* SpawnedActor = Cast<AActor>(StaticDuplicateObject(Actor, &OwnerMovieScene, TemplateName, RF_AllFlags & ~RF_Transient));
SpawnedActor->DetachFromActor(FDetachmentTransformRules(EDetachmentRule::KeepRelative, false));
#if WITH_EDITORONLY_DATA
SpawnedActor->bIsEditorPreviewActor = false;
#endif
NewCustomBinding->SetObjectTemplate(SpawnedActor);
if (!bWasTransactional)
{
Actor->ClearFlags(RF_Transactional);
}
// This achieves that the template actor keeps no reference to any external private objects.
// Such references would cause a crash when saving the owning UMovieScene / sequence.
// Example: An actor that has a reference to another actor in the level.
// SpawnedActor keep referencing the actor in the original map, which is a private external object.
// FArchiveReplaceOrClearExternalReferences handles recursively serializing subobjects, such as components.
UPackage* NewPackage = SpawnedActor->GetOutermost();
const UE::MovieSceneTracks::FArchiveClearExternalReferences ReplaceActorInvalidReferences(SpawnedActor, {}, NewPackage);
// User should be warned in case they notice weird behaviour (e.g. if the spawnable actor is selected in details panel,
// saving nulls out the references which the user may notice).
UE::MovieSceneTracks::LogRemovedExternalObjects(this, ReplaceActorInvalidReferences.GetRemovedObjects());
}
}
// If it's a blueprint, we need some special handling
else if (UBlueprint* SourceBlueprint = Cast<UBlueprint>(SourceObject))
{
if (!OwnerMovieScene.GetClass()->IsChildOf(SourceBlueprint->GeneratedClass->ClassWithin) || !SourceBlueprint->GeneratedClass->IsChildOf(AActor::StaticClass()))
{
return nullptr;
}
NewCustomBinding = CreateBinding();
if (NewCustomBinding)
{
NewCustomBinding->SetObjectTemplate(NewObject<UObject>(NewCustomBinding, SourceBlueprint->GeneratedClass, TemplateName, RF_Transactional));
}
}
#if WITH_EDITORONLY_DATA
else if (UBlueprintGeneratedClass* SourceBlueprintGeneratedClass = Cast<UBlueprintGeneratedClass>(SourceObject))
{
if (UBlueprint* BlueprintGeneratedBy = Cast<UBlueprint>(SourceBlueprintGeneratedClass->ClassGeneratedBy))
{
if (!OwnerMovieScene.GetClass()->IsChildOf(BlueprintGeneratedBy->GeneratedClass->ClassWithin) || !BlueprintGeneratedBy->GeneratedClass->IsChildOf(AActor::StaticClass()))
{
return nullptr;
}
NewCustomBinding = CreateBinding();
if (NewCustomBinding)
{
NewCustomBinding->SetObjectTemplate(NewObject<UObject>(NewCustomBinding, BlueprintGeneratedBy->GeneratedClass, TemplateName, RF_Transactional));
}
}
}
#endif
if (!NewCustomBinding)
{
if (UClass* InClass = Cast<UClass>(SourceObject ? SourceObject : AActor::StaticClass()))
{
if (!InClass->IsChildOf(AActor::StaticClass()))
{
return nullptr;
}
NewCustomBinding = CreateBinding();
if (NewCustomBinding)
{
NewCustomBinding->SetObjectTemplate(NewObject<UObject>(&OwnerMovieScene, InClass, TemplateName, RF_Transactional));
}
}
}
return NewCustomBinding;
}
#if WITH_EDITOR
bool UMovieSceneSpawnableActorBinding::SupportsConversionFromBinding(const FMovieSceneBindingReference& BindingReference, const UObject* SourceObject) const
{
return SupportsBindingCreationFromObject(SourceObject);
}
UMovieSceneCustomBinding* UMovieSceneSpawnableActorBinding::CreateCustomBindingFromBinding(const FMovieSceneBindingReference& BindingReference, UObject* SourceObject, UMovieScene& OwnerMovieScene)
{
return CreateNewCustomBinding(SourceObject, OwnerMovieScene);
}
FText UMovieSceneSpawnableActorBinding::GetBindingTypePrettyName() const
{
return LOCTEXT("MovieSceneSpawnableActorBinding", "Spawnable Actor");
}
#endif
void UMovieSceneSpawnableActorBinding::PostDuplicate(EDuplicateMode::Type DuplicateMode)
{
Super::PostDuplicate(DuplicateMode);
// Post duplication, the Actor Template will still be pointing to the original Sequence.
// We therefore need to rebuild the object template
if (ActorTemplate != nullptr)
{
// Only duplicate the inner ActorTemplate if this binding has been bound into a different valid UMovieScene.
if (UMovieScene* OuterMovieScene = GetTypedOuter<UMovieScene>();
OuterMovieScene != nullptr && ActorTemplate->GetTypedOuter<UMovieScene>() != OuterMovieScene)
{
// Reparent the actor template to the duplicated movie scene
ActorTemplate = Cast<AActor>(StaticDuplicateObject(ActorTemplate, OuterMovieScene));
if (ActorTemplate)
{
FMovieSceneSpawnable::MarkSpawnableTemplate(*ActorTemplate);
}
AutoSetNetAddressableName();
}
}
}
#undef LOCTEXT_NAMESPACE