UE5

Unreal Object Handling

뽀또치즈맛 2024. 12. 19. 08:11

 

언리얼 오브젝트 핸들링이란?

 
UObject 시스템의 기능에 대한 개요로,
클래스, 프로퍼티, 함수에 적합한 매크로로 마킹해주면,
UClass, UProperty, UFunction 으로 변한다.
그러면 언리얼 엔진이 접근할 수 있어,
다수의 내부적인 처리 기능을 구현할 수 있다.
 

자동 프로퍼티 초기화

 
UObject는 생성자 호출 전 초기화시 자동으로 0으로 채워진다.
이러한 초기화 과정은

1) 전체 클래스,
2) UProperties(GC시스템에 의해 관리되는 변수들),
3) 네이티브 멤버(C++로 선언된 일반 멤버 변수)

모두를 포함한다.
 
이후 생성자에서 원하는 값으로 초기화할 수 있다.
 
즉, 이 말은
자동 초기화 후에, 생성자에서 커스텀 초기화가 가능하다는 뜻이다.
자동 추기화는 앞서 말했듯이
UObject가 생성될 때, 메모리가 먼저 0으로 설정되는 것이다.
(= 이는 필자의 생각에는 자바의 작동 방식과 비슷한 것으로 느꼈다.
자바의 GC작동 방식과 변수를 직접 클라이언트가 관리하지 않음과
직접 생성자에서 값을 설정해주지 않아도
프로그램이 알아서 0으로 초기화해주는 것들의 개념이 흡사했다.)
 
생성과 동시에 0으로 먼저 설정되어 안전성을 보장하며,
이후 프로그래머가 생성자에서 커스텀 초기화를 할 수 있는 것이다.
<예시>

UMyObject::UMyObject()
{
    CustomValue = 42; // 멤버 변수에 사용자 정의 값 할당
}

 
해당 기능이 언리얼에 있으면 다음과 같은 장점이 있다.

  1. 메모리 안전성 보장
    • 초기화되지 않은 변수로 인해 발생할 수 있는
      예기치 않은 동작이나 버그를 방지한다.
  2. 일관성 제공
    • 모든 멤버가 동일한 초기 상태 (0 또는 nullptr)에서 시작하므로,
      디버깅과 테스트가 용이해진다.
  3. 커스텀 초기화의 유연성
    • 자동 초기화 이후, 생성자에서 필요한 값을 할당할 수 있으므로
      기본값 설정과 사용자 정의 초기화를 모두 지원한다.

 

레퍼런스 자동 업데이트

 
AActor 또는 UActorComponent 가 소멸되거나
다른 식으로 플레이에서 제거되면,
리플랙션 시스템에 보이고 있는 그에 대한 모든
(TArray 같은 언리얼 엔진 컨테이너 클래스에 저장된 포인터와
Uproperty 포인터 등의) 레퍼런스는 자동으로 null이 된다.
이는 허상 참조를 예방하여 문제의 소지를 줄인다는 장점도 있지만,
다른 코드 부분에서 AActor와
UActorComponent 포인터를 소멸시키는 경우
null이 된다는 것을 뜻하기도 한다.
여기서 최고의 장점은 null 검사 안전성이 높다는 것인데,
일반적인 null 포인터의 경우와
null이 아닌 포인터가 삭제된 메모리를 가리키는 경우
둘 다 감지해 내기 때문이다.
 
여기서 한 가지 중요한 점은,
이 기능은 UPROPERTY로 마킹되어 있거나
언리얼 엔진 컨테이너 클래스에 저장된
UActorComponent 또는 AActor 레퍼런스에만 적용된다는 점이다.
raw 포인터에 저장된 오브젝트 레퍼런스 언리얼 엔진이 알지 못하기 때문에,
자동으로 null 되거나, 가비지 컬렉션이 방지되지 않는다.
 

TWeakObjectPtr와 가비지 컬렉션의 관계

 
그렇다고 모든 UObject* 변수가 UPropert가 되어야 한다는 뜻은 아니다.
UProperty가 아닌 오브젝트 포인터가 필요한 경우,
TWeakObjectPtr 사용을 고려해보자.
이는 약한 포인터로, 가비지 컬렉션을 방지하지는 않지만,
가비지 컬렉션을 방지하지 않는다는 말은 즉슨
"1. TWeakObjectPtr은 객체의 수명 관리에 관여하지 않는다는 것
 2. TWeakObjectPtr은 GC의 가비지 컬렉터 참조 카운터를 증가시키지 않는다."
