* 효율적인 TArray 사용 방법

 

- Add

 

@ 과정

Add를 호출하면 (내부적으로 Emplace -> AddUninitialized 과정을 수행한다.) 요소를 추가할 수 있으며, 이 과정에서 용량이 부족하다면 Relocation을 진행한다. Relocation은 TArray에 넘겨준 Allocator 타입에 따라서 다양하게 동작한다.(Allocator의 CalculateSlackGrow, ResizeAllocation 함수를 통해 진행된다.)

 

@ Add와 Relocation

Relocation 과정은 새롭게 공간을 할당하고 이전 값들을 새로운 공간에 복사한 후, 이전 공간을 삭제하는 단계로 이루어진다. (성능에 좋지 않다.)

 

Relocation을 진행할 때, 기본 Allocator에서는 배열에 요소가 없는 경우를 제외하고는 여분의 공간을 더 할당한다. 하지만 요소가 몇 개 추가된다면 다시 Relocation 해야 한다.

 

이를 방지하기 위해 요소의 개수를 알고 있다면 그 공간만큼 미리 할당해두면 Relocation 을 한번만 수행할 수 있다. TArray에서도 std::vector와 마찬가지로 Reserve 라는 함수를 제공한다.


이미 TArray에 요소가 존재하여 추가할당해야하는 경우에는 여분의 공간을 더 할당하긴하지만, 요소가 빠르게 추가되면 다시 Relocation을 해야한다. 그리고 너무 당연하지만, 함수의 입력으로 넘겨줄 때 &을 사용하여 새로운 할당을 방지해야한다.

 

 

- Remove

 

@ Remove, RemoveSwap 과 Relocation

Remove 함수는 배열에서 요소를 삭제 후, 뒤에 있는 요소들의 메모리를 이동하여 빈 공간을 채운다. RemoveSwap 함수는 배열에서 순서가 중요하지 않은 경우 사용할 수 있는 함수로, 맨 뒤의 요소와 스왑하여 빈 공간을 채운다.

 

RemoveSwap함수를 사용할 때, 배열의 크기를 줄이기 위해(Shrink) Relocation이 발생할 수 있다. 오래 사용되는 배열의 경우 사용되지 않는 공간을 확보하는 것이 좋기 때문에 필요한 옵션이다. 하지만 짧게 사용되는 배열의 경우 Relocation으로 성능이 나빠질 수 있기 때문에 bAllowShrinking 옵션을 통해 꺼주는 것이 좋다.

 

 

- Allocator : FDefaultAllocator, TInlineAllocator, TFixedAllocator

 

@  FDefaultAllocator(TSizedHeapAllocator<32>)

TArray에 기본적으로 사용되는 Allocator는 FDefaultAllocator(TSizedHeapAllocator<32>)로 이는 동적 메모리 Allocator이다. (힙에 할당) 상황에 따라 동적으로 메모리 할당, 해제하게 된다. 힙 메모리 할당 과정은 lock이 필요하고, 빈 블록 찾는 과정을 거치며, 또 새로운 주소의 공간을 할당하기 때문에 기존에 사용되던 캐시는 전부 버려진다.

 

@  TInlineAllocator

이를 방지하기 위해 Unreal Engine에서는 TInlineAllocator를 제공하고 있다. 이 Allocator를 사용하면 처음 N개의 공간을 미리 할당해 둘 수 있다.(힙에 할당하지 않고, TArray 내부에 포함된다.)

// typedef TArray<FOverlapInfo, TInlineAllocator<3>> TInlineOverlapInfoArray;와 같이 사용된다.

 

만약 사용할 공간을 예측할 수 있다면 힙에 메모리를 할당하는 것을 피할 수 있다. 만약 추가 메모리가 필요한 경우에는 TInlineAllocator의 SecondaryAllocator(FDefaultAllocator)를 통해 ( Secondary가 FDefaultAllocator 인 경우에 힙에) 할당된다. (Relocation 과정을 거친다.)

 

@  TFixedAllocator

TFixedAllocator는 TInlineAllocator와 동일하지만, SecondaryAllocator가 존재하지 않는다.

 

 

* Unreal Engine에서의 UObject 관리 간단 분석

 

- 생성 과정 분석

 

@ 간단 과정

UObject 를 생성할 때 NewObject 함수나 Actor의 경우 SpawnActor를 사용한다. SpawnActor도 내부적으로 NewObject 함수를 호출하고 있다.

내부 코드는 너무 길지만 간단히 일부 과정을 설명하면, 메모리를 적절한 사이즈에 맞게 할당하고 Archetype(Prefab, Template)을 CDO나 Blueprint 로 설정되어 초기화된다. (내부코드까지 자세히 보진 않아서 정확하지는 않다.)

 

그 과정에서 UObjectBase(UObject의 최상위 부모클래스) 생성자를 호출하고, 생성자에서 AddObject 함수를 통해 싱글톤 해시테이블(FUObjectHashTables)과 전역 UObject 배열(GUObjectArray)에 객체를 등록한다.

 

@ FUObjectHashTables
FUObjectHashTables는 다양한 역할을 하고 있지만 주로 클래스 종류별로 UObject를 관리하고 있다. 예를 들어 TActorIterator에서 특정 클래스의 객체를 찾을 때 FUObjectHashTables가 활용된다.

 

@ GUObjectArray
GUObjectArray은 객체의 배열을 가지고 있는 풀 자료구조로 실제 생성된 객체와 할당할 수 있는 공간을 관리한다. 객체를 등록할 때는 일반적으로 비어있는 풀의 인덱스를 가지고 있는 배열에서 인덱스를 받아서 풀에 등록하게 된다. (UObjectBase(UObject의 최상위 부모클래스)의 InternalIndex에 해당 풀 인덱스를 저장하고 있다.)


객체가 풀에 저장될 때는 실제 객체가 저장되지 않고 FUObjectItem 형태로 저장된다. FUObjectItem 객체를 담는 자료구조는 UObjectBase 포인터와 WeakObjectPtr 에서 사용되는 SerialNumber 등을 가지고 있다.

둘 다 전역객체이기 때문에 크리티컬섹션을 통해 데이터 경쟁 상태를 방지한다.

 

 

- TWeakObjectPtr 동작 방식

 

UObject를 생성하면 GUObjectArray이라는 풀에 저장되고 풀의 객체는 FUObjectItem형식이다. FUObjectItem 내부에 SerialNumber는 멤버로 가지고 있는데, 실제 존재하는(사용중인) 객체인 경우에만 다른 객체와 다른 고유한 값을 가진다.


따라서 최초로 TWeakObjectPtr을 할당하게 되면 객체는 고유한 SerialNumber을 FUObjectItem와 TWeakObjectPtr에 저장한다. 이후 TWeakObjecPtr을 통해 해당 객체에 접근할 때, SerialNumber가 일치하는 지 확인한다. (추가 검증 과정이 더 존재한다.)


