배움 저장소

[UE4] Introduction to C++ Programming in UE4 본문

Dev/Unreal Engine4

[UE4] Introduction to C++ Programming in UE4

시옷지읏 2021. 11. 8. 20:45
더보기

이 글은 다음 문서를 읽고 공부한 글입니다. 틀린 내용이 있다면 알려주세요

 

Introduction to C++ Programming in UE4

Introductory guide for C++ programmers new to Unreal Engine

docs.unrealengine.com

 

1. UPROPERTY( Transient )

Transient 개념이 생소하다.

UPROPERTY(BlueprintReadOnly, VisibleAnywhere, Transient, Category="Damage")

Transient로 설정된 변수는 값을 디스크에 저장하지 않는다. 따로 바이너리로 만들어 디스크에 저장해 쓸데없는 낭비가 일어날 수 있는데, 그걸 막는다는 뜻이다. "이 값은 초기값 없이 다른 변수들에 의해 결정된다"를 뜻한다.

 

UPROPERTY 관련 내용은 이하에서 다루었다.

 

[UE4] Unreal Property System (Reflection)

Unreal Property System (Reflection) Reflection is the ability of a program to examine itself at runtime. This is hugely useful and is a foundational technology of the Unreal engine, powering many sy..

hoplite.tistory.com

 

 

2. 생성자(Constructor)를 사용하는 방법 두 가지

AMyActor::AMyActor()
{
    TotalDamage = 200;
    DamageTimeInSeconds = 1.0f;
}

AMyActor::AMyActor() :
    TotalDamage(200),
    DamageTimeInSeconds(1.0f)
{
}

알고있는 방법이지만 두 번째 방법을 언리얼에서 써본 적이 없어 새로웠다.

 

2. Constructor() vs Beginplay() vs PostInitProperties()

 World 안에 있는 다른 Actor와 상호작용하려면 Beginplay()에 구현해야 한다. Constructor()가 호출 될 시점에는 레벨이 아직 생성되기 이전이거나 생성되고 있는 중이다. Constructor()와 Beginplay() 사이에 동작되는 함수가 PosInitPorperties()이다. 이 함수는 Serialization(바이너리로 바꾸어 데이터를 디스크에 저장) 되기 전에 호출 된다. 데이터가 저장 되기 전이므로, Blueprint 혹은 Editor에 공개된 값과 함께 상호작용하여 값을 할당하고싶다면 이 함수를 사용하면 된다. Beginplay처럼 게임 동작 이후 값이 메모리를 잡아먹지 않고 디스크에 데이터를 저장하고 동작할 것.

 

 C++ Class에서 파생된 Blueprint를 다룰 때, Constructor에 할당한 값이 도중에 바꼈는데도 Unreal Editor에 반영되지 않는 경우가 있다. 해당 데이터가 Load될 때 Constructor가 호출되기 때문이다. Constructor의 값을 바꾸었더라도 해당 데이터를 새로 Load하지는 않았기 때문에 Blueprint의 데이터는 이전 데이터를 그대로 들고있는 것이다.

 이러한 상황에서 Unreal Editor에서 값이 바뀔 때마다 값을 업데이트 할 수 있도록 특별한 함수를 지원하고 있다.

PostEditChangeProperty() 이다. 이 함수는 #if 문 내부에 있는데, Unreal Editor에서 해당 객체의 값이 바뀔 때만 호출할 수 있다. 컴파일 때 포함되면 낭비니까

void AMyActor::PostInitProperties()
{
    Super::PostInitProperties();

    CalculateValues();
}

void AMyActor::CalculateValues()
{
    DamagePerSecond = TotalDamage / DamageTimeInSeconds;
}

#if WITH_EDITOR
void AMyActor::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
{
    CalculateValues();

    Super::PostEditChangeProperty(PropertyChangedEvent);
}
#endif

 

3. UFUCTION

1) Category를 지정하자

UFUNCTION(BlueprintCallable, Category="Damage")
void CalculateValues();

specifier는 이전 글에서 다루었으므로 넘어가자. UFUNCTION을 사용할 떄 반드시 Category를 지정하라고 나온다. 그래야 Blueprint에서 오른쪽 클릭 기능이 제대로 작동한다고.

 

2) Blueprint에서 구현되는 함수 만들기