이라는 것을 유추할 수 있다.
 
실질적으로
공식 문서에서도,
TWeakObjectPtr에 대해서
가비지 컬렉션을 방지하지 않지만(== 가비지 컬렉션에 영향을 끼치지 않는다) 이란 구문과
접근 전 유효성 검사가 가능하며
(유효성 검사를 한다 == 있는지 없는지 확인해야 하는 애)
거기서 가리키는 오브젝트가 소멸된 경우 null 설정도 가능하다.
(소멸된 경우가 특이 케이스가 아니니,
그에 대한 대안책이 마련되어 있음을 나타내는 구문)
라고 되어있다.
 
그렇다면 객체가 더 이상 사용되지 않는다고 GC가 판단하면,
TWeakObjectPtr이 객체를 약하게 참조하고 있다하더라도,
객체가 delete 된다는 뜻이다.
 
이렇게 객체가 파괴되면 TWeakObjectPtr은 자동으로 nullptr로 설정된다.
따라서 TWeakObjectPtr은 참조 객체가 파괴된다 하더라도,
자동으로 내부 참조를 nullptr로 설정되기 때문에
가비지 컬렉션이 해당 객체를 제거하더라도 안전성을 보장한다.
 
그럼 반대로 메모리를 삭제하고 싶지 않다면?
강한 참조 즉, TObjectPtr와 같은 참조 카운트를 증가시키는 포인터를 사용하자.
 

UObject*와 UProperty의 null 설정 가능의 2가지 경우

 
참조된 UObject*와 UProperty는
자동 초기화로 인해 값을 설정해주지 않으면 null 된다고 하였다.
 
그럼 UObjec*와 UProperty 가 자동으로 null이 되는 또 한가지 경우는
에디터에서 에섯을 'Force Delete'(강제 삭제)한 경우이다.
그에 따라, 에셋인 UObject에 대한 모든 코드 작업시
이 포인터가 null이 되도록 처리해야만 한다.
 
지금까지 나온 포인터 타입을 비교해보자.
 
 

TWeakObjectPtr, UProperty, UObject* 동작 비교

포인터 타입객체 생존 여부에 영향객체 파괴 시 동작사용 목적
TWeakObjectPtrx(영향 없음)nullptr로 설정객체가 삭제될 수 있는 상태에서
안전한 접근
TObjectPtrO(영향 있음)객체 생존 보장
(강제 삭제시 null로 처리)
UObject를 강하게 참조해야 하는 경우
UPROPERTYO(영향 있음)객체 생존 보장
(강제 삭제시 null)
직렬화,
가비지 컬렉션 관리가 필요한 경

 
TWeakObjectPtr 추가 설명 및 설명을 위한 예제 코드

TWeakObjectPtr<AActor> WeakActor = MyActor; // 약한 참조

// 객체가 여전히 존재하는지 확인
if (WeakActor.IsValid()) {
	WeakActor->SetActorLocation(FVector(0.f, 0.f, 100.f));
}
else {
	UE_LOC(LogTemp, Warning, TEXT("Actor has been garbage collected!"));
}

 
다음 코드는 WeakActor의 기능을 가시화 한 것이다.

  1. WeakActor는 객체의 생존 여부에 영향을 미치지 않음
    • 가비지 컬렉터는 MyActor가 더 이상 GC 카운트가 0이라면
      메모리를 회수한다.( 강한 참조가 아니라면 메모리 회수)
    • 이 과정에서 WeakActor는 nullptr로 설정된다.
  2. GC 참조(강한 참조)가 없는 경우 객체는 회수된다
    • 다른 곳에서 객체를 강하게 참조하고 있지 않다면, 
      (== GC 참조 카운트가 0이라면)
      MyActor는 가비지 컬렉션에 의해 제거가 된 상태이다.