일치한다면 경우라면 아직 존재하는 객체이고, SerialNumber가 다르다면(다시 풀에 반납할 때 SerialNumber를 0으로 초기화) 널포인터를 리턴한다.

 

 

* Role

 

Unreal Engine에는 리플리케이션과 관련된 Role과 RemoteRole이라는 두 가지 속성이 존재한다. Role은 간단히 현재 이 시스템에서의 역할을 의미하고, RemoteRole은 연결된 다른 쪽의 역할을 의마한다.  이 두 가지 속성을 통해 다음과 같은 사실을 확인할 수 있다.

 

  • 누가 Actor의 권한을 가졌는지 확인할 수 있다.
  • 해당 Actor가 리플리케이션된 Actor인지 확인할 수 있다.
  • 리플리케이션 되는 방식에 대해 알 수 있다.

 

 

- Role의 종류와 Replication

 

기본적으로 Role에는 3가지 종류가 있다. (더 존재하지만, 기본적으로 3가지가 가장 많이 활용된다.)

 

이러한 3가지 종류의 Role은 게임을 실행중인 곳에서 각각 다르며, 이를 활용하여 적절하게 리플리케이션한다. 서버는 대역폭, 성능의 문제로 매 프레임마다 Actor들을 클라이언트로 리플리케이션하지 않기 때문에 서버와 클라이언트의 동기화를 위해서는 Role에 따라 적절한 리플리케이션이 필요하다.

 

 

@ ROLE_Authority 

 : 기본적으로 서버의 모든 Actor의 Role이 ROLE_Authority 로 설정되어 있다. 이로 인해 자연스럽게 클라이언트의 RemoteRole은 ROLE_Authority 가 된다. 가끔 클라이언트에만 스폰되어 존재하는 Actor나 싱글 플레이어 게임에서는 Role 이 ROLE_Authority로 설정된다. 

 

(기본적으로 Actor를 스폰한 곳에서는 Role은 ROLE_Authority 이지만, 클라이언트가 멀티플레이어 게임에서 이 특성을 악용할 수 없다. 클라이언트에서 스폰된 Actor는 서버로 리플리케이션되지 않기 때문이다.)

 

 

@ ROLE_SimulatedProxy

 : 클라이언트에서 직접 컨트롤하지 않고 서버에서 리플리케이션되어 시뮬레이션되는 Actor의 Role이 ROLE_SimulatedProxy 로 설정되어 있다.

 

서버에서의 통보받은 해당 Actor의 마지막 속도 등의 정보를 기반으로 시뮬레이션하고, 보간한다.

 

 

@ ROLE_AutonomousProxy  

 : 클라이언트에서 직접 컨트롤하는 Actor의 Role이 ROLE_AutonomousProxy 로 설정되어 있다. 간단하게 말하면 PlayerController 에 의해 소유되는 Actor 들이고, 사용자의 입력을 받게 된다. 이때 서버의 RemoteRole은 ROLE_AutonomousProxy 로 설정된다.

 

ROLE_SimulatedProxy 와 달리 마지막으로 받은 정보를 기반으로 시뮬레이션, 보간을 하지 않고, 입력을 기반으로 시뮬레이션하게 된다. 훨씬 더 정확한 시뮬레이션이 가능해지며, 컨트롤하고 있는 Actor가 원하는대로 시뮬레이션 될 것이다.

 

 

@ Role과 관련된 함수  

  • HasAuthority 함수를 통해 ROLE_Authority 를 체크할 수 있다. 
  • Pawn::IsLocallyControlled 함수를 사용하면 이 폰이 실제로 로컬에서 컨트롤 되는 지 알 수 있다. 싱글플레이어 게임일 때 true를 반환하고, 멀티플레이어 게임인 경우에는 클라이언트이고 ROLE_AutonomousProxy Role을 가졌을 때나, ROLE_Authority Role와 ROLE_AutonomousProxy RemoteRole을 가졌을 때 true를 반환한다.(현재 다른 플레이어가 컨트롤하고 있지 않고, 시스템이 서버인 경우) 

 

 

* Replication

 

서버의 Actor들을 각 클라이언트로 리플리케이션하기 위한 조건은 다음과 같다.

  • 리플리케이션할 Actor는 SetReplicates(true); 를 호출한다. 폰 종류는 PossessedBy가 호출될 때 알아서 호출된다.
  • 컴포넌트의 경우 SetIsReplicated(true); 를 호출한다.
  • SetReplicateMovement를 사용하여 움직임도 리플리케이션할 수 있다. 리플리케이션 비용이 비싸기 때문에 보통 게임에 영향을 주지 않는 경우에는 꺼두는 편이 좋다.
  • 리플리케이션할 속성을 GetLifetimeReplicatedProps 함수 내부에 추가한다.
  • 리플리케이션할 속성을 UPROPERTY(Replicated) 또는 UPROPERTY(ReplicatedUsing=OnRep_PropertyName)으로 지정한다.

 

- 속성 리플리케이션 세부 설명

 

Actor 들은 리플리케이션되는 속성의 리스트를 가지고 있으며 서버는 속성이 변경되었을 때 클라이언트로 리플리케이션한다. 속성 리플리케이션은 항상 보장되며, MinNetUpdateFrequency, NetUpdateFrequency 를 참조하여 특정 간격마다 가장 최신의 값이 리플리케이션된다. 만약 서버에서 리플리케이션되기 전에 값이 빠르게 변경되었을 때 가장 최종적으로 변경된 값이 리플리케이션된다.

 

 

@ 속성 지정자

속성 지정자를 통해 리플리케이션을 설정할 수 있다. 속성 지정자로 Replicated, ReplicatedUsing을 사용할 수 있으며, ReplicatedUsing 을 사용했을 경우 리플리케이션되었을 때 호출될 함수를 지정할 수 있다. 

 

보통 함수명에 접두사로 OnRep을 붙여서 OnRep_PropertyName의 형태로 많이 사용한다.

바운딩되는 함수는 OnRep_PropertyName는 UFUNCTION 이여야 한다.

 

 

@ 리플리케이션될 속성 반환 함수 오버라이딩

GetLifetimeReplicatedProps 함수를 재정의 하여 어떤 속성을 어디로 리플리케이션할지 결정한다.

 

간단히 리플리케이션할 변수를 지정한다고 생각하면 된다. 이 함수에 다음과 같이 추가하면 다음 변수는 모든 클라이언트에게 리플리케이션된다.

 

다음과 같이 내부에 코드를 작성한다. 모든 클라이언트로 리플리케이션할 수 있고, 조건에 따라 리플리케이션할 수 있다.

  • DOREPLIFETIME(ClassName, PropertyName);
  • DOREPLIFETIME_CONDITION(ClassName, PropertyName, COND_OwnerOnly);

 

