Files
UnrealEngine/Engine/Plugins/Runtime/MassGameplay/Source/MassSpawner/Private/MassSpawner.cpp
2025-05-18 13:04:45 +08:00

628 lines
18 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "MassSpawner.h"
#include "Engine/Engine.h"
#include "Engine/World.h"
#include "UObject/ConstructorHelpers.h"
#include "Components/SceneComponent.h"
#include "Components/BillboardComponent.h"
#include "MassSpawnerTypes.h"
#include "MassSpawnerSubsystem.h"
#include "MassSimulationSubsystem.h"
#include "VisualLogger/VisualLogger.h"
#include "MassEntityConfigAsset.h"
#include "MassEntityManager.h"
#include "EngineUtils.h"
#include "Engine/StreamableManager.h"
#include "Engine/AssetManager.h"
#include "MassSpawnLocationProcessor.h"
#include "MassExecutor.h"
#include "MassEntityUtils.h"
#include "MassProcessingContext.h"
#if WITH_EDITOR
#include "Engine/Texture2D.h"
#endif
namespace UE::MassSpawner
{
float ScalabilitySpawnDensityMultiplier = 1.f;
FAutoConsoleVariableRef CVarScalabilitySpawnDensityMultiplier(TEXT("ai.mass.scalability.SpawnDensityMultiplier"), ScalabilitySpawnDensityMultiplier, TEXT("Spawn Density Multiplier, must be set before Mass Spawn Init"), ECVF_Scalability);
#if WITH_EDITOR
static FAutoConsoleCommandWithWorld ForceSpawningCommand(
TEXT("ai.mass.ForceSpawn"),
TEXT("Command to Force Spawn all mass entities generated by MassSpawners"),
FConsoleCommandWithWorldDelegate::CreateLambda([](UWorld* World)
{
for (TActorIterator<AActor> It(World, AMassSpawner::StaticClass()); It; ++It)
{
if (AMassSpawner* Spawner = Cast<AMassSpawner>(*It))
{
Spawner->DoSpawning();
}
}
}));
static FAutoConsoleCommandWithWorld ForceDespawningCommand(
TEXT("ai.mass.ForceDespawn"),
TEXT("Command to Force Despawn all mass entities generated by MassSpawners"),
FConsoleCommandWithWorldDelegate::CreateLambda([](UWorld* World)
{
for (TActorIterator<AActor> It(World, AMassSpawner::StaticClass()); It; ++It)
{
if (AMassSpawner* Spawner = Cast<AMassSpawner>(*It))
{
Spawner->DoDespawning();
}
}
}));
static FAutoConsoleCommandWithWorld ResetSpawningCommand(
TEXT("ai.mass.ResetSpawning"),
TEXT("Command to Force Despawn and Respawn all mass entities generated by MassSpawners"),
FConsoleCommandWithWorldDelegate::CreateLambda([](UWorld* World)
{
for (TActorIterator<AActor> It(World, AMassSpawner::StaticClass()); It; ++It)
{
if (AMassSpawner* Spawner = Cast<AMassSpawner>(*It))
{
Spawner->DoDespawning();
Spawner->DoSpawning();
}
}
}));
#endif // WITH_EDITOR
}
AMassSpawner::AMassSpawner()
{
RootComponent = CreateDefaultSubobject<USceneComponent>(TEXT("SceneComp"));
RootComponent->Mobility = EComponentMobility::Static;
#if WITH_EDITOR
SpriteComponent = CreateEditorOnlyDefaultSubobject<UBillboardComponent>(TEXT("Sprite"));
// SpriteComponent can be null for editor builds running "-game|server"
if (!IsRunningCommandlet() && SpriteComponent != nullptr)
{
// Structure to hold one-time initialization
struct FConstructorStatics
{
ConstructorHelpers::FObjectFinderOptional<UTexture2D> IconTextureObject;
FName MassSpawnerID;
FText MassSpawnerName;
FConstructorStatics()
: IconTextureObject(TEXT("/MassGameplay/S_MassCrowd"))
, MassSpawnerID(TEXT("MassSpawner"))
, MassSpawnerName(NSLOCTEXT("SpriteCategory", "MassSpawner", "MassSpawner"))
{
}
};
static FConstructorStatics ConstructorStatics;
SpriteComponent->Sprite = ConstructorStatics.IconTextureObject.Get();
SpriteComponent->SetRelativeScale3D(FVector(0.5f, 0.5f, 0.5f));
SpriteComponent->SpriteInfo.Category = ConstructorStatics.MassSpawnerID;
SpriteComponent->SpriteInfo.DisplayName = ConstructorStatics.MassSpawnerName;
SpriteComponent->SetupAttachment(RootComponent);
SpriteComponent->Mobility = EComponentMobility::Static;
}
#endif // WITH_EDITOR
SetCanBeDamaged(false);
#if UE_BUILD_SHIPPING || UE_BUILD_TEST
SetActorHiddenInGame(true);
#endif
bAutoSpawnOnBeginPlay = true;
bOverrideSchematics = false;
}
void AMassSpawner::PostLoad()
{
Super::PostLoad();
for (FMassSpawnDataGenerator& SpawnPointsGenerator : SpawnDataGenerators)
{
if (SpawnPointsGenerator.GeneratorClass)
{
SpawnPointsGenerator.GeneratorInstance = NewObject<UMassEntitySpawnDataGeneratorBase>(this, SpawnPointsGenerator.GeneratorClass);
SpawnPointsGenerator.GeneratorClass = nullptr;
MarkPackageDirty();
}
}
}
void AMassSpawner::PostRegisterAllComponents()
{
Super::PostRegisterAllComponents();
if (HasAnyFlags(RF_ClassDefaultObject) == false)
{
UWorld* World = GetWorld();
check(World);
// This is a temp fix for streaming levels async loading MassSpawners after UMassSpawnerSubsystem::OnPostWorldInit,
// in the long run we are going to need a better system for making sure all the entity templates are registered
// on the clients before replication of Agents occurs. This is only required to be done for clients.
if (GEngine->GetNetMode(GetWorld()) == NM_Client)
{
UMassSpawnerSubsystem* MassSpawnerSubsystem = UWorld::GetSubsystem<UMassSpawnerSubsystem>(World);
if (MassSpawnerSubsystem)
{
RegisterEntityTemplates();
}
else
{
FWorldDelegates::OnPostWorldInitialization.AddUObject(this, &AMassSpawner::OnPostWorldInit);
}
}
}
}
void AMassSpawner::OnPostWorldInit(UWorld* World, const UWorld::InitializationValues)
{
if (World == GetWorld())
{
UMassSpawnerSubsystem* MassSpawnerSubsystem = UWorld::GetSubsystem<UMassSpawnerSubsystem>(World);
check(MassSpawnerSubsystem);
RegisterEntityTemplates();
FWorldDelegates::OnPostWorldInitialization.Remove(OnPostWorldInitDelegateHandle);
}
}
void AMassSpawner::BeginDestroy()
{
FWorldDelegates::OnPostWorldInitialization.Remove(OnPostWorldInitDelegateHandle);
DoDespawning();
if (StreamingHandle.IsValid() && StreamingHandle->IsActive())
{
StreamingHandle->CancelHandle();
}
Super::BeginDestroy();
}
void AMassSpawner::BeginPlay()
{
check(GEngine);
Super::BeginPlay();
const ENetMode NetMode = GEngine->GetNetMode(GetWorld());
if (bAutoSpawnOnBeginPlay && NetMode != NM_Client)
{
const UMassSimulationSubsystem* MassSimulationSubsystem = UWorld::GetSubsystem<UMassSimulationSubsystem>(GetWorld());
if (MassSimulationSubsystem == nullptr || MassSimulationSubsystem->IsSimulationStarted())
{
DoSpawning();
}
else
{
SimulationStartedHandle = UMassSimulationSubsystem::GetOnSimulationStarted().AddLambda([this](UWorld* InWorld)
{
UWorld* World = GetWorld();
if (World == InWorld)
{
DoSpawning();
}
});
}
}
}
void AMassSpawner::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
UMassSimulationSubsystem::GetOnSimulationStarted().Remove(SimulationStartedHandle);
DoDespawning();
Super::EndPlay(EndPlayReason);
}
#if WITH_EDITOR
void AMassSpawner::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
{
static const FName EntityTypesName = GET_MEMBER_NAME_CHECKED(AMassSpawner, EntityTypes);
Super::PostEditChangeProperty(PropertyChangedEvent);
if (PropertyChangedEvent.Property)
{
const FName PropName = PropertyChangedEvent.Property->GetFName();
if (PropName == EntityTypesName)
{
// TODO: Should optimize this, i.e. set a dirty flag and update only when needed.
UMassSpawnerSubsystem* SpawnerSystem = UWorld::GetSubsystem<UMassSpawnerSubsystem>(GetWorld());
if (SpawnerSystem)
{
RegisterEntityTemplates();
}
}
}
}
void AMassSpawner::DEBUG_Spawn()
{
DoSpawning();
}
void AMassSpawner::DEBUG_Clear()
{
DoDespawning();
}
#endif // WITH_EDITOR
void AMassSpawner::RegisterEntityTemplates()
{
UWorld* World = GetWorld();
check(World);
for (FMassSpawnedEntityType& EntityType : EntityTypes)
{
if (const UMassEntityConfigAsset* EntityConfig = EntityType.GetEntityConfig())
{
EntityConfig->GetOrCreateEntityTemplate(*World);
}
}
}
void AMassSpawner::DoSpawning()
{
// no spawn point generators configured. Let user know and fall back to the spawner's location
if (SpawnDataGenerators.Num() == 0)
{
UE_VLOG_UELOG(this, LogMassSpawner, Warning, TEXT("No Spawn Data Generators configured."));
return;
}
if (EntityTypes.Num() == 0)
{
UE_VLOG_UELOG(this, LogMassSpawner, Warning, TEXT("No EntityTypes configured."));
return;
}
AllGeneratedResults.Reset();
float TotalProportion = 0.0f;
for (FMassSpawnDataGenerator& Generator : SpawnDataGenerators)
{
if (Generator.GeneratorInstance)
{
Generator.bDataGenerated = false;
TotalProportion += Generator.Proportion;
}
}
if (TotalProportion <= 0.0f)
{
UE_VLOG_UELOG(this, LogMassSpawner, Error, TEXT("The total combined proportion of all the generator needs to be greater than 0."));
return;
}
// Check if it needs loading
if (StreamingHandle.IsValid() && StreamingHandle->IsActive())
{
// @todo, instead of blindly canceling, we should remember what was asked to load with that handle and compare if more is needed?
StreamingHandle->CancelHandle();
}
TArray<FSoftObjectPath> AssetsToLoad;
for (const FMassSpawnedEntityType& EntityType : EntityTypes)
{
if (!EntityType.IsLoaded())
{
AssetsToLoad.Add(EntityType.EntityConfig.ToSoftObjectPath());
}
}
const int32 TotalSpawnCount = GetSpawnCount();
auto GenerateSpawningPoints = [this, TotalSpawnCount, TotalProportion]()
{
int32 SpawnCountRemaining = TotalSpawnCount;
float ProportionRemaining = TotalProportion;
for (FMassSpawnDataGenerator& Generator : SpawnDataGenerators)
{
if (Generator.Proportion == 0.0f || ProportionRemaining <= 0.0f)
{
// If there's nothing to spawn, mark the generator done as OnSpawnDataGenerationFinished() will wait for all generators to complete before the actual spawning.
Generator.bDataGenerated = true;
continue;
}
if (Generator.GeneratorInstance)
{
const float ProportionRatio = FMath::Min(Generator.Proportion / ProportionRemaining, 1.0f);
const int32 SpawnCount = FMath::CeilToInt(static_cast<float>(SpawnCountRemaining) * ProportionRatio);
FFinishedGeneratingSpawnDataSignature Delegate = FFinishedGeneratingSpawnDataSignature::CreateUObject(this, &AMassSpawner::OnSpawnDataGenerationFinished, &Generator);
Generator.GeneratorInstance->Generate(*this, EntityTypes, SpawnCount, Delegate);
SpawnCountRemaining -= SpawnCount;
ProportionRemaining -= Generator.Proportion;
}
}
};
if (AssetsToLoad.Num())
{
FStreamableManager& StreamableManager = UAssetManager::GetStreamableManager();
StreamingHandle = StreamableManager.RequestAsyncLoad(AssetsToLoad, FStreamableDelegate::CreateWeakLambda(this, GenerateSpawningPoints));
}
else
{
GenerateSpawningPoints();
}
}
void AMassSpawner::OnSpawnDataGenerationFinished(TConstArrayView<FMassEntitySpawnDataGeneratorResult> Results, FMassSpawnDataGenerator* FinishedGenerator)
{
// @todo: this can be potentially expensive copy for the instanced structs, could there be a way to use move gere instead?
AllGeneratedResults.Append(Results.GetData(), Results.Num());
bool bAllSpawnPointsGenerated = true;
bool bFoundFinishedGenerator = false;
for (FMassSpawnDataGenerator& Generator : SpawnDataGenerators)
{
if (&Generator == FinishedGenerator)
{
Generator.bDataGenerated = true;
bFoundFinishedGenerator = true;
}
bAllSpawnPointsGenerated &= Generator.bDataGenerated;
}
checkf(bFoundFinishedGenerator, TEXT("Something went wrong, we are receiving a callback on an unknow spawn point generator"));
if (bAllSpawnPointsGenerated)
{
SpawnGeneratedEntities(AllGeneratedResults);
AllGeneratedResults.Reset();
}
}
int32 AMassSpawner::GetSpawnCount() const
{
const float FinalSpawningCountScale = SpawningCountScale * UE::MassSpawner::ScalabilitySpawnDensityMultiplier;
return static_cast<int32>(FinalSpawningCountScale * static_cast<float>(Count));
}
UMassProcessor* AMassSpawner::GetPostSpawnProcessor(TSubclassOf<UMassProcessor> ProcessorClass)
{
if (!ProcessorClass)
{
return nullptr;
}
TObjectPtr<UMassProcessor>* const Initializer = PostSpawnProcessors.FindByPredicate([ProcessorClass](const UMassProcessor* Processor)
{
return Processor && Processor->GetClass() == ProcessorClass;
}
);
if (Initializer)
{
return *Initializer;
}
UMassProcessor* NewInitializer = NewObject<UMassProcessor>(this, ProcessorClass);
FMassEntityManager* EntityManager = UE::Mass::Utils::GetEntityManager(this);
if (ensureMsgf(EntityManager, TEXT("Unable to determine the current MassEntityManager")))
{
NewInitializer->CallInitialize(this, EntityManager->AsShared());
PostSpawnProcessors.Add(NewInitializer);
return NewInitializer;
}
return nullptr;
}
void AMassSpawner::SpawnGeneratedEntities(TConstArrayView<FMassEntitySpawnDataGeneratorResult> Results)
{
UMassSpawnerSubsystem* SpawnerSystem = UWorld::GetSubsystem<UMassSpawnerSubsystem>(GetWorld());
if (SpawnerSystem == nullptr)
{
UE_VLOG_UELOG(this, LogMassSpawner, Error, TEXT("UMassSpawnerSubsystem missing while trying to spawn entities"));
return;
}
UWorld* World = GetWorld();
check(World);
int32 TotalNum = 0;
const int32 StartIndex = AllSpawnedEntities.Num();
for (const FMassEntitySpawnDataGeneratorResult& Result : Results)
{
if (Result.NumEntities <= 0)
{
continue;
}
check(EntityTypes.IsValidIndex(Result.EntityConfigIndex));
check(Result.SpawnDataProcessor != nullptr);
const FMassSpawnedEntityType& EntityType = EntityTypes[Result.EntityConfigIndex];
if (const UMassEntityConfigAsset* EntityConfig = EntityType.GetEntityConfig())
{
const FMassEntityTemplate& EntityTemplate = EntityConfig->GetOrCreateEntityTemplate(*World);
if (EntityTemplate.IsValid())
{
FSpawnedEntities& SpawnedEntities = AllSpawnedEntities.AddDefaulted_GetRef();
SpawnedEntities.TemplateID = EntityTemplate.GetTemplateID();
SpawnerSystem->SpawnEntities(EntityTemplate.GetTemplateID(), Result.NumEntities, Result.SpawnData, Result.SpawnDataProcessor, SpawnedEntities.Entities);
TotalNum += SpawnedEntities.Entities.Num();
}
}
}
// Run post spawn processors only on the freshly spawned entities.
if (TotalNum)
{
TArray<UMassProcessor*> Processors;
TSet<TSubclassOf<UMassProcessor>> AddedProcessorClasses;
for (const FMassEntitySpawnDataGeneratorResult& Result : Results)
{
for (const TSubclassOf<UMassProcessor>& ProcessorClass : Result.PostSpawnProcessors)
{
if (AddedProcessorClasses.Contains(ProcessorClass) == false)
{
if (UMassProcessor* Processor = GetPostSpawnProcessor(ProcessorClass))
{
Processors.Add(Processor);
}
AddedProcessorClasses.Add(ProcessorClass);
}
}
}
if (Processors.Num() > 0)
{
FMassEntityManager& EntityManager = UE::Mass::Utils::GetEntityManagerChecked(*World);
FMassProcessingContext ProcessingContext(EntityManager, /*TimeDelta=*/0.0f);
// gather freshly spawned entities
TArray<FMassEntityHandle> AllEntities;
AllEntities.Reserve(TotalNum);
for (int32 Index = StartIndex; Index < AllSpawnedEntities.Num(); ++Index)
{
AllEntities.Append(AllSpawnedEntities[Index].Entities);
}
// create entity collections and run Processors on them.
TArray<FMassArchetypeEntityCollection> EntityCollections;
UE::Mass::Utils::CreateEntityCollections(EntityManager, AllEntities, FMassArchetypeEntityCollection::NoDuplicates, EntityCollections);
UE::Mass::Executor::RunProcessorsView(Processors, ProcessingContext, EntityCollections);
}
}
OnSpawningFinishedEvent.Broadcast();
}
void AMassSpawner::DoDespawning()
{
if (AllSpawnedEntities.IsEmpty())
{
return;
}
UMassSpawnerSubsystem* SpawnerSystem = UWorld::GetSubsystem<UMassSpawnerSubsystem>(GetWorld());
if (SpawnerSystem == nullptr)
{
UE_LOG(LogMassSpawner, Error, TEXT("UMassSpawnerSubsystem missing while trying to despawn entities"));
return;
}
for (const FSpawnedEntities& SpawnedEntities : AllSpawnedEntities)
{
SpawnerSystem->DestroyEntities(SpawnedEntities.Entities);
}
AllSpawnedEntities.Reset();
OnDespawningFinishedEvent.Broadcast();
}
void AMassSpawner::DoDespawning(TConstArrayView<FMassEntityHandle> EntitiesToIgnore)
{
// Remove EntitiesToIgnore from SpawnedEntities so they get skipped by DoDespawning() and add to AllEntitiesToKeep
// to restore after.
TArray<FSpawnedEntities> AllEntitiesToKeep;
for (FSpawnedEntities& SpawnedEntities : AllSpawnedEntities)
{
FSpawnedEntities* EntitiesToKeep = nullptr;
for (const FMassEntityHandle& EntityToIgnore : EntitiesToIgnore)
{
if (SpawnedEntities.Entities.RemoveSingleSwap(EntityToIgnore, EAllowShrinking::No))
{
if (!EntitiesToKeep)
{
EntitiesToKeep = &AllEntitiesToKeep.AddDefaulted_GetRef();
EntitiesToKeep->TemplateID = SpawnedEntities.TemplateID;
}
EntitiesToKeep->Entities.Add(EntityToIgnore);
}
}
}
// Despawn the remaining entities in AllSpawnedEntities
DoDespawning();
// Restore AllEntitiesToKeep to AllSpawnedEntities so they remain tracked
AllSpawnedEntities = AllEntitiesToKeep;
}
bool AMassSpawner::DespawnEntity(const FMassEntityHandle Entity)
{
UMassSpawnerSubsystem* SpawnerSystem = UWorld::GetSubsystem<UMassSpawnerSubsystem>(GetWorld());
if (SpawnerSystem == nullptr)
{
UE_LOG(LogMassSpawner, Error, TEXT("UMassSpawnerSubsystem missing while trying to despawn a single entity"));
return false;
}
for (FSpawnedEntities& SpawnedEntities : AllSpawnedEntities)
{
const int32 Index = SpawnedEntities.Entities.Find(Entity);
if (Index != INDEX_NONE)
{
SpawnerSystem->DestroyEntities(MakeArrayView(&Entity, 1));
SpawnedEntities.Entities.RemoveAtSwap(Index, EAllowShrinking::No);
return true;
}
}
return false;
}
int32 AMassSpawner::GetCount() const
{
return Count;
}
float AMassSpawner::GetSpawningCountScale() const
{
return SpawningCountScale;
}
void AMassSpawner::ClearTemplates()
{
UWorld* World = GetWorld();
check(World);
for (FMassSpawnedEntityType& EntityType : EntityTypes)
{
if (const UMassEntityConfigAsset* EntityConfig = EntityType.GetEntityConfig())
{
EntityConfig->DestroyEntityTemplate(*World);
}
}
}
void AMassSpawner::UnloadConfig()
{
// Clear all templates that were created by the config
ClearTemplates();
for (FMassSpawnedEntityType& EntityType : EntityTypes)
{
EntityType.UnloadEntityConfig();
}
if (StreamingHandle.IsValid() && StreamingHandle->IsActive())
{
StreamingHandle->CancelHandle();
}
}