UFUNCTION(BlueprintImplementableEvent, Category="Damage")
void CalledFromCpp();

 

3) Blueprint에서 구현되거나 C++에서 구현된걸 쓰거나

// header
UFUNCTION(BlueprintNativeEvent, Category="Damage")
void CalledFromCpp();

// cpp
void AMyActor::CalledFromCpp_Implementation()
{
    // Do something cool here
}

CalledFromCpp()가 Blueprint에서 Override 되지 않는 경우에 _Implementation() 함수가 실행된다.

Specifier, BlueprintNativeEvent를 사용했다면 반드시 _Implementation()함수를 구현해야 한다. 하지 않으면 에러남

 

 

4. GamePlay Classes

1) UObject

https://docs.unrealengine.com/4.27/en-US/ProgrammingAndScripting/ProgrammingWithCPP/IntroductionToCPP/#unrealobjects_uobject_

 

 

Introduction to C++ Programming in UE4

Introductory guide for C++ programmers new to Unreal Engine

docs.unrealengine.com

 

2) AActor

- UScene Component가  Actor안에 있어야 Level에 등장시킬 수 있다. UScene Component를 기준으로 transform이 결정된다.

- Actor는 Physics, tick, transform 같이 여러 runtime system과 상호작용 한다. Actor를 Level 안에 생성할 때 이러한 runtime system과 상호작용을 직접 구현하기는 어렵기 때문에 SpawnActor()를 구현해놓았다. Actor가 Level 안에 성공적으로 만들어진 뒤에 Unreal Editor는 Actor의 Beginplay() 함수를 호출한다. 그 다음 프레임에 Tick()함수가 호출된다.

 

Actor Class의 파괴

- Endplay()

- Destroy()

- Set Lifespan

// Actor.cpp
void AActor::SetLifeSpan( float InLifespan )
{
	// Store the new value
	InitialLifeSpan = InLifespan;
	// Initialize a timer for the actors lifespan if there is one. Otherwise clear any existing timer
	if ((GetLocalRole() == ROLE_Authority || GetTearOff()) && !IsPendingKill())
	{
		if( InLifespan > 0.0f)
		{
			GetWorldTimerManager().SetTimer( TimerHandle_LifeSpanExpired, this, &AActor::LifeSpanExpired, InLifespan );
		}
		else
		{
			GetWorldTimerManager().ClearTimer( TimerHandle_LifeSpanExpired );		
		}
	}
}

// Actor.h
public:
	/** How long this Actor lives before dying, 0=forever. Note this is the INITIAL value and should not be modified once play has begun. */
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category=Actor)
	float InitialLifeSpan;

Actor 내부에 구현되어있는 SetLifeSpan 함수이다. LifeSpanExpired 함수는 내부에서 Destroy()를 호출한다.

 

 

3) UActorComponent

- Actor에 붙여서 사용한다.

- Ticking 함수는 Owner Actor의 Tick 함수에서 호출된다.

 

Inheritance vs Composition

class Legs
{
public:
   void WalkAround() {... code for walking around goes here...}
};

class Arms
{
public:
   void GrabThings() {... code for grabbing things goes here...}
};

class InheritanceRobot : public Legs, public Arms
{
public:
   // WalkAround() and GrabThings() methods are implicitly
   // defined for this class since it inherited those
   // methods from its two superclasses
};

class CompositionRobot
{
public:
   void WalkAround() {legs.WalkAround();}
   void GrabThings() {arms.GrabThings();}

private:
   Legs legs;
   Arms arms;
};

 

4) UStruct

다른 UObject와 다르게 Garbage Collecting 대상이 아니다. 프로그래머가 직접 관리해주어야한다.

 

 

 

 

5. Reflection System

#include "MyObject.generated.h" 
// Reflection system을 위한 코드를 생성하여 현재 파일에 넣어준다.
// 반드시 마지막 위치에서 #include 실행하여야함.

UCLASS(Blueprintable)
class UMyObject : public UObject
{
    GENERATED_BODY()

public:
    MyUObject();

    UPROPERTY(BlueprintReadOnly, EditAnywhere)
    float ExampleProperty;

    UFUNCTION(BlueprintCallable)
    void ExampleFunction();
};

Generated_body()가 Garbage Collecting을 위한 코드를 실행해준다.

 