@ 속성 리플리케이션 시 주의할 점

  • 서버에서 속성의 값이 변화가 없다면 클라이언트로 리플리케이션하지 않기 때문에 매번 리플리케이션이 필요한 환경에서는 이를 고려해야한다. (내부 최적화 로직)
  • 즉시 리플리케이션이 되지 않아도 될 Actor는 NetUpdateFrequency를 작게 두는 것이 좋다.

 

 

* RPC

 

- RPC 종류

 

RPC에는 크게 3가지 종류가 있다.

  • Server
  • Client
  • NetMulticast

 

이 3가지 함수를 구현하기 위해 적절한 속성지정자를 설정해야 한다. 각 속성 지정자는 다음과 같은 의미를 지닌다.

  • Server : 서버에서 호출되는 함수
  • Client : 클라이언트에서 호출되는 함수
  • NetMulticast :  서버와 서버에 연결된 모든 클라이언트에서 호출되는 함수
  • Reliable : 신뢰성 있는 호출을 보장한다.
  • WithValidation : 보통 서버 함수를 호출하기 전에 직접 구현한 검증 함수를 추가로 호출하여 패킷이 비정상적인 상황인지 확인한다. 만약 검증에 실패했다면 클라이언트를 로그아웃 시킨다. (클라이언트 함수도 선택적으로 사용할 수 있다고 나와있다.)

 

 

- 함수 정의

 

예를 들어 서버 함수를 만드는 방법은 다음과 같다. 필요에 따라 속성 지정자를 설정하면 된다. 보통 함수 이름으로 서버 함수는 Server, 클라이언트 함수는 Client, 멀티캐스트 함수는 Multicast 접두사를 붙인다.

UFUNCTION(Server, Reliable, WithValidation)
void ServerFunctionName()

 

 

- 함수 구현

 

함수명 그대로 구현하지 않고 함수명 뒤에 _Implementation을 붙인 이름의 함수를 구현한다. 

void ClassName::ServerFunctionName_Implementation()
{
	// ...
}

 

WithValidation 속성 지정자를 사용한 경우에 다음과 같이 검증 함수도 구현해야한다. 함수명 뒤에 _Validate 을 붙인 형태다. 만약 false를 반환하게 되면 시스템이 연결을 끊어낸다.

bool ClassName::ServerFunctionName_Validate()
{
	// ...
}

 

 

- 함수 호출

 

실제로 함수를 호출할 때는 구현한 함수명(XXX_Implementation)이 아닌 선언한 함수명을 사용하여 호출한다. (ServerFunctionName(); 와 같이 호출한다.)

 

서버에서 클라이언트 함수를 호출하면 해당 Actor를 소유한 클라이언트만 호출되고, 멀티캐스트 함수를 호출하면 서버와 모든 연결된 클라이언트가 호출된다. 나머지 모든 함수는 서버에서 호출된다.

 

클라이언트에서 서버 함수를 호출하면 해당 클라이언트가 Actor를 소유하고 있는 경우 내부적으로 패킷을 서버로 보내어 서버 함수를 호출한다. 나머지 모든 경우는 해당 클라이언트만 호출된다.

 

 

* 기타(너무 당연한 내용은 제외) - 추후 세부 내용 정리

 

- 게임 관련 상태 정보

 

* GameMode는 서버에만 존재하는 게임에 관련된 정보를 가지고 있는 클래스로 AGameMode는 너무 많은 정보를 가지고 있기 때문에 보통 AGameModeBase를 상속받아 구현한다. (GameMode 도 Actor를 상속 받고 있기 때문에 Tcik 함수를 사용할 수 있다.)

 

* GameMode는 서버에만 존재하기 때문에 클라이언트로 게임의 여러 상태를 리플리케이션해주기 위해 GameState 클래스가 존재한다. 주로 현재 동기화된 게임 시간 등의 정보를 담고 있다.

 

* 플레이어에 대한 개별적인 정보를 관리하는 PlayerState라는 클래스도 존재한다. 클라이언트로 리플리케이션된다. PlayerController가 이미 존재해서 플레이어의 정보들을 여기서 관리해도 될 것 같지만, 클라이언트는 본인의 PlayerController만 가지고 있기 때문에 PlayerState가 필요하다. (서버는 전부 다 볼 수 있다.)

 

 

- 액터

 

* SceneComponent 은 ActorComponent을 상속받은 클래스로 Transform 정보를 가지고 있기 때문에 월드에서 사용할 수 있다. SceneComponent를 상속받은 여러 클래스는 충돌처리를 담당하거나, 시각적인 요소를 담당하기도 한다. 일반적인 ActorComponent와 다르게 계층 구조를 이룰 수 있다.

 

* ActorComponent은 기능만 거의 담당하고 있으며, SceneComponent 계층 구조에 포함되지 않는다.

 

 

- 에디터 활용(블루프린트 등)

 

* 크래시가 발생하면 현재 켜둔 에디터에 VS를 붙여서 확인할 수도 있지만, DebugGame Editor 모드(최적화하지 않아서 조금 더 자세히 확인할 수 있다.)로 바꾸고 VS를 실행하여 새로운 에디터를 열어서 확인할 수도 있다.

 

* EditDefaultOnly를 통해 해당 클래스를 상속받은 블루프린트 클래스에서만 해당 속성이 수정하도록 설정할 수 있는데, 시각적으로 작업해야할 때(Mesh정보의 위치를 조정 등), 빠르게 여러 값을 테스트 해볼 때 유용하게 사용될 수 있다.

 

* 블루프린트는 C++ 의 접근 지정자 공식을 따르지 않는다. 만약 Protected 수준의 접근성을 부여하고 싶다면 다음과 같이 해야한다. UPROPERTY(BlueprintCallable, BlueprintProtected)

 

* 애니메이션 블루프린트의 애님 그래프에서 내부에 존재하는 로컬 변수를 통해 다양한 애니메이션 전이와 효과를 구현할 수 있다.

 

* 애니메이션 블루프린트에 존재하는 로컬 변수는 CPP 코드 상에서 직접 수정해도 되지만, 이벤트 그래프를 이용하여 세팅하여 편하게 갱신시킬 수 있다.

 

* UENUM(BlueprintType) 로 설정하면 블루 프린트에서도 볼 수 있다. 예를 들어 Enum 타입에 따라서 블루프린트를 통해서 UI를 쉽게 컨트롤하기 위해서 사용될 수 있다. (전방 선언시 다음과 같이 사용 enum EnumName : XXX(int8, int32 ...);)

 

* 델리게이트를 쓰거나 이벤트에 바인딩하는 함수는 무조건 UFUNCTION이다. 다이나믹 델리게이트는 블루프린트와 연동할 수 있고, BlueprintAssignable로 설정하면 블루프린트에서 구현할 수 있다.

 

* ClassGroup을 설정하면 커스텀 컴포넌트를 정렬해서 볼수 있기 때문에 편리하다.

 

 

- 수학, 물리

 

* FRotator 나 FQuat 를 사용하여 회전에 대항 방향을 받아올 수 있다.

 

