* UObject 시스템

 

- 속성 초기화 자동화

 

UObject는 초기화 시 자동으로 0으로 초기화 된다. 이후 클래스 생성자에서 사용자가 멤버를 초기화할 수 있다.

 

 

- AActor, UActorComponent의 참조 업데이트 자동화

 

AActor와 UActorComponent가 파괴되는 경우 리플렉션 시스템이 이 객체를 관리하고 있다면 이 객체에 대한 모든 참조는 자동으로 널로 초기화된다. 여기서 리플렉션 시스템의 관리를 받기 위해서는 UProperty 나 TArray와 같은 언리얼 엔진에서 제공하는 컨테이너를 사용해야한다.

 

이는 이러한 다른 코드에 의해 삭제될 때 언제든 널로 초기화되기 때문에 허상 포인터(dangling pointer)는 따로 신경쓰지 않고 널 체크만 잘하면 안정적으로 사용할 수 있다. 널 체크를 통해 일반적인 널 포인터와 삭제된 객체 두 가지 케이스를 판별할 수 있다.

 

이 때문에 대부분 UObject들은 엔진의 관리를 받아 자동 널 초기화와 가비지컬렉션을 방지하기 위해  UProperty나 TArray와 같은 언리얼 엔진 제공 컨테이너를 사용해야한다. 만약 가비지컬렉션을 방지하지 않고(삭제에 관여하지 않고, 소유하지 않고) 널만 체크하고 싶다면 TWeakObjectPtr을 사용해야한다. TWeakObjectPtr은 객체 접근 전 유효성 검증을 요청하여 삭제된 객체라면 널로 설정된다.

 

에디터에서 에셋을 Force Delete하는 경우에는 모든 UProperty는 널로 설정된다. 따라서 에셋 UObejct의 경우 널 체크 후 사용하도록 코드를 작성해야 한다.

 

 

- 직렬화

 

UObject를 직렬화 시 transient로 지정되지 않고, 생성자 이후 기본 값에서 변경되었다면 해당 UProperty 값들은 자동으로 읽고 쓰게 된다. 예를 들어 레벨에서 한 인스턴스의 값이 A로 설정되었다면 값 A가 저장되고 리로드된다.

 

UProperty가 추가되거나 삭제되었을 때 기존 로딩된 컨텐츠는 원활하게 처리되며 새로운 속성은 새 CDO에서 복사된 기본 값을 가져오고, 삭제된 속성은 무시된다.

 

UObject::Serialize를 재정의 함으로써 커스텀 직렬화 로직을 만들 수 있다. 주로 데이터 검증,  추가 업데이트 로직 등을 위해 사용된다.

 

 

- 속성 값 업데이트

 

UClass의 CDO가 변경되었을 때(생성자에서 속성 값이 변경되었을 때) 일반적으로 로드된 해당 클래스의 모든 인스턴스에 변경사항을 적용한다.

 

예외가 존재한다. 인스턴스 중에 이전 CDO의 해당 변수의 값과 동일하지 않다면, 새로운 CDO의 해당 변수의 값으로 초기화되지 않는다. 예를 들어 HP값이 CDO에서 100인 인스턴스가 CDO에서 200으로 변경된다면 일반적인 경우라면 모두 200으로 업데이트 된다. 하지만 100이 아닌 다른 값으로 변경한 인스턴스(HP를 500 등)은 200으로 업데이트 되지 않는다.

 

 

- 에디터 통합

 

에디터에서도 UProperty를 인지할 수 있기 때문에 여러 옵션을 통해 에디터에 노출시킬 수 있다.

 

 

- 란타임 타입 정보와 캐스팅

 

UObject 들은 리플렉션 시스템의 일부이기 때문에 UClass에 대한 정보를 가지고 있다. 

 

정보를 기반으로 다음과 같은 기능을 제공한다.

  • Super : 부모 클래스의 이름을 typedef로 정의하고 있다. 이 키워드를 통해 부모 클래스에 접근하면 된다.
  • IsA : 해당 클래스가 입력으로 들어온 클래스의 자식 클래스인지 확인한다. (부모클래스와 자식클래스의 계층을 표현하는 배열과 해당 클래스의 인덱스를 통해 판별한다.)
  • Cast : 부모 클래스에서 안전하게 자식 클래스로 캐스팅할 수 있다. 만약 자식 클래스 객체가 아니면 널 포인터를 반환한다.

 

 

- 가비지 컬렉션

 

언리얼 엔진은 UObject에 대해 가비지컬렉션 기능을 제공하고 있으며, 가비지 컬렉션은 더 이상 참조되지 않거나 명시적으로 표시(PendingKill)된 객체들을 일정 간격으로 확인하여 삭제한다.

 

 

@ 참조 그래프(Reference Graph)

 

언리얼 엔진은 참조 그래프를 만들어 관리함으로써 어떤 UObject가 사용 여부를 판단할 수 있다. 참조 그래프의 루트에는 루트 셋(root set)으로 지정된 UObject들이 존재한다. 어떤 UObject라도 루트 셋에 추가될 수 있으며 가비지 컬렉션 수행 시 루트 셋부터 탐색을 시작하게 된다. 참조 트리에서 찾을 수 없는 참조 되지 않는 UObject들은 불필요한 것으로 간주되어 삭제된다.

 

 