6. Object / Actor Iterators

// Will find ALL current UObject instances
for (TObjectIterator<UObject> It; It; ++It)
{
    UObject* CurrentObject = *It;
    UE_LOG(LogTemp, Log, TEXT("Found UObject named: %s"), *CurrentObject->GetName());
}

// Searching for more specified class
for (TObjectIterator<UMyClass> It; It; ++It)
{
    // ...
}

UObject 반복문을 사용할 때 주의. 해당 레벨 안에 있는 UObject만 가져오는 것이 아니다. Unreal Editor가 Loading 될 때 가지고 있었던 UObject를 모두 탐색해서 반환한다.

 

다음 코드는 해당 World에 속해있는 UObject만 탐색한다. World를 구체적으로 명시해주자.

APlayerController* MyPC = GetMyPlayerControllerFromSomewhere();
UWorld* World = MyPC->GetWorld();

// Like object iterators, you can provide a specific class to get only objects that are
// or derive from that class
for (TActorIterator<AEnemy> It(World); It; ++It)
{
    // ...
}

 

7. Memory Management and Garbage Collection

 UObject가 아무 참조 없이 사용되고 있다면 Garbage Collecting의 대상이 된다. 만약 class 내부에 있는 UObject의 life cycle을 class의 instance와 일치시키고 싶다면 UProperty()매크로를 지정해주자. 

  UProperty() 내부에 저장되거나 TArray에 저장된 UObject는 참조된 것으로 간주되므로 Garbage Collecting의 대상이 아니다. 다음 예제를 보자.

void CreateDoomedObject()
{
    MyGCType* DoomedObject = NewObject<MyGCType>();
}

해당 DoomedObject는 Garbage Collecting의 대상이 될 것이다. 만약 함수 내부에서 다양한 UObject를 생성한 후 해당 class와 Life cycle을 일치시켜 주고 싶다면 TArray를 이용하자.

 

Actors

 UEEditor에서 Actor는 자동적으로 rootset에 포함되기 때문에 Garbage Collection의 대상이 되지 않는다. Actor는 직접 Destroy() 함수를 호출해야 다음 Garbage Collecting에서 수집되어 삭제된다. 해당 게임의 레벨이 끝나지 않는 이상 Actor는 GC의 수집대상이 아니기 때문에 주의해서 관리하자.

 

UStructs

 UObject의 여러 기능이 빠져있지만 그만큼 경량화된 데이터 타입. UStruct는 Garbage Collecting이 지원되지 않는다.

 

Non-UObject References

UObject에서 상속된 자식은 Garbage Collecting을 제어할 수 있지만 그렇지 않은 class는 Garbage Collecting을 제어하기 어렵다. UObject에서 상속되지 않았지만 Garbage Collecting을 제어하고 싶다면 FGCObject를 활용하자

대상 class는 FGCObject를 상속받아야 하며 AddReferencedObjects()함수를 Overriding 해야한다.

class FMyNormalClass : public FGCObject
{
public:
    UCustomObject* SafeObject;

    FMyNormalClass(UObject* Object)
        : SafeObject(Object)
    {
    }
	
    // 해당 함수를 이용하여 Hard Reference를 추가해주면 Garbage Collecting대상이 아니게 된다.
    void AddReferencedObjects(FReferenceCollector& Collector) override
    {
        Collector.AddReferencedObject(SafeObject);
    }
};

8. Class Nameing Prefixes

Actor (액터) 파생 클래스 접두사는 A 입니다. 예: AController.

Object (오브젝트) 파생 클래스 접두사는 U 입니다. 예: UComponent.

Enum (열거형) 접두사는 E 입니다. 예: EFortificationType.

Interface (인터페이스) 클래스 접두사는 보통 I 입니다. 예: IAbilitySystemInterface.

Template (템플릿) 클래스 접두사는 T 입니다. 예: TArray.

SWidget (슬레이트 UI) 파생 클래스 접두사는 S 입니다. 예: SButton.

그 외의 접두사는 letter F 입니다. 예: FVector.

 

9. Strings

https://docs.unrealengine.com/4.27/en-US/ProgrammingAndScripting/ProgrammingWithCPP/IntroductionToCPP/#ftext

// FString
FString MyStr = TEXT("Hello, Unreal 4!");


