게임 개발자를 향해

함수와 UFUNCTION() 메크로 본문

언리얼 엔진/C++

함수와 UFUNCTION() 메크로

뿌단이 2022. 8. 24. 16:26

 

 저번 글에 이어서 C++ 파일에 함수를 만들어 볼 것이다. 데미지라는 변수가 있다면 그 데미지의  DPS(Damage Per Second)를 구하는 간단한 함수를 짜 볼 것이다.

 

 

...

public:
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Damage")
    int32 TotalDamage;
    
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Damage")
    float DamageTimeInSeconds;
    
    UPROPERTY(BlueprintReadOnly, VisibleAnywhere, Transient, Category = "Damage")
    float DamagePerSecond;
    
...

(코드로 설명하여 스크립트 에디터를 제대로 보여주지 못하지만 코드부분을 보시면 어디에 선언 해야할지 보이실 겁니다.)

 

 먼저 헤더파일에 위 코드처럼 변수를 선언해준다. 

 

 

...

protected:
	virtual void BeginPlay() override;

public:	
	virtual void Tick(float DeltaTime) override;

	void CalculateDPS();
    
...

 

헤더파일에 위 코드 같이 .CalculateDPS(); (DPS 계산 함수) 를 선언한다.

 

 

...

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

...

 cpp 파일에 함수를 선언해준다. (연산 되게 간단하죠?)

 

 그런데 이함 수가 게임 실행 중 특정한 시점에서 자동으로 실행되게 해야한다. 맨 처음 우리는 부모클래스를 선택한 적이 있다. 그리고 이 부모클래스는는 AActer클래스이며, 이 AActer 클래스에 액터의 기능을 만들기 쉽게 여러 함수들이 존재한다. 우리는 아래 함수들을 부모 클래스에게 상속으로 받아 올 것이다.

 

PostInitProperties() : 오브젝트가 초기활 될 때 호출되는 함수

 

PostEditChangeProperty() :  property가 수정될 때 호출되는 함수

 

 

 

...

public:	
	virtual void Tick(float DeltaTime) override;

	void CalculateDPS();

	virtual void PostInitProperties() override;

	virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override;
    
...

 

헤더파일로 가서 위와 같이 클래스에 상속받을 함수들을 선언해준다.

virtual 키워드와 재정의 override 키워드를 사용해 기존함수의 기능에 새 기능을 덮어 쓸 것이다.

 

 이제 cpp함수에 가서 정의를 해도 되지만 Visual Studio의 스크립트 에디터에서 직접 함수를 만들어주는 기능이 있다.

아마 위처럼 함수를 선언하면 초록색 밑줄이 그어질 것인데 그 이유는 선언한 함수가 아무 기능이 없기 때문이다. 이때 선언한 함수에 마우스를 갖다대면 드라이버 모양이 나오는데 이걸 클릭하거나 혹은 함수를 누르고 Ctrl + . 을 눌러주면 정의 만들기라는 선택항목이 뜰 것이다. 이를 눌러주면 아래와 같이 창이 뜬다.

 

 

이곳에서 함수 구현을 해주면 cpp 파일에 알아서 함수 구현이 되는 편리한 기능이다.

 

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

이제 빈 함수에 위 코드를 넣어준다.

 아까 설명했듯이 AMyActoroverride로 선언한 PostInitProperties() 함수는 부모클래스인 AActor PostInitProperties()를 덮어씌워 만든 함수이다. 여기서 함수 구현 맨 윗줄의 Super는 Unreal C++에서 상속받은 부모 클래스에 있는 원본 property나 함수를 가져오는데 사용되는 키워드로 AMyActor에서 만든 PostInitProperties() 에서 부모 클래스의 PostInitProperties() 함수를 다시 호출해주지 않으면 실제로 처리하는 작업이 실행되지 않을 수 있어 Super 키워드를 이용해서 부모클래스의 원본함수를 실행시켜주는것이 좋다.

 

이후 오브젝트를 초기화할 때 CalculateDPS() 함수를 실행해주면 DamagePerSecond 변수의 값이 바뀔 것이다.

 