@ UProperty와 언리얼 엔진 컨테이너(TArray 등)

 

일반적으로 생명을 유지하고 싶은 UObject들은 UProperty나 언리얼 엔진 컨테이너를 사용해야 한다. 단 AActor와 UActorComponent 종류의 클래스는 종종 예외다.

 

 

@ AActor와 UActorComponent 종류의 특별한 경우

 

Actor는 일반적으로 루트 셋에 연결된 객체(Actor가 속한 레벨 등)에 의해 참조된다. ActorComponent 역시 Actor에 의해 참조된다.

 

레벨과 같은 객체는 게임이 종료되거나 스트리밍 중인 레벨이 언로드되거나 레벨이 전이되지 않는 한 계속 유지되기 때문에 그냥 놔두면 원할 때 삭제할 수 없다. 이를 위해 게임 진행중에 Actor를 명시적으로 삭제할 수 있는 방법을 제공하고 있다. 이 방법은 Destroy 함수이며 Actor에 PendingKill이라는 플래그를 켜주는 역할을 한다. PendingKill로 마킹된 Actor는 다음 가비지 컬렉션 사이클 때 할당 해제된다. ActorComponent도 비슷하게 DestroyComponent라는 함수를 제공한다. 하지만 일반적으로 이를 소유중인 Actor가 파괴될 때 같이 파괴된다.

 

Actor가 명시적으로 Destroy를 호출 했을 때 바로 삭제되지 않고 PendingKill만 켜지게 된다. 그러면 이를 참조하는 다른 곳에서 해당 Actor가 삭졔 예정임에도 사용할 가능성이 높다. 이를 방지하기 위해 Actor 포인터에 대해 널 포인터(Actor 포인터가 UProperty 종류로 되어있어야 할당 해제될 때 널 포인터로 초기화된다.)와 PendingKill을 동시에 검사하는 IsValid라는 함수를 사용하여 체크하고 사용하도록 해야한다.

 

 

https://forums.unrealengine.com/t/garbage-collection-doesnt-null-my-pointer/469022

 

Garbage collection doesn't null my pointer

Are you checking right after destroying or when the GC has actually kicked in?

forums.unrealengine.com

 

https://create-new-worlds.tistory.com/145

 

[UnrealEngine] Actor 생명주기(Actor Lifecycle)

* 정의 Actor가 로드되거나 스폰되고, 소멸할 때까지의 과정을 Actor 생명주기(Actor Lifecycle)라고 한다. * 동작 방식 세부 내용은 여기서 확인하면 된다. https://docs.unrealengine.com/4.27/en-US/Programmin..

create-new-worlds.tistory.com

 

 

 

- 네트워크 리플리케이션

 

UObject 시스템은 네트워크 통신관련 강력한 기능들도 포함하고 있다.

 

UProperty의 속성 지정자를 통해 리플리케이션하도록 지정할 수 있다. 일반적으로 서버에서 변수가 변경되면 이 변경된 내용을 감지하여 모든 클라이언트에게 안정적으로 보낸다. 클라이언트는 리플리케이션을 통해 변수가 변경될 때 선택적으로 콜백 함수를 지정할 수 있다.

 

UFunction은 속성 지정자를 통해 원격 머신에서 수행되도록 할 수 있다.

  • server 함수는 클라이언트 머신에서 호출되며 서버 머신에서 서버 버전 Actor의 함수가 호출되도록 한다.
  • client 함수는 서버 머신에서 호출되며 해당 Actor의 소유 클라이언트 머신에서 클라이언트 버전 Actor의 함수를 호출한다.

 

* 정의

 

언리얼 엔진에서 게임 객체는 UObject라는 클래스의 객체로 표현된다. Actor와 함께(Actor도 UObject이다.) 게임 플레이 요소 중에 가장 기초적인 요소라고 할 수 있다. UCLASS 매크로를 사용하여 UObject에서 파생된 클래스의 태그를 지정할 수 있다.

 

 

* 동작 방식

 

- UObject의 기능

 

UObject는 다음과 같은 기능에서 효율적으로 사용될 수 있다.

 

  • Garbage collection
  • Reference updating
  • Reflection
  • Serialization
  • Automatic updating of default property changes
  • Automatic property initialization
  • Automatic editor integration
  • Type information available at runtime
  • Network replication

 

 

- UClass와 CDO(Class Default Object)

 

@ 정의

// UObject를 UObject 종류(언리얼 오브젝트)라고 간주하고 작성

UClass는 UObject에 대한 클래스를 정의하는 변수들과 함수들의 집합으로 구성되어 있다. 여기서 말하는 변수와 함수는 일반적인 C++ 함수와 변수이지만  객체 시스템에서 동작 방식을 제어하는 언리얼 전용 메타데이터로 태그가 지정되어있다. 간단히 정리하면 클래스 계층 구조 정보, 변수, 함수에 대한 정보를 기록하고 있는 메타 데이터이다.

 

