Object Pooling을 하는데 InitializePool은 잘되지만 GetPooled함수가 정상적으로 작동이 되지 않아 디버깅 하면서 오류를 해결해야 했다. 우선 디버깅로그를 찍어보기로 했다. 우선 GetPooled함수가 호출되는 GameMode클래스의 SpawnEnemiesForStage 함수부터 살펴 보았다.
AFPSGameMode::SpawnEnemiesForStage
void AFPSGameMode::SpawnEnemiesForStage(int32 StageNumber)
{
UFPSGameInstance* FPSGameInstance = Cast<UFPSGameInstance>(GetGameInstance());
if (!FPSGameInstance) return;
AFPSGameState* FPSGameState = Cast<AFPSGameState>(GetWorld()->GetGameState());
if (!IsValid(FPSGameState)) return;
TArray<AActor*> FoundVolumes;
UGameplayStatics::GetAllActorsOfClass(GetWorld(), ASpawnVolume::StaticClass(), FoundVolumes);
if (FoundVolumes.Num() == 0) return;
TMap<TSubclassOf<ABaseEnemy>, int32> EnemyData = GetEnemySpawnData(StageNumber);
if (FoundVolumes.Num() > 0)
{
ASpawnVolume* SpawnVolume = Cast<ASpawnVolume>(FoundVolumes[0]);
if (SpawnVolume)
{
UE_LOG(LogTemp, Warning, TEXT("Found SpawnVolume"));
for (const TPair<TSubclassOf<ABaseEnemy>, int32>& Pair : EnemyData)
{
TSubclassOf<ABaseEnemy> EnemyClass = Pair.Key;
int32 EnemyCount = Pair.Value;
UE_LOG(LogTemp, Warning, TEXT("Get Pair Value: %d"), EnemyCount);
for (int32 i = 0; i < EnemyCount; i++)
{
UE_LOG(LogTemp, Warning, TEXT("Try Get Enemy :%s from Pool"), *EnemyClass->GetName());
ABaseEnemy* SpawnedEnemy = ObjectPoolInstance->GetPooledAI(SpawnVolume, EnemyClass);
if (SpawnedEnemy)
{
UE_LOG(LogTemp, Warning, TEXT("Get Enemy from Pool Success"), EnemyCount);
FPSGameState->RemainingEnemies++;
}
}
}
}
}
}
✅ FoundVolume.Num() == 0
이 될 수 있으므로 if (SpawnVolume)
이 되면 실행되는 구현부에 디버깅용 로그를 찍어주었다.
잘 찍히는 모습을 볼 수 있다.
✅ 다음은 EnemyData를 루프돌리는 로직인데 일단 이것이 제대로 돌아가기 위해서는 EnemyData가 DT(DataTable)에서 값을 잘 받아오는지부터 확인하여야 한다.
TMap<TSubclassOf<ABaseEnemy>, int32> EnemyData = GetEnemySpawnData(StageNumber)
을 보면 GetEnemySpawnData함수를 호출해서 값을 받아오는걸 확인할 수 있다.
AFPSGameMode::GetEnemySpawnData
TMap<TSubclassOf<ABaseEnemy>, int32> AFPSGameMode::GetEnemySpawnData(int32 StageNumber)
{
TMap<TSubclassOf<ABaseEnemy>, int32> EnemyData;
if (!EnemySpawnTable) return EnemyData;
static const FString ContextString(TEXT("EnemySpawnContext"));
TArray<FAIEnemySpawnRaw*> AllRows;
EnemySpawnTable->GetAllRows(ContextString, AllRows);
UE_LOG(LogTemp, Warning, TEXT("Sucess Get DTspawnData! AllRows.Num : %d"),AllRows.Num());
for (FAIEnemySpawnRaw* Row : AllRows)
{
if (Row && Row->StageNumber == StageNumber)
{
if (EnemyData.Contains(Row->EnemyClass))
{
EnemyData[Row->EnemyClass] += Row->EnemyCount;
}
else
{
EnemyData.Add(Row->EnemyClass, Row->EnemyCount);
}
}
}
if (EnemyData.Num()>0)
{
UE_LOG(LogTemp, Warning, TEXT("Get SpawnRowdata Succeed"));
}
return EnemyData;
}
우선 EnemySpawnTable
은 UDataTable* EnemySpawnTable;
타입이다. 에디터에서 내가 만든 DT이랑 바인딩해주었다.
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataTable.h"
#include "AI/BaseEnemy.h"
#include "AIEnemySpawnRaw.generated.h"
USTRUCT(BlueprintType)
struct FAIEnemySpawnRaw : public FTableRowBase
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AIData")
FName EnemyName;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AIData")
TSubclassOf<ABaseEnemy> EnemyClass;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AIData")
int32 EnemyCount;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AIData")
int32 StageNumber;
};
✅ 다시 GetEnemySpawnData
함수로 돌아가서 보면 EnemySpawnTable에 있는 모든 행을 들고와서 AllRows배열에 넣어 주었다. 현재 에디터에서 설정해둔 행의 갯수는 총 9개. 제대로 들고온다면 AllRows.Num()
또한 9가 되어야 했다. 다행히 제대로 값을 들고오는 모습.
✅ 이제 AllRows 배열을 루프하면서 Row의 StageNumber의 값을 통해서 현재 StageNumber과 일치한 적클래스와 개체 수를 EnemyData에 저장한다. Contain은 Find와 비슷하지만 Find는 키를 찾으면 해당 값을 가져오고 없으면 nullptr을 반환하는 반면 Contain은 키를 찾으면 True, 못찾으면 False값을 들고오는 bool 타입이다.
✅ 따라서 해당 키 값이 있으면 개체 수를 더하고, 키가 없으면 해당 키와 값을 생성하는 방식으로 루프를 돌려주었다. 다음 잘 들어갔는지 확인하기 위해 EnemyData.Num()>0
이면 로그가 나오도록 설정해주었다.
잘 출력되는 모습. 그렇다면 DataTable 로부터 스테이지마다 적 클래스와 개체수를 들고오는 로직은 이상이 없다는 것이다.
✅ 다시 SpawnEnemiesForStage
함수로 돌아가서 가져온 EnemyData를 루프를 돌린다. 우선 EnemyData의 키와 값을 분리 하여 각각의 타입으로 저장
TSubClassOf<ABaseEnemy> EnemyClass = Pair.Key;
int32 EnemyCount = Pair.Value;
그리고 EnemyCount만큼 루프를 돌리는 로직으로 EnemyClass를 GetPooledAI 하였다. 우선 GetPooledAI로 넘어가기전에 키와 값이 잘 분리되어서 저장되었는지, 해당 클래스가 정보가 잘 저장되어있는채로 GetPooledAI 매개변수로 넘어갔는지 확인하기 위하여 아래와 같이 디버깅로그를 넣어주었다.
UE_LOG(LogTemp, Warning, TEXT("Get Pair Value: %d"), EnemyCount);
for (int32 i = 0; i < EnemyCount; i++)
{
UE_LOG(LogTemp, Warning, TEXT("Try Get Enemy :%s from Pool"), *EnemyClass->GetName());
.
.
.
잘 받아오는 모습. Pool is Nullptr은 추후에 설명.
✅ 이제 SpawnVolume도 잘 받아왔고 EnemyClass도 확실하게 잘 받아와졌으므로 DT에서 데이터를 받아서 GetPooledAI 호출을 하는것 까지는 문제없는 것이 확인되었다. 이제 실질적으로 ObjectPool에서 적 개체를 활성화시키는 GetPooledAI(SpawnVolume, EnemyClass)
함수를 확인해보아야 한다.
AFPSGameMode::GetPooledAI
ABaseEnemy* AAIObjectPool::GetPooledAI(ASpawnVolume* SpawnVolume, TSubclassOf<ABaseEnemy> EnemyClass)
{
if (!SpawnVolume || !EnemyClass) return nullptr;
if (EnemyPools.Contains(EnemyClass))
{
UE_LOG(LogTemp, Warning, TEXT("EnemyPools Contains BaseEnemyClass"));
Pool = EnemyPools[EnemyClass];
if (Pool.IsValid())
{
UE_LOG(LogTemp, Warning, TEXT("Pool Is Valid"));
for (ABaseEnemy* Enemy : *Pool)
{
if (!Enemy->IsActorTickEnabled())
{
FVector SpawnLocation = SpawnVolume->GetSafeSpawnPoint();
Enemy->SetActorLocation(SpawnLocation);
Enemy->SetActorHiddenInGame(false);
Enemy->SetActorEnableCollision(true);
Enemy->SetActorTickEnabled(true);
UE_LOG(LogTemp, Log, TEXT("%s spawned from pool"), *EnemyClass->GetName());
return Enemy;
}
}
}
else
{
UE_LOG(LogTemp, Warning, TEXT("Pool is Nullptr"));
}
}
return nullptr;
}
✅ 우선 가져온 매개변수가 유효하지 않으면 nullptr을 반환하게 하였다. 다음으로 EnemyPools 라는 변수를 확인할 수 있는데 이건 InitializePool함수를 통해 어떻게 저장되어 있는지 확인을 해야한다.
AIObjectPool.h
TMap<TSubclassOf<ABaseEnemy>, TSharedPtr<TArray<ABaseEnemy*>>> EnemyPools;
TSharedPtr<TArray<ABaseEnemy*>> Pool;
AAIObjectPool::InitializePool
void AAIObjectPool::InitializePool(TMap<TSubclassOf<ABaseEnemy>, int32> EnemyClasses)
{
for (const TPair<TSubclassOf<ABaseEnemy>, int32> Pair : EnemyClasses)
{
TSubclassOf<ABaseEnemy> EnemyClass = Pair.Key;
int32 Count = Pair.Value;
if (!EnemyClass) continue;
Pool = EnemyPools.FindOrAdd(EnemyClass);
FActorSpawnParameters SpawnParams;
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
for (int32 i = 0; i < Count; i++)
{
ABaseEnemy* NewEnemy = GetWorld()->SpawnActor<ABaseEnemy>(
EnemyClass,
SpawnParams
);
if (NewEnemy->IsValidLowLevelFast())
{
NewEnemy->SetActorHiddenInGame(true);
NewEnemy->SetActorEnableCollision(false);
NewEnemy->SetActorTickEnabled(false);
UE_LOG(LogTemp, Warning, TEXT("add"));
if (Pool.IsValid())
{
Pool->Add(NewEnemy);
UE_LOG(LogTemp, Warning, TEXT("Pool Save Data %d"), Pool->Num())
}
}
UE_LOG(LogTemp, Warning, TEXT("Created %d enemies of type!!!: %s"), i, *EnemyClass->GetName());
}
}
}
✅ 우선 InitializePool
은 GameMode BeginPlay에서 호출되는 함수로써 레벨이 생성되고 게임이 시작하면 미리 적 개체를 스폰하여 비활성화 시켜놓는 로직이다.
✅ 또다른 DataTable을 통해서 매개변수를 받아오며 호출이된다. 매개변수는 ABaseEnemy기반의 서브클래스와 개체 수를 받아오고 있다.
✅ 받아온 TMap을 루프시키고 키값인 (EnmeyClass)Class와 개체수 (Count)int32 타입으로 나눈다.
그리고 EnemyPools.FindOrAdd(EnemyClass)을 통해
EnemyClass키가 EnemyPools에 존재하는 경우, 기존의 TSharedPtr<TArray<ABaseEnemy*>> 값을 가져와 Pool에 저장한다.
EnemyClass키가 EnemyPools에 존재하지 않는 경우, 새로운 TSharedPtr<TArray<ABaseEnemy*>>를 생성하여 EnemyPools[EnemyClass]에 추가한다. 그리고 Pool에도 해당 TSharedPtr을 저장. Nullptr
상태
✅ SpawnCollisionHandlingOverride 설정을 통해 충돌 여부와 상관없이 강제로 스폰되게 하였다. 이후 테스트 여하에 따라서 AdjustIfPossibleButAlwaysSpawn으로 설정할지 고려해볼 예정
✅ 이제 Count만큼(개체 수) 루프를 돌리고 적을 월드에 스폰한 다음, SetActor 함수들을 통해 비활성화 시켜준다. 그리고 Pool에 저장한다. 하지만 디버깅하면 Pool->Add(NewEnemy)가 되지않아 "Pool Save Data %d" 로그가 뜨질않는다.
🔥 문제를 찾았으니 이제 해결을 해야한다. 왜 Pool.IsValid()
가 false값을 반환한걸까? 이건 FindOrAdd가 새로운 TShared<TArray<ABaseEnemy*>>
를 생성할 때, 내부 TArray<ABaseEnemy*>
를 자동으로 할당해주지 않기 때문이다.
즉, Pool이 nullptr을 가리키는 TSharedPtr 일 수도 있는 것이다.
그래서 Pool.IsValid()
를 검사했을 때 false가 나온 것.
✅ 그래서 직접 MakeShared
로 초기화해줘야 한다.
if (!Pool.IsValid()) // Pool이 nullptr을 가리키는 상태라면
{
Pool = MakeShared<TArray<ABaseEnemy*>>(); // 내부 객체를 생성
EnemyPools.Add(EnemyClass, Pool); // Map에 저장 (값이 공유됨)
}
결과적으로는
✔️ FindOrAdd의 반환값을 Pool이 가리키고 있음.
✔️ Pool이 EnemyPools[EnemyClass]의 값과 동일한 메모리를 가리킴.
✔️ 그래서 Pool->Add(NewEnemy); 하면 EnemyPools[EnemyClass]의 내부 값도 바뀜
즉, 최종적으로
FindOrAdd가 새TSharedPtr<TArray<ABaseEnemy*>>
을 만들었는데 내부 객체는 nullptr일 수 있음.
그래서 직접 MakeShared를 호출해서 내부 TArray<ABaseEnemy*>를 생성해줘야 함.
Pool->Add(NewEnemy); 하면 자동으로 EnemyPools[EnemyClass]도 변경됨
이 로직을 추가한 뒤 디버깅을 해보면
정상적으로 Pool Save Data 1이 출력되는 것을 볼 수 있다.
✅ 이제 Pool이 정상적으로 저장이 되었으니 이제 GetPooledAI도 정상적으로 작동될 것이다.
정상적으로 잘 작동되고 스폰까지 마무리 되는 모습을 볼 수 있다.