오늘은 GunManager가 플레이 중간에 갑자기 nullptr이 되는 치명적인 버그를 해결했음 🔫
기존 방식은 BeginPlay()에서 NewObject로 UGunManager를 동적 생성하는 방식이었는데, 언리얼 엔진의 가비지 컬렉션 시스템에 의해 예기치 않게 객체가 삭제되는 문제가 발생했음
새로운 방식은 GunManager를 UActorComponent로 변경하고 생성자에서 CreateDefaultSubobject로 생성하여 액터의 생명주기와 완전히 동기화시키는 안전한 구조로 개선했음
플레이어가 게임을 하다가 갑자기 총기 관련 기능이 모두 작동하지 않는 버그가 계속 발생했음
증상:
기존 방식의 한계:
핵심 문제
// 문제가 되었던 코드 - BeginPlay()에서 동적 생성
void APlayerCharacter::BeginPlay()
{
Super::BeginPlay();
if (!GunManager)
{
GunManager = NewObject<UGunManager>(this); // ❌ 가비지 컬렉션 대상
}
}
핵심 문제 발견
NewObject로 생성된 UObject는 언리얼의 가비지 수집 대상UPROPERTY로 선언했음에도 불구하고 특정 상황에서 참조 추적 실패소멸자 로그로 확인:
UGunManager::~UGunManager()
{
UE_LOG(LogTemp, Error, TEXT("건매니저 사라짐!!!!!")); // 실제로 삭제되고 있었음
}
이 로그가 플레이 중간에 출력되는 것을 보고 가비지 컬렉션이 원인임을 확신하게 됨
핵심 아이디어: "UActorComponent로 안전하게 관리"
기존의 불안정한 동적 생성 대신 컴포넌트 시스템 활용:
기존: BeginPlay에서 NewObject → 가비지 컬렉션 위험
새로운: 생성자에서 CreateDefaultSubobject → 액터와 생명주기 동기화
UObject vs UActorComponent 비교
UObject: 독립적인 객체, 가비지 컬렉션 대상UActorComponent: 액터에 종속, 액터와 생명주기 공유// 기존: UObject 상속
#include "UObject/NoExportTypes.h"
class LOSTINEDEN_API UGunManager : public UObject
// 새로운: UActorComponent 상속
#include "Components/ActorComponent.h"
UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
class LOSTINEDEN_API UGunManager : public UActorComponent
{
GENERATED_BODY()
protected:
TMap<EGunType, int32> OwnedGuns;
public:
UGunManager();
TArray<EGunType> GetOwnedGunList() const;
void UpdateGunData(AGun*);
void SetCurrentGun(AGun*);
~UGunManager();
};
컴포넌트 기본 설정 추가
UGunManager::UGunManager()
{
// 컴포넌트 기본 설정
PrimaryComponentTick.bCanEverTick = false; // 틱 불필요하므로 비활성화
// 기존 총기 데이터 초기화 로직은 그대로 유지
OwnedGuns.Add(EGunType::PISTOL, 500);
OwnedGuns.Add({EGunType::RIFLE, 0});
OwnedGuns.Add({EGunType::SHOTGUN, 0});
}
GunManager는 매 프레임 업데이트가 필요 없으므로 틱을 비활성화하여 성능 최적화
생성자에서 컴포넌트로 생성
APlayerCharacter::APlayerCharacter()
{
// 다른 컴포넌트들과 함께 안전하게 생성
SpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm"));
Camera = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));
BoxCollisionComp = CreateDefaultSubobject<UBoxComponent>(TEXT("BoxCollision"));
// ✅ GunManager를 컴포넌트로 생성
GunManager = CreateDefaultSubobject<UGunManager>(TEXT("GunManager"));
// ... 나머지 초기화
CurrWeapon = nullptr;
bIsDead = false;
bCanChangeGun = true;
}
void APlayerCharacter::BeginPlay()
{
Super::BeginPlay();
// ❌ 이 부분 완전 삭제 - 더 이상 동적 생성 불필요
// if (!GunManager)
// {
// GunManager = NewObject<UGunManager>(this);
// }
// 기본 권총 장착은 그대로
EquipWeapon(EGunType::PISTOL);
HealPotion = GetWorld()->SpawnActor<AHealingItem>(AHealingItem::StaticClass());
Shield = GetWorld()->SpawnActor<AShield>(AShield::StaticClass());
}
완전한 문제 해결
개발 편의성 향상
안정성과 표준 준수
UObject vs UActorComponent 선택 기준
// UObject 사용하는 경우:
- 독립적인 데이터 클래스
- 전역적으로 공유되는 매니저
- 싱글톤 패턴이 필요한 경우
// UActorComponent 사용하는 경우:
- 특정 액터와 밀접한 관련이 있는 기능
- 액터의 생명주기와 동기화가 필요한 경우
- 에디터에서 설정 가능해야 하는 경우
가비지 컬렉션 관련 주의사항
NewObject로 생성한 객체는 항상 가비지 컬렉션 위험이 있음UPROPERTY만으로는 완전한 보호가 되지 않을 수 있음CreateDefaultSubobject를 통한 컴포넌트 생성 권장1. CreateDefaultSubobject (생성자에서만 사용)
- 가장 안전한 방식
- 액터와 생명주기 동기화
- 에디터 지원
2. NewObject (런타임 동적 생성)
- 유연하지만 가비지 컬렉션 위험
- 수동 생명주기 관리 필요
- 특별한 경우에만 사용
3. CreateWidget (UI 위젯용)
- UI 전용
- 부모 위젯에 의해 관리
// 틱 설정
PrimaryComponentTick.bCanEverTick = false; // 불필요시 비활성화
// 블루프린트 노출
UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
// 에디터에서 보이기
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Weapon")