CDO는 UObject 객체의 속성, 컴포넌트 등의 값들이 기본 값으로 설정되어 있는 클래스 기본 객체를 의미한다.

 

UClass와 CDO 모두 해당 객체 인스턴스에서 접근할 수 있지만 일반적으로 읽기 전용이다. UClass는  UObject 객체에서 GetClass()를 통해 접근할 수 있다. 

 

 

@ 생성 과정

엔진이 초기화될 때 클래스의 UClass 객체가 만들어지고 이 과정에서 기본 값들을 설정한 UObject의 생성자가 호출되어 CDO를 같이 생성한다. UClass는 CDO를 포함하고 있으며, 생성된 CDO를 수정되지 않은 상태로 유지된다.

 

 

@ 주의 사항

UObject의 생성자는 CDO 생성 목적으로 엔진 초기화 시점에 한번 호출되고 게임플레이에서는 사용되지 않기 때문에 레벨을 포함한 많은 객체들은 생성자 호출 시점에 존재하지 않을 것이다. 따라서 생성자 내부에서 객체를 사용하거나 게임플레이 코드를 넣으면 안된다. 이러한 코드는 다양한 시점에서 호출되는 제공되는 함수(BeginPlay 등)에서 작성되어야 한다.

 

 

- UCLASS, UPROPERTY, UFUNCTION 매크로

 

UCLASS 매크로는 UObject에 언리얼 기반 타입을 설명하는 UClass 참조를 제공하며 UObject를 사용하기 위해 필요한 매크로다.

 

UObject의 멤버 변수와 함수는 언리얼 엔진이 인식할 수 있게 설정할 수 있는데, 이는 UPROPERTY와 UFUNCTION과 같은 특별한 매크로를 사용함으로써 가능하다.

 

 

- 헤더 파일 형식

 

위에서 설명한 기능을 활용하기 위해서는 UHT(UnrealHeaderTool)이 전처리 과정을 수행할 때 정보를 수집할 수 있도록 특정한 형태를 갖춰야 한다.

 

헤더 파일에 이러한 형태를 적절하기 갖춰야 하는데, 가장 쉽게 에디터를 통해 클래스를 생성하면 기본적인 형태를 알 수 있다.  (UObject 파생 클래스 이름을 MyObject, 프로젝트 이름을 MyProject라고 가정하자.)

#pragma once

#include 'Object.h'
#include 'MyObject.generated.h'

/**
 * 
 */
UCLASS()
class MYPROJECT_API UMyObject : public UObject
{
    GENERATED_BODY()

};

 

@ generated.h : 클래스 생성 시 만들어지는 헤더로 여러가지 매크로를 정의하고 있다. 이러한 매크로는 언리얼 헤더 툴에 의해 파싱되어 코드로 변환되어 StaticClass, typedef Super, ThisClass 등과 같은 기본적인 함수를 제공하게 된다. include하는 파일 중에 가장 마지막에 작성해야한다.

#include "MyObject.generated.h"

 

@ UCLASS : 이 매크로는 언리얼 엔진이 이 클래스를 인식할 수 있도록 하는 역할을 한다. 여러 클래스 속성 지정자를 지원하고 있다.

UCLASS()

 

@ MYPROJECT_API : UMyObject를 다른 모듈에도 노출시키고 싶을 때 필요하다. 모듈 구현이나 플러그인에서 유용하게 사용된다.

class MYPROJECT_API UMyObject : public UObject

 

@ GENERATED_BODY : generated 헤더에서 정의된 매크로를 사용하여 여러 기본적인 기능을 제공한다. 클래스 내에 작성되어야 한다.

GENERATED_BODY()

 

 

- 생석과 삭제

 

NewObject() 팩토리 함수를 통해 간단히 생성될 수 있다.

 

UPROPERTY 매크로를 사용하는 UObject는 가비지컬렉션의 관리를 받게 된다. 이는 더 이상 사용되지 않는(참조되는 않는) 객체는 자동으로 처리된다는 것을 의미한다. MarkPendingKill 함수가 호출 되면 모든 객체에 대한 포인터를 NULL로 만들고 Global Searches에서 제거하게 된다. 이 후 다음 가비지컬렉션 사이클에서 삭제된다.

 

 

* 정의

 

언리얼에서는 리플렉션 시스템을 지원한다. 언리얼에서 리플렉션은 프로그램이 런타임에 자신을 검사하는 기능이다.

 

간단히 리플렉션은 구체적인 클래스 타입을 알지 못해도, 그 클래스의 메소드, 타입, 변수들에 접근할 수 있도록 해주는 설계 기법이라고 생각하면 된다.

 

 

* 사용 이유

 

리플렉션은 언리얼 엔진의 기본 적인 부분으로 에디터의 디테일 패널, 직렬화, 가비지컬렉션, 네트워크 리플리케이션, 블루프린트/C++ 통신 등 여러 곳에서 유용하게 사용된다.

 

 