// FText1
// NSLOCTEXT(namespace, key, value)
FText MyText = NSLOCTEXT("Game UI", "Health Warning Message", "Low Health!");


// FText2
// In GameUI.cpp
#define LOCTEXT_NAMESPACE "Game UI"

//...
FText MyText = LOCTEXT("Health Warning Message", "Low Health!")
//...

#undef LOCTEXT_NAMESPACE
// End of file

 LOCTEXT macro를 사용한다면 FText2처럼 할 수 있다.

 

FName

- FName은 Asset 이름처럼 고유한 값을 가지는 대상의 이름을 정할 때 사용하며, 내부적으로 string에 매칭되는 index로 hash map을 만들기 때문에 빠르게 탐색할 수 있다.

 

 

TChar

- OS마다 character를 저장하는 방법이 달라 Unreal에서 이 문제를 다루어주는 type을 만들어주었다.

- Printf함수에서 %s는 string이 아니라 TChar로 불러와야 한다.

FString Str1 = TEXT("World");
int32 Val1 = 123;
FString Str2 = FString::Printf(TEXT("Hello, %s! You have %i points."), *Str1, Val1);

다음과 같은 기능도 가지고있다.

TCHAR Upper('A');
TCHAR Lower = FChar::ToLower(Upper); // 'a'

 

 

10. Container

TArray

std::vector와 같은 역할을 한다.

TArray<AActor*> ActorArray = GetActorArrayFromSomewhere();

// Tells how many elements (AActors) are currently stored in ActorArray.
int32 ArraySize = ActorArray.Num();

// TArrays are 0-based (the first element will be at index 0)
int32 Index = 0;
// Attempts to retrieve an element at the given index
AActor* FirstActor = ActorArray[Index];

// Adds a new element to the end of the array
AActor* NewActor = GetNewActor();
ActorArray.Add(NewActor);

// Adds an element to the end of the array only if it is not already in the array
ActorArray.AddUnique(NewActor); // Won't change the array because NewActor was already added

// Removes all instances of 'NewActor' from the array
ActorArray.Remove(NewActor);

// Removes the element at the specified index
// Elements above the index will be shifted down by one to fill the empty space
ActorArray.RemoveAt(Index);

// More efficient version of 'RemoveAt', but does not maintain order of the elements
ActorArray.RemoveAtSwap(Index);

// Removes all elements in the array
ActorArray.Empty();

TMap

std::map과 같이 키와 값을 가지고있는 데이터 타입이다. 만약 특정 class가 GetTypeHash를 가지고 있다면 Tmap의 키값으로 사용가능하다. 체스를 구현한다고 할 때 예제

enum class EPieceType
{
    King,
    Queen,
    Rook,
    Bishop,
    Knight,
    Pawn
};

struct FPiece
{
    int32 PlayerId;
    EPieceType Type;
    FIntPoint Position;

    FPiece(int32 InPlayerId, EPieceType InType, FIntVector InPosition) :
        PlayerId(InPlayerId),
        Type(InType),
        Position(InPosition)
    {
    }
};

class FBoard
{
private:

    // Using a TMap, we can refer to each piece by its position
    TMap<FIntPoint, FPiece> Data;

public:
    bool HasPieceAtPosition(FIntPoint Position)
    {
        return Data.Contains(Position);
    }
    FPiece GetPieceAtPosition(FIntPoint Position)
    {
        return Data[Position];
    }

    void AddNewPiece(int32 PlayerId, EPieceType Type, FIntPoint Position)
    {
        FPiece NewPiece(PlayerId, Type, Position);
        Data.Add(Position, NewPiece);
    }

    void MovePiece(FIntPoint OldPosition, FIntPoint NewPosition)
    {
        FPiece Piece = Data[OldPosition];
        Piece.Position = NewPosition;
        Data.Remove(OldPosition);
        Data.Add(NewPosition, Piece);
    }

    void RemovePieceAtPosition(FIntPoint Position)
    {
        Data.Remove(Position);
    }

    void ClearBoard()
    {
        Data.Empty();
    }
};

TSet

std::set과 같은 역할. TArray가 AddUnique() 함수를 지원하지만 TSet이 더 효율적으로 동작한다.

TSet<AActor*> ActorSet = GetActorSetFromSomewhere();

