언리얼에서는 객체가 UObject를 상속받아야 GC의 관리 대상이 된다
따라서 NewObject<> / SpawnActor<> 같은 엔진 API로 객체를 생성해야 하고, 그 포인터를 UPROPERTY로 들고 있어야 추적 및 수명 관리가 가능하다
Root부터 OuterChain을 따라가며 Mark & Sweep 알고리즘으로 관리
new로 만들면 안 된다는 것
또한 생성한 객체를 delete로 직접 제거하면 엔진 내부가 망가져서 하면 안 됨
class UMyClass : public UObject
{
UPROPERTY() // UPROPERTY 설정
UObject* SafeRef;
};
void UMyClass::Example()
{
SafeRef = NewObject<UInventory>(); // 엔진 API로 객체 생성
UObject* obj = NewObject<UObject>();
obj = nullptr; // delete가 아니라 참조만 끊으면 GC가 알아서 제거
}
UCLASS(BlueprintType)
class MYGAME_API UMyCharacter : public UObject
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite)
// ... //
UFUNCTION(BlueprintCallable)
// ... //
};
UPROPERTY(SaveGame)
FName PlayerName; // 자동으로 세이브 파일에 저장
UPROPERTY(Replicated) // 네트워크 동기화
int32 PlayerScore;
// 함수도 네트워크를 통해 호출 가능
UFUNCTION(Server, Reliable)
void ServerFireWeapon();
UCharacter* player = NewObject<UCharacter>();
player = nullptr;
객체에 필요한것들을 생성자에서 미리 준비해놓는 것
게임 시작 전부터 존재하고, 에디터와 연동도 됨
보통 컴포넌트 생성에 사용
class AMyActor : public AActor
{
public:
AMyActor()
{
MeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("MeshComp"));
}
private:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
UStaticMeshComponent* MeshComp;
};
안정적이고 예측 가능
자동으로 객체에 등록되고, 객체 제거시 자동으로 사라짐
런타임에 필요할 때만 동적으로 생성 가능
컴포넌트의 경우 Register Component로 등록해주고, Attach to Component로 붙여줘야하는 등 작업이 필요
TSubclassOf<UMyClass> BlueprintClass; // 블루프린트 클래스 변수
UObject* NewObj = NewObject<UMyClass>(
this, // Outer (주인)
BlueprintClass.Get(), // 클래스 타입
TEXT("ObjectName") // 이름 (선택)
);
Outer : Outer의 하위체계로 객체를 생성. 즉, GC의 관리체계에서 연결이 이어지도록 해서 Mark과정에서 표시되도록 함
<T> : 컴파일 타임에 객체 생성이 안전한지를 확인하게 해줌
매개인자에서의 클래스 : 런타임에 해당 클래스로 생성.
동적필요 없으면, 정적으로 UMyClass::StaticClass() 처럼 생성도 가능
NewObject<UObject>(GetTransientPackage()) : 파일에 저장은 안 할 임시 객체일 때. 영구보존
NewObject<UObject>(GetWorld()) : 레벨소유. 레벨 바뀌면 사라짐
NewObject<UObject>(GetGameInstance()) : 게임인스턴스 소유. 영구보존
NewObject<UActorComponent>(MyActor) : 액터 소유. 액터의 생명주기와 함께 감
// this -> parent -> child
UObject* Parent = NewObject<UObject>(this);
UObject* Child = NewObject<UObject>(Parent);
// SomeOtherObject -> parent
Parent->Rename(nullptr, OtherObject);
Rename을 통해 Parent의 이름을 바꾸고, OtherObject로 Outer를 설정
그럼 Parent에 달려있던 Child는 고아 객체가 되어버려 에러 발생할 수 있다
따라서, Outer는 설정한 이후에 수정 안 하는 것이 좋고 하게 된다면 Child도 다 해주어야 함
UCLASS()
class UMyObject : public UObject
{
GENERATED_BODY()
public:
virtual void PostInitProperties() override;
virtual void PostLoad() override;
virtual void BeginDestroy() override;
virtual void FinishDestroy() override;
};
PostInitProperties : 객체가 생성된 직후, 생성자가 끝난 다음에 호출.
초기 상태를 수정하거나 유효성 검사할 때 주로 사용
PostLoad : 세이브파일(Asset이나 레벨)로부터 이 객체가 메모리에 로드되고 나서 호출.
로드된 정보 검사할 때 사용
BeginDestroy : GC가 삭제하려고 결정한 직후에 호출(레벨에서 사라질때).
타이머, 델리게이트, 바인딩, 네트워크 등 정리 해제 작업 함
FinishDestroy : 메모리에서 제거되기 직전에 호출. 로그 출력할 때 사용
Super::함수 빼먹지 말기
UPROPERTY와 UObject 사용하기
Outer Chain 사용
정말 최후의 방법으로 Root로 설정
// 비동기 로딩 중에 삭제되면 안 되는 객체
class UAssetLoader : public UObject
{
public:
void LoadAssetAsync(const FSoftObjectPath& AssetPath)
{
AddToRoot(); // 로딩 시작 전에 자신을 보호
// 비동기 로딩 시작
UAssetManager& AssetManager = UAssetManager::Get();
StreamableHandle = AssetManager.LoadAssetAsync(AssetPath,
FStreamableDelegate::CreateUObject(this, &UAssetLoader::OnLoadComplete)
);
}
private:
void OnLoadComplete()
{
RemoveFromRoot(); // 로딩 완료 → 보호 해제
}
};
UCLASS()
class UInventory : public UObject
{
GENERATED_BODY()
public:
UPROPERTY()
TArray<UObject*> Items; // Hard 레퍼런스
};
하드레퍼런스로 하면, 아이템은 인벤토리에 의해 관리되고, GC도 관리해줌
GC가 Items 배열의 아이템들을 다 관리해줌
인벤토리가 사라지면 아이템도 같이 GC에 의해 사라짐
부모-자식 관계처럼 소유 관계가 명확할 때 하드 레퍼런스를 사용
UCLASS()
class UItemWidget : public UObject
{
GENERATED_BODY()
public:
UPROPERTY()
TWeakObjectPtr<AActor> ObservedActor; // Weak 레퍼런스
};
하드 레퍼런스와 다르게 Weak 레퍼런스는 객체에 대해 읽기만 하지 존재를 책임지진 않음
따라서 GC에 의해 제거될 수 있다
따라서 항상 사용하기 전에 IsValid로 유효성 검사를 해주어야함
책임의 정도가 약할 때, 값을 읽고만 싶을 때(관찰) 소프트 레퍼런스를 사용
// Hard Reference 체크
UPROPERTY()
AActor* MyActor;
void CheckHardRef()
{
if (IsValid(MyActor)) // 전역 함수 IsValid() 사용
{
MyActor->DoSomething(); // 바로 사용
}
}
// Weak Reference 체크
UPROPERTY()
TWeakObjectPtr<AActor> MyActorWeak;
void CheckWeakRef()
{
if (MyActorWeak.IsValid()) // 포인터의 멤버 함수 IsValid()
{
AActor* Actor = MyActorWeak.Get(); // Get()으로 포인터 획득
Actor->DoSomething(); // 이제 안전하게 사용
}
}
하드 레퍼런스의 경우, 전역함수인 IsValid를 통해 사용
Weak 레퍼런스의 경우, Weak포인터의 멤버함수인 IsValid를 사용하고 Get멤버함수까지 사용해야 함