* 동작 방식

 

C++ 언어에서는 리플렉션 시스템을 지원하고 있지 않기 때문에 언리얼 엔진은 자체적인 리플렉션 시스템을 가지고 있다. 이 리플렉션 시스템은 클래스, 구조체, 함수, 멤버 함수, 열거형에 대한 정보를 수집, 요청, 조작할 수 있도록 만들어져있다.

 

- 해더 파일이 리플렉션의 관리를 받기 위한 방법

 

가장 먼저 리플렉션 타입을 포함하는 해더을 구별하기 위해 특별한 include를 추가 해주면  그러면 언리얼 해더 툴이 이 파일을 인식할 수 있다. 다음과 같이 추가한다. #include "파일이름.generated.h"

 

예를 들어 다음과 같다.

#include "StrategyChar.generated.h"

 

 

- 속성 매크로

 

리플렉션 시스템의 관리를 받기 위해서는 유형이나 속성에 특정 주석을 달아두어야 한다. 그러면 언리얼 해더 툴(Unreal Header Tool, UHT)이 컴파일 시, 이 정보를 수집하게 된다. 특정 주석은 다음과 같은 형태를 띄고 있다.

  • UENUM()
  • UCLASS()
  • USTRUCT()
  • UFUNCTION()
  • UPROPERTY()

이러한 각 매크로는 해당 타입의 선언 앞에 작성하여야 하며 추가 지정자 키워드도 포함될 수 있으며 다음과 같은 계층 구조를 가지고 있다.

UField
	UStruct
		UClass (C++ class)
		UScriptStruct (C++ struct)
		UFunction (C++ function)

	UEnum (C++ enumeration)

	UProperty (C++ member variable or function parameter)

		(Many subclasses for different types)

UStruct는 구조에서 가장 기본적인 타입으로 클래스, 구조체, 함수와 같은 것들을 포함하고 있다. UClass는 함수나 속성들을 자식으로 가지고 있을 수 있지만 UFunction, UScriptStruct는 속성들만 가지고 있을 수 있도록 제한된다.

 

 

- 클레스와 구조체의 속성 매크로

 

클래스가 리플렉션의 관리를 받기 위해서는 UCLASS()을 붙여줘야 한다. 또한 GENERATED_UCLASS_BODY(), GENERATED_STRUCT_BODY()를 클래스나 구조체 정의 안에 작성하여야 한다. 내부에 작성하는 매크로는 클래스 바디에 typedef나 추가 함수들을 작성할 수 있기 때문에 필요하다.

UCLASS(Abstract)

class AStrategyChar : public ACharacter, public IStrategyTeamInterface
{
	GENERATED_UCLASS_BODY()
  // ...  
};

 

 

- 클레스의 멤버 변수와 함수의 속성 매크로

 

클래스 멤버 변수의 경우 UPROPERTY()를 붙여주면 된다. 괄호() 안에 여러 속성 지정자를 작성할 수 있다.

클래스 멤버 함수의 경우 UFUNCTION()을 붙여주면 된다. 괄호() 안에 여러 속성 지정자를 작성할 수 있다.

#include "StrategyTypes.h"
#include "StrategyChar.generated.h"

UCLASS(Abstract)
class AStrategyChar : public ACharacter, public IStrategyTeamInterface
{

	GENERATED_UCLASS_BODY()
    
	UPROPERTY(EditAnywhere, Category=Pawn)
	int32 ResourcesToGather;

	UFUNCTION(BlueprintCallable, Category=Attachment)
	void SetWeaponAttachment(class UStrategyAttachment* Weapon);

protected:
	uint8 MyTeamNum;
};

 

속성 매크로 안에 EditAnywhere과 같은 값들이 들어가 있는 것을 볼 수 있다. 이를 속성 지정자라고 하며 많이 쓰이는 속성 지정자는 다음과 같다.

https://create-new-worlds.tistory.com/150

 

[UnrealEngine] 속성 지정자(Property Specifiers)

* 정의 UPROPERTY(), UFUNCTION()과 같은 속성들을 정의할 때 엔진과 에디터의 다양한 측면에서 행동 방식을 제어하기 위해 EditAnywhere, BlueprintCallable 등과 같은 키워드를 넣을 수 있다. 이들을 속성 지정.

create-new-worlds.tistory.com

 

리플렉션의 관리를 받지 않는 변수와 받는 변수가 공존할 수 있지만 관리를 받지 않는 변수는 리플렉션에 의존하는 시스템에서 이 변수를 확인할 수 없다. 또한 리플렉션의 관리를 받지 않는 UObject의 생 포인터를 사용하는 것은 리플렉션에 의존하는 가비지컬렉션의 도움을 받지 못하니 주의하여야 한다.

 

 

- 리플렉션 활용 예시

 

많은 게임 코드는 시스템의 장점을 활용하기 위해 런타임에서 리플렉션 시스템을 무시할 수 있지만 툴 코드나 게임 플레이 시스템을 구축할 때 리플렉션은 유용하게 사용된다. 다음과 같은 활용 예시가 있다.

 