* FQuat를 FRotator 로 반환할 수 있지만 변환하기 위해서 FQuat::Rotator() 함수 내부에서 Atan2를 사용하고 있기 때문에 각도는 [-180, 180]로 제한된다.

 

* FCollisionQueryParams를 적절히 활용하면 LineTrace에 여러 정보를 설정할 수 있다. (AddIgnoredActor : 무시할 액터 결정, bTraceComplex : 메시기반으로 조금 더 정확한 트레이싱 등)

 

* 프로젝트 설정에서 Physical Surface 를 커스텀으로 설정할 수 있다. 이를 설정함으로써 다양한 표면을 정의할 수 있고, 표면에 따라 다양한 효과를 표현할 수 있다.Physical Material 생성해서 Physical Surface 를 설정할 수 있고, 스켈레탈 메시의 Physica Asset으로 가서 각 부위를 Physical Material로 설정할 수 있다.

 

* PhysMaterial를 사용하면 타격 지점의 세부적인 정보를 설정할 수 있다. LineTrace가 적중했을 때, FHitResult에서 PhysicalMaterial의 정보(SurfaceType 등)를 받아올 수 있고, 이에 따라 다양한 처리가 가능하다. (부위 별로 다른 데미지 공식 계산 등)

 

* OnActorBeginOverlap 델리게이트는 액터가 다른 액터와 겹쳐졌을 때 브로드캐스팅된다.(겹쳐지는 조건은 액터의 모든 컴포넌트를 확인하여 PrimitiveComponent를 찾고, 충돌되었는지 확인한다.)

 

* 3차 스플라인 보간을 활용하기 위한 함수를 Unreal Engine에서도 제공한다. (CubicInterp, CubicInterpDerivative) 주로 자연스럽게 경로를 예측하여 연결하기 위해 사용된다.

 

 

- AI

 

* 모든 BehaviorTree 는 BlackBoard 컴포넌트와 함꼐 사용되며, BlackBoard는 내부에 데이터를 가지고 있다.

 

* BehaviorTree에서 데코레이터를 통해 특정 조건을 체크할 수 있으며, 기본적으로 제공하는 NPC, Player 체크 이외에 직접 구현하여 사용할 수 있다. 데코레이터에는 Observer aborts 속성이 존재하고, 다음과 같은 의미를 갖는다.

  • Self : 조건이 만족되었을 때 바로 본인의 서브트리를 빠져나감
  • Lower Priority : 앞의 조건(더 높은 우선순위의 서브트리)이 만족되었을 때 뒤에서 실행되는 서브트리를 빠져나감. 앞에서 설정한다.

 

* BehaviorTree에는 매번 실행되는 서비스라는 것이 존재한다. 예를 들어 서비스를 통해 계속 타겟을 바라보게 할 수 있다.

 

 

- EQS

 

* EQS(Environment Query System) 시스템은 AI와 함께 사용되는 시스템으로, 주변 환경에 대한 여러가지 정보를 얻을 수 있다. 이러한 쿼리는 Unreal Engine의 행동트리에서 노드로 사용 가능하다.

 

이 시스템을 사용하면 주변 환경의 노드 포인트들을 원하는 대로 생성할 수 있고, 조건에 따라 노드를 결정할 수 있다. 조건들은 어떤 액터들에 대한(컨텍스트라고 한다.) 거리나, LineTrace 가능 여부 등 으로 정말 다양하게 설정할 수 있으며, 모든 조건을 확인하고 노드에 조건들에 따라 최종 점수를 산정하여 최적의 노드를 결정한다.

 

이를 활용하면 체력이 얼마 남지 않았을 때 플레이어가 보지 못하는 곳으로 도망치는 AI를 만들 수도 있고, 플레이어가 보지 못하고 5000cm 이상 떨어진 무작위의 노드를 새로운 AI의 스폰지점으로 설정할 수도 있다.

 

하지만 아무리 최적화를 잘했다고 하더라도 조건들이 많아지거나 조건 검사 비용이 비싸지고, 쿼리가 많아짐에 따라 성능에 영향이 갈 수 밖에 없는 구조인 것 같다. 주의해서 사용해야 한다고 생각한다.

 

 

- Material

 

* Material 을 만들고 Material Instance를 만들어서 Mesh에 적용할 수 있다. 이를 활용한 예시를 들어보면 Material 1개 만들어 두고 여러 Material Instance를 만들고 색만 바꿔서 사용할 수 있다. 효과는 같지만 색만 다른 형태로 사용할 수 있다.

 

* Material의 변수를 런타임에 동적으로 변경하고 싶으면 바로 직접 변경해서는 안된다. 직접 변경하게 되면 해당 Material을 사용하는 모든 곳이 다 변경된다. Mesh에서 해당 Material의 MaterialInstanceDynamic를 만들고, 이를 변경해야 한다. (Material이 적절하게 들어가 있는 상황에서 CreateAndSetMaterialInstanceDynamicFromMaterial 함수를 사용한다.)

 

* Material의 내부 변수를 변경하기 위해서 SetScalarParameterValue함수를 많이 사용한다.

 

 

- Sound

 

* 엔진에서 Sound를 사용할 때 넣을 때 실제 파일로부터 Sound Cue를 만들어서 사용한다. 특정 위치에서 플레이할 수도 있고, 특정 컴포넌트에 붙여서 사용할 수도 있다. Sound Cue에 Attenuation(감쇠)라는 것이 있기 때문에 거리에 따라 소리를 미세하게 조절할 수 있다.

 

 

- 기타

 

* 리플리케이션되는 Enum 값을 그대로 넘겨주지 않고, TEnumAsByte로 래핑해서 보내준다.

 

* 멀티플레이어 게임에서 PlayerStart 를 통해 스폰 위치를 결정할 수 있다.

 

* FName은 조회 용도로 많이 사용되고, FText로 변환될 수 있다.

 

* ConsoleVariable 을 사용하면 런타임에 변수를 바꿀 수 있고, 이는 디버깅에 유용하다.

// 치트가 활성화된 경우에만 쓸 수 있다.
int32 DebugWeaponDrawing = 0;

FAutoConsoleVariableRef CVARDebugWeaponDrawing(TEXT("B.DebugWeapons"),
                                            DebugWeaponDrawing,
                                            TEXT("Draw DebugLine for Weapon"),
                                            ECVF_Cheat);

 

 

 

 

* 개인 지식 기반과 여러 곳에서 읽은 것들을 기반으로 작성하였습니다. 틀린 부분이 많을 수 있습니다. 틀린 부분이 있으면 알려주시면 감사하겠습니다.

 

* 상속 기반 설계와 컴포넌트 기반 설계

 

언리얼 엔진에서는 객체 지향형으로 설계되어 있고(상속 기반), 유니티에서는 컴포넌트 기반으로 설계되어 있다.

 

 

- 클래스 예시

 