TWeakObjectPtr은 가비지 컬렉션에 의해 제거될 수 있으니,
참조는 필요하지만,
해당 참조로 인해 메모리가 생명주기에 영향을 끼치고 싶지 않을 때 사용하면 좋다.
이러한 특징은 참조로 불필요한 생명주기로 인한 메모리 관리 효율성을 저하시키지 않는다.
즉, 참조로 인한 생명주기 메모리 관리 효율성을 높인다는 뜻이다.
좀 더 쉽게 말하면, 필요하지 않는 객체가 계속 유지되는 것을 방지하는 참조이다.
 

Serialization(직렬화)

UObject가 직렬화 될 때, 모든 UProperty 값은 명시적으로 
"transient"(휘발성) 로 표시되었거나 생성자 이후의 기본값에서
변경되지 않은 경우를 제외하고는 자동으로 기록되거나 읽힌다.
예를 들어, AEnemy 인스턴스를 레벨에 배치하고,
Health 값을 500으로 설정한 뒤 저장하면,
UClass 정의 외에 별도의 코드를 작성하지 않아도
이를 성공적으로 다시 불러올 수 있다.
 
Uproperty가 추가 또는 제거될 때,
기존 컨텐츠 로드는 매끄럽게 처리된다.
새 프로퍼티는 새 CDO에서 기본값을 복사해 온다.
제거된 프로퍼티는 말없이 무시된다.
 
커스텀 작동 방식이 필요한 경우,
UObject::Serialize 함수를 덮어쓰면 된다.
데이터 오류, 비전 번호 검사, 데이터 포맷 변경 시 자동 변환 또는
업데이트 수행 등에 유용하게 쓰일 수 있다.
 

프로퍼티 값 업데이트 하기

UClass의 클래스 디폴트 오브젝트(CDO)가 변경되면,
엔진은 해당 변경 사항이 로드 될 때,
클래스의 모든 인스턴스에 적용하려 시도한다.
주어진 오브젝트 인스턴스에 대해서,
업데이트된 변수 값이 이전 CDO 값과 일치한다면,
새로운 CDO에 저장된 값으로 업데이트된다.
변수의 값이 변경되었을 경우,
그 변수가 의도적으로 설정되었다고 가정하고
그 변경사항을 보존한다.
 
예를 들어, AEnemy 몇몇의 오브젝트를 레벨에 여럿 배치했고,
체력 기본값이 생성자에서 100으로 설정되었다고 해보자.
Enemy_3의 체력이 500으로 설정했다고 가정해보자.
왜냐하면 그들은 특히 강하기 때문이다.
근데 이제 마음이 바뀌어서
체력 기본 값이 150으로 증가했다고 가정해보자. 
그 이후 우리가 다시 레벨을 로드하면,
언리얼은 우리가 CDO가 변경했음을 인식하고
Enemy의 모든 인스턴스를 체력의 기존 기본 값인 100에서
150으로 업데이트 해준다.
여기서 Enemy_3의 체력의 기본 값은 500으로 변경하지 않고 남겨둔다.
왜냐하면 이전에 사용된 기본 값을 적용하지 않았기 때문이다.
 

Editor Intergration (에디터 통합)

UObject 와 UProperties 들은 에디터에서 인식되며,
에디터는 별도의 특별한 코드를 작성할 필요 없이,
이 값들을 자동으로 노출시킬 수 있다.
이는 선택적으로 블루프린 비주얼 스크립팅 시스템으로 통합이 가능하다.
변수와 함수의 노출 및 접근 여부를 제어할 수 있는 옵션이 많이 있기 때문이다.
 

런타임 유형 정보 및 형변환

UObject는 언리얼 엔진 리플렉션 시스템의 일부이므로,
항상 자신이 무슨 UClass인지 알고 있으며,
런타임에 유형에 대한 결정과 캐스트를 내릴 수 있다.
 
네이티브 코드에서,
모든 UObject 클래스에는 그 부모 클래스로 설정된 커스텀
"Super" typedef가 있어,
오버라이드 동작을 쉽게 제어할 수 있다.
 
예시 코드

class AEnemy : public ACharacter
{
	virtual void Speak() {
    	Say("Time to fight!");
    }
};