@ C++ 타입 반환

 

리플렉션의 관리를 받는 UClass나 UScriptStruct에서 C++ 타입을 얻기 위해서는 다음과 같이 작성한다.

UTypeName::StaticClass() or FTypeName::StaticStruct()

 

또한 다음과 같이 UObject 인스턴스를 통해서도 타입을 얻을 수 있다. (구조체의 경우는 공통 기본 클래스나 스토리지가 없어서 불가능하다고 한다.)

Instance->GetClass()

 

 

@ 멤버 순회

 

리플렉션 시스템의 관리를 받고 있기 때문에 모든 멤버들을 순회할 수 있다. 예를 들어 UStruct의 모든 멤버를 순회하는 과정은 다음과 같다.

for (TFieldIterator<UProperty> PropIt(GetClass()); PropIt; ++PropIt)
{
	UProperty* Property = *PropIt;
	// Do something with the property
}

(UField을 사용하면 속성과 함수을 확인할 수 있다.) TFieldIterator의 템플릿 인자는 필터 용도로 사용되며, 두 번째 인자를 전달하면 부모 클래스의 것들도 순회할 수 있다.

 

 

@ 속성 지정자 활용

 

UClass와 같은 유형에는 고유한 플래그들과 UField에서 상속받은 메타데이터 저장소 시스템이 존재한다. 키워드 지정자(속성 지정자)는 사용하는 위치에 따라서(런타임 또는 에디터) 플래그나 메타데이터로 저장된다. 에디터 전용 메타데이터는 메모리 절약을 위해 제거할 수 있으며 런타임 플래그는 항상 사용할 수 있다.

 

 

@ 기타

 

리플렉션 데이터를 이용하면 속성 열겨, 데이터 기반의 get set, 리플렉션 관리를 받는 함수 호출, 새 객체 생성과 같은 것들을 수행할 수 있다. 

 

 

- Unreal Build Tool(UBT)와 Unreal Header Tool(UHT)

 

과정은 다음과 같이 정리된다.

  1. UBT와 UHT는 리플렉션이 런타임에 구동하기 위한 필요한 정보를 만들어 낸다.
  2. UBT는 헤더를 스캔하여 최소 하나의 리플렉션 타입을 가진 헤더를 포함하는 모듈을 기억해두어야 한다.
  3. 헤더들 중에 저번 컴파일 이후로 변경된 헤더가 존재한다면 UHT가 리플렉션 데이터를 수집하고 업데이트하기 위해 호출된다.
  4. UHT는 헤더를 파싱해서 리플렉션 데이터를 구축한다.
  5. 그리고 리플렉션 데이터(per-module.generated.inl)와 various helpers(용어 확인)와 thunks functions(per-header.generated.h)을 포함하는 C++ 코드를 생성한다. 여기서 리플렉션 데이터를 생성된 C++ 코드로 저장하기 때문에 바이너리와 동기화가 보장된다.
  6. 오래된 리플렉션 데이터를 로드하지 않도록 남은 C++ 엔진 코드와 함께 컴파일을 진행하고 시작할 때 C++표현식을 사용하여 멤버들의 오프셋을 계산한다.
  7. UHT는 상호 종속성을 피하기 위해 독립 실행형 프로그램으로도 구축된다.
  8. 생성된 함수에는 StaticClass(), StaticStruct()와 같이 쉽게 리플렉션 데이터를 얻을 수 있는 함수와 thunk functions와 같이 블루프린트나 네트워크 리플리케이션에서 C++함수를 호출하는데 사용되는 함수를 포함한다. 이들은 클래스나 구조체에 정의되어야 하기 때문에 내부에 GENERATED_UCLASS_BODY() or GENERATED_USTRUCT_BODY()와 같은 매크로가 필요하다.
  9. 또한 이러한 매크로는 "TypeName.generated.h"에 정의되어 있기 때문에 이를 include 해야 한다.

 

* 정의

 

언리얼 엔진에서도 스마트 포인터 라이브러리를 제공하고 있다. 이러한 스마트 포인터는 C++ 11의 커스텀 구현으로 메모리 할당 추적을 쉽게 할 수 있도록 설계되어 있다.

 

종류는 TSharedPtr, TWeakPtr, TUniquePtr를 제공하며 non-nullable 형식의 TSharedRef을 제공한다. 이러한 스마트 포인터 클래스는 UObject 시스템과 별도의 시스템이다. 따라서 UObject(언리얼 오브젝트)는 게임 코드에 맞게 더 잘 조정된 메모리 추적 시스템의 관리를 받기 때문에 스마트 포인터 클래스를 사용하지 않는다. (다만 TWeakObjectPtr은 UObejct에서 사용된다.)

 

 

* 사용 이유

 