예를 언리얼 엔진에서는 Pawn를 컨트롤할 수 있는 Actor 종류, Character을 인간형 Pawn으로 계층적으로 정의하고 있다. 하지만 유니티에서는 GameObject라는 Entity(기본 빈 객체)만 있고 그 아래에 컴포넌트를 붙여나가는 식으로 프로그래밍하도록 되어있다.

 

 

- 프로그래밍 예시

 

어떤 액션 게임에서 검, 활, 총 등과 같은 여러 가지 무기를 설계하는 과정을 생각해보자.

 

언리얼 엔진에서는 어떤 클래스를 만들 때 어떤 클래스를 상속 받아서 작성할지 자세히 지정할 수 있게 되어있다. 따라서 Actor를 상속하여 원거리 무기 클래스와 근거리 무기 클래스로 나누고 검은 근거리 무기 클래스를 상속받고, 활과 총은 원거리 무기를 상속받아 작성할 수 있을 것이다. 잘 짜여진 계층구조를 설계해야한다.

 

유니티에서는 클래스를 만들 때 기본적으로 MonoBehaviour라는 컴포넌트를 상속받은 클래스가 생성된다. 무기를 구성하는데 필요한 여러 컴포넌트를 설계한다. 공격력 컴포넌트, 공격범위 컴포넌트, 발사체 컴포넌트 등의 컴포넌트를 설계하고 무기의 특징에 따라 컴포넌트를 조합하여 구성한다.

 

 

- 장단점 비교

 

  객체 지향형 설계 컴포넌트 기반 설계
장점 @ 컴포넌트와 달리 하나로 되어있기 때문에 그 안에서 모두 처리할 수 있다.

@ 계층적 구조를 잘 나타낼 수 있기 때문에 구조를 쉽게 파악할 수 있다.

@ 코드 수정이 쉽다. // 위의 예시에서 발사체에 대한 처리를 고치려면 발사체 컴포넌트 하나를 수정하면 된다.

@ 코드 유지 보수가 쉽고, 확장이 유연하다.
단점 @ 관리해야할 클래스가 많아질수록 계층구조가 더욱 복잡해지고 이를 유지 관리하는데 어려움을 겪거나 시간이 오래걸릴 수 있다.

@ 코드 수정, 추가 과정이 복잡할 수 있다. // 위의 예시에서 발사체에 대한 처리를 고치려면 계층 구조를 확인하고 적절한 곳에 수정해야 한다. 계층 구조를 다시 설계해야할 수도 있다.
@ 여러 컴포넌트가 복합적으로 사용되어야 하는 구조에서 컴포넌트 간 복잡한 상호작용이 필요하다.

@ 계층적 구조를 나타낼 수 없기 때문에 시스템을 세부적인 수준에서 설명하기 어렵다.

 

 

https://spin.atomicobject.com/2020/09/05/unity-component-based-design/?nowprocket=1 

 

Why You Should Use Component-Based Design in Unity

Component-based design in Unity can save developers a lot of time by making the project easier to maintain than the traditional inheritance model.

spin.atomicobject.com

 

https://www.cs.cmu.edu/afs/cs/project/able/ftp/acme-fcbs/acme-fcbs.pdf

 

 

 

 

컴포넌트 설계의 단점

 

First, they provide only a single form of primitive interconnection - method invocation. 
This makes it difficult to represent richer types of component interaction as first class design elements.

Second, they have weak support for hierarchical description, making it difficult to describe systems at increasing levels of detail. 

Third, they do not support the definition of families of systems. 
While they can be used to describe patterns and to define a vocabulary of object types,  they don't have explicit syntactic support for characterizing a class of system in terms of the design constraints that each member of the family must observe. 

Fourth, they do not provide direct support for characterizing and analyzing non-functional properties. This makes it difficult to reason about critical system design properties, such as system performance and reliability

 

 

'게임 엔진 > 비교' 카테고리의 다른 글

[Unity / UnrealEngine] 간단 비교  (0) 2022.04.03

* 전반적인 용어 비교

 

Category Unity UE4
Gameplay Types Component Component
  GameObject Actor, Pawn
  Prefab Blueprint Class
Editor UI Hierarchy Panel World Outliner
  Inspector Details Panel
  Project Browser Content Browser
  Scene View Viewport
Meshes Mesh Static Mesh
  Skinned Mesh Skeletal Mesh
Materials Shader Material, Material Editor
  Material Material Instance
Effects Particle Effect Effect, Particle, Cascade
  Shuriken Cascade
Game UI UI UMG (Unreal Motion Graphics)
Animation Animation Skeletal Animation System
  Mecanim Persona , Animation Blueprint
2D Sprite Editor Paper2D
Programming C# C++
  Script Blueprint
Physics Raycast Line Trace, Shape Trace
  Rigid Body Collision, Physics
Runtime Platforms iOS Player, Web Player Platforms

 

 

* 프로그래밍

 

 

 

* 월드 표현

 

  UnrealEngine Unity
표현 방식 Actor는 RootComponent를 가지고 있으며 이는 SceneComponent의 서브클래스이다. SceneComponent는 계층적인 구조를 이루며 이에 따라 계층적으로 적용되는 위치, 회전, 스케일 정보를 가지고 있다. GameObject는 Transform 컴포넌트를 가지고 있다. 이 Transform는 계층적인 구조를 이룰 수 있으며 위치, 회전, 스케일 정보를 계층적으로 나타낸다.

 

 

 

 

* 객체 생성

 

- 유형별 생성 방법

 

 객체  유형 UnrealEngine Unity 역할
Actor
GameObject
Actor : World 객체를 찾고(일부 객체가 World 객체를 가져올 수 있도록 지원), World 객체의 SpawnActor로 생성하고자 하는 Actor의 클래스를 넘겨서 생성 GameObject : Instantiate함수를 통해 생성 월드에 생성되는 객체들로 대부분의 게임 로직을 담당한다.
UObject
ScriptableObject
UObject : NewObject를 통해 생성 ScriptableObject : ScriptableObject.CreateInstance함수를 통해 생성 월드에 스폰할 필요없거나 Actor처럼 컴포넌트를 포함하는 게임플레이 관련 클래스에 유용하다.

 

// Unity의 GameObject
GameObject NewGO = (GameObject)Instantiate(EnemyPrefab, SpawnPosition, SpawnRotation);
NewGO.name = "MyNewGameObject";

// UE의 Actor
UWorld* World = ExistingActor->GetWorld();
FActorSpawnParameters SpawnParams;
SpawnParams.Template = ExistingActor;
World->SpawnActor<AMyActor>(ExistingActor->GetClass(), SpawnLocation, SpawnRotation, SpawnParams);

 

// Unity의 ScriptableObject
MyScriptableObject NewSO = ScriptableObject.CreateInstance<MyScriptableObject>();

// UE의 UObject
UMyObject* NewObj = NewObject<UMyObject>();

 

 

- 생성 과정

 