class AMegaBoss : public AEnemy {
	virtual void Speak() {
    	Say("Powering up! ");
        Super::Speak();
    }
};

 
우리가 보다 싶이,
Speak 함수를 호출하면, 결과 값으로는
MegaBoss는 "Power up! Time to fight!"라는 내용의
Say 함수를 호출할 것이다.
 
또한 우리는 템플릿 기반 Cast 함수를 사용하여 
기본 클래스에서 파생된 클래스로 부터 개체를 안전하게 캐스팅 하거나,
isA를 사용하여 개체가 특정 클래스에 속하는지 확인할 수도 있다.
 
간단한 예시

class ALegendryWeapon : pulbic AWeapon
{
	void SlayMegaBoss() {
    	TArray<AEnemy> EnemyList = GetEnemyListFromSomewhere();
        
        // The legendary weapon is only effective against the MegaBoss
        for(AEnemy Enemy : EnemyList) {
        	AMegaBoss* MegaBoss = Cast<AMegaBoss>(Enemy);
            if(MegaBoss)
            {
            	Incinerate(MegaBoss);
            }
        }
    }
};

 
 
여기서 Cast를 사용하여 AEnemy를 AMegaBoss로 형변환 시도했다.
문제의 오브젝트가 실제로 AMegaBoss(또는 그 자손 클래스)가 아닌 경우
Cast는 널 포인터를 반환하므로 절절한 대응이 가능하다.
MegaBoss에 대해서는 Incinerate 함수만 호출한다.
 

Garbage Collection

언리얼에서는 더이상 참조되지 않거나 명시적으로 소멸 예약시킨
UObject를 주기적으로 정리하는 가비지 컬렉션 스키마를 사용한다.
엔진에서는 레퍼런스 그래프를 만들어 어느 오브젝트가 사용중이고
어떤 것이 참조된 것이 없는 root인지 확인한다.
 
이 그래프 루트에는 "루트 세트"로 지정된 UObject 세트가 있다.
모든 UObject는 루트 세트에 추가할 수 있다.
 
가비지 컬렉션이 실행될 때,
엔진은 루트 세트에서 시작하며
알려진 UObject 참조 트리를 탐색함으로써
모든 참조된 UObject를 추적할 수 있다.
트리 탐색 중 발견되지 않은 것들 
즉, 참조되지 않은 오브젝트는
더 이상 필요하지 않은 오브젝트라 가정하고 제거한다.
 
즉,
루트 집합은 가비지 컬렉션 프로세스가 시작되면
루트 집합은 계속 초기화된다.
루트 집합은 트리 구조를 가지며,
그래프 순회를 통한 깊이 우선 탐색을 실행한다.
도달 가능성 분석이 완료되면, 
참조된 개체는 활성 상태로 간주하고,
참조되지 않은 개체는 비활성화 상태로 간주하여
비활성화 상태로 분류된 개체는 메모리에서 제거된다.
 
이러한 가비지 컬렉터를 사용하기 위해서는
UObject* 를 사용하거나 혹은 UPROPERTY를 사용해야 한다.
원시 포인터 사용은 지양하자.
 
여기서 한 가지 실질적인 의미는 일반적으로 살려두고자 하는
모든 Object에 대한 참조를 유지해야 한다는 것이다.
이는 간단한 Object 포인터이든
Object 포인터 유형을 포함하는 UE 컨테이너 클래스이든 상관없다.
예를 들어 TArray<UObject*>와 같이 말이다.
Actor와 해당 Component는 종종 예외인데,
Actor는 일반적으로 Level과 같은 루트 세트로 다시 연결되는
Object에 의해 참조되고 Actor의 Component는
Actor 자체에 의해 참조되기 때문이다.
Actor는 해당 Destroy 함수를 호출하여
명시적으로 파괴되도록 표시할 수 있으며,
이는 진행 중인 게임에서 Actor를 제거하는 표준 방법이다.
Component는 DestroyComponent 함수를 사용하여
명시적으로 파괴할 수 있지만,
일반적으로 소유한 Actor가 게임에서 제거될 때 파괴된다.
 