스마트 포인터의 장점

  • 메모리 누수 방지 : 스마트 포인터는 자동으로 객체를 참조하지 않을 때 자동으로 삭제한다. (약 포인터 제외)
  • 약 참조 제공 : 약 포인터는 참조 사이클을 방지하고 허상 포인터를 방지한다.
  • 런타임 안정성 보장 : 공유 포인터들은 공동 소유권을 가지고 있기 때문에 참조 객체는 null이 아니고 언제든 객체에 접근을 보장한다.
  • 객체 소유 현황 파악 : 관찰자를 통해 객체의 소유 현황을 파악할 수 있다.
  • 역참조 속도 : 스마트 포인터를 역참조하는 것은 일반 C++ 포인터 역참조 속도만큼 빠르다.
  • 포인터 복사 : 스마트 포인터를 복사할 때 메모리를 할당하지 않는다.

 

언리얼 엔진의 스마트 포인터의 장점

  • thread-safe 옵션 제공 : 스레드 안정성 옵션을 상황에 따라 켜고 끌 수 있다. 멀티스레드 환경이 아니라면 이를 비활성화하여 성능을 올릴 수 있다.
  • 메모리 : 64비트 환경에서 스마트 포인터는 C++ 포인터 크기의 2배밖에 되지 않는다. 또한 참조 컨트롤러(stl의 컨트롤 블록과 비슷한 개념인 것 같다.)는 16바이트이다. 또한 유니크 포인터는 C++ 포인터의 크기와 같다.

 

스마트 포인터의 단점

  • 참조를 관리하기 위한 오버헤드가 추가로 들어가기 때문에 일반 C++ 포인터보다 느리다. 렌더링과 같은 저수준 엔진 코드에서 사용하기에 적절하지 않다.
  • 참조 사이클이 생길 수 있다.
  • 일반 C++ 포인터보다 메모리가 많이 든다.
  • 참조 컨트롤러를 생성할 때 2번의 힙할당이 이루어질 수 있다. (이를 방지하기 위해 MakeShareable이 아닌 MakeShared를 사용하여 생성해야한다.)

 

 

* 동작 방식

 

스마트 포인터는 유형마다 참조하는 객체에 다른 영향을 미치게 된다. 따라서 상황에 따라 적절히 선택하여 사용해야한다.

 

- TSharedPtr

 

참조하고 있는 객체를 소유한다. 참조하고 있는 TSharedPtr이 있는 한 객체의 삭제는 무기한 연장된다. 그러다가 공유 참조가 없을 때 삭제된다. TSharedPtr은 아무것도 참조하지 않을 수 있다. 참조하고 있는 TSharedPtr은 참조하는 객체의 공유 참조를 제공할 수 있다. 

 

 

- TSharedRef

 

TSharedRef 역시 TSharedPtr과 비슷한 방식으로 동작한다. 하지만 TSharedRef는 항상 non-nullable 객체를 참조해야한다. TSharedRef가 더 엄격한 규칙을 가지고 있기 때문에 느슨한 TSharedPtr로 변환할 수 있다. 이때 TSharedPtr이 참조하는 객체는 non-nullable이 보장된다. 주로 non-nullable을 보장하거나 객체의 공동 소유권을 나타낼 때 사용된다.

 

 

- TWeakPtr

 

TWeakPtr 는 TSharedPtr과 유사하지만 참조하는 객체를 소유하지 않는다는 특징이 있다. 그러므로 해당 객체의 생명주기에 영향을 주지 못한다. 공유 포인터(TSharedPtr)를 사용하다보면 참조에 사이클이 생겨 객체가 삭제되지 못하는 현상이 생길 수 있는데, 약 포인터(TWeakPtr)로 이를 방지할 수 있다.

 

객체는 TWeakPtr과 관계 없이 삭제될 수 있기 때문에 어떤 순간에도 TWeakPtr이 참조하는 객체가 null이 될 수 있다. TWeakPtr에서 참조하는 객체에 안전하게 접근하기 위해 TSharedPtr을 생성할 수 있도록 기능을 제공하고 있다. TSharedPtr을 임시로 만들어서 접근하면 TSharedPtr가 소멸하기 전까지 무조건 참조하는 객체가 살아있음을 보장할 수 있다.

 

 

- TUniquePtr

 

TUniquePtr은 참조하는 객체를 단독으로 소유하고 있음을 보장한다. 객체에 대한 TUniquePtr은 하나만 존재할 수 있기 때문에 소유권을 이전할 수 있지만 공유 소유권을 가질 수 없다. 따라서 TUniquePtr에 대한 복사 연산은 막혀있고 이동 연산만 지원하고 있다.

 

 

* 스레드와 공유 포인터

 

stl에서 제공하는 std::shared_ptr의 경우 참조 횟수(Referece Count)에 대한 연산은 항상 원자적으로 수행되기 때문에 스레드 안전성을 보장한다. 언리얼 엔진의 경우 모드를 설정할 수 있도록 되어 있는데, 이에 따라 원자적 연산 수행 여부가 결정된다. 

  • TSharedPtr<T, ESPMode::ThreadSafe>
  • TSharedRef<T, ESPMode::ThreadSafe>
  • TWeakPtr<T, ESPMode::ThreadSafe>
  • TSharedFromThis<T, ESPMode::ThreadSafe>

 