Unity UnrealEngine
디폴트 값을 설정하기 위해 선언과 동시에 초기화한다. 각 객체 클래스에는 디폴트 값의 속성들과 컴포넌트들을 클래스 기본 객체(CDO)를 포함하고 있다. 이는 엔진 초기화 시 생성자를 통해서 최초로 생성되며 수정되지 않은 상태로 유지된다.

해당 객체를 생성할때 CDO에서 복사해서 생성한다.

 

Unity / UnrealEngine

 

 

* 캐스팅

 

 

 

* 트리거

 

Unity

public class MyComponent : MonoBehaviour
{
    void Start()
    {
        collider.isTrigger = true;
    }
    void OnTriggerEnter(Collider Other)
    {
        // ...
    }
    void OnTriggerExit(Collider Other)
    {
        // ...
    }
}

 

UnrealEngine

UCLASS()
class AMyActor : public AActor
{
    GENERATED_BODY()

    // My trigger component
    UPROPERTY()
    UPrimitiveComponent* Trigger;

    AMyActor()
    {
        Trigger = CreateDefaultSubobject<USphereComponent>(TEXT("TriggerCollider"));

        // Both colliders need to have this set to true for events to fire
        Trigger.bGenerateOverlapEvents = true;

        // Set the collision mode for the collider
        // This mode will only enable the collider for raycasts, sweeps, and overlaps
        Trigger.SetCollisionEnabled(ECollisionEnabled::QueryOnly);
    }

    virtual void NotifyActorBeginOverlap(AActor* Other) override;

    virtual void NotifyActorEndOverlap(AActor* Other) override;
};

 

 

* 입력

 

Unity

public class MyPlayerController : MonoBehaviour
{
    void Update()
    {
        if (Input.GetButtonDown("Fire"))
        {
            // ...
        }
        float Horiz = Input.GetAxis("Horizontal");
        float Vert = Input.GetAxis("Vertical");
        // ...
    }
}

 

UnrealEngine

UCLASS()
class AMyPlayerController : public APlayerController
{
    GENERATED_BODY()

    void SetupInputComponent()
    {
        Super::SetupInputComponent();

        InputComponent->BindAction("Fire", IE_Pressed, this, &AMyPlayerController::HandleFireInputEvent);
        InputComponent->BindAxis("Horizontal", this, &AMyPlayerController::HandleHorizontalAxisInputEvent);
        InputComponent->BindAxis("Vertical", this, &AMyPlayerController::HandleVerticalAxisInputEvent);
    }

    void HandleFireInputEvent();
    void HandleHorizontalAxisInputEvent(float Value);
    void HandleVerticalAxisInputEvent(float Value);
};

 

 

* 주요 함수

 

- GameObject / Actor 에서 Component 가져오기

 

Unity

MyComponent MyComp = gameObject.GetComponent<MyComponent>();

 

UnrealEngine

UMyComponent* MyComp = MyActor->FindComponentByClass<UMyComponent>();

 

 

- Component 에서 GameObject / Actor 가져오기

 

Unity

MyComponent.gameObject;

 

UnrealEngine

MyComponent->GetOwner();

 

 

- 특정 GameObject / Actor 찾기

 

Unity

// Find GameObject by name
GameObject MyGO = GameObject.Find("MyNamedGameObject");

// Find Objects by type
MyComponent[] Components = Object.FindObjectsOfType(typeof(MyComponent)) as MyComponent[];
foreach (MyComponent Component in Components)
{
        // ...
}

// Find GameObjects by tag
GameObject[] GameObjects = GameObject.FindGameObjectsWithTag("MyTag");
foreach (GameObject GO in GameObjects)
{
        // ...
}

// Find Actor by name (also works on UObjects)
AActor* MyActor = FindObject<AActor>(nullptr, TEXT("MyNamedActor"));

// Find Actors by type (needs a UWorld object)
for (TActorIterator<AMyActor> It(GetWorld()); It; ++It)
{
        AMyActor* MyActor = *It;
        // ...
}

 

UnrealEngine

// Find UObjects by type
for (TObjectIterator<UMyObject> It; It; ++It)
{
    UMyObject* MyObject = *It;
    // ...
}

// Find Actors by tag (also works on ActorComponents, use TObjectIterator instead)
for (TActorIterator<AActor> It(GetWorld()); It; ++It)
{
    AActor* Actor = *It;
    if (Actor->ActorHasTag(FName(TEXT("Mytag"))))
    {
        // ...
    }
}

 

 

* Dynamic Delegate

 

- 정의

 

다이나믹 델리게이트는 직렬화될 수 있고 리플렉션을 지원하는 델리게이트이다. 함수들을 이름으로 찾기 때문에 일반적인 델리게이트보다 느리다.

 

 

- 선언

 

일반적인 델리게이트와 비슷하게 선언된다. 다만 매크로의 종류만 조금 다르다.

다이나믹 델리게이트 타입 선언 매크로
설명
DECLARE_DYNAMIC_DELEGATE[_RetVal, ...]( DelegateName ) 다이나믹 델리게이트 타입을 만든다.
DECLARE_DYNAMIC_MULTICAST_DELEGATE[_RetVal, ...]( DelegateName ) 다이나믹 멀티캐스트 델리게이트 타입을 만든다.

 

 

- 바인딩

 

매크로를 통해 바인딩할 수 있다.

매크로 설명
BindDynamic( UserObject, FuncName ) 다이나믹 델리게이트에서 BindDynamic을 호출하는 매크로로 자동으로 함수 이름 문자열을 생성한다.
AddDynamic( UserObject, FuncName ) 다이나믹 멀티캐스트 델리게이트에서 AddDynamic을 호출하는 매크로로 자동으로 함수 이름 문자열을 생성한다.
RemoveDynamic( UserObject, FuncName ) 다이나믹 멀티캐스트 델리게이트에서 RemoveDynamic을 호출하는 매크로로 자동으로 함수 이름 문자열을 생성한다.

 

 

- 실행

 

일반적인 싱글캐스트 델리게이트 실행 방법과 동일하다.

 

 

* Multicast Delegate

 

- 정의

 

멀티캐스트 델리게이트는 여러 함수들에 바인딩할 수 있으며 한번에 모두 실행시키는 것이 가능하다.

 

싱글캐스트 델리게이트와 대부분 비슷한 특징을 가지고 있다. 멀티캐스트 델리게이트는 객체에 대해 약 참조만을 가지고 있으며 구조체와 함께 사용될 수 있으며 쉽게 복사될 수 있다.

 

일반적인 델리게이트와 같이 멀티캐스트 델리게이트는 로드 세이브 할 수 있고 원격으로 트리거할 수 있다. 하지만 멀티캐스트 델리게이트 함수들은 반환값을 가질 수 없다.

 

이벤트는 Broadcast, IsBound, Clear함수를 사용할 수 없는 멀티캐스트 델리게이트이다.

 

 

- 선언

 

일반적인 델리게이트와 비슷하게 선언된다. 다만 매크로의 종류만 조금 다르다.