int32 Size = ActorSet.Num();

// Adds an element to the set, if the set does not already contain it
AActor* NewActor = GetNewActor();
ActorSet.Add(NewActor);

// Check if an element is already contained by the set
if (ActorSet.Contains(NewActor))
{
    // ...
}

// Remove an element from the set
ActorSet.Remove(NewActor);

// Removes all elements from the set
ActorSet.Empty();

// Creates a TArray that contains the elements of your TSet
TArray<AActor*> ActorArrayFromSet = ActorSet.Array();

 

Iterator

// Moves the iterator back one element
--EnemyIterator;

// Moves the iterator forward/backward by some offset, where Offset is an integer
EnemyIterator += Offset;
EnemyIterator -= Offset;

// Gets the index of the current element
int32 Index = EnemyIterator.GetIndex();

// Resets the iterator to the first element
EnemyIterator.Reset();

void RemoveDeadEnemies(TSet<AEnemy*>& EnemySet)
{
    // Start at the beginning of the set, and iterate to the end of the set
    for (auto EnemyIterator = EnemySet.CreateIterator(); EnemyIterator; ++EnemyIterator)
    {
        // The * operator gets the current element
        AEnemy* Enemy = *EnemyIterator;
        if (Enemy.Health == 0)
        {
            // 'RemoveCurrent' is supported by TSets and TMaps
            EnemyIterator.RemoveCurrent();
        }
    }
}

RemoveCurrent()함수는 삭제된 index부터 뒤에있는 index를 당겨오기 때문에 비효율적이다. 효율적으로 하려면

// Remove a bunch of stuff without changing allocations
for (blah)
{
     Array.RemoveAtSwap(Index, 1, false);
}

// Shrink allocation to fit.
Array.Shrink();

RemoveAtSwap()함수를 이용하고 Shrink()를 써주자.

for each

c++ for(int i: v) 처럼 for-each 함수도 지원한다.

// TArray
TArray<AActor*> ActorArray = GetArrayFromSomewhere();
for (AActor* OneActor : ActorArray)
{
    // ...
}

// TSet - Same as TArray
TSet<AActor*> ActorSet = GetSetFromSomewhere();
for (AActor* UniqueActor : ActorSet)
{
    // ...
}

// TMap - Iterator returns a key-value pair
TMap<FName, AActor*> NameToActorMap = GetMapFromSomewhere();
for (auto& KVP : NameToActorMap)
{
    FName Name = KVP.Key;
    AActor* Actor = KVP.Value;

    // ...
}

 

 

Using Custom Type with TSet/Tmap

Custom Type을 생성하여 Tset 혹은 Tmap으로 사용하고 싶다면 Custom Type 내부에 GetTypeHash()함수와 == 연산자 함수를 구현해주어야 한다. Set에서 중복되는 객체가 있는지 없는지 검색할 때 내부의 HashCode를 비교하는 것 같다.

Map에서도 String 대신에 uint32를 키로 사용해 성능을 올리지 않을까?

class FMyClass
{
    uint32 ExampleProperty1;
    uint32 ExampleProperty2;

    // Hash Function
    friend uint32 GetTypeHash(const FMyClass& MyClass)
    {
        // HashCombine is a utility function for combining two hash values.
        uint32 HashCode = HashCombine(MyClass.ExampleProperty1, MyClass.ExampleProperty2);
        return HashCode;
    }

    // For demonstration purposes, two objects that are equal
    // should always return the same hash code.
    bool operator==(const FMyClass& LHS, const FMyClass& RHS)
    {
        return LHS.ExampleProperty1 == RHS.ExampleProperty1
            && LHS.ExampleProperty2 == RHS.ExampleProperty2;
    }
     // If you needs TSet<FMyClass*>, then implement this.
     uint32 GetTypeHash(const FMyClass* MyClass) 
};

 

'Dev > Unreal Engine4' 카테고리의 다른 글

[UE4] Asset Manger and Soft Reference  (0) 2021.11.07
[UE4] Unreal Property System (Reflection)  (0) 2021.11.02
[UE4] Profiling and Optimization  (0) 2021.10.31
[UE4] UI 최적화  (0) 2021.10.30
[UE4] Programming KickStart  (0) 2021.10.28
Comments