stl이든 언리얼 엔진이든 참조 횟수 연산만 원자적으로 수행하는 것이기 때문에 참조 객체에 대한 동기화는 따로 처리해줘야 한다. 

 

 

C++ 스마트 포인터 참고자료

 

https://create-new-worlds.tistory.com/115

 

스마트 포인터 개요

* 정의 스마트 포인터(smart pointer)는 생 포인터를 감싸는 일종의 래퍼(wrapper)로 생 포인터와 아주 비슷한 방식으로 동작하되 생 포인터로 인해 발생하는 문제점들을 해결할 수 있다. 간단히 오류

create-new-worlds.tistory.com

 

https://create-new-worlds.tistory.com/116

 

스마트 포인터 - std::unique_ptr

* 정의 std::unique_ptr 스마트 포인터는 생 포인터와 비교했을 때 크기가 거의 비슷하고 동일한 명령들을 실행하며 독점적 소유권(exclusive ownership)를 가진다. 소유권을 독점해야하는 자원관리에 사

create-new-worlds.tistory.com

 

https://create-new-worlds.tistory.com/120

 

스마트 포인터 - std::shared_ptr

* 정의 std::shared_ptr은 공유 포인터를 의미하며 이를 통해 접근되는 객체의 수명은 그 공유 포인터가 공유된 소유권(shared ownership)을 통해 관리된다. 특정한 하나의 std::shared_ptr가 객체를 소유하는

create-new-worlds.tistory.com

 

https://create-new-worlds.tistory.com/125

 

스마트 포인터 - std::weak_ptr

* 정의 std::shared_ptr와 비슷하게 동작하면서 객체의 참조 횟수에는 영향을 미치지 않는 포인터가 바람직한 경우도 있다. 이러한 스마트 포인터는 자신이 가리키는 객체가 이미 파괴된 상황에서

create-new-worlds.tistory.com

 

* 정의

 

Actor는 레벨에 배치될 수 있는 Camera, StaticMesh, PlayerStartLocation과 같은 객체를 의미한다. Actor는 이동, 회전, 스케일링을 지원하며 게임플레이 코드를 통해 생성되거나 파괴될 수 있다. 이러한 데이터는 Actor의 RootComponent에 존재한다. AActor 클래스는 모든 Actor들의 기본 클래스이다.

 

 

* 동작 방식

 

- 생성

 

특수화된 템플릿 버전의 SpawnActor()를 통해서 생성할 수 있다.

 

정확하게는 UWorld::SpawnActor() 함수를 통해 이루어지며 간단한 예시는 다음과 같다.

AMyActor* actor1 = (AMyActor*) GetWorld()->SpawnActor(AMyActor::StaticClass(), NAME_None, &currentLocation);

 

자세한 사용 방법은 다음 문서에서 확인할 수 있다.

https://docs.unrealengine.com/4.27/en-US/ProgrammingAndScripting/ProgrammingWithCPP/UnrealArchitecture/Actors/Spawning/

 

Spawning Actors

Methods of creating new instances of Actors in gameplay code.

docs.unrealengine.com

 

 

- 삭제

 

World Object는 Actor의 참조 리스트를 가지고 있기 때문에 일반적으로 가비지 컬렉트되지 않는다. Destroy() 라는 함수를 통해 명시적으로 삭제 가능하다. 이 함수를 Actor를 레벨에서 삭제하고 pending kill로 마킹함으로써 다음 가비지 컬렉션에 의해 처리될 수 있다.

 

 

- Component

 

https://create-new-worlds.tistory.com/137

 

[UnrealEngine] ActorComponent

* 정의 Component는 특별한 타입의 Object로 서브오브젝트로서 Actor에 부착될 수 있다. Actor 측면에서는 Component라는 특별한 타입의 Object들을 담고 있는 컨테이너라고 볼 수 있다. 여러 타입의 Component

create-new-worlds.tistory.com

 

 

- Tick

 

https://create-new-worlds.tistory.com/142

 

[UnrealEngine] Tick