멀티캐스트 델리게이트 타입 선언 매크로 설명
DECLARE_MULTICAST_DELEGATE[_RetVal, ...]( DelegateName ) 멀티캐스트 델리게이트 타입을 만든다.
DECLARE_DYNAMIC_MULTICAST_DELEGATE[_RetVal, ...]( DelegateName ) 다이나믹 멀티캐스트 델리게이트 타입을 만든다.

 

 

- 바인딩

 

함수 설명
Add() 멀티캐스트 델리게이트 호출 목록에 함수를 추가한다.
AddStatic() 전역 생 함수 포인터를 추가한다.  Adds a raw C++ pointer global function delegate.
AddRaw()  포인터를 추가한다. 생 포인터는 어떤 참조도 사용하지 않기 때문에 삭제된 객체에 대해 Execute를 안전하게 사용할 수 없다.
AddSP() 공유 포인터 기반 멤버 함수를 추가한다. 스레드 세이프하지 않다. 공유 포인터 델리게이트는 바인딩한 객체를 약 참조로 관리한다.
AddUObject() UObject 멤버 함수를 추가한다. UObject 델리게이트는 해당 UObject를 약 참조로 관리한다.
Remove() 멀티캐스트 델리게이트 호출 목록에서 제거한다. 성능은 O(n)이며 순서는 보존되지 않는다.
RemoveAll() 멀티캐스트 델리게이트 호출 목록에서 모든 함수를 제거한다. 순서는 보존되지 않는다.
객체 포인터로 바인딩되지 않은 함수는 삭제되지 않는다.

 

 

- 실행

 

Broadcast를 통해 멀티캐스트 델리게이트에 등록된 만료된 것을 제외한 모든 함수를 호출할 수 있다. 바인딩되지 않아도 항상 안전하다. 순서가 보존되지 않기 때문에 어떤 함수가 먼저 호출될 지 모른다.

 

 

* Event

 

- 정의

 

이벤트는 멀티캐스트 델리게이트와 매우 비슷하며 어떤 클래스도 이벤트를 바인드할 수 있지만 이벤트를 선언한 클래스에서만 Broadcast, IsBound, Clear 함수를 사용할 수 있다.

 

외부에 노출되어도 위의 이벤트의 함수들을 호출할 수 없으며, 순수 추상 클래스에 콜백을 포함하고 있다.

 

 

- 선언

 

멀티캐스트 델리게이트와 비슷하게 선언된다. 다만 이벤트의 함수 호출을 제한하기 위해 소유자를 설정해야한다.

이벤트 타입 선언 매크로 설명
DECLARE_EVENT( OwningType, EventName ) 이벤트를 만든다.
DECLARE_EVENT_OneParam( OwningType, EventName, Param1Type ) 매개변수 1개인 이벤트를 만든다.
DECLARE_EVENT_TwoParams( OwningType, EventName, Param1Type, Param2Type ) 매개변수 2개인 이벤트를 만든다.
DECLARE_EVENT_<Num>Params( OwningType, EventName, Param1Type, Param2Type, ... ) 매개변수 N개인 이벤트를 만든다.

 

 

- 바인딩

 

멀티캐스트 바인딩 방식과 동일하다.

 

 

- 실행

 

멀티캐스트 바인딩 방식과 동일하다.

 

* 정의

 

일반적으로 델리게이트(delegate)는 함수를 안전하게 호출할 있는 기능을 의미하며 콜백 이벤트 리스너를 구현하는데 사용된다. 

 

언리얼 엔진에서는 바인드 있는 함수의 수에 따라 싱글, 멀티 캐스트로 구분하며 블루프린트와 연동할 있는 다이나믹 델리게이트를 추가로 지원하고 있다.

 

델리게이트는 객체의 멤버 함수에 동적으로 바인딩 되어 호출자가 객체의 유형을 알지 못해도 나중에 객체의 함수를 호출할 수 있다. 복사에 대해 안전하지만 값으로 넘겨줄 경우 힙에 추가 할당을 하게 되니 참조로 넘겨주는게 좋다.

 

 

* 동작 방식

 

- 선언

 

언리얼 엔진에서는 델리게이트 타입을 선언하기 위한 여러가지 매크로를 제공한다. 이러한 매크로는 델리게이트 타입의 이름, 반환형, 매개 변수를 포함하고 있으며 상황에 따라 적절한 조합을 사용해야한다.

 

현재는 다음과 같은 것들을 제공하고 있다.

  • 값을 반환하는 함수
  • const로 선언된 함수
  • 최대 4개의 페이로드 변수
  • 최대 8개까지의 함수 매개변수

 

실제 사용하는 델리게이트 매크로는 다음과 같다.

함수 형식 델리게이트 타입 선언 매크로
void Function() DECLARE_DELEGATE(DelegateName)
void Function(Param1) DECLARE_DELEGATE_OneParam(DelegateName, Param1Type)
void Function(Param1, Param2) DECLARE_DELEGATE_TwoParams(DelegateName, Param1Type, Param2Type)
void Function(Param1, Param2, ...) DECLARE_DELEGATE_<Num>Params(DelegateName, Param1Type, Param2Type, ...)
<RetValType> Function() DECLARE_DELEGATE_RetVal(RetValType, DelegateName)
<RetValType> Function(Param1) DECLARE_DELEGATE_RetVal_OneParam(RetValType, DelegateName, Param1Type)
<RetValType> Function(Param1, Param2) DECLARE_DELEGATE_RetVal_TwoParams(RetValType, DelegateName, Param1Type, Param2Type)
<RetValType> Function(Param1, Param2, ...) DECLARE_DELEGATE_RetVal_<Num>Params(RetValType, DelegateName, Param1Type, Param2Type, ...)

 

사용 예시는 다음과 같다.

UDELEGATE(BlueprintAuthorityOnly)
DECLARE_DYNAMIC_MULTICAST_DELEGATE_FourParams(FInstigatedAnyDamageSignature, float, Damage, const UDamageType*, DamageType, AActor*, DamagedActor, AActor*, DamageCauser);

 

 

Dynamic, Multi-Cast 형식의 델리게이트가 존재하며 이에 대한 설명은 다음과 같다.

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

 

[UnrealEngine] Dynamic Delegate, Multicast Delegate

* Dynamic Delegate - 정의 다이나믹 델리게이트는 직렬화될 수 있고 리플렉션을 지원하는 델리게이트이다. 함수들을 이름으로 찾기 때문에 일반적인 델리게이트보다 느리다. - 선언 일반적인 델리게

create-new-worlds.tistory.com

 

 

- 바인딩

 

델리게이트 시스템은 특정 객체 타입에 대해 이해하고 있으며 이를 관리할 때 추가 처리를 해준다.

만약 UObject나 SharedPtr의 멤버에 델리게이트를 바인딩하면 델리게이트는 이를 약 참조(weak reference)로 유지한다. 만약 이러한 객체가 파괴되면 IsBound나 ExecuteIfBound 함수를 호출하여 이를 처리할 수 있다.

 

