[언리얼 엔진] 프로그래밍 입문

    언리얼 C++

    언리얼 엔진의 C++ 코드에 대한 안내서

    C++ 및 블루프린트

    언리얼 엔진에서 새로운 게임플레이 요소를 만들기 위해서는 C++나 블루프린트 스크립트, 두 방법중 하나로 선택하여 만들 수 있다.

    클래스 마법사

    언리얼 에디터안의 클래스 마법사를 사용하여 C++클래스를 만든다. 부모클래스를 설정하고, 이름을 설정하면 마법사가 파일을 자동으로 생성해줘 편집을 시작할 수 있다.

    #include "GameFramework/Actor.h"
    #include "MyActor.generated.h"
    
    UCLASS()
    class AMyActor : public AActor
    {
        GENERATED_BODY()
    
    public:
        // 이 액터의 프로퍼티에 기본값을 설정합니다.
        AMyActor();
    
        // 매 프레임 호출됩니다.
        virtual void Tick( float DeltaSeconds ) override;
    
    protected:
        // 게임 시작 또는 스폰시 호출됩니다.
        virtual void BeginPlay() override;
    };

    클래스 마법사는 BeginPlay()와 Tick을 오버로딩하는 클래스를 생성한다. 필요하지 않은경우 제거해도 된다. Tick을 활성화 하는 코드는 다음과 같다.

    AMyActor::AMyActor()
    {
        // 이 액터가 Tick() 을 매 프레임 호출하도록 설정합니다. 필요치 않은 경우 이 옵션을 끄면 퍼포먼스가 향상됩니다.
    
        PrimaryActorTick.bCanEverTick = true;
    }

    프로퍼티가 에디터에 보이도록

    클래스 생성 이후 디자이너가 에디터에서 설정할 수 있는 프로퍼티를 만들어보자. 프로퍼티를 에디터에 노출시키려면 UPROPERTY 지정자를 사용한다.

    UCLASS()
    class AMyActor : public AActor
    {
        GENERATED_BODY()
    public:
    
        UPROPERTY(EditAnywhere)
        int32 TotalDamage;
    
        ...
    };

    TotalDamage 프로퍼티를 다른 프로퍼티와 같은 섹션에 나타내도록 하려면 카테고리 분류 기능을 사용할 수 있다.

    UPROPERTY(EditAnywhere, Category="Damage")
    int32 TotalDamage;

    블루프린트로 노출시키기 위해서 BlueprintReadWrite 지정자를 추가할 수 있다. 프로퍼티를 const 취급하려고 한다면 BlueprintReadOnly 지정자를 사용 할 수도 있다.

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Damage")
    int32 TotalDamage;

    여기에 디자이너 설정가능 프로퍼티와 보이긴 하지만 변경은 불가능한 프로퍼티를 하나 추가해보자

    UCLASS()
    class AMyActor : public AActor
    {
        GENERATED_BODY()
    public:
    
        UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Damage")
        int32 TotalDamage;
    
        UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Damage")
        float DamageTimeInSeconds;
    
        UPROPERTY(BlueprintReadOnly, VisibleAnywhere, Transient, Category="Damage")
        float DamagePerSecond;
    
        ...
    };

    DamageTimeInSeconds는 디자이너 변경가능 프로퍼티이며, DamagePerSecond 프로퍼티는 계산된 값이다. VisibleAnyWhere 지정자는 프로퍼티가 보이기는 하지만 언리얼 에디터에서 편집은 불가능 하도록 마킹한다. Trasient 지정자는 디스크에서 저장이나 로드되지 않는 뜻이다.

    생성자에서 기본값 설정

    생성자에서 프로퍼티의 기본값을 설정하는 것은 일반적인 C++클래스와 동일하다.

    AMyActor::AMyActor()
    {
        TotalDamage = 200;
        DamageTimeInSeconds = 1.0f;
    }
    
    AMyActor::AMyActor() :
        TotalDamage(200),
        DamageTimeInSeconds(1.0f)
    {
    }
    • 프로퍼티에 기본값을 지정하지 않으면 엔진에서 자동으로 0이나 nullptr로 설정한다.

    PostInitProperties() 함수로 인스턴스 데이터를 기반으로 기본값을 만들 수 있다.

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

    핫 리로드

    에디터를 닫지 않고도 C++ 변경 내용을 컴파일 할수 있는 기능이다.

    1. 에디터를 열어두고 빌드하여 에디터가 새로 컴파일된 DLL을 감지하여 변경한 내용을 리로드 한다.
      (디버거가 붙어있다면 떼어줘야한다)
    2. 에디터 메인 툴바의 컴파일 버튼을 클릭한다.

    블루프린트를 통한 C++ 클래스 확장

    위에서 작업한 MyActor를 생성하여 TotalDamage와 Time을 변경할 경우 PostInitProperties() 함수에 의해 DamagePerSecond가 여전히 기본값인 200으로 표시되고 있음을 확인 할 수 있다.
    PostEditChangeProperty() 메서드를 이용해 에디터에서 값이 변경된 이벤트를 받아서 처리할 수 있다.

    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

    C++ 및 블루프린트 경계를 넘는 함수 호출

    C++ 코드에서 함수에 UFUNCTION 매크로를 추가하여 리플렉션 시스템으로 함수노출을 처리하고, BlueprintCallable 옵션으로 블루프린트에 노출시킨다.

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

    반대로 블루프린트에서 구현된 함수를 나타내 주는 코드는 다음과 같이 할 수 있다.

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

    블루프린트와 C++ 에서 구현을 제공하려면 다음과 같이 할 수 있다. 코드에서는 _Implementation() 함수선언으로 코드 함수를 제공할 수 있다.

    UFUNCTION(BlueprintNativeEvent, Category="Damage")
    void CalledFromCpp();
    
    void AMyActor::CalledFromCpp_Implementation()
    {
        // 여기서 어떤 작업을 해라
    }

    심화 학습

    기본 구성 요소의 연관성과 언리얼 엔진에서 상속과 구성을 통해 커스텀 플레이 요소를 만드느 방법을 소개한다.

    게임플레이 클래스

    대표적인 파생클래스 유형은 크게 4가지 이다.

    • UObject
    • AActor
    • UActorComponent
    • UStruct

    언리얼 오브젝트 (UObject)

    언리얼 엔진의 기본 구성요소는 UObject 이다.

    • 프로퍼티와 메서드의 리플렉션(반영)
    • 프로퍼티의 시리얼라이제이션
    • 가비지 컬렉션
    • 이름으로 UObject 찾기
    • 프로퍼티에 환경 설정 가능 값
    • 프로퍼티와 메서드에 네트워킹 지원

    AActor

    액터는 게임플레이 경험의 일부로 사용될 UObject이다. 디자이너가 레벨에 배치하거나 게임플레이 시스템을 통해 런타임에 생성이 가능하다. 레벨에 배치할수 있는 오브젝트는 모두 AActor에서 확장된다. AActor는 게임플레이 코드를 통해 명시적으로 소멸시킬 수 있고, 소유중인 레벨이 메모리에서 언로드 될때 표준 가비지 컬렉션 메커니즘을 통해 소멸시킬수 있다. AActor는 게임 오브젝트가 하이레벨에서 작동되는 방식을 담당한다.

    액터는 별도의 작동방식이 있는데, 액터 컴포넌트 계층구조에 대한 컨테이너 역할을 한다. 액터를 레벨에 배치하려면 반드시 하나의 씬 컴포넌트가 필요하다. 액터는 해당 컴포넌트의 트랜스폼 정보를 얻는다.

    액터는 수명도중 호출할 수 있는 이벤트 항목이 있다.

    • BeginPlay : 게임플레이 도중 오브젝트가 처음 등장했을 때 호출된다.
    • Tick : 지속적으로 이루어지는 작업을 하기 위해 프레임 당 한번 호출
    • EndPlay : 오브젝트가 게임플레이 공간을 떠날 때 호출된다.

    런타임 수명

    액터의 스폰을 전담하는 함수는 UWorld의 멤버인 SpawnActor 함수이다. 액터 스폰에 성공하면 BeginPlay 함수가 호출되고 다음프레임에 Tick 함수가 잇따른다. 그리고 원하는 타이밍에 Destroy를 호출하여 소멸시킬 수 있다. 해당 프로세스에서 EndPlay가 호출되고 여기서 액터가 가비지 컬렉션으로 보내기전에 로직을 수행할 수 있다. Lifespan 멤버를 활영하여 설정한 시간 이후에 자동으로 Destroy 호출되게 할 수도 있다.

    UActorComponent

    액터 컴포넌트에는 개별의 작동 방식이 포함되어있어 AActor의 여러 유형에 공유되는 기능을 담당한다. 컴포넌트는 다른 컴포넌트에 붙일 수도 있고, 액터의 루트 컴포넌트가 될 수도 있다.

    • RootComponent : 액터의 컴포넌트 트리 내 최상위 컴포넌트를 담는 AActor 멤버
    • Ticking - 소유한 액터의 Tick 함수 일부분으로 실행되는 컴포넌트

    UStruct

    구조체를 사용하기위해서는 특정 클래스를 확장하지 않고 그냥 구조체에 USTRUCT() 마킹을 추가해 주면 빌드툴이 바탕 작업을 해준다. UStruct는 가비지 컬렉션 대상이 아니다. 그래서 수명을 직접 관리해줘야 한다. UStruct는 언리얼 에디터 내 편집, 블루프린트 조작, 시리얼라이제이션, 네트워크 등 UObject 리플렉션이 지원되는 일반적인 이전 데이터 유형들을 모아놓는 용도로 사용된다.

    더 심화 학습

    언리얼 리플렉션 시스템

    UE4는 별도의 리플렉션 구현을 통해 가비지 컬렉션, 시리얼라이제이션, 네트워크 리플리케이션, 블루프린트/C++ 통신과 같은 동적인 기능을 활용한다.

    • UCLASS() 언리얼에게 클래스의 리플렉션 데이터를 생성하라고 한다. UObject의 파생클래스여야 한다
    • USTRUCT() 언리얼에게 구조체의 리플레션 데이터를 생성하라고 한다.
    • GENERATED_BODY() 언리얼에서는 해당 유형에 대해 생성되는 전체 필수 표준(boilerplate)코드로 대체
    • UPROPERTY UCLASS 또는 USTRUCT의 멤버 변수를 UPROPERTY로 사용할 수 있도록 한다. 변수의 리플레케이트, 시리얼라이즈, 블루프린트 접근 여부등을 지정한다. UObject로의 레퍼런스가 몇개나 되는지 가비지 컬렉터가 추적하는데도 사용된다.
    • UFUCTION() UCLASS또는 USTRUCT의 클래스 메서드를 UFUNCTIOPN으로 사용. 플로프린트에서 클래스 메서드를 호출할 수 있도록, RPC(Remote Procedure Call)로 사용할 수 있도록 해준다.
    //UCLASS 선언 예제
    #include "MyObject.generated.h"
    
    UCLASS(Blueprintable)
    class UMyObject : public UObject
    {
        GENERATED_BODY()
    
    public:
        MyUObject();
    
        UPROPERTY(BlueprintReadOnly, EditAnywhere)
        float ExampleProperty;
    
        UFUNCTION(BlueprintCallable)
        void ExampleFunction();
    };

    오브젝트/액터 이터레이터

    오브젝트 이터레이터(반복처리기)는 특정 UOBject유형과 그 서브클래스의 모든 인스턴스를 대상으로 반복 처리할 때 매우 유용하게 사용되는 툴이다.

    // 현재 UObject 인스턴스를 전부 찾습니다.
    for (TObjectIterator<UObject> It; It; ++It)
    {
        UObject* CurrentObject = *It;
        UE_LOG(LogTemp, Log, TEXT("Found UObject named: %s"), *CurrentObject->GetName());
    }

    이터레이터에 구체적인 타입을 넣어서 검색범위를 제한할 수 있다.

    for (TObjectIterator<UMyClass> It; It; ++It)
    {
        // ...
    }

    액터이터레이터를 생성할 때는, UWorld 인스턴스로의 포인터를 주어야 한다. 액터 반복자인 TActorIterator를 사용한다.

    APlayerController* MyPC = GetMyPlayerControllerFromSomewhere();
    UWorld* World = MyPC->GetWorld();
    
    // 오브젝트 이터레이터처럼, 구체적인 클래스를 제공하여 해당 클래스 또는 그 
    // 파생 클래스의 오브젝트만 구할 수 있습니다.
    for (TActorIterator<AEnemy> It(World); It; ++It)
    {
        // ...
    }

    메모리 관리 및 가비지 컬렉션

    UObject 및 가비지 컬렉션

    UE4에서는 가비지 컬렉션 시스템 구현을 위해 리플렉션 시스템을 사용한다. 가비지 컬렉션 덕에 UObject 삭제를 수동 관리할 필요 없고, 레퍼런스만 유지해 주면 된다. UObject의 파생 클래스여야만 가비지 컬렉션이 활성화 된다.

    UCLASS()
    class MyGCType : public UObject
    {
        GENERATED_BODY()
    };

    가비지 컬렉터에는 루트 세트라는 개념이 있다. 루트 세트는 기본적으로 컬렉터가 하는 오브젝트 중 절대 가비지 컬렉션 대상이 되지 않는 오브젝트 목록이다. 루트 세트의 한 오브젝트에서 해당 오브젝트로의 참조가 있으면, 그 오브젝트는 가비지 컬렉션 대상이 되지 않는다. 반대로 참조한 오브젝트가 없으면 unreachable(도달불가능)이라 하며, 다음 가비지 컬렉터에서 삭제된다.

    레퍼런스(참조)는 UPROPERTY에 저장된 UObject 포인터를 뜻한다.

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

    위함수의 DoomedObject는 새로운 UObject를 생성하지만 그 포인터를 UPROPERTY에 저장하지 않기때문에 가비지 컬렉터에서 소멸 시킨다.

    액터 및 가비지 컬렉션

    액터는 보통 가비지 컬렉팅 되지 않고, 스폰 이후 Destroy를 호출하면 레벨을 끝내지 않고도 레벨에서 제거할 수 있다. 다음 가비지 컬렉션 단계에서 지워진다.

    UCLASS()
    class AMyActor : public AActor
    {
        GENERATED_BODY()
    
    public:
        UPROPERTY()
        MyGCType* SafeObject;
    
        MyGCType* DoomedObject;
    
        AMyActor(const FObjectInitializer& ObjectInitializer)
            : Super(ObjectInitializer)
        {
            SafeObject = NewObject<MyGCType>();
            DoomedObject = NewObject<MyGCType>();
        }
    };
    
    void SpawnMyActor(UWorld* World, FVector Location, FRotator Rotation)
    {
        World->SpawnActor<AMyActor>(Location, Rotation);
    }

    UObject가 삭제 대기중인지는 IsPendingKill 함수를 사용하여 검사한다.

    UStructs

    USTructs 구조체는 UObject의 경량 버전이다. 가비지 컬렉팅이 불가능하다. UStruct 의 다이내믹 인스턴스를 반드시 사용해야한다면, 스마트 포인터를 사용하도록 하자.

    UObject 이외의 레퍼런스

    UObject 이외의 것도 오브젝트로의 레퍼런스를 추가하고 가비지 컬렉션을 막을 수 있다. 반드시 FGCObject에서 바생되어 AddReferencedObjects 함수를 오버라이드 해야한다.

    class FMyNormalClass : public FGCObject
    {
    public:
        UObject* SafeObject;
    
        FMyNormalClass(UObject* Object)
            : SafeObject(Object)
        {
        }
    
        void AddReferencedObjects(FReferenceCollector& Collector) override
        {
            Collector.AddReferencedObject(SafeObject);
        }
    };

    FReferenceCollector를 사용하여 레퍼런스를 수동으로 추가해준다. 해당 오브젝트가 삭제되어 소멸자가 실행될 때, 그 오브젝트는 추가한 모든 레퍼런스를 자동으로 지운다.

    클래스 이름 접두사

    • Actor 클래스 접두사는 A이다. (AController)
    • Object 클래스 접두사는 U이다. (UComponent)
    • Enum 접두사는 E이다. (ECharType)
    • Interface 클래스 접두사는 I이다. (ISystemInterface)
    • Template 클래스 접두사는 T이다. (TArray)
    • Swidget 클래스 접두사는 S이다. (SButton)
    • 그 외의 접두사는 F이다. (FVector)

    숫자 유형

    short, int, long 과 같은 기본 유형도 플랫폼마다 크기가 각기 다를수 있기에 다음과 같이 제공한다.

    • int8/uint8 : 8비트 정수
    • int16/uint16 : 16비트 정수
    • int32/uint32 : 32비트 정수
    • int64/uint64 : 64비트 정수
    • float : 32비트 부동 소수점
    • double : 64비트 부동 소수점

    문자열

    FString

    가변 문자열로 std::string과 유사하다. FString 생성은 TEXT()매크로로 사용한다.

    FText

    FText는 Fstring과 유사하나 현지화 텍스트용이다. FText생성을 위해서는 NSLOCTEXT 매크로를 사용한다.

    FName

    자주 반복되는 문자열을 식별자로 저장하여 비교할 때 메모리와 CPU시간을 절약하는데 사용한다.

    TCHAR

    플랫폼마다 다를수 있는 캐릭터 셋과 무관하게 캐릭터를 저장하기 위한 방편이다. UE4의 문자열은 TCHAR배열을 사용하여 UTF-16 인코딩 데이터를 저장한다.

    반응형

    댓글

    Designed by JB FACTORY