void AMyActor::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
{
	CalculateDPS();
	Super::PostEditChangeProperty(PropertyChangedEvent);
}

 

  PostEditChangeProperty() 함수도 위와같이 작성해준다. 이는 CalculateDPS() 함수를 먼저 실행되게 먼저 선언해준다.

(property가 바뀔 때 호출되는 함수니까.. 으음..이유는 아무리 생각해도 모르겠다.. )

 

<&nbsp; PostEditChangeProperty()의 기능 확인>

 이제 컴파일 후  디테일 패널에서 DamageTimeInSeconds변수의 값을 2로 초기화를 해보자.

오... DamagePerSecond 변수의 값이 초기화되었다.. 

 

 하나 더 나아가서 이 함수를 블루프린트 클래스에서 호출할 수 있도록 만들어보겠다.

...

    UFUNCTION(BlueprintCallable, Category = "Damage")
    void CalculateDPS();
    
...

 

 스크립트 에디터(Visual Studio)로 돌아가서 헤더파일에 함수 위에 UFUNCTION()을 선언해준다. 이전 게시글에서 property를 언리얼 에디터에서 볼 수 있게 하기 위해 UPROPERTY() 메크로를 사용했다. 이번에는 함수를 블루프린트 에디터에서 사용할 수 있게 하기위해서 UFUNCTION() 라는 메크로를 사용해 줄 것이다.

매개변수(파라메터)값을 해석해보겠다.

 

BlueprintCallable : 이 지정자를 넣어줌으로서 해당함수를 블루프린트에서 사용할 수 있게 해준다.

 

Category = " " :  블루프린트에서 사용하는 모든 함수들은 카테고리를 할당해주어야 블루프린트에서 정상적으로 동작한다.

 

 자 이제 컴파일을 하고 콘텐츠 브라우저에 우리가 만든 C++ 파일인 MyActor를 우클릭 하고 "MyActor 기반 블루프린트 생성" 을 눌러준다.

 

 

블루프린트 파일 이름은 BP_MyActor 라고 지정하겠다. 이후 블루프린트창이 뜨면 이벤트그래프 탭을 보겠다.

 

 

지금 보고있는것은 AActor를 부모로 상속받은 C++ 클래스인 MyActor에 정의되어있는 함수들이다.

 

 

 이제 빈 공간을 우클릭하고 데미지 카테고리를 찾아보자. 그럼 그 카테고리 안에 우리가 만들었던 CalulateDPS() 함수가 떡하니 있을 것이다.  CalulateDPS() 함수를 클릭하여 노드로 만들어준다. 그리고 한번더 빈 공간을 우클릭 해서 Set Total Damage를 검색해서 노드화 시켜준다.

 

 

 

이후 Set Total Damage 500이란 값을 넣고 컴파일 해준다.

 

 이후 BP_MyActor 파일을 뷰포트에 드래그해주고 실행시켜준다. 그리고 월드 아웃라이너 창에 있는 BP_MyActor 파일을 클릭해 디테일 패널을 확인하면 Total Damage의 값이 500으로 바뀌는 것을 볼 수 있다. 이렇게  프로그래머가 C++에서 작성한 기능을 디자이너들이 블루프린트에서 사용할 수 있게 해줄 수 있다. 그리고 반대로 블루프린트에서 작성한 기능을 C++에서도 사용할 수 있다.

 

지금부터 아래 과정으로 프로그래머가 블루프린트 기능을 사용하는 간단한 예제를 만들어보겠다.

  1. C++ 파일에 기능없는 함수를 만든다.
  2. 블루프린트를 이용해서 해당 함수의 기능을 만든다.
  3. C++로 블루프린트로 만든 기능을 사용할 수 있다.

자 이제 스크립트 에디터로 돌아가보자.

 

public:
...

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Damage")
    FString str;
    
...

헤더파일에 str 변수를 선언해주고 

 

 

AMyActor::AMyActor() : TotalDamage(200), DamageTimeInSeconds(1.0f), str(TEXT("Test"))
{

	PrimaryActorTick.bCanEverTick = true;
	
}

생성자 함수에 TEXT("Test") 로 초기화를 해준다.

 

 

public:
...

    UFUNCTION(BlueprintImplementableEvent, Category = "Damage")
    void CallFromCpp();
    