바인딩 함수는 다음과 같다.

함수 설명
Bind 존재하는 델리게이트 객체에 바인딩한다.
BindStatic 전역 생 함수 포인터를 바인딩한다.
BindRaw 포인터를 바인딩한다. 생 포인터는 어떤 참조도 사용하지 않기 때문에 삭제된 객체에 대해 Execute나 ExcuteIfBound를 안전하게 사용할 수 없다.
BindLambda 주로 람다 함수를 바인딩한다.
BindSP 공유 포인터 기반 멤버 함수를 바인딩한다. 공유 포인터 델리게이트는 바인딩한 객체를 약 참조로 관리한다. ExcuteIfBound 함수를 통해 안전하게 사용할 수 있다.
BindUObject UObject 멤버 함수를 바인딩한다. UObject 델리게이트는 해당 UObject를 약 참조로 관리한다. ExcuteIfBound 함수를 통해 안전하게 사용할 수 있다.
UnBind 언바인딩한다.

 

 

- 페이로드

// 페이로드라는 용어를 사용한 이유는 바로 함수 호출에 사용되지 않고 저장되어 실제 함수 호출 때 함수 인자로 전송되기 때문인 것 같다. 페이로드라는 용어로 보아 직렬화된 데이터를 델리게이트 내부에 유지하지 않을까 싶다.

델리게이트에 바인딩할 때 넘겨주는 데이터를 의미하며 바인딩된 함수가 호출될 때 해당 함수로 직접 전달된다. 페이로드는 델리게이트 내부에 저장되며 Dynamic을 제외한 모든 델리게이트에서 자동으로 페이로드 변수를 지원한다. 이러한 추가 인수들은 반드시 델리게이트의 매개변수 인수 뒤에 위치해야한다.

 

 

- 실행

 

델리게이트에 바인딩된 함수는 Execute라는 함수를 통해 호출된다. 실행하기 전에 무조건 델리게이트가 바인딩 되었는 지 확인 해야한다. 이와 같은 방식으로 실행하면 특히 델리게이트에 초기화되지 않고 접근되는 반환값, 출력 매개변수가 있는 경우 더욱 안전하게 처리할 수 있다.

 

바인딩되지 않은 델리게이트를 수행하면 메모리를 망칠 수 있기 때문에 IsBound 함수를 호출하여 체크 후에 안전하기 Execute를 호출한다. 만약 반환값이 없다면 ExecuteIfBound를 사용할 수 있다.

실행 함수 설명
Execute 바인딩 체크를 하지 않고 델리게이트를 실행한다.
ExecuteIfBound 델리게이트가 바인딩되었는 지 확인하고 Execute함수를 호출한다. 
IsBound 델리게이트의 바인딩 여부를 체크한다. 보통 Execute함수 전에 호출한다.

 

* 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에서 제거하게 된다. 이 후 다음 가비지컬렉션 사이클에서 삭제된다.

 

 

* 정의

 

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

 

 

* 동작 방식

 

- Archetype과 Instance

 

속성 지정자를 확인하려면 ArcheType과 Instance에 대해 간단히 알아야 한다.

 

ArcheType은 속성의 집합인 객체의 템플릿이다. Unity3D의 Prefab이라고 보면 된다.

Instance는 ArcheType을 통해 생성된 객체이다.

 

 

- 속성 지정자 종류

 

자주 쓰는 몇 가지를 정리해보면 다음과 같다.

속성 태그(Property Tag) 효과(Effect)
Edit ~  
EditAnywhere 속성 윈도우에서 ArcheType, Instance에 대해 수정할 수 있다.
EditDefaultsOnly 속성 윈도우에서 ArcheType에 대해서만 수정할 수 있다.
EditInstanceOnly 속성 윈도우에서 Instance에 대해서만 수정할 수 있다.
Visible ~  
VisibleAnywhere 속성 윈도우에서 ArcheType, Instance가 보여진다. 수정 불가능하다.
VisibleDefaultsOnly 속성 윈도우에서 ArcheType만 보여진다. 수정 불가능하다.
VisibleInstanceOnly 속성 윈도우에서 Instance만 보여진다. 수정 불가능하다.
Blueprint ~  
BlueprintCallable 블루프린트 코드에서 호출할 수 있다.
BlueprintAssignable 멀티캐스트 델리게이트에서 사용 가능, 블루프린트에서 해당 속성을 할당할 수 있다.
BlueprintReadOnly 블루프린트에서 읽을 수 있지만 수정은 불가능하다.
BlueprintReadWrite 블루프린트에서 읽고 쓸 수 있다.
BlueprintImplementableEvent 블루프린트에서만 함수를 정의해야 한다. 만약 CPP에서 정의한다면 에러가 발생한다.
BlueprintNativeEvent CPP에서 정의하고, 블루프린트에서 오버라이딩할 수 있다.
CPP에서 정의할 때 XXX_Implementation라는 함수명으로 정의하여야 한다.

 

 

- 주의할 점

 

@ Visible, Edit의 적용 범위는 해당 객체 그 자체라는 것을 주의하여야 한다.

예를 들어 UStaticMeshComponent를 정의할 때 VisibleAnywhere 의 속성 지정자를 사용해도, UStaticMeshComponent 내부에 EditAnywhere 속성 지정자로 정의된 UStaticMesh는 어디서든 수정할 수 있다. 하지만 Visible 속성 지정자로 설정되어 있기 때문에 새로운 UStaticMeshComponent 를 할당할 수 없다.

 

그래서 보통 UStaticMeshComponent와 같은 UObject는 Visible 속성 지정자를 많이 사용하고, int 형과 같은 자료형에는 Edit 속성 지정자를 많이 사용하는 편이다.

 

@ Edit, Visible은 하나만 선택하여 사용할 수 있다. (Blueprint 관련 속성 지정자와 조합하여 사용 가능하다.)

 

 

- 사용 팁

 

모두 그런것 은 아니지만, 보통 로직의 대부분을 CPP에서 처리하고, 리소스(Mesh, UI)와 관련된 정보들을 블루프린트를 통해서 많이 작성하는 것 같다.

 

예를 들어 플레이어 캐릭터의 로직을 모두 CPP내에 작성해두고, SkeletalMesh 정보를 블루프린트에서 적절한 Mesh로 연결하여 사용한다.(물론 CPP내에서도 Mesh정보를 불러와서 연결 시킬 수 있다. 하지만 블루프린트를 사용하면 직접 눈으로 보면서 작업할 수 있기 때문에 편리하다.)

 

 

자세한 속성에 대한 정보와 속성 지정자는 다음에서 확인할 수 있다.

https://docs.unrealengine.com/5.0/en-US/unreal-engine-uproperties/

 

Properties

Reference for creating and implementing properties for gameplay classes.

docs.unrealengine.com

 

+ Recent posts