언리얼 엔진에서는 가비지 컬렉션은 빠르고 효율적이며,
오버헤드를 최소화하도록 설계된 여러 가지 기본 제공 기능이 있다.
여기에는 비참조 객체를 식별하기 위한 멀티스레드 도달 가능성 분석,
컨테이너에서 액터를 가능한 빨리 제거하기 위해
최적화된 코드 해시 제거 코드가 포함된다.
가비지 컬렉션이 수행되는 방법과 시기를
보다 정밀하게 제어하기 위해 조정할 수 있는 다른 기능이 있으며,
대부분은 프로젝트 설정의 엔진 - 가비지 컬렉션 에서 찾을 수 있다.
 
다음 설정은 일반적으로 프로젝트의 가비지 컬렉터 성능을 조정하는 데 사용된다.

세팅기능 설명
Create Garbage
Collector UObject Clusters
이 기능은 프로젝트 설정에서 켜거나 끌 수 있다
(기본적으로는 켜져있다.).
이 기능을 키면 관련 객체가
가비지 수집 클러스터에 그룹화되므로
각 개별 객체가 아닌 클러스터 자체만 검사하면 된다.
즉, 전체 클러스터를 하나의 개체로 처리할 수 있기 때문에
도달 가능성이 더 빨리 수행되지만,
해당 클러스터의 개별 항목은 모두 해시되지 않고
동일한 프레임에서 삭제될 준비가 되어
클러스터가 너무 클 경우 버벅임이 발생할 수 있다.
일반적인 경우에서는,
클러스터를 만들면
가비지 컬렉션 퍼포먼스가 향상되며, 
도달가능성 분서겡 소요되는 시간이 단축된다.
Merge GC Clusters해당 클러스터를 키면
한 클러스터의 오브젝트가
다른 클러스터의 오브젝트를 참조할 때
클러스터를 합치도록 한다.
참고로 병합을 유발시킨 레퍼런스를 지워도 새로 병합된
크러스터는 어떤 식로든 분해되거나 분리되지 않는다.
이 기능을 작동시키려면
Create Garbage Collector UObject Clusters
옵션도 켜져있어야 한다.
그러면 가비지 컬렉터의
이렇게 하면 가비지 컬렉터가
객체를 해싱 해제하고 파기하는 빈도가 줄어들지만
더 많은 수의 객체가 한 번에 해싱 해제되고 파기다.
또한 클러스터의 객체를 참조하면
전체 클러스터가 가비지 수집되지 않으므로
병합된 클러스터에서는 가비지 수집이 발생하지 않는 경우가 있을 수 있다.
Actor Clustering Enabled 액터 클러스터 활성화 - 프로젝트 세팅 에서 이 옵션을 켜고, 
bCanBeInCluster 변수를 true 로 설정하거나,
코드에서 CanBeInCluster 함수가 true 를 반환하도록
덮어써 주면 액터를 클러스터에 넣을 수 있다.

기본적으로
액터와 구성 요소는 이 기능이 꺼져 있지만
정적 메시 액터와 반사 캡처 구성 요소는 예외입니다.
이 기능은 한 번에 모두 파괴될 것으로 예상되는 액터,
일반적으로 이를 포함하는 하위 수준을 언로드하지 않고는
파괴될 수 없는 수준에
배치된 정적 메시를 그룹화하는 데 유용합니다.

Blueprint Clustering Enabled 블루프린트 클러스터 활성화
블루프린트의 UBlueprintGeneratedClass 및 관련 데이터,
이를테면 공유 UPROPERTY 및 UFUNCTION 데이터같은 것은,
이 세팅을 켜서 클러스터로 묶을 수 있다.
여기서 한 가지 중요한 점은,
이렇게 생긴 클러스터는 블루프린트의 개별 인스턴스가 아닌,
Blueprint Generated Class 자체를 참조한다는 점이다.
Time Between Purging
Pending Kill Objects
킬 대기중 오브젝트 제거 간격
프로젝트 세팅에서 가비지 컬렉션 발동 빈도를 조절할 수 있다.
이 하이 레벨 컨트롤은 특히나 버벅임 방지에 좋다.
컬렉션 발동 간격을 줄임으로써,
다음 도달가능성 분석 패스에 걸릴
도달불가 오브젝트 발생 가능성을 낮추고,
동시에 너무 많은 액터를 정리하느라
발생할 수 있는 버벅임도 피할 수 있다.

 
 
