TIL [25.07.30] - Lost In Eden 건매니저 오류 해결

chooha·2025년 7월 30일

TIL

목록 보기
54/60

📝 개발일지 - GunManager nullptr 문제 해결

오늘은 GunManager가 플레이 중간에 갑자기 nullptr이 되는 치명적인 버그를 해결했음 🔫

기존 방식은 BeginPlay()에서 NewObject로 UGunManager를 동적 생성하는 방식이었는데, 언리얼 엔진의 가비지 컬렉션 시스템에 의해 예기치 않게 객체가 삭제되는 문제가 발생했음

새로운 방식은 GunManager를 UActorComponent로 변경하고 생성자에서 CreateDefaultSubobject로 생성하여 액터의 생명주기와 완전히 동기화시키는 안전한 구조로 개선했음


💡 오늘의 5분 기록

1. 문제 발견 과정

플레이어가 게임을 하다가 갑자기 총기 관련 기능이 모두 작동하지 않는 버그가 계속 발생했음

증상:

  • 게임 시작 시에는 정상 작동
  • 플레이 중간에 갑자기 총기 교체가 불가능해짐
  • "장착할 수 있는 다른 총이 없습니다" 메시지만 계속 출력
  • 로그에서 "건매니저 없음!!" 에러 메시지 확인

기존 방식의 한계:

  • BeginPlay()에서 NewObject를 통한 동적 생성
  • 가비지 컬렉션 시스템의 예측 불가능한 정리
  • UPROPERTY 선언에도 불구하고 참조 추적 실패

핵심 문제

// 문제가 되었던 코드 - BeginPlay()에서 동적 생성
void APlayerCharacter::BeginPlay()
{
    Super::BeginPlay();
    
    if (!GunManager)
    {
        GunManager = NewObject<UGunManager>(this); // ❌ 가비지 컬렉션 대상
    }
}

2. 가비지 컬렉션 함정 발견 🔍

핵심 문제 발견

  • NewObject로 생성된 UObject는 언리얼의 가비지 수집 대상
  • UPROPERTY로 선언했음에도 불구하고 특정 상황에서 참조 추적 실패
  • 엔진이 "사용되지 않는 객체"로 판단하여 자동 삭제

소멸자 로그로 확인:

UGunManager::~UGunManager()
{
    UE_LOG(LogTemp, Error, TEXT("건매니저 사라짐!!!!!")); // 실제로 삭제되고 있었음
}

이 로그가 플레이 중간에 출력되는 것을 보고 가비지 컬렉션이 원인임을 확신하게 됨

3. 해결 방향 설정 - 컴포넌트 시스템 활용 💡

핵심 아이디어: "UActorComponent로 안전하게 관리"

기존의 불안정한 동적 생성 대신 컴포넌트 시스템 활용:

기존: BeginPlay에서 NewObject → 가비지 컬렉션 위험
새로운: 생성자에서 CreateDefaultSubobject → 액터와 생명주기 동기화

UObject vs UActorComponent 비교

  • UObject: 독립적인 객체, 가비지 컬렉션 대상
  • UActorComponent: 액터에 종속, 액터와 생명주기 공유

4. GunManager.h 구조 변경 🛠️

// 기존: 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();
};

5. 생성자 로직 개선

컴포넌트 기본 설정 추가

UGunManager::UGunManager()
{
    // 컴포넌트 기본 설정
    PrimaryComponentTick.bCanEverTick = false; // 틱 불필요하므로 비활성화
    
    // 기존 총기 데이터 초기화 로직은 그대로 유지
    OwnedGuns.Add(EGunType::PISTOL, 500);
    OwnedGuns.Add({EGunType::RIFLE, 0});
    OwnedGuns.Add({EGunType::SHOTGUN, 0});
}

GunManager는 매 프레임 업데이트가 필요 없으므로 틱을 비활성화하여 성능 최적화

6. PlayerCharacter 생성 로직 전면 수정

생성자에서 컴포넌트로 생성

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;
}

7. BeginPlay 정리 - 불필요한 코드 제거

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());
}

8. 개선 효과와 최종 결과 🎯

완전한 문제 해결

  • 가비지 컬렉션 문제 100% 해결: 컴포넌트는 액터에 의해 자동 보호
  • 생명주기 동기화: 플레이어 캐릭터가 파괴될 때 자동으로 함께 파괴
  • 메모리 관리 자동화: 수동 메모리 관리 불필요

개발 편의성 향상

  • 에디터 지원: Details 패널에서 컴포넌트로 표시되어 디버깅 용이
  • 블루프린트 호환: 나중에 블루프린트에서도 접근 가능
  • 코드 단순화: 복잡한 생명주기 관리 로직 제거

안정성과 표준 준수

  • 언리얼 표준: 엔진의 권장 패턴을 따르는 안전한 구조
  • 예측 가능성: 액터와 동일한 생명주기로 예측 가능한 동작

9. 트러블슈팅 과정에서 배운 점

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")

핵심 포인트

  • 생명주기 관리: UObject → UActorComponent로 변경하여 안전한 메모리 관리
  • 생성 시점: BeginPlay() → 생성자로 이동하여 컴포넌트 시스템 활용
  • 가비지 컬렉션: NewObject의 함정을 피하고 CreateDefaultSubobject 사용
  • 디버깅 강화: 각 단계마다 로그를 추가하여 문제 발생 시점 추적 가능
  • 표준 준수: 언리얼 엔진의 권장 패턴을 따라 안정성과 유지보수성 확보

0개의 댓글