* 정의 Tick은 특정 코드를 특정 시간 간격(매 프레임, 특정 간격 등) 마다 수행하는 것을 말하며, Actor는 Tick(), ActorComponent는 TickComponent()라는 함수를 통해 기본적으로 수행될 수 있다. Tick 순서(Act..

create-new-worlds.tistory.com

 

* 구조

 

  • 게임은 GameMode와 GameState로 이루어진다.
  • 플레이어는 PlayerController와 연결된다.
  •  PlayerController는 플레이어가 Pawn을 소유할 수 있도록 해주기 때문에 레벨에서 물리적인 표현을 지닐 수 있다.
  • PlayerController는 플레이어에게 입력 컨트롤, HUD, 카메라 시점을 조절하기 위한 PlayerCameraManager을 제공한다.

 

 

- 행위 다이어그램

 

 

 

- 클래스 상속, 종속성 다이어그램

 

 

 

* UObject 와 Actor

 

- 기본 상속 구조

 

Actor는 AActor 클래스를 상속받은 클래스들의 인스턴스, Object는 UObject 클래스를 상속받은 클래스들의 인스턴스이며 UObject 클래스는 언리얼 엔진의 모든 객체들의 기본 클래스이다. 따라서 언리얼 엔진에서 Actor를 포함한 모든 인스턴스들은 Object이다.

 

 

- 일반적 구분

 

근본적으로 모두 Object이지만 보통 Actor는 AActor 클래스를 상속받은 클래스의 인스턴스를 의미하고 Ojbect는 AActor 클래스의 상속을 받지 않은 클래스의 인스턴스를 의미한다.

 

Actor는 한 개체로서 많이 평가되고, Object는 좀 더 세부적인 부분으로 평가된다.

Actor는 종종 특정 기능을 정의하거나 특정 속성들을 보유하기 위해 특수 Object인 컴포넌트를 사용한다.

 

 

* Gameplay Framework 클래스

 

기본적인 Gameplay 클래스는 플레이어, 아군, 적군 등을  플레이어 입력이나 AI 로직으로 여러 캐릭터를 제어하는 기능이 포함된다. GameMode, GameState, PlayerState와 같은 Gameplay 클래스는 게임의 규칙을 정하고 게임과 플레이어가 어떻게 진행되는지 추적한다.

 

 

- GameMode

 

게임의 개념은 2가지 클래스로 나뉜다. 이 클래스들은 GameMode와 GameState로 게임 규칙과 승리 조건과 같은 게임의 정의와 관련되어 있다. 서버에서만 존재하게 되며 게임이 플레이 되는 동안 변경되는 데이터가 많지 않다. 클라이언트가 알아야하는 일시적인 데이터와 같은 것들은 존재해서는 안된다.

 

 

- GameState

 

GameState는 게임의 상태를 포함하고 있다. 접속한 플레이어, 점수, 체스 게임에서는 각 위치, 오픈 월드 게임에서는 어떤 퀘스트의 완료 정보 등을 포함한다. GameState는 서버와 클라이언트 모두 존재하며 모든 시스템을 최신 상태로 유지하기 위해 Replication할 수 있다.

 

 

- PlayerState

 

PlayerState는 참가자(플레이어, 플레이어를 시뮬레이션하고 있는 봇)의 상태이다. 플레이어가 아닌 AI는 이를 PlayerState를 가지지 못한다. 간단히 예를 들어보면 플레이어 이름, 점수, 플레이어의 깃발 획득 여부 등을 포함한다. 모든 플레이어에 대한 PlayerStates 모든 시스템에 존재하며(PlayerController와 다르게) 동기화 시키기 위해 Replication 될 수 있다.

 

 

* Pawn 클래스 종류

 

- Pawn

 

Pawn은 Actor로 월드에서 큰 역할을 수행한다. Pawn은 Controller에 의해 소유되며 입력을 쉽게 받을 수 있도록 설계되어 있다. 다른 플레이어와 같이 다양한 일을 할 수 있다. 휴머노이드로 간주되지는 않는다.

 

 

- Character

 

Pawn에서 상속 받은 휴머노이드 스타일의 Pawn 이다. 충돌 처리를 위한 CapsuleComponent와 CharacterMovementComponent가 기본적으로 제공된다. 기본적인 사람과 같은 움직임을 할 수 있으며 네트워크를 통해 움직임을 자연스럽게 Replication할 수 있다. 또한 애니메이션 관련 기능도 제공한다.

 

 

* Controller 클래스 종류

 

- Controller

 

Controller는 Actor로 Pawn을 제어하는 역할을 담당한다. 일반적으로 AIController, PlayerController 2가지 버전을 제공한다. Controller는 Pawn을 제어하기 위해 소유할 수 있다.

 

 

- PlayerController

 

PlayerController는 Pawn과 이를 제어하는 플레이어 사이의 인터페이스로 사용된다. PlayerController는 플레이어의 의지를 나타낸다.

 

 

- AIController

 

AIController는 Pawn을 제어할 수 있는 시뮬레이션된 의지(AI) 이다.

 

 

* 화면 관련 정보

 

- HUD

 

HUD는 heads-up display를 의미하며 이는 게임 플레이 시 현재 플레이어, 게임 상황에 대한 정보를 알리는데 사용된다. 간단히 예를 들어보면 hp, 탄약, 조준점 등이 있다. 각 PlayerController는 일반적으로 이를 하나씩 가지고 있다.

 

 

- Camera

 

PlayerCameraManager는 플레이어의 안구를 의미하며, 이의 동작 방식을 관리한다. 각 PlayerController는 일반적으로 이를 하나씩 가지고 있다.

'게임 엔진 > Unreal Engine' 카테고리의 다른 글

[UnrealEngine] 스마트 포인터(Smart Pointer)  (0) 2022.03.28
[UnrealEngine] Actor 생명주기(Actor Lifecycle)  (0) 2022.03.28
[UnrealEngine] Tick  (0) 2022.03.28
[UnrealEngine] ActorComponent  (0) 2022.03.26
[UnrealEngine] Actor  (0) 2022.03.24

+ Recent posts