++Plus++
클러스터(Cluster)
Unreal Engine의
GC 클러스터는 메모리 관리 및 성능 최적화를 목적으로,
UObject들을 하나의 그룹으로 묶어서 처리한다.
이를 통해 가비지 컬렉션의 효율성을 높이고 오버헤드를 줄일 수 있다.
++++++
 
++Plus++
 
Garbage Collection Schema(가비지 컬렉션 스키마)
가비지 컬렉션 스키마는
객체를 추적하고, 참조를 관리한다.
현재 어떤 객체가 여전히 사용 가능한 상태인지 추적하여
객체가 루트 객체에서 참조되거나
다른 객체에서 참조되면 수거 대상이 아님으로 간주한다.
 
수거 기준은 객체가 더 이상 참조되지 않으면 수거 대상으로 표시된다.
참조 카운터가 0이 되거나,
도달 가능성 분석에서 제외된 객체이다.
 
언리얼 엔진은 마크 스윕(Mark-and-Sweep)을 사용한다.
마크 스윕은 2가지 단계를 가진다.
 
 

마크 단계

마크 단계에서는 객체가 생성되면 마크 비트가 9으로 설정된다.
마크 단계에서 도달 가능한 모든 개체에 대한 마크 비트를 1로 설정한다.
이 작업을 수행하면 그래프 순회를 수행하여, 깊이 우선 탐색 방식이 효과적이다.
 
예시 코드

Mark(root) 
markedBit(root) = false이면 
                     markedBit(root) = true 
                                       root가 참조하는 각 v에 대해 
                                       Mark(v)

 

스윕 단계

이름에서 알 수 있듯이 도달할 수 없는 개체를 "스윕"한다.
즉, 도달할 수 없는 개체에 대한 힙 메모리를 지운다.
표시된 값이 false로 설정된 모든 개체는 힙 메모리에서 지워지고,
다른 모든 개체 즉, 도달 가능한 개체의 경우
표시된 비트가 true로 설정됩니다.
이제 모든 도달 가능한 개체에 대한 표시 값이 false로 설정됩니다.
알고리즘을 실행하고 다시 표시 단계를 거쳐 모든 도달 가능한 개체를 표시하기 때문입니다.

Sweep() 
힙의 각 객체 p에 대해 
markedBit(p) = true이면 
                  markedBit(p) = false 
                                 그렇지 않으면 
                                     heap.release(p)

++++
 
네트워크 리플리케이션
 
이 시스템에는 네트워크 통신과 멀티플레이어 게임을
UObject 원활하게 하는 견고한 기능 세트가 포함되어 있다.

UProperties 네트워크 플레이 중에 Engine에 
데이터를 복제 하라고 알리기 위해 태그를 지정할 수 있습니다 . 
 여기서 일반적인 모델은 변수가 서버에서 변경되고
Engine이 이 변경 사항을 감지하여
모든 클라이언트에 안정적으로 전송한다는 것이다.
클라이언트는 리플리케이션에서 변수가 변경될 때
콜백 함수를 선택적으로 수신할 수 있다.
 
UFunctions또한 원격 머신에서 실행되도록 태그할 수 있다. 
예를 들어, 클라이언트 머신에서 호출되는 "서버" 함수는
해당 Actor의 서버 버전에 대해 서버 머신에서 함수를 실행합니다.
반면, "클라이언트" 함수는 서버에서 호출될 수 있으며
해당 Actor의 소유 클라이언트 버전에서 실행됩니다.
 
 
 
 
https://www.geeksforgeeks.org/mark-and-sweep-garbage-collection-algorithm/
https://dev.epicgames.com/documentation/en-us/unreal-engine/unreal-object-handling-in-unreal-engine
 

'UE5' 카테고리의 다른 글

언리얼 내부 - 모듈 (Modules) 2  (0) 2024.11.25
언리얼 내부 - 모듈 (Modules)  (0) 2024.11.25
점진적 가비지 컬렉션  (0) 2024.11.23
UE5 하마치를 이용한 서버 열기  (0) 2024.10.22
서버 시작하기  (2) 2024.10.14