...

헤더파일에  CallFromCpp() 함수를 선언해준다. 이 함수에 블루프린트로 기능을 넣을 것인데 일단, 우리가 원하는 기능을 만들기 위해서 이 함수 위에 UFUNCTION() 메크로를 추가하고 매개변수를 넣어주었다.

 

매개변수를 해석해보면

 

BlueprintImplementableEvent : 블루프린트로 구현한 기능이 없다면 빈 함수를 호출하게 된다.

이는 함수의 기능을 디자이너가 만들어야하기 때문에 C++ 파일에 함수의 본체가 필요없다.

 

 

일단 BlueprintImplementableEvent를 사용해서 위 코드를 제작하고 블루프린트 창에서 기능을 구현해보겠다.

 

 

CallFromCpp 이벤트set str, get str, Append 함수를 검색해서 노드로 변경 후 위 사진처럼 잇는다.

Append는 str변수에 문자열을 붙이는 함수이며 B에 "_Blueprint"를 작성해주겠다. 

 

이후 BP_MyActor를 뷰포트에 드래그해주고 플레이 해보면

 

컴파일 후 플레이하면 Damage 카테고리에 Str의 값이 Test 에서 Test_Blueprint 라고 바뀐 것을 확인할 수 있다.

 

하지만 이보다 더 좋은 기능이 있다. 여기서 CallFromCpp() 함수의 UFUNCTION() 메크로의 매개변수인  BlueprintImplementableEvent를 아래의 매개변수로 바꾸어주겠다.

 

BlueprintNativeEvent : 블루프린트로 구현한 기능이 없다면 C++에서 구현된 기능을 실행하게 해준다.

 

public:
...

    UFUNCTION(BlueprintNativeEvent, Category = "Damage")
    void CallFromCpp();
    virtual void CallFromCpp_Implementation();
...

 

 이를 만들기 위해선 위 코드처럼 UFUNCTION() 의 매개변수를 BlueprintNativeEvent로 바꿔준다. 이때 바꿔주기만 해서는 안된다.

 

 만약 블루프린트에 구현된 기능이 없을 시 실행해야하는 기능이 필요한데, 이는 CallFromCpp() 함수에 구현하면 안되고,선언한 함수의 가상함수를 선언해주어 해당 가상함수에 실행할 기능을 구현해주어야 한다.

 

void AMyActor::CallFromCpp_Implementation()
{
	str.Append(TEXT("_implimentation"));
}

그리고 구현한 가상함수에도 Append함수를 사용하여 블루프린트로 기능구현을 하지 않았을 경우 "_implimentation" 이 str 문자열변수값 "Test" 뒤에 붙을 것이다.

 

 

지금 당장은 CallFromCpp가 기능구현이 되어있어 Test_Blueprint로 나올 것이다.

 

블루프린트 창에 있는 CallFromCpp 이벤트 노드를 제거해주고 컴파일플레이해주면 Str의 문자열이 "Test_inlplimentation"으로 바껴있는 것을 볼 수 있다.

 

여기서 프로그래머가 만든 함수기능과 디자이너가만든 함수기능을 전부 실행시킬 수도 있다.

 

 CallFromCpp 이벤트를 다시 노드화 시키고, 해당 이벤트를 우클릭해서 부모함수로 호출 추가를 눌러주면 부모 이벤트가 만들어지는데 이게 바로 우리가 만들었던 가상함수인 CallFromCpp_Implimentation() 함수인 것이다. 

 

 위 사진처럼 부모 함수를 먼저 실행시킨후에 블루프린트의 기능을 실행시키면  디테일 패널에 Str 변수값이 "Test_implimentation_Blueprint"로 되어있는 것을 볼 수 있다.

 

 순서를 바꿀 수도 있다. 블루프린트 기능을 먼저 실행하고 부모 이벤트를 실행하면 "Test_Blueprint_implimentation"로 바뀐다.

 

 

긴 글 읽어주셔서 감사합니다.. 

언젠가 나만의 게임을 만들 수 있는 그날까지..

 

 

<참고자료>

-베르의 게임개발 유튜브

https://www.youtube.com/watch?v=afl0uIfsS7o