[Unreal_C++_DarkSoul]#17_Grid 클래스
공간 분할 패턴을 활용한 Grid 클래스를 구현합니다.
Overview
- 개요
- 코드
- 영상
#0. 개요
1. Grid 클래스?
- Grid 객체는 월드에 배치되어 Grid 객체가 정의한 지정 구간 내 위치한 Enemy 객체들을 관리합니다.
- 공간 분할 패턴(Spatial Partition Pattern) : Grid 객체는 공간 분할 패턴을 통해 "주변 객체 탐색" 성능을 최적화합니다. 월드 내 객체가 많아질수록, 이들을 탐색하는 작업은 느려집니다. 따라서, 플레이어의 현재 위치 값을 통해 Grid 객체를 특정하고, Grid 객체가 관리하는 적 객체 목록을 순회하여 탐색 성능의 최적화를 구현합니다.
- 객체 풀(Object Pooling) : Grid 객체는 객체 풀을 활용해 적 객체들의 런 타임 메모리 할당/해제를 반복하지 않고, 고정된 크기의 풀에 할당된 객체들을 재사용함으로써 메모리 사용 성능을 개선합니다.
2. 공간 분할 패턴
- "공간 분할 패턴"은 월드 내 주변 객체 탐색 성능을 최적화하기 위해 객체의 "위치 값"에 따라 구성되는 자료구조에 각 객체를 저장하는 디자인 패턴입니다. "공간 분할 패턴"은 위치 값을 가지는 객체들이 많아질수록, 또한 이들의 위치에 따라서 탐색 성능에 영향을 줄 경우 유용합니다.
- Grid 객체는 월드 내 위치해 FVector(800.f, 800.f, 100.f) 크기의 공간을 지정해 해당 지역에 위치하는 Enemy 객체들을 관리합니다. 사전에 배치한 Enemy 객체들을 위치 값에 따라 자료구조를 구성하기보단, 사용자가 지정한 Enemy 개수만큼 직접 스폰하는 방법을 생각했습니다. 이러한 방법을 선택한 이유는 아래 설명할 "객체 풀"을 활용하기 위함입니다.
3. 객체 풀(Object Pooling)
- "객체 풀"은 런타임 중 객체의 메모리 할당과 해제를 반복하지 않고, 고정 크기 "풀"에 할당된 객체들을 재사용함으로써 메모리 사용 성능을 개선하는 디자인 패턴입니다. 잘 알다시피, 런 타임 메모리 할당/해제는 모두 "힙"에서 수행합니다. 힙의 메모리 할당과 해제는 성능이 비교적 느리며, 메모리 단편화의 위험성 또한 갖고 있습니다.
- Grid 객체는 지정된 구역 내 Enemy 객체들을 고정 크기 풀에 할당함으로써, 런 타임 오버헤드를 방지합니다. "객체 풀"과 더불어 Grid 객체를 통해 Enemy 객체의 "리스폰"을 구현했습니다. HP가 0이 되어 "Death" 상태의 Enemy 객체들은 Visibility를 끄고, 대기 지점으로 이동합니다. 해당 객체는 리스폰 쿨 타임이 주어지고, 쿨 타임이 0이 되는 시점에 Grid 객체의 지정된 구역 내 랜덤한 위치에 다시 리스폰됩니다. 이를 통해, 객체 풀에 할당된 객체들을 재사용할 수 있게 됩니다!
#1. 코드
1. Grid.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Grid.generated.h"
UCLASS()
class DARKSOUL_API AGrid : public AActor
{
//...
private:
UPROPERTY(VisibleAnywhere, Category = "Components")
class UBoxComponent* Box;
public:
UPROPERTY(EditDefaultsOnly, Category = "Grid")
TSubclassOf<class AEnemyBase> BossEnemyClass;
private:
TSubclassOf<AEnemyBase> CastleKnightClass;
TSubclassOf<AEnemyBase> CastleArcheryClass;
private:
const uint8 Max_EnemyGroupSize = 1;
class AEnemyBase* BossEnemy;
TArray<class AEnemyBase*> EnemyGroupOnGrid;
TArray<class ASmartObject*> SmartObjectsOnGrid;
const uint8 Max_AIManagers = 3;
TArray<class AAIManager*> AIManagersOnGrid;
private:
FVector ReadyLocation;
FTimerHandle ReadyToRegenHandler;
const float MoveToReadyToRegenLocationRate = 2.f;
private:
const float CoolTime = 20.f;
TArray<bool> EnemyReadyToRespawn;
TArray<float> RespawnCoolTimer;
};
Details
- BoxComponent: FVector(800.f, 800.f, 100.f)의 크기의 육면체입니다. Grid 객체가 관리할 적 객체, AI Manager 객체, 그리고 Smart Object 객체들이 스폰될 지역을 설정합니다.
- EnemyGroupOnGrid : 해당 Grid 객체에 속한 적 객체 그룹입니다.
- SmartObjectsOnGrid: Grid 상에 존재하는 적 객체들이 이동할 다음 위치를 지정하는 Smart Object 유형의 객체를 저장하는 배열입니다.
- AIManagaersOnGrid : Gird 상에 존재하는 적 객체들의 AI 행동 방식을 관리하는 AI 매니저 객체들을 저장하는 배열입니다. 총 세 가지 AI 매니저가 존재하며, 하나는 "Enemy 그룹", 다른 하나는 "Neutral 그룹", 그리고 마지막 하나는 "Friendly 그룹"입니다.
- ReadyLocation : 현재 죽은 상태로, 플레이어 화면에 보이지 않는 적 객체들을 "죽은 상태 ~ 리스폰" 기간 동안 대기할 위치를 지정해 놓습니다.
- EnemyReadyToRespawn : 현재는 죽어 있지만, 리스폰되기를 기다리는 적 객체들을 저장하는 배열입니다. 이와 함께 이들의 리스폰 쿨 타임을 카운트하는 RespawnCoolTimer 배열 또한 선언해 줍니다.
2. AGrid::SpawnEnemyGroupOnGrid()
void AGrid::SpawnEnemyGroupOnGrid()
{
CheckTrue(Max_EnemyGroupSize <= 0 || !CastleKnightClass || !CastleArcheryClass);
CheckTrue(SmartObjectsOnGrid.Num() <= 0);
uint8 RandEnemyCategory = -1;
uint8 RandSmartObjectIdx = -1;
FVector SpawnLocation = FVector::ZeroVector;
// Smart Objects
RandSmartObjectIdx = FMath::RandRange(0, SmartObjectsOnGrid.Num() - 1);
CheckTrue(!SmartObjectsOnGrid.IsValidIndex(RandSmartObjectIdx));
// Spawn Location
SpawnLocation = UKismetMathLibrary::RandomPointInBoundingBox(Box->Bounds.GetBox().GetCenter(), Box->Bounds.GetBox().GetExtent());
// #0. Spawn Boss Enemy
BossEnemy = GetWorld()->SpawnActor<AEnemyBase>(BossEnemyClass, SpawnLocation, FRotator::ZeroRotator);
if (!!BossEnemy)
{
BossEnemy->SetSmartObject(SmartObjectsOnGrid[RandSmartObjectIdx]);
}
// #1. Spawn Enemies
for (uint8 i = 0; i < Max_EnemyGroupSize; i++)
{
AEnemyBase* Enemy = nullptr;
RandEnemyCategory = FMath::RandRange(0, 1);
RandSmartObjectIdx = FMath::RandRange(0, SmartObjectsOnGrid.Num()-1);
CheckTrue(!SmartObjectsOnGrid.IsValidIndex(RandSmartObjectIdx));
SpawnLocation = UKismetMathLibrary::RandomPointInBoundingBox(Box->Bounds.GetBox().GetCenter(), Box->Bounds.GetBox().GetExtent());
switch (RandEnemyCategory)
{
case 0:
Enemy = GetWorld()->SpawnActor<AEnemyBase>(CastleKnightClass, SpawnLocation, FRotator::ZeroRotator);
break;
case 1:
Enemy = GetWorld()->SpawnActor<AEnemyBase>(CastleArcheryClass, SpawnLocation, FRotator::ZeroRotator);
break;
}
if (!!Enemy)
{
Enemy->SetIndex(i);
Enemy->SetSmartObject(SmartObjectsOnGrid[RandSmartObjectIdx]);
EnemyGroupOnGrid[i] = Enemy;
}
}
}
Details
- UKismetMathLibrary::RandomPointInBoundingBox() : Box 내 랜덤 위치 값을 반환하는 함수입니다. 해당 함수를 통해 손쉽게 Enemy 객체를 스폰할 랜덤 위치 값을 구할 수 있습니다.
3. AGrid::BeginPlay()
void AGrid::BeginPlay()
{
//...
// #3. Delegate
if(!!BossEnemy) BossEnemy->OnEnemyIsDead.BindUFunction(this, "EnemyIsDead");
if (EnemyGroupOnGrid.Num() > 0)
{
for (auto Enemy : EnemyGroupOnGrid)
{
if (!!Enemy)
{
Enemy->OnEnemyIsDead.BindUFunction(this, "EnemyIsDead");
}
}
}
}
Details
- BeginPlay 함수는 게임 시작시점에 호출되는 함수입니다. EnemyBase 클래스(Enemy 객체들이 상속받는 상위 클래스)에서 정의한 OnEnemyIsDead 델리게이트 함수에 EnemyIsDead함수를 동적 바인딩합니다. 쉽게 말해, Enemy 객체가 죽을 때 OnEnemyIsDead 델리게이트 함수에 동적 바인딩된 EnemyIsDead 함수가 호출됩니다.
4. AGrid::EnemyIsDead()
void AGrid::EnemyIsDead(AActor* InDamageCauser, bool IsBoss, uint8 InIndex)
{
if (IsBoss)
{
MoveToReadyToRegenLocation(InDamageCauser, true);
}
else
{
// #0. Move To Ready Location
MoveToReadyToRegenLocation(InDamageCauser, false, InIndex);
// #1. Start Regen CoolTime
StartRegenCoolTime(InIndex);
}
}
Details
- EnemyIsDead 함수의 인자로 전달받은 "IsBoss"는 해당 Enemy 객체가 EnemyGroup을 대표하는 Boss Enemy 객체인지 확인합니다. 죽은 Enemy 객체가 Boss Enemy일 경우 모든 Enemy 객체가 죽도록 구현합니다.
- IsBoss가 참일 경우, MoveToReadyToRegenLocation() 함수가 호출되어 Grid 객체 상에 존재하는 모든 Enemy객체가 모두 리스폰 대기 위치로 이동합니다.
- IsBoss가 거짓일 경우, MoveToReadyToRegenLocation() 함수가 호출되어 해당 객체가 리스폰 위치로 이동합니다. 더불어, 해당 객체의 리스폰 쿨 타임을 시작시키는 StartRegenCoolTime() 함수 또한 호출합니다.
5. AGrid::MoveToReadyToRegenLocation()
void AGrid::MoveToReadyToRegenLocation(AActor* InDamageCauser, bool IsAllEnemyDead, uint8 InIndex)
{
if (IsAllEnemyDead)
{
for (auto Enemy : EnemyGroupOnGrid)
{
Enemy->IsDead(InDamageCauser);
GetWorld()->GetTimerManager().SetTimer(ReadyToRegenHandler, FTimerDelegate::CreateLambda([&](class ACharacter* Enemy) {
Enemy->TeleportTo(ReadyLocation, FRotator::ZeroRotator);
}, Enemy), MoveToReadyToRegenLocationRate, false);
}
}
else
{
GetWorld()->GetTimerManager().SetTimer(ReadyToRegenHandler, FTimerDelegate::CreateLambda([=]() {
if (EnemyGroupOnGrid.IsValidIndex(InIndex))
{
EnemyGroupOnGrid[InIndex]->TeleportTo(ReadyLocation, FRotator::ZeroRotator);
}
}), MoveToReadyToRegenLocationRate, false);
}
}
Details
- 위에서 설명한 대로, Grid 객체가 관리하는 모든 Enemy 객체들을 죽일지, 특정 Enemy 객체만 죽일지 결정하는 함수입니다.
- 죽은 Enemy 객체는 Grid 객체가 정의한 임의의 위치(리스폰 대기 장소, ReadyLocation)로 이동합니다.
6. AGrid::StartRegenCoolTime()
void AGrid::StartRegenCoolTime(uint8 InIndex)
{
EnemyReadyToRespawn[InIndex] = true;
RespawnCoolTimer[InIndex] = GetWorld()->GetTimeSeconds();
}
void AGrid::CountRegenCoolTime()
{
for (uint8 i = 0; i < EnemyReadyToRespawn.Num(); i++)
{
if (EnemyReadyToRespawn[i])
{
float CurrentCoolTime = fabs(RespawnCoolTimer[i] - GetWorld()->GetTimeSeconds());
if (CurrentCoolTime >= CoolTime)
{
RegenEnemy(i);
}
}
}
}
Details
- StartRegenCoolTime 함수는 죽은 Enemy 객체의 Index를 가져와 해당 Enemy의 리스폰 여부를 참으로 변경합니다.
- CountRegenCoolTime 함수는 "AGrid::Tick()" 함수 본문에서 호출되어, Enemy 객체가 죽은 시점의 시간과 현재 시간을 통해 현재 남은 리스폰 쿨 타임을 계산합니다. 주어진 쿨 타임 시간을 모두 채운 Enemy 객체는 RegenEnemy 함수를 호출해 리스폰을 구현합니다.
7. AGrid::RegenEnemy()
void AGrid::RegenEnemy(uint8 InIndex)
{
CheckTrue(!EnemyGroupOnGrid[InIndex]);
// #0. Enemy Respawn
FVector RespawnLocation;
RespawnLocation = UKismetMathLibrary::RandomPointInBoundingBox(Box->Bounds.GetBox().GetCenter(), Box->Bounds.GetBox().GetExtent());
EnemyGroupOnGrid[InIndex]->Respawn(RespawnLocation);
// #5. Respawn Cooltime
EnemyReadyToRespawn[InIndex] = false;
RespawnCoolTimer[InIndex] = 0.f;
}
Details
- RegenEnemy 함수는 죽어있던 Enemy 객체의 리스폰을 구현합니다.
#2. 영상
- 포트폴리오 영상 : 유튜브 채널 Hard2
'개인프로젝트' 카테고리의 다른 글
[Unreal_C++_DarkSoul]#19_기능 구현, Portal (0) | 2023.07.23 |
---|---|
[Unreal_C++_DarkSoul]#18_기능 구현, Sound (0) | 2023.07.23 |
[Unreal_C++_DarkSoul]#16_문제 해결, 런 타임 AI 실행 여부 (0) | 2023.05.27 |
[Unreal_C++_DarkSoul]#15_문제 해결, Data Table 로드 함수 (1) | 2023.05.08 |
[Unreal_C++_DarkSoul]#14_문제 해결, Actor Component 간 소통 (0) | 2023.03.26 |