팀 프로젝트 미니맵 UI 제작 관련 중 발생한 문제입니다.
※ 미니맵 관련 포스팅 - https://mynameiskgws.tistory.com/50
미니맵을 초기에 설계했을 때, 각 액터 컴포넌트에 타임 핸들러를 사용하여 일정 시간마다 위치값을 전송하려고 했습니다.
이 때 각 액터 컴포넌트들이 타임핸들러를 가지고 있을 경우, 미니맵에 100개의 몬스터가 렌더링 된다 하면 타이머도 100개를 소지하게 되며 메모리 낭비 및 오버헤드 발생 우려가 있었습니다.
이를 해결하기 위해 액터의 시간을 전송하는 싱글톤패턴 기반의 Minimap Manager를 설계하게 되었습니다.
UObject 기반의 Manager 클래스
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnTargetUpdated, const TArray<FMinimapTargetLocation>&, UpdateLocations);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnTargetRemoved, AActor*, RemovedTarget);
UCLASS()
class GUNFIREPARAGON_API UMinimapManager : public UObject
{
GENERATED_BODY()
public:
void Initialize();
void RegisterTarget(AActor* Target);
void UnRegisterTarget(AActor* Target);
void RecvLocationToBroadCast();
public:
UPROPERTY(BlueprintAssignable, Category = "Minimap")
FOnTargetUpdated OnAllLocationsUpdated;
UPROPERTY(BlueprintAssignable, Category = "Minimap")
FOnTargetRemoved OnTargetRemoved;
private:
TMap<AActor*,FVector> TargetLocations;
FTimerHandle UpdateTimer;
void UpdateTargetsLocation();
};
void UMinimapManager::Initialize()
{
TargetLocations.Empty();
}
void UMinimapManager::RegisterTarget(AActor* Target)
{
if(!GetWorld())
{
UE_LOG(LogTemp, Display, TEXT("World Is Null"));
return;
}
if (IsValid(Target) && !TargetLocations.Contains(Target))
{
TargetLocations.Add(Target, Target->GetActorLocation());
if (!UpdateTimer.IsValid())
{
UpdateTargetsLocation();
}
UE_LOG(LogTemp, Display, TEXT("Register Test : %s"), *Target->GetName());
}
}
void UMinimapManager::UnRegisterTarget(AActor* Target)
{
if (TargetLocations.Contains(Target))
{
TargetLocations.Remove(Target);
OnTargetRemoved.Broadcast(Target);
}
if (TargetLocations.Num() == 0)
{
if (GetWorld())
{
GetWorld()->GetTimerManager().ClearTimer(UpdateTimer);
}
}
}
void UMinimapManager::RecvLocationToBroadCast()
{
TArray<FMinimapTargetLocation> TargetData;
for (const auto& Target : TargetLocations)
{
if (Target.Key)
{
FMinimapTargetLocation Data;
Data.TargetActor = Target.Key;
Data.Location = Target.Key->GetActorLocation();
TargetData.Add(Data);
}
else
{
UnRegisterTarget(Target.Key);
}
}
OnAllLocationsUpdated.Broadcast(TargetData);
}
void UMinimapManager::UpdateTargetsLocation()
{
float UpdateCycleTime = 0.1f;
if (GetWorld())
{
GetWorld()->GetTimerManager().SetTimer(UpdateTimer, this, &UMinimapManager::RecvLocationToBroadCast, UpdateCycleTime, true);
}
}
해당 클래스를 기존에는 static Get을 통한 싱글톤 패턴의 기반으로 하였으나,
미니맵에 해당 액터클래스를 등록하는 과정에서 GetWorld()가 Null이 되는 로그가 출력되었습니다.
이를 해결하기 위해, GameInstance에서 등록을 하였지만, 마찬가지로 GetWorld()가 Null이 출력되며, 크래시되는 현상이 발생하였습니다.
해당 과정은 UObject 기반의 클래스를 정적 으로 사용시에 발생한 문제입니다.
원인 분석
- 게임 컨셉이 레벨과 레벨을 이동하는 과정이 발생한다.
- 해당 과정에서 레벨이 다시 실행 될 경우, 액터 라이프사이클과 같이 새 월드를 가져온다.
- 해당 과정에서 전역 클래스인 MinimapManager를 재 호출하게되며, 순서를 보장하지 않은 상태.
- 이로인해 MinimapManager를 다시 불러오며 동시에 새 월드가 생성되기 전 호출되어 Null반환을 하게된다.
- 순서가 보장이 되지않는다 -> 매 실행마다 출력되는 로그가 다름. Null일경우, 아닐경우 존재를 통해 검증.
- --- 다른 원인 ---
- AActor에서 파생된 모든 클래스는 배치 할 수 있는 클래스. 즉, 자신이 어느 World인지 인지 할 수 있다.
- UObject는 GetWorld()를 호출 할 수 있으나, 배치 되지 않은 경우 의미가 없음.
해결 과정
- 게임 시작부터 유지되는 GEngine()->GetWorld... 를 사용한 호출
- 수명동안 다른 세계를 로드할 수 있으므로 특정 세계를 반환할 수 없음. 실패.
- 게임 시작부터 끝까지 데이터를 가지고 있는 Instance에서 생성, 초기화에서 호출
- 동일 현상, 원인 파악을 못했음. 실패.
- 기본 자료형 또는 함수는 사용 할 수 있으나, 월드간 변화시 동일 월드가 아니라서 발생한것같다.
- Actor기반의 클래스 내 호출하여 사용하기
- 해당 액터의 GetWorld()를 가져오기 때문에 정상적으로 작동.
- 하지만, BeginPlay()후 GetWorld()를 호출하여 사용하면 초기 설계시 고려한 문제가 발생한다. 실패.
- Timer를 사용하지 않고, Delegate를 사용한 이벤트 기반으로 리팩토링
- 특정 조건에 바인딩하여 이벤트 기반 데이터 전송. 구현 성공
정리
- UObject에는 GetWorld() 함수가 존재하기는 하나, 실제로는 null을 반환 할 수 있다.
- GEngine()->GetWorld()... 를 사용한 호출은 서로 다른 세계를 가져올 수 있어 적합하지 않다.
- Instance 내 GetWorld()를 사용할 순 있지만, 정적 클래스에는 부적합 (추정)
- Timer 사용을 하지 않은 Delegate 이벤트 바인딩, 전송으로 인한 우회, 구현 성공
참고내역
https://forums.unrealengine.com/t/gengine-getworld-return-null/377571
https://husk321.tistory.com/426
'Unreal Engine > Debugging' 카테고리의 다른 글
[UE5] - 유효성 검사 (0) | 2025.01.27 |
---|