제대로 따라가기 (5) C++ 프로그래밍 튜토리얼 :: UMG와 유저 인터페이스

 


작성버전 :: 4.21.0

언리얼 엔진 튜토리얼인 UMG와 유저 인터페이스에서는 언리얼 모션 그래픽(UMG)을 사용해서 여러가지 화면과 버튼이 있는 기본적인 메뉴 시스템을 만드는 법을 배울 수 있다.

 

튜토리얼대로 하면 문제가 발생해서 제대로 따라갈 수 없는 부분으로 동작이 가능하게 수정해야하는 부분은 빨간 블럭으로 표시되어 있다.


이번 튜토리얼에서 새로 배우게 되는 내용은 글 제일 끝에 "이번 섹션에서 배운 것"에 정리된다.

 

 

1. UMG용 모듈 종속성 구성(문서)

 

C++탭에서 기본 코드를 선택하고 "HowTo_UMG"라는 이름으로 프로젝트를 새로 생성한다.

 

 

언리얼 모션 그래픽(UMG)을 사용하는 코드를 작성할 것이기 때문에 비주얼 스튜디오(Visual Studio)에 들어가서 기본으로 포함되어 있지 않은 몇 가지 모듈을 포함시키도록 하자.

 

언리얼 에디터의 파일 드롭다운 메뉴에서 Visual Studio 열기를 선택해서 비주얼 스튜디오를 연다.

 

 

UMG는 몇 가지 모듈에 종속되어 있는데, 그 모듈을 "HowTo_UMG.Build.cs"에 추가해야 한다.

 

 

"HowTo_UMG.Build.CS"에서 포함된 퍼블릭 모듈 목록에 "UMG"를, 포함된 프라이빗 모듈 목록에 "Slate" 와 "SlateCore" 를 추가해야 한다.

 

PublicDependencyModuleNames에 UMG를 다음과 같이 추가한다.

 

PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "UMG" });

 

비어있는 PrivateDependencyModuleNams를 주석 처리하고 "Slate"와 "SlateCore"가 있는 부분을 주석 해제 한다.

 

//PrivateDependencyModuleNames.AddRange(new string[] {  });

// Uncomment if you are using Slate UI
PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });

 

UMG 구성이 완료되었다면, 프로젝트의 커스텀 게임 모드에 코드를 추가하여 게임 메뉴를 만들고 표시할 수 있다.

 

 

2. 게임 모드 확장(문서)

 

게임이 시작되면 유저 위젯(User Widget)을 새로 만들어 표시하거나, 나중에 제거할 수 있도록 하기 위해서 Game Mode(게임 모드) 클래스에 함수와 프로퍼티를 추가하자. 각 프로젝트에는 커스텀 게임 모드 클래스가 딸려오므로, HowTo_UMGGameMode.h에 다음 코드를 추가하면 된다.

 

버전이 바뀌면서 GameMode 클래스 파일의 이름이 "ProjectNameGameMode.h", "ProjectNameGameMode.cpp"에서 "ProjectNameGameModeBase.h", "ProjectNameGameModeBase.cpp"로 바뀌었다. 비주얼 스튜디오의 솔루션 탐색기에서 HowTo_UMGGameModeBase.h를 열어서 작업하자.

 

public:
    UFUNCTION(BlueprintCallable, Category = "UMG_Game")
    void ChangeMenuWidget(TSubclassOf NewWidgetClass);

protected:
    virtual void BeginPlay() override;

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UMG_Game")
    TSubclassOf StartingWidgetClass;

    UPROPERTY()
    UUserWidget* CurrentWidget;

 

TSubclassOf 클래스를 사용할 때, [클래스 템플릿 "TSubclassOf"에 대한 인수 목록이 없다]는 에러가 발생한다. 전체 코드를 보고 유추해보건데, BeginPlay() 함수에서 StartingWidgetClass의 내용물을 CurrentWidget 변수에 넣어주거나 ChangeMenuWidget() 함수가 동작할 때, 매개변수로 받은 NewWidgetClass를 CurrentWidget에 대입해주는 방식으로 동작할 것임을 알 수 있다. 그렇기 때문에 여기에서는 TSubclassOf의 템플릿 인수로 UUserWidget 타입을 넣어주는 것이 올바른 해결책일 것이다.

 

다음과 같이 코드를 수정해주자.

 

public:
    UFUNCTION(BlueprintCallable, Category = "UMG_Game")
    void ChangeMenuWidget(TSubclassOf<UUserWidget> NewWidgetClass);

protected:
    virtual void BeginPlay() override;

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UMG_Game")
    TSubclassOf<UUserWidget> StartingWidgetClass;

 

이제 HowTo_UMGGameModeBase.cpp로 가서, 선언한 두 함수의 구현을 해야 한다. 우선 BeginPlay()를 덮어쓰는 것으로 시작하자.

 

void AHowTo_UMGGameModeBase::BeginPlay()
{
    Super::BeginPlay();
    ChangeMenuWidget(StartingWidgetClass);
}

 

부모 클래스의 함수를 덮어쓸 때는, 여기 BeginPlay에서 하듯이 해당 함수의 부모 클래스 버전을 호줄하는 것이 중요한 경우가 많다. 우리가 구현하는 함수의 버전은 기존의 절차의 끝 부분에 한 단계를 추가하기 위한 것이므로, 함수 첫 줄에 Super::BeginPlay()를 호출한다.

 

그리고 여기서는 UUserWidget을 사용하기 위해서, HowTo_UMGGameModeBase.cpp 상단에 "Blueprint/UserWidget.h"를 포함시켜 주어야 한다.

 

#include "Blueprint/UserWidget.h"

 

계속 해서, 메뉴 간의 전환 방식을 구현해야 한다. 뷰포트에 활성화된 유저 위젯이 있다면 제거하고 난 다음에 유저 위젯을 새로 만들어 뷰포트에 추가해주도록 구현한다.

 

void AHowTo_UMGGameModeBase::ChangeMenuWidget(TSubclassOf NewWidgetClass)
{
    if (CurrentWidget != nullptr)
    {
        CurrentWidget->RemoveFromViewport();
        CurrentWidget = nullptr;
    }

    if (NewWidgetClass != nullptr)
    {
        CurrentWidget = CreateWidget(GetWorld(), NewWidgetClass);
        if (CurrentWidget != nullptr)
        {
            CurrentWidget->AddToViewport();
        }
    }
}

 

함수 구현할 때도, 변경된 선언에 맞춰서 매개변수 타입을 아래와 같이 수정하자.

 

void AHowTo_UMGGameModeBase::ChangeMenuWidget(TSubclassOf<UUserWidget> NewWidgetClass)

 

이 코드는 제공된 위젯 인스턴스를 만들어 화면에 넣는다. 언리얼 엔진은 한 번에 다수의 위젯을 표시하고 상호작용처리가 가능하며, 한 번에 하나만 활성화 되도록 제거를 할 수도 있다. 하지만 위젯을 직접 소멸시킬 필요는 없는데, 뷰포트에서의 제거 밑 레퍼런싱하는 모든 변수 소거(또는 변경) 작업은 언리얼 엔진의 가비지 컬렉션 시스템에서 해주기 때문이다.

 

마지막으로 Player Controller 클래스에 입력 모드를 설정해야 한다. 그러기 위해서 Player Controller를 기반으로 새로운 C++ 클래스를 추가하자. 이 클래스 안해서 게임이 시작될 때 함수 하나를 추가로 호출해주기만 하면 UI 요소와 상호작용이 가능하도록 할 수 있다.

 

 

 

HowTo_UMGPlayerController.h에서 클래스에 다음 오버라이드를 추가하고 .cpp에 구현한다.

 

public:
    virtual void BeginPlay() override;

 

void AHowTo_UMGPlayerController::BeginPlay()
{
    Super::BeginPlay();
    SetInputMode(FInputModeGameAndUI());
}

 

메뉴 생성 및 표시와 필요없어진 메뉴를 제거하기 위한 코드 프레임워크를 완성했다. 이제 언리얼 에디터로 돌아가 메뉴 에셋을 디자인 해보자.

 

 

 

 

 

3. 메뉴 위젯 블루프린트 생성(문서)

 

언리얼 에디터에서 컴파일 버튼을 누르면 수정된 코드가 빌드된다. 이를 통해서 유저 위젯을 메뉴로 사용할수 있게 된다.

 

 

이제 게임 모드가 메뉴로 사용할 유저 위젯을 생성해보자. 콘텐츠 브라우저의 "신규 추가" 버튼을 누르고 유저 인터페이스 카테고리에서 위젯 블루프린트(Widget Blueprint) 클래스를 선택해서, "MainMenu"와 "NewGameMenu"라는 이름으로 두 개의 유저 위젯을 만든다.

 

 

방금 만든 "MainMenu" 위젯을 더블클릭하면 블루프린트 디자이너 창이 열리며, 여기서 메뉴 레이아웃을 만들 수 있다.

 

팔레트 패널의 일반 섹션에서 버튼(Button)과 텍스트(Text)를 끌어 그래프에 배치한다. 이 버튼은 새 게임 메뉴를 여는데 사용될 것이다.

 

 

버튼의 위치와 크기를 다음과 같이 수정하고, 함수성 연결을 해줄 때 알아보기 쉽게 하기 위해서 이름을 "NewGameButton"으로 변경한다.

 

 

그리고 이 버튼이 무슨 버튼인지 보여주기 위해서 텍스트 블록(Text Block)을 버튼 위로 끌어다 놓고 디테일을 다음과 같이 수정한다.

 

- Text를 "New Game"으로 변경

 

- Visibility를 Hit Test Visibility로 변경한다. 그러면 버튼을 누르려는 클릭을 텍스트 블록이 막지 않는다.

 

- 이름을 "NewGameText"로 변경한다. 필수는 아니지만 나중에 계층구조에서 어떤 UI인지 찾기 쉬워지기 때문에 들여두면 좋은 습관이 된다.

 

 

 

두 번째 버튼과 텍스트 블록을 만들어서 "Quit"(종료) 기능을 만든다. 버튼 이름은 "QuitButton", 버튼 위치는 (600, 100), 텍스트 블록 이름은 "QuitText"로 설정한다.

 

그 다음은, 버튼을 클릭했을 때, 코드가 실행되도록 버튼에 이벤트를 추가하는 작업을 해야한다. 디테일 패널에서 적합한 이벤트의 이름 옆에 "+"버튼을 찾아서 누르면 되는데 이 경우에는 "OnClicked" 이벤트를 추가하면 된다.

 

 

NewGameButton의 OnClicked 이벤트를 다음과 같이 구성한다.

 

 

QuitButton의 OnClicked 이벤트를 다음과 같이 구성한다.

 

 

 

메인 메뉴를 만들었으니, 레벨이 시작되면 메인 메뉴를 로드하는 게임 모드 애셋을 구성하면 된다.

 

 

4. 게임 모드 환경설정(문서)

 

콘텐츠 브라우저에서 프로젝트의 게임 모드에 맞는 블루프린트 클래스를 두 개 추가할 것이다. 그러면 그 두 클래스에 노출된 변수를 원하는 값으로 설정하는 것이 가능하다.

 

콘텐츠 브라우저에서 신규 추가버튼에서 블루프린트 클래스를 클릭한다.

 

 

부모 클래스로 HowTo_UMGGameModeBase를 선택해서 "MenuGameMode" 블루프린트 클래스를 만든다.

 

 

그리고 게임 내에서 마우스 커서를 보이게 하기 위해서, 플레이어 컨트롤러의 블루프린트 클래스도 만들어 주어야 한다. 콘텐츠 브라우저에서 블루프린트 클래스를 클릭하고 Player Controller 클래스 상속받아서 "MenuPlayerController"라는 이름으로 클래스를 생성하자.

 

"MenuPlayerController" 클래스가 생성되었으면, 콘텐츠 브라우저에서 블루프린트 파일을 더블클릭해서 블루프린트 에디터를 연다. 그리고 디테일 창에서 "Show Mouse Cursor" 박스를 체크한다.

 

 

다음은 "MenuGameMode"를 편집한다.

 

Starting Widget Class를 "MainMenu" 애셋으로 설정해서 게임 시작시 메뉴가 뜨도록 만든다.

 

Default Pawn Class를 Default Pawn이 아닌 Pawn으로 설정해서 플레이어가 메뉴에 있을 때, 이리저리 날아다니지 않도록 만든다.

 

Player Controller Class를 방금 만든 "MenuPlayerController" 애셋으로 설정해서 메인 메뉴에서 마우스 커서가 표시되도록 만든다.

 

 

우리가 만든 게임 모드 블루프린트를 사용하려면, 레벨 에디터 창으로 돌아와 세팅 버튼을 통해 현재 레벨에 대한 월드 세팅을 변경해야 한다.

 

 

프로젝트 세팅 메뉴의 맵 & 모드에서도 기본 게임 모드 설정이 가능하다. 이 방법을 사용하면 따로 덮어쓰지 않는 한 모든 레벨에서 기본 게임 모드로 설정된다. 어느 방법을 사용할지는 프로젝트 구성에 따라 달라질 수 있다.

 

월드 세팅 패널에서 Game Mode Override 항목을 "MenuGameMode" 애셋으로 설정한다.

 

 

이제 레벨에 메인 메뉴를 로드하고, 마우스 커서를 표시하는 플레이어 컨트롤러를 사용하도록 환경설정된 커스텀 게임 모드 애셋이 적용되었다. 이제 게임을 실행하면 Quit 버튼은 정상적으로 작동하지만, 아직 New Game 버튼은 빈 메뉴 화면으로 이동한다. 다음 단계에서는 New Game Menu를 구성해주자.

 

 

 

 

 

5. 2차 메뉴 제작(문서)

 

콘텐츠 브라우저에서 아까 만든 "NewGameMenu" 애셋을 연다. 이 메뉴는 이름을 입력할 수 있는 텍스트 박스와, 이름을 입력하기 전에는 누를 수 없는 '게임 플레이' 버튼, 메인 메뉴로 돌아가는 버튼으로 구성된다.

 

이름 입력 박스를 만들기 위해, 레이아웃에 Text Box(텍스트 박스)를 배치한다.

 

 

텍스트 박스의 설정은 다음과 같다.

 

 

이전 메뉴에서 버튼을 만들었던 것과 같은 방식으로 텍스트 블록 라벨이 있는 게임 플레이 버튼을 만든다.

 

버튼 : 이름은 PlayGameButton, 위치는 200, 300, 크기는 200, 100으로 변경한다.

 

텍스트 블록 : 이름은 PlayGameText, Visibility는 Hit Test Visible로, 내용은 Play Game으로 변경한 다음 PlayGameButton위에 배치한다.

 

게임 플레이 버튼의 경우, 만약 플레이어 이름 입력란이 비어있다면 작동하지 않도록 특수한 기능을 추가한다. UMG의 바인드 기능을 사용하여 (Behavior섹션 아래) "Is Enabled" 칸에 새로운 함수를 만들면 된다.

 

 

텍스트 박스가 공백이 아니어서 버튼이 활성화 될 수 있는 상태인지 확인하려면, 텍스트 박스에서의 텍스트를 스트링으로 변환한 다음 길이가 0보다 큰지 검사하면 된다.

 

 

이제 메인 메뉴로 돌아갈 수 있도록 버튼을 하나 추가해보자. 메인 메뉴에서 게임 플레이 버튼과 비슷하지만, 위치 기준이 좌상단이 아닌 우하단 구석이 될 것이다. 그러기 위해서는 디테일 패널에서 "앵커" 드롭다운을 클릭한 다음, 팝업 메뉴에서 우하단 부분을 나타내는 모양을 선택한다.

 

버튼 이름을 MainMenuButton으로 설정한다.

 

위치를 -400, -200으로 설정한다.

 

크기를 200x100으로 설정한다.

 

 

이제 NewGameMenu 위젯의 버튼들에도 OnClicked 이벤트들을 추가하자

 

 

메인 메뉴 버튼의 경우, 다시 메인 메뉴 위젯을 열어주지만, 게임 플레이 버튼은 누르면 메뉴를 비활성시킨 후, 게임에서 더 이상 아무것도 할 수 없게 만든다. 보통 이 시점에서 첫 레벨을 로드하고, 오프닝 동영상을 재상하거나 폰을 스폰시켜 빙의하는 등의 처리를 하게 된다.

 

모든 작업을 마치고 플레이 해보면 다음 스크린샷과 같은 장면을 얻을 수 있다.

 

 

 

 


 

이번 섹션에서 배운 것

 

1. 언리얼 모듈 종속성

언리얼 엔진의 기능은 다수의 모듈로 나누어져 있고, 그 중에 필요한 모듈을 묶어서 사용하는 방식이다. 이번 섹션 처음 부분에 build.cs파일에서 모듈 종속성을 구성할 때도 보았겠지만, 기본적으로 언리얼은 Core, CoreUObject, Engine, InputCore 모듈을 사용하고 있었고, UI와 관련된 기능을 사용하기 위해서 UMG 모듈과 Slate, SlateCore 모듈을 구성에 추가해주었다.

 

추후의 일이지만, 언리얼 엔진을 커스터마이징하고자 할 때, 새롭게 추가하는 기능을 이러한 모듈로 만들어 덧붙이게 될 것이다.

 

2. TSubClassOf<T>

UClass 타입 안정성을 보장하는 템플릿 클래스. TSubClassOf에 전달된 인수가 템플릿 인자로 받은 타입과 일치하거나 템플릿 인자로 받은 타입을 상속받은 타입인지를 런타임 중에 확인하도록 도와주는 클래스이다.

 
3. UUserWidget
UUserWidget* UserWidget;

Widget Blueprint를 통해서 확장할 수 있는 사용자 위젯.

UserWidget->AddToViewport();

유저 위젯을 뷰 포트에 추가하는 함수.

UserWidget->RemoveToViewport();

유저 위젯을 뷰 포트에서 제거하는 함수.

 

4. AActor::GetWorld()

GetWorld();

UWorld 객체를 가져오는 함수. UWorld는 액터나 컴포넌트들을 포함하는 맵이나 샌드박스의 최상위 객체이다.

 

5. CreateWidget()

CreateWidget(GetWorld(), newWidget);

위젯을 생성하는 함수

 

6. APlayerController::SetInputMode()

SetInputMode(FInputModeGameAndUI());

플레이어 컨트롤러의 입력 모드를 설정하는 함수. Game 입력만 받을지, UI 입력만 받을지, 아니면 둘 다 받을지를 정할 수 있다.

 

7. UPROPERTY()

UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UMG_Game")

클래스의 멤버 변수에 붙이는 매크로. 에디터의 노출 수준을 정할 수 있다.

 

EditAnywhere는 에디터 디테일 패널에 노출이 가능하다.

 

BlueprintReadOnly는 블루프린트에서 수정을 불가능하고 읽기만 가능하다.

 

Category는 블루프린트에서 불러올 때, 카테고리를 분류해서 찾기 쉽게 만들어준다.

 

8. UFUNCTION()

UFUNCTION(BlueprintCallable, Category = "UMG_Game")

함수에 붙이는 매크로.

 

BlueprintCallable은 블루프린트에서 호출만 가능하다는 의미이다.

 

[유니티 어필리에이트 프로그램]

아래의 링크를 통해 에셋을 구매하시거나 유니티를 구독하시면 수익의 일부가 베르에게 수수료로 지급되어 채널의 운영에 도움이 됩니다.

 

에셋스토어

여러분의 작업에 필요한 베스트 에셋을 찾아보세요. 유니티 에셋스토어가 2D, 3D 모델, SDK, 템플릿, 툴 등 여러분의 콘텐츠 제작에 날개를 달아줄 다양한 에셋을 제공합니다.

assetstore.unity.com

 

Easy 2D, 3D, VR, & AR software for cross-platform development of games and mobile apps. - Unity Store

Have a 2D, 3D, VR, or AR project that needs cross-platform functionality? We can help. Take a look at the easy-to-use Unity Plus real-time dev platform!

store.unity.com

 

Create 2D & 3D Experiences With Unity's Game Engine | Unity Pro - Unity Store

Unity Pro software is a real-time 3D platform for teams who want to design cross-platform, 2D, 3D, VR, AR & mobile experiences with a full suite of advanced tools.

store.unity.com

[투네이션]

 

-

 

toon.at

[Patreon]

 

WER's GAME DEVELOP CHANNEL님이 Game making class videos 창작 중 | Patreon

WER's GAME DEVELOP CHANNEL의 후원자가 되어보세요. 아티스트와 크리에이터를 위한 세계 최대의 멤버십 플랫폼에서 멤버십 전용 콘텐츠와 체험을 즐길 수 있습니다.

www.patreon.com

[디스코드 채널]

 

Join the 베르의 게임 개발 채널 Discord Server!

Check out the 베르의 게임 개발 채널 community on Discord - hang out with 399 other members and enjoy free voice and text chat.

discord.com

 

반응형

제대로 따라가기 (3) C++ 프로그래밍 튜토리얼 :: 컴포넌트와 콜리전

 

작성버전 :: 4.21.0

 

언리얼 엔진 튜토리얼인 컴포넌트와 콜리전에서는 컴포넌트를 만들어 계층구조에 넣고 게임플레이 도중 제어하는 법과, 컴포넌트를 사용하여 폰이 입체 오브젝트로 된 월드를 돌아다니도록 만드는 법을 배울 수 있다..

 

튜토리얼대로 하면 문제가 발생해서 제대로 따라갈 수 없는 부분으로 동작이 가능하게 수정해야하는 부분은 빨간 블럭으로 표시되어 있다.
 
이번 튜토리얼에서 새로 배우게 되는 내용은 글 제일 끝에 "이번 섹션에서 배운 것"에 정리된다.

출처: https://wergia.tistory.com/ [베르의 프로그래밍 노트]

제대로 따라가기 (3) C++ 프로그래밍 튜토리얼 :: 컴포넌트와 콜리전

 

작성버전 :: 4.21.0

 

언리얼 엔진 튜토리얼인 컴포넌트와 콜리전에서는 컴포넌트를 만들어 계층구조에 넣고 게임플레이 도중 제어하는 법과, 컴포넌트를 사용하여 폰이 입체 오브젝트로 된 월드를 돌아다니도록 만드는 법을 배울 수 있다..

 

튜토리얼대로 하면 문제가 발생해서 제대로 따라갈 수 없는 부분으로 동작이 가능하게 수정해야하는 부분은 빨간 블럭으로 표시되어 있다.
 
이번 튜토리얼에서 새로 배우게 되는 내용은 글 제일 끝에 "이번 섹션에서 배운 것"에 정리된다.

출처: https://wergia.tistory.com/ [베르의 프로그래밍 노트]

제대로 따라가기 (3) C++ 프로그래밍 튜토리얼 :: 컴포넌트와 콜리전

 

작성버전 :: 4.21.0

 

언리얼 엔진 튜토리얼인 컴포넌트와 콜리전에서는 컴포넌트를 만들어 계층구조에 넣고 게임플레이 도중 제어하는 법과, 컴포넌트를 사용하여 폰이 입체 오브젝트로 된 월드를 돌아다니도록 만드는 법을 배울 수 있다..

 

튜토리얼대로 하면 문제가 발생해서 제대로 따라갈 수 없는 부분으로 동작이 가능하게 수정해야하는 부분은 빨간 블럭으로 표시되어 있다.
 
이번 튜토리얼에서 새로 배우게 되는 내용은 글 제일 끝에 "이번 섹션에서 배운 것"에 정리된다.

출처: https://wergia.tistory.com/ [베르의 프로그래밍 노트]

제대로 따라가기 (4) C++ 프로그래밍 튜토리얼 :: 플레이어 제어 카메라

작성버전 :: 4.21.0

언리얼 엔진 튜토리얼인 플레이어 제어 카메라에서는 카메라를 활성화시키고 전환하는 법을 배울 수 있다. 제대로 따라가기 :: 컴포넌트와 콜리전에서 카메라 컴포넌트와 스프링 암 컴포넌트를 다루는 법에 대해서 확실히 숙지했다면 이번 파트는 넘어가도 상관은 없다.

 

튜토리얼대로 하면 문제가 발생해서 제대로 따라갈 수 없는 부분으로 동작이 가능하게 수정해야하는 부분은 빨간 블럭으로 표시되어 있다.


이번 튜토리얼에서 새로 배우게 되는 내용은 글 제일 끝에 "이번 섹션에서 배운 것"에 정리된다.

 

 

1. 폰에 카메라 붙이기(문서)

 

새로운 프로젝트를 만들고, Pawn 클래스를 상속받는 "PawnWithCamera" 클래스를 생성해보자.

 

 

 

 

다음은, PawnWithCamera.h의 클래스 정의 아래에 다음 코드를 추가한다.

 

protected:
    UPROPERTY(EditAnywhere)
    USpringArmComponent* OurCameraSpringArm;
    UCameraComponent* OurCamera;

 

위의 변수들을 사용해서 Camera Component가 붙은 Spring Arm Component를 만든다. 스프링 암은 카메라가 이동하면서 유연한 느낌으로 따라붙을 수 있게 도와준다.

 

여기서 USpringArmComponent와 UCameraComponent가 정의되지 않았다고 에러가 발생하는 문제는 앞에 class를 붙여주면 해결된다.

 

protected:
    UPROPERTY(EditAnywhere)
    class USpringArmComponent* OurCameraSpringArm;
    class UCameraComponent* OurCamera;

 

PawnWithCamera.cpp의 APawnWithCamera::APawnWithCamera() 생성자 함수에서 실제 컴포넌트를 생성하는 작업을 할 차례이다.

 

RootComponent = CreateDefaultSubobject(TEXT("RootComponent"));
OurCameraSpringArm = CreateDefaultSubobject(TEXT("CameraSpringArm"));
OurCameraSpringArm->SetupAttachment(RootComponent);
OurCameraSpringArm->SetRelativeLocationAndRotation(FVector(.0f, .0f, 50.f), FRotator(-60.f, .0f, .0f));
OurCameraSpringArm->TargetArmLength = 400.f;
OurCameraSpringArm->bEnableCameraLag = true;
OurCameraSpringArm->CameraLagSpeed = 3.0f;

 

RootComponent에 대입하는 CreateDefaultSubobject() 함수의 문제는 템플릿 인자에 USceneComponent를 넣어주면 해결된다.

 

RootComponent = CreateDefaultSubobject<USceneComponent>(TEXT("RootComponent"));

 

OurCameraSpringArm에 대입하는 CreateDefaultSubobject() 함수의 문제는 템플릿 인자에 USpringArmComponent를 넣어주면 해결된다.

 

OurCameraSpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraSpringArm"));

 

OurCameraSpringArm에 불완전한 형식은 사용할 수 없다는 에러가 뜨는 문제는 PawnWithCamera.cpp 상단 전처리기 리스트에서 "Engine/Classes/GameFramework/SpringArmComponent.h"를 포함시켜주면 해결된다.

 

#include "Engine/Classes/GameFramework/SpringArmComponent.h"

 

위의 코드에 따르면, 비어있는 Scene Component가 루트로 생성되며, 거기에 Spring Arm Component를 만들어 붙인다. 그리고 Spring Arm의 기본 피치(Pitch)를 -60도로(60도 아래쪽으로) 설정하고, 위치는 루트의 50유닛 위로 정한다. Spring Arm Component의 길이와 유연성을 위한 변수도 설정해주었다.

 

Spring Arm의 설정이 끝났다면 Spring Arm Component 끝의 소켓에 Camera Componenet를 만들어서 연결해주면 된다.

 

OurCamera = CreateDefaultSubobject(TEXT("GameCamera"));
OurCamera->SetupAttachment(OurCameraSpringArm, USpringArmComponent::SocketName);

 

OurCamera에 대입하는 CreateDefaultSubobject() 함수의 문제는 템플릿 인자에 UCameraComponent를 넣어주면 해결된다.

 

OurCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("GameCamera"));

 

OurCamera에 불완전한 형식은 사용할 수 없다는 에러가 뜨는 문제는 PawnWithCamera.cpp 상단 전처리기 리스트에서 "Engine/Classes/Camera/CameraComponent.h"를 포함시켜주면 해결된다.

 

#include "Engine/Classes/Camera/CameraComponent.h"

 

마지막으로 로컬 플레이어 스폰시 자동으로 Pawn을 조종하도록 다음 코드를 추가한다.

 

AutoPossessPlayer = EAutoReceiveInput::Player0;

 

 

2. 카메라 제어 입력 환경설정(문서)

 

어떤 방식으로 카메라를 제어할지 결정하고 그에 맞게 입력을 구성해야 한다. 이 프로젝트에서는 마우스 오른쪽 버튼을 클릭하면 카메라를 따라다니는 거리를 줄이고 시야를 확대하고, 마우스로는 카메라의 각도를, WASD로는 폰을 이동을 제어하도록 하자.

 

에디터에서 편집 드롭다운 메뉴에서 프로젝트 세팅을 선택하자.

 

 

[프로젝트 세팅>엔진>입력]에서 액션 매핑을 다음과 같이 설정하자.

 

 

 

3. 입력에 반응하는 C++ 코드 작성(문서)

 

이제 게임에서 사용할 수 있는 입력 매핑이 생겼으니, 입력 매핑으로부터 받은 데이터를 저장할 멤버 변수들을 구성할 차례이다.

 

업데이트 중에 이동과 마우스 방향 축을 알아야 하는데 이 값으로는 FVector2D 타입이 적합하다. 그리고 줌인 중인지 줌아웃 중인지도 알아야하며, 얼마나 줌된 상태인지를 알아야한다. 그것을 위해서 PawnWithCamera.h의 클래스 정의에 다음과 같이 멤버 변수 선언을 추가해주자.

 

FVector2D MovementInput;
FVector2D CameraInput;
float ZoomFactor;
bool bZoomingIn;

 

그 다음엔, 입력에 대한 기록을 유지할 함수도 그 아래에 추가하자.

 

void MoveForward(float AxisValue);
void MoveRight(float AxisValue);
void PitchCamera(float AxisValue);
void YawCameara(float AxisValue);
void ZoomIn();
void ZoomOut();

 

그리고 PawnWithCamera.cpp에서 위 함수들의 구현을 작성하면 된다.

 

void APawnWithCamera::MoveForward(float AxisValue)
{
    MovementInput.X = FMath::Clamp(AxisValue, -1.0f, 1.0f);
}

void APawnWithCamera::MoveRight(float AxisValue)
{
    MovementInput.Y = FMath::Clamp(AxisValue, -1.0f, 1.0f);
}

void APawnWithCamera::PitchCamera(float AxisValue)
{
    CameraInput.Y = AxisValue;
}

void APawnWithCamera::YawCamera(float AxisValue)
{
    CameraInput.X = AxisValue;
}

void APawnWithCamera::ZoomIn()
{
    bZoomingIn = true;
}

void APawnWithCamera::ZoomOut()
{
    bZoomingIn = false;
}

 

입력 데이터를 저장할 코드를 모두 구현했으니, 이제 APawnWithCamera::SetupPlayerInputComponent() 함수에서 입력 이벤트와 함수를 바인딩할 차례이다.

 

InputComponent->BindAction("ZoomIn", IE_Pressed, this, &APawnWithCamera::ZoomIn);
InputComponent->BindAction("ZoomOut", IE_Released, this, &APawnWithCamera::ZoomOut);

InputComponent->BindAxis("MoveForward", this, &APawnWithCamera::MoveForward);
InputComponent->BindAxis("MoveRight", this, &APawnWithCamera::MoveRight);
InputComponent->BindAxis("CameraPitch", this, &APawnWithCamera::PitchCamera);

InputComponent->BindAxis("CameraYaw", this, &APawnWithCamera::YawCamera);

 

만약 InputComponent의 함수를 호출하려고 할 때, 불완전한 형식은 사용할 수 없다는 에러가 발생한다면 PawnWithCamera.cpp 전처리기에 "Engine/Classes/Components/InputComponent.h"를 포함하면 된다.

 

#include "Engine/Classes/Components/InputComponent.h"

 

바인딩이 모두 끝났다면, 이제 입력을 통해 들어오는 변수 값에 따라서 Tick() 함수에서 매프레임 Pawn과 Camera를 업데이트하도록 처리하자.

 

{
    if (bZoomingIn)
    {
        ZoomFactor += DeltaTime * 2.0f;
    }
    else
    {
        ZoomFactor -= DeltaTime * 4.0f;
    }
    ZoomFactor = FMath::Clamp(ZoomFactor, 0.0f, 1.0f);
    OurCamera->FieldOfView = FMath::Lerp(90.0f, 60.0f, ZoomFactor);
    OurCameraSpringArm->TargetArmLength = FMath::Lerp(400.0f, 300.0f, ZoomFactor);
}

 

이 코드에서는 줌인/줌아웃할 때, 걸이는 시간, FOV 값, 스프링 암의 거리 등을 하드코딩해서 사용하고 있지만, 이 값들을 멤버 변수로 만들어서 UPROPERTY(EditAnywhere)로 설정해서 에디터에 노출시키면 프로그래머가 아닌 개발자들도 에디터에서 값을 변경할 수 있고, 프로그래머도 값을 바꿀때마다 컴파일을 새로 할 필요가 없게 만들 수 있다.

 

{
    FRotator NewRotation = GetActorRotation();
    NewRotation.Yaw += CameraInput.X;
    SetActorRotation(NewRotation);
}

{
    FRotator NewRotation = OurCameraSpringArm->GetComponentRotation();
    NewRotation.Pitch = FMath::Clamp(NewRotation.Pitch + CameraInput.Y, -80.0f, -15.0f);
    OurCameraSpringArm->SetWorldRotation(NewRotation);
}

 

이 코드 블록은 Pawn의 요(Yaw)를 마우스 X축으로 직접 회전시키되, 카메라 시스템은 마우스 Y축의 피치(Pitch) 변화에만 반응한다. 액터나 그 서브클래스를 회전시키면, 실제로 루트 레벨의 컴포넌트가 회전되어 거기에 붙어있는 모든 오브젝트에 간접적으로 영향을 미친다.

 

{
    if (!MovementInput.IsZero())
    {
        MovementInput = MovementInput.GetSafeNormal() * 100.0f;
        FVector NewLocation = GetActorLocation();
        NewLocation += GetActorForwardVector() * MovementInput.X * DeltaTime;
        NewLocation += GetActorRightVector() * MovementInput.Y * DeltaTime;
        SetActorLocation(NewLocation);
    }
}

 

GetActorForwardVector()와 GetActorRightVector()를 사용하면 액터가 향하는 방향을 기준으로 이동하는 것이 가능하다. 카메라가 액터와 같은 방향을 향하고 있기 때문에 전방 키가 항상 플레이어가 바라보는 방향이 앞쪽이 되게 해준다.

 

모든 코딩 작업이 끝났다. 언리얼 에디터로 돌아가서 컴파일 한 뒤, 레벨에 배치해보자.

 

추가로 폰에 스태틱 메시나 비주얼 컴포넌트를 추가해서 자유롭게 플레이해보자.

 

폰이 움직일때는 카메라가 부드럽게 따라가지만 회전할 때는 카메라가 즉각 반응하는 것을 느낄 수 있을 것이다. Camera Rotation Lag를 켜거나 Camera Lag Speed를 수정해서 조작감에 어떤 영향을 미치는지 확인해보자.

 

 

 

 

 

 


 

이번 섹션에서 배운 것

 

1. USceneComponent

 

SetRelativeLocationAndRotation(FVector(), FRotator());

 

루트 오브젝트로부터의 위치와 회전을 동시에 설정할 수 있는 함수.

 

2. FVector2D

 

FVector2D Vector2D;

 

FVector의 2D 버전 구조체. FVector는 3차원 상의 X, Y, Z 좌표를 가지지만 FVector2D는 2차원 상의 X, Y 좌표만을 가진다.

 

3. UCameraComponent

 

UCameraComponent* CameraComponent;

 

CameraComponent->FieldOfView = 60.0f;

 

원근감 모드(Projection Mode)에서의 수평 시야각을 Field of view라고 한다. 수평 시야각이 넓어지면 물체가 확대되어서 보이기 때문에 주로 FPS게임에서 저격 소총의 줌 효과에 주로 사용된다.

 

4. FMath::Lerp()

 

FMath::Lerp(ValueA, ValueB, Factor);

 

선형 보간 함수이다. ValueA와 ValueB 사이의 Factor(0.0~1.0)값의 위치에 해당 하는 값을 구해준다.

 

ex) ValueA = 0, ValueB = 2일 때, Factor = 0.5이면 1을 돌려준다.

 

5. AActor

 

GetActorForwardVector();

 

액터의 Forward 방향을 구하는 함수

 

GetActorRightVector();

 

액터의 Right 방향으로 구하는 함수

 

[투네이션]

 

-

 

toon.at

[Patreon]

 

WER's GAME DEVELOP CHANNEL님이 Game making class videos 창작 중 | Patreon

WER's GAME DEVELOP CHANNEL의 후원자가 되어보세요. 아티스트와 크리에이터를 위한 세계 최대의 멤버십 플랫폼에서 멤버십 전용 콘텐츠와 체험을 즐길 수 있습니다.

www.patreon.com

[디스코드 채널]

 

Join the 베르의 게임 개발 채널 Discord Server!

Check out the 베르의 게임 개발 채널 community on Discord - hang out with 399 other members and enjoy free voice and text chat.

discord.com

 

반응형

제대로 따라가기 (3) C++ 프로그래밍 튜토리얼 :: 컴포넌트와 콜리전

 

작성버전 :: 4.21.0

 

언리얼 엔진 튜토리얼인 컴포넌트와 콜리전에서는 컴포넌트를 만들어 계층구조에 넣고 게임플레이 도중 제어하는 법과, 컴포넌트를 사용하여 폰이 입체 오브젝트로 된 월드를 돌아다니도록 만드는 법을 배울 수 있다..

 

튜토리얼대로 하면 문제가 발생해서 제대로 따라갈 수 없는 부분으로 동작이 가능하게 수정해야하는 부분은 빨간 블럭으로 표시되어 있다.
 

이번 튜토리얼에서 새로 배우게 되는 내용은 글 제일 끝에 "이번 섹션에서 배운 것"에 정리된다.

 

 

1. 컴포넌트 만들고 붙이기(문서)

 

프로젝트를 새로 생성하고 Pawn 클래스를 상속받는 "CollidingPawn"을 생성한다. 이 폰은 컴포넌트를 가지고 레벨 안에서 이동하고 입체 오브젝트와 충돌하게 된다.

 

 

 

 

CollidingPawn.h의 클래스 정의 하단부에 UParticleSystemComponent를 추가한다.

 

UParticleSystemComponent* OurParticleSystem;

 

UParticleSystemComponent가 정의되어 있지 않다고 에러가 발생한다면, CollidingPawn.generated.h 포함 전처리기 위쪽에서 "Engine/Classes/Particles/ParticleSystemComponent.h"을 포함시켜 주면 된다.

 

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Pawn.h"
#include "Engine/Classes/Particles/ParticleSystemComponent.h"
#include "CollidingPawn.generated.h"

 

여기에 대한 또 다른 해결책으로는 UParticleSystemComponent 타입의 변수를 선언할 때, 아래처럼 앞에 class를 붙여주면 헤더를 .h에 포함하지 않아도 에러가 발생하지 않는다.

 

class UParticleSystemComponent* OurParticleSystem;

 

대신 이 경우에는 .cpp에서 해당 타입의 변수를 사용할 때, 불완전한 형식을 사용할 수 없다는 에러가 발생할 것이기 때문에 .cpp의 헤더 포함 전처리기에 "Engine/Classes/Particles/ParticleSystemComponent.h"를 포함하는 코드를 추가시켜주어야 한다.

 

멤버 변수로 만들지 않아도 컴포넌트를 만들 수 있지만, 코드에서 컴포넌트를 사용하려면 클래스 멤버 변수로 만들어야 한다.

 

이 다음에는 CollidingPawn.cpp의 ACollidingPawn::ACollidingPawn() 생성자 함수를 편집해서 필요한 컴포넌트들을 스폰할 코드를 추가하고 계층구조로 배치해야 한다. 물리 월드와 상호작용을 위한 Sphere Component, 콜리전 모양을 시각적으로 보여줄 Static Mesh Component, 시각적인 효과를 더하며 켜고 끌 수 있는 Particle System Component, 게임 내의 시점 제어를 위해 Camera Component에 덧붙일 Spring Arm Component를 만든다.

 

먼저 계층구조에서 루트가 될 컴포넌트를 결정해야 한다. 이 튜토리얼에서는 Sphere Component가 루트 컴포넌트가 된다. 물리적으로 실존이 있고, 게임 월드와의 상호작용이 가능하기 때문이다. 참고로 액터에는 계층구조 안에서 다수의 물리 기반 컴포넌트가 있을 수 있지만, 이 튜토리얼에서는 하나만 사용한다.

 

USphereComponent* SphereComponent = CreateDefaultSubobject(TEXT("RootComponent"));
RootComponent = SphereComponent;
SphereComponent->InitSphereRadius(40.0f);
SphereComponent->SetCollisionProfileName(TEXT("Pawn"));

 

이 파트에서는 두 가지 문제로 진행이 방해받는다. 언리얼 튜토리얼 문서의 고질적인 문제로 CreateDefaultSubobject() 함수 문제와 USphereComponent가 정의되어 있지 않다고 하는 문제이다.

 

CreateDefaultSubobject() 함수 문제는 템플릿 매개변수에 값을 반환받는 변수에 맞는 타입을 넣어주면 해결된다.

 

USphereComponent* SphereComponent = CreateDefaultSubobject<USphereComponent>(TEXT("RootComponent"));

 

USphereComponent가 정의되지 않은 문제는 CollidingPawn.cpp의 전처리기에 "Engine/Classes/Components/SphereComponent.h"를 포함시켜주면 된다.

 

// Fill out your copyright notice in the Description page of Project Settings.

#include "CollidingPawn.h"
#include "Engine/Classes/Components/SphereComponent.h"

 

다음은, 구형의 스태틱 메시 컴포넌트를 만들어서 적절한 크기와 위치로 만들어서 루트 컴포넌트에 붙여준다.

 

UStaticMeshComponent* SphereVisual = CreateDefaultSubobject(TEXT("VisualRepresentation"));
SphereVisual->SetupAttachment(RootComponent);
static ConstructorHelpers::FObjectFinder SphereVisualAsset(TEXT("/Game/StarterContent/Shapes/Shape_Sphere.Shape_Sphere"));
if (SphereVisualAsset.Succeeded())
{
    SphereVisual->SetStaticMesh(SphereVisualAsset.Object);
    SphereVisual->SetRelativeLocation(FVector(0.0f, 0.0f, -40.0f));
    SphereVisual->SetWorldScale3D(FVector(0.8f));
}

 

UStaticMeshComponent 정의되지 않음 문제는 CollidingPawn.cpp에 "Engine/Classes/Components/StaticMeshComponent.h"를 포함시켜주면 해결된다.

 

#include "Engine/Classes/Components/StaticMeshComponent.h"

 

CreateDefaultSubobject() 함수 문제는 템플릿 매개변수에 UStaticMeshComponent 타입을 넣어주면 해결된다.

 

UStaticMeshComponent* SphereVisual = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("VisualRepresentation"));

 

ConstructorHelpers가 정의되어 있지 않은 문제는 CollidingPawh.cpp에 "ConstructorHelpers.h"를 포함시켜주면 된다.

 

#include "ConstructorHelpers.h"

 

여기까지 해결하고 나면 ConstructorHelpers::FObjectFinder에서 [클래스 템플릿 "ConstructorHelpers::FObjectFinder"에 대한 인수 목록이 없습니다.] 라는 에러가 발생할 것이다. 이 문제를 해결하기 위해서 ConstructorHelpers::FObjectFinder의 원형을 살펴보면 ConstructorHelpers::FObjectFinder는 템플릿을 사용하는 것을 알 수 있다. 그렇다면 여기서 중요한 점은 템플릿 인자에 어떤 타입이 들어가야 하는가가 문제인데, 이 것은 SphereVisualAsset의 선언 2줄 아래를 보면 이 변수가 SetStaticMesh() 함수에 대입되는 것을 알 수 있다. 이 함수가 받는 매개변수의 타입은 UStaticMesh로서 SphereVisualAsset.Object는 UStaticMesh 타입임을 유추할 수 있다.

 

static ConstructorHelpers::FObjectFinder<UStaticMesh> SphereVisualAsset(TEXT("/Game/StarterContent/Shapes/Shape_Sphere.Shape_Sphere"));

 

이번엔 Particle System Component를 붙인다. 이 컴포넌트는 코드를 통해서 켜고 끄는 등의 제어를 할 수 있으며, 루트가 아닌 스태틱 메시에 붙어있으며 게임 플레이 도중에 더 잘보이게 하기 위해 메시의 정중앙이 아닌 약간 아래쪽에 오프셋되어 있다.

 

OurParticleSystem = CreateDefaultSubobject(TEXT("MovementParticles"));
OurParticleSystem->SetupAttachment(SphereVisual);
OurParticleSystem->bAutoActivate = false;
OurParticleSystem->SetRelativeLocation(FVector(-20.0f, 0.0f, 20.0f));
static ConstructorHelpers::FObjectFinder ParticleAsset(TEXT("/Game/StarterContent/Particles/P_Fire.P_Fire"));
if (ParticleAsset.Succeeded())
{
    OurParticleSystem->SetTemplate(ParticleAsset.Object);
}

 

CreateDefaultSubobject() 함수 문제는 템플릿 매개변수에 UParticleSystemComponent 타입을 넣어주면 해결된다.

 

OurParticleSystem = CreateDefaultSubobject<UParticleSystemComponent>(TEXT("MovementParticles"));

 

SetTamplate() 함수의 매개변수를 확인해본 결과 ParticleAsset의 템플릿 인자는 UParticleSystem 타입임을 알 수 있다.

 

static ConstructorHelpers::FObjectFinder<UParticleSystem> ParticleAsset(TEXT("/Game/StarterContent/Particles/P_Fire.P_Fire"));

 

Spring Arm Component는 폰보다 느린 가속/감속을 따라다니는 카메라에 적용시킬 수 있기 때문에, 카메라의 부드러운 부착점이 된다. 또한 카메라가 입체 오브젝트를 뚫고 지나가지 못하게 하는 기능을 내장하고 있어서, 삼인칭 게임에서 구석에서 벽을 등지는 상황에 유용하게 사용된다.

 

USpringArmComponent* SpringArm = CreateDefaultSubobject(TEXT("CameraAttachmentArm"));
SpringArm->SetupAttachment(RootComponent);
SpringArm->SetRelativeRotation(FRotator(-45.0f, 0.0f, 0.0f));
SpringArm->TargetArmLength = 400.0f;
SpringArm->bEnableCameraLag = true;
SpringArm->CameraLagSpeed = 3.0f;

 

USpringArmComponent가 정의되지 않은 문제는 CollidingPawn.cpp에 "Engine/Classes/GameFramework/SpringArmComponent.h"를 포함시켜주면 해결된다.

 

#include "Engine/Classes/GameFramework/SpringArmComponent.h"

 

CreateDefaultSubobject() 함수 문제는 템플릿 매개변수에 USpringArmComponent 타입을 넣어주면 해결된다.

 

USpringArmComponent* SpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraAttachmentArm"));

 

Camera Component를 생성해서 Spring Arm Component에 붙여준다. Spring Arm Component에는 소켓이 내장되어 있어서 베이스가 아닌 소켓에 카메라를 붙일 수 있다.

 

UCameraComponent* Camera = CreateDefaultSubobject(TEXT("ActualCamera"));
Camera->SetupAttachment(SpringArm, USpringArmComponent::SocketName);

 

UCameraComponent가 정의되지 않은 문제는 CollidingPawn.cpp에 "Engine/Classes/Camera/CameraComponent.h"를 포함시켜주면 해결된다.

 

#include "Engine/Classes/Camera/CameraComponent.h"

 

CreateDefaultSubobject() 함수 문제는 템플릿 매개변수에 UCameraComponent 타입을 넣어주면 해결된다.

 

UCameraComponent* Camera = CreateDefaultSubobject<UCameraComponent>(TEXT("ActualCamera"));

 

모든 컴포넌트를 붙인 뒤에는, 기본 플레이어가 이 폰을 조종하도록 설정해야 한다.

 

AutoPossessPlayer = EAutoReceiveInput::Player0;

 

위의 작업이 모두 끝났다면 언리얼 에디터로 돌아가자.

 

 

 

 

 

2. 입력 환경설정 및 폰 무브먼트 컴포넌트 생성(문서)

 

언리얼 에디터로 돌아왔다면, 프로젝트의 입력 세팅을 할 차례다. 이 세팅은 편집 드롭다운 메뉴의 프로젝트 세팅에서 찾을 수 있다.

 

 

 

프로젝트 세팅 창을 열었다면, 좌측의 엔진 섹션에서 입력을 찾아서 클릭한 뒤 아래와 같이 입력 매핑을 세팅하자.

 

 

 

이번에는 Pawn에서 모든 이동 처리를 하는 대신에, Movement Component를 만들어서 관리를 시키도록 해보자. 이 튜토리얼에서 Pawn Movement Component 클래스를 확장해서 사용한다.[각주:1] 파일 드롭다운 메뉴의 [새로운 C++ 클래스] 명령을 선택한다.

 

 

 

Pawn 클래스와 달리 Pawn Movement Component 클래스는 기본적으로 보이지 않기 때문에 모든 클래스 보기 옵션을 선택해야 한다.

 

 

 

검색창에 movement를 검색하면 찾고자 하는 클래스의 범위를 빠르게 좁힐 수 있다.

 

 

우리가 만든 Pawn 클래스의 이름이 "CollidingPawn"이기 때문에 이 Movement Component의 이름은 "CollidingPawnMovementComponent"로 정하자.

 

 

입력 환경설정에 대한 정의와 CollidingPawnMovementComponent의 생성으로 모두 끝마쳤으므로, 비주얼 스튜디오로 돌아가서 다시 코드 작업을 해야한다.

 

 

3. 폰 무브먼트 컴포넌트의 작동방식 코딩(문서)

 

비주얼 스튜디오로 돌아왔으면 이제 커스텀 폰 무브먼트 컴포넌트의 작동방식을 코딩하면 된다. Actor의 Tick() 함수 역할을 하는 TickComponent() 함수가 각 프레임 별로 어떻게 동작할지를 정의해야 한다. 우선은 부모 클래스의 TickComponent() 함수를 덮어쓰는 것으로 시작한다.

 

public:
    virtual void TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;

 

정의한 함수를 CollidingPawnMovementComponent.cpp에 구현한다.

 

void UCollidingPawnMovementComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
    Super::TickComponent(DeltaTime, TickType, ThisTickFunction);

    if (!PawnOwner || !UpdatedComponent || ShouldSkipUpdate(DeltaTime))
    {
        return;
    }

    FVector DesiredMovementThisFrame = ConsumeInputVector().GetClampedToMaxSize(1.0f) * DeltaTime * 150.0f;
    if (!DesiredMovementThisFrame.IsNearlyZero())
    {
        FHitResult Hit;
        SafeMoveUpdatedComponent(DesiredMovementThisFrame, UpdatedComponent->GetComponentRotation(), true, Hit);

        if (Hit.IsValidBlockingHit())
        {
            SlideAlongSurface(DesiredMovementThisFrame, 1.0f - Hit.Time, Hit.Normal, Hit);
        }
    }
}

 

이 코드는 적합한 면을 미끄러져 다니며 월드를 부드럽게 움직이도록 폰을 이동시킨다. 폰에는 중력이 적용되지 않으며, 최대 속력은 초당 150 언리얼 유닛 으로 하드코딩되어 있다.

 

 

4. 폰과 컴포넌트 함께 사용하기(문서)

 

CollidingPawnMovementComponent를 CollidingPawn 클래스에서 사용하기 위해서 CollidingPawn.h의 클래스 정의 내에 다음 코드를 추가한다.

 

class UCollidingPawnMovementComponent* OurMovementComponent;

 

그리고 CollidingPawn.cpp에 "CollidingPawnMovementComponent.h"를 포함시킨다.

 

#include "CollidingPawnMovementComponent.h"

 

그 다음엔 CollidingPawn.cpp의 ACollidingPawn::ACollidingPawn() 생성자 함수 하단에서 CollidingPawnMovementComponent의 인스턴스를 생성하고 루트 컴포넌트를 업데이트하게 코드를 작성한다.

 

OurMovementComponent = CreateDefaultSubobject(TEXT("CustomMovementComponent"));
OurMovementComponent->UpdatedComponent = RootComponent;

 

CreateDefaultSubobject() 함수 문제는 템플릿 매개변수에 UCollidingPawnMovementComponet 타입을 넣어주면 해결된다.

 

OurMovementComponent = CreateDefaultSubobject<UCollidingPawnMovementComponent>(TEXT("CustomMovementComponent"));

 

이 컴포넌트는 다른 컴포넌트들과 달리 컴포넌트 계층구조에 붙일 필요가 없다. 다른 컴포넌트들의 경우에는 모두 씬 컴포넌트로 물리적인 위치가 필요한 것들이었지만, 이 컴포넌트는 물리적 오브젝트를 나타내는 것이 아니기 때문에, 물리적인 위치에 존재한다든가 다른 컴포넌트에 덧붙인다던가 하는 개념을 가지지 않는다.

 

Pawn 클래스에는 GetMovementComponent() 라는 함수가 있는데 이것은 엔진의 다른 클래스들이 현재 Pawn이 사용중인 Pawn Movement Component에 접근할 수 있도록 하는데 사용된다. 이 함수가 커스터마이징한 CollidingPawnMovementComponent를 반환하도록 하려면 이 함수를 덮어씌워야 한다. CollidingPawn.h에 다음 코드를 추가한다.

 

virtual UPawnMovementComponent* GetMovementComponent() const override;

 

그리고 CollidingPawn.cpp에 이 함수의 구현을 추가한다.

 

UPawnMovementComponent * ACollidingPawn::GetMovementComponent() const
{
    return OurMovementComponent;
}

 

Pawn Movement Component에 대한 구성이 끝났다면, Pawn이 받을 입력 처리에 대한 코드를 만들자. CollidingPawn.h에 함수 몇 개를 선언한다.

 

void MoveForward(float AxisValue);
void MoveRight(float AxisValue);
void Turn(float AxisValue);
void ParticleToggle();

 

그리고 CollidingPawn.cpp에 함수들을 구현한다.

 

void ACollidingPawn::MoveForward(float AxisValue)
{
    if (OurMovementComponent && OurMovementComponent->UpdatedComponent == RootComponent)
    {
        OurMovementComponent->AddInputVector(GetActorForwardVector() * AxisValue);
    }
}

void ACollidingPawn::MoveRight(float AxisValue)
{
    if (OurMovementComponent && OurMovementComponent->UpdatedComponent == RootComponent)
    {
        OurMovementComponent->AddInputVector(GetActorRightVector() * AxisValue);
    }
}

void ACollidingPawn::Turn(float AxisValue)
{
    FRotator NewRotation = GetActorRotation();
    NewRotation.Yaw += AxisValue;
    SetActorRotation(NewRotation);
}

void ACollidingPawn::ParticleToggle()
{
    if (OurParticleSystem && OurParticleSystem->Template)
    {
        OurParticleSystem->ToggleActive();
    }
}

 

남은 것은 함수들을 입력 이벤트에 바인딩하는 것이다. 다음 코드를 ACollidingPawn::SetupPlayerInputComponent() 함수에 추가하자.

 

InputComponent->BindAction("ParticleToggle", IE_Pressed, this, &ACollidingPawn::ParticleToggle);
InputComponent->BindAxis("MoveForward", this, &ACollidingPawn::MoveForward);
InputComponent->BindAxis("MoveRight", this, &ACollidingPawn::MoveRight);
InputComponent->BindAxis("Turn", this, &ACollidingPawn::Turn);

 

이로써 프로그래밍 작업은 모두 끝났다. 에디터로 돌아가서 컴파일을 진행하고 테스트해보자.

 

 

 

 

 

 


 

이번 섹션에서 배운 것

 

1. UParticleSystemComponent

 

UParticleSystemComponent* ParticleSystemComponent;

 

액터에 파티클 시스템을 덧붙일 수 있는 컴포넌트

 

ParticleSystemComponent->bAutoActivate = true;

 

파티클 시스템이 생성되자마자 자동으로 켜질지에 대한 변수

 

ParticleSystemComponent->SetTemplate(ParticleAsset.Object);

 

파티클 시스템 컴포넌트의 파티클을 설정하는 함수

 

ParticleSystemComponent->ToggleActive();

 

파티클을 켜고 끄는 함수

 

2. USphereComponent

 

USphereComponent* SphereComponent;

 

액터에 구형 충돌 물리 효과를 줄 수 있는 컴포넌트

 

SphereComponent->InitSphereRadius(40.0f);

 

스피어 컴포넌트의 반지름은 설정하는 함수

 

SphereComponent->SetCollisionProfileName(TEXT("Pawn"));

 

콜리전의 프로필을 설정하는 함수. [프로젝트 세팅>엔진>콜리전] 하단에 Preset을 열어보면 각 콜리전 프로필마다 어떤 물리 설정을 가지고 있는지 확인할 수 있다.

 

3. UStaticMeshComponent

 

UStaticMeshComponent* StaticMeshComponent;

 

월드에 렌더링되는 스태틱 메시를 가진 컴포넌트

 

StaticMeshComponent->SetStaticMesh(SphereVisualAsset.Object);

 

스태틱 메시 컴포넌트의 스태틱 메시를 설정하는 함수

 

4. ConstructorHelpers::FObjectFinder<T>

 

static ConstructorHelpers::FObjectFinder<T> Asset(TEXT("AssetPath"));

 

프로젝트에서 필요한 콘텐츠나 리소스, 에셋을 불러오는데 쓰이는 구조체

 

Asset.Succeeded();

 

에셋을 불러오는데 성공했는지를 반환하는 함수

 

Asset.Object;

 

불러온 에셋을 담고 있는 변수

 

5. USpringArmComponent

 

USpringArmComponent* SpringArmComponent;

 

부모 오브젝트와 자식 오브젝트 사이에 일정한 거리를 유지하게 도와주는 컴포넌트. 충돌이 있는 경우라면 유연하게 부모와 자식 사이의 거리를 좁혔다가 충돌이 사라지면 다시 원래대로 돌아가게하는 기능을 제공한다.

 

SpringArmComponent->TargetArmLength = 400.0f;

 

아무런 충돌이 없을 때, 스프링 암의 자연적인 거리를 정할 수 있는 변수

 

SpringArmComponent->bEnableCameraLag = true;

 

true인 경우, 카메라가 목표 위치보다 뒤떨어져서 따라가도록 한다.

 

SpringArmComponent->CameraLagSpeed = 3.0f;

 

bEnableCameraLag가 true인 경우, 카메라가 목표 위치에 도달하는 속도를 제어한다.

 

6. UPawnMovementComponent

 

Pawn의 움직임을 업데이트하는데 사용되는 컴포넌트

 

PawnOwner;

 

이 컴포넌트를 소유하고 있는 폰

 

UMovementComponent::UpdatedComponent;

 

UPawnMovementComponent의 부모 클래스인 UMovementComponent 클래스에 속하는 변수로 이 무브먼트 컴포넌트가 이동시키고 업데이트 해야할 컴포넌트

 

UMovementComponent::ShouldSkipUpdate(DeltaTime);

 

이동된 컴포넌트가 이동할 수 없거나 렌더링되지 않은 경우인지를 판별하여 알려주는 함수

 

ConsumeInputVector();

 

대기중인 입력을 반환하고 다시 0으로 설정하는 함수

 

SafeMoveUpdatedComponent(DesiredMovementThisFrame, UpdatedComponent->GetComponentRotation(), true, Hit);

 

언리얼 엔진 피직스를 이용해서 입체 장애물을 피해서 폰 무브먼트 컴포넌트를 이동시키는 함수

 

SlideAlongSurface(DesiredMovementThisFrame, 1.0f - Hit.Time, Hit.Normal, Hit);

 

컴포넌트가 이동하다가 충돌이 발생했을 때, 제자리에 멈추는 대신 충돌체의 표면을 타고 미끄러지듯이 이동하도록 도와주는 함수

 

AddInputVector(Vector);

 

매개변수로 받은 벡터를 누적 입력에 더하는 함수

 

7. FVector

 

FVector Vector;

 

언리얼 엔진에서 3D 상의 위치나, 속도를 나타내는데 쓰이는 구조체

 

Vector.GetClampedToMaxSize(Value);

 

길이가 Value인 이 벡터의 복사본을 만들어서 반환하는 함수

 

Vector.IsNearlyZero();

 

지정된 허용오차 내에서 벡터의 길이가 0에 근접하는지 확인하는 함수

 

8. FHitResult

 

FHitResult Hit;

 

충돌에 대한 정보를 담고 있는 구조체

 

Hit.Time;

 

Hit가 발생했을 때, TraceStart와 TraceEnd 사이의 충돌이 발생한 시간을 의미한다. (0.0~1.0)

 

Hit.Normal

 

충돌이 발생한 오브젝트의 월드 공간 상의 법선 방향

 

Hit.IsValidBlockingHit();

 

막히는 충돌이 발생했을 때 true를 반환하는 함수

 

9. AActor

 

GetActorRotation();

 

액터의 현재 회전을 반환하는 함수

 

SetActorRotation(FRotator());

 

액터의 회전을 설정하는 함수

 

 

  1. Pawn Movement Component 에는 흔한 물리 함수성에 도움이 되는 강력한 내장 기능이 몇 가지 들어있어, 여러가지 폰 유형에 무브먼트 코드를 공유하기가 좋다. 컴포넌트 를 사용하여 함수성을 분리시켜 놓는 것은 매우 좋은 습관인데, 프로젝트의 덩치가 커지면서 폰 도 복잡해 지기 때문이다. [본문으로]

 

[투네이션]

 

-

 

toon.at

[Patreon]

 

WER's GAME DEVELOP CHANNEL님이 Game making class videos 창작 중 | Patreon

WER's GAME DEVELOP CHANNEL의 후원자가 되어보세요. 아티스트와 크리에이터를 위한 세계 최대의 멤버십 플랫폼에서 멤버십 전용 콘텐츠와 체험을 즐길 수 있습니다.

www.patreon.com

[디스코드 채널]

 

Join the 베르의 게임 개발 채널 Discord Server!

Check out the 베르의 게임 개발 채널 community on Discord - hang out with 399 other members and enjoy free voice and text chat.

discord.com

 

반응형

필요없어진 C++ 클래스 삭제하기

 

작성버전 :: 4.20.3

 

처음부터 끝까지 설계가 완벽하고 수정할 일이 없다면 그럴 일이 없겠지만, 코드 작업을 하다보면 기존에 있던 클래스를 삭제해야하는 일이 가끔 발생한다. 특히 아직 프로토타입 작업을 하는 과정이라면 작성해둔 클래스가 필요없어져서 삭제해야하는 일이 생각보다 자주 발생할 수 있다.

 

 

 

하지만 위의 이미지와 같이 간단하게 삭제할 수 있는 블루프린트 클래스와 달리 C++ 클래스는 에디터 내에서 삭제할 수 있는 방법이 존재하지 않는다. 그렇다고 더이상 사용하지 않게된 C++ 클래스를 무작정 쌓아두고 있을 수만은 없는 법이다.

 

 

 

1. 필요 없어진 C++ 클래스를 삭제하기 전에 에디터를 닫는다.

 

 

 

2. 비주얼 스튜디오로 가서 솔루션 탐색기에서 지우고자 하는 클래스의 헤더(.h)와 소스파일(.cpp)를 선택한 뒤 제거한다.

 

 

 

3. 프로젝트 폴더의 Source 폴더 안에 남아있는 클래스의 헤더(.h)와 소스파일(.cpp) 역시 삭제해준다.

 

 

4. 비주얼 스튜디오로 돌아가서 [빌드 > 솔루션 다시 빌드]를 선택해서 프로젝트를 다시 빌드한다.

 

 

 

5. 프로젝트 빌드가 성공적으로 끝났다면 에디터를 다시 실행시킨다. 그렇게 하고 콘텐츠 브라우저를 보면 필요없는 C++ 클래스가 성공적으로 삭제된 것을 확인할 수 있다.

 

 

주의사항

 

블루프린트 클래스는 관련되어 있거나 레퍼런스가 있는 상태라면 삭제하기 전에 경고창을 띄워주고 정말로 삭제할 것인지 확인을 하지만, C++ 클래스는 그런 과정이 없기 때문에 지우고자하는 클래스가 레벨에 배치되어있는지, 다른 곳에서의 레퍼런스가 있는지, 또는 다른 클래스에서 헤더를 포함시켜서 사용하고 있는 것은 아닌지 신중하게 확인하고 삭제하는 것이 좋다.

 

또한 필요없어진 C++ 클래스를 삭제함으로서 신텍스 에러가 발생한다면 4번 과정에서 프로젝트를 리빌드가 실패하게 될 것이다. 그렇기 때문에 클래스를 삭제한 뒤에 오류목록을 살펴서 클래스를 삭제한 여파로 발생한 에러가 없는지 확인하는 과정 역시 필요하다.

 

 

[투네이션]

 

-

 

toon.at

[Patreon]

 

WER's GAME DEVELOP CHANNEL님이 Game making class videos 창작 중 | Patreon

WER's GAME DEVELOP CHANNEL의 후원자가 되어보세요. 아티스트와 크리에이터를 위한 세계 최대의 멤버십 플랫폼에서 멤버십 전용 콘텐츠와 체험을 즐길 수 있습니다.

www.patreon.com

[디스코드 채널]

 

Join the 베르의 게임 개발 채널 Discord Server!

Check out the 베르의 게임 개발 채널 community on Discord - hang out with 399 other members and enjoy free voice and text chat.

discord.com

 

반응형

프로그래밍 작업시 헤더(Header) 포함(Include) 문제

 

작성버전 :: 4.20.3

 

언리얼 엔진4(이하 언리얼 엔진 혹은 언리얼)에서의 프로그래밍은 C++기반으로 구성되어 있기 때문에 다른 헤더 파일에 정의된 클래스 등을 사용하기 위해서는 헤더를 포함하는 전처리가 필수적이다.

 

일반적인 C++ 프로젝트에서 헤더가 꼬이거나 중복 호출되는 경우만 조심하면 주의하면 되고 헤더의 순서는 별로 중요하지 않은 것에 비해서 언리얼에서의 헤더 포함은 약간 복잡하고 귀찮은데다가 버그까지 있다.

 

언리얼 엔진이 아닌 다른 C++ 프로젝트에서는 프로그래밍 작업 도중에 필요한 헤더가 생긴다면 상단의 전처리기들 아래에 필요한 헤더의 포함 선언을 추가할 것이다. 하지만 언리얼에서는 헤더 포함의 위치가 중요하다.

 

 

 

만약 AMyActor 클래스에서 AMyActorComponent를 사용하기 위해서 MyActorComponent.h의 선언을 일반적인 C++ 프로젝트에서 하듯이 MyActor.h의 헤더 포함 리스트 제일 아래에 추가하면 위의 이미지처럼 신텍스 에러(E0077 this declaration has no storage class or type specifier)가 발생하고 에디터에서의 컴파일 역시 실패한다.

 

언리얼에서는 다른 헤더를 포함할 때, 항상 generated.h 헤더보다 위쪽에 포함 선언을 추가해야 된다.

 

 

 

위의 이미지처럼 새롭게 추가하는 헤더 포함 선언을 generated.h 헤더 선언 위로 올리고 수정한 소스 파일을 저장하면 대부분은 신텍스 에러가 사라진다. 하지만 여기서 고질적인 문제가 발생하는데 꽤나 높은 빈도로 정상적으로 generated.h 위쪽으로 헤더 포함 선언을 옮겼는데도 불구하고 아래의 이미지처럼 신텍스 에러가 사라지지 않는 경우가 있다.

 

 

 

이러한 문제는 인텔리전스 버그로 실제 컴파일에서는 전혀 문제가 되지 않는다. 실제로도 에디터에서 컴파일을 해보면 전혀 문제 없이 컴파일이 진행되는 것을 알 수 있다. 이런 인텔리전스 버그는 잠시 후에 없어지기도 하고, 에디터에서 컴파일하거나, 비주얼 스튜디오나 언리얼을 재실행 하는 것으로도 없어진다.

 

이런 헤더 순서 문제가 매우 중요함에도 불구하고 언리얼 엔진 레퍼런스 문서에서는 쉽게 찾을 수 없는게 문제다.

 

 

[투네이션]

 

-

 

toon.at

[Patreon]

 

WER's GAME DEVELOP CHANNEL님이 Game making class videos 창작 중 | Patreon

WER's GAME DEVELOP CHANNEL의 후원자가 되어보세요. 아티스트와 크리에이터를 위한 세계 최대의 멤버십 플랫폼에서 멤버십 전용 콘텐츠와 체험을 즐길 수 있습니다.

www.patreon.com

[디스코드 채널]

 

Join the 베르의 게임 개발 채널 Discord Server!

Check out the 베르의 게임 개발 채널 community on Discord - hang out with 399 other members and enjoy free voice and text chat.

discord.com

 

반응형

enum class

 

인간은 문자와 기호 그리고 숫자를 이용하고 컴퓨터는 모든 것을 숫자로 받아들인다. 그렇기 때문에 인간이 알아보기 쉽게 문자열로 만들면 프로그램이 느려지고 컴퓨터가 알아보기 쉽게 숫자로 만들면 인간이 알아보기가 어려워진다. 이것을 해결하기 위한 방법의 하나가 바로 enum(열거형)이다.

 

이 enum은 일반적으로 이름을 가지는 정수형의 상수를 정의해서 알아보기 쉽게 해주는데, 이 기존의 enum은 몇가지의 문제점이 존재한다.

 

 

1. 열거형에 존재하는 이름과 같은 이름의 클래스를 선언할 수 없다.

일반적인 enum 내에 선언된 열거자의 이름은 전역적이다. 그렇기 때문에 enum 내부에 선언된 이름과 같은 이름을 가진 클래스를 선언하고 사용하려고 하면 컴파일 에러가 발생한다. 다음의 코드는 이것에 대한 예시이다.

 

#include <iostream>
using namespace std;

enum EColor
{
    Red,
    Green,
    Blue,
};

class Red { };

int main()
{
    Red r;    // 컴파일 에러가 발생한다.
    EColor color = Red;

    return 0;
}

 

위와 같은 일이 발생하는 이유는 MSDN에 다음과 같이 설명되어 있다.

 

enum EColor
{
    Red,
    Green,
    Blue,
};

 

위와 같이 정의된 enum의 의미 체계는 다음과 같다고 한다.

 

static const int Red = 0;
static const int Green = 1;
static const int Blue = 2;

 

다시 말하자면 이미 전역 변수에 같은 이름의 변수가 선언되어 있기 때문에 같은 이름의 클래스를 선언할 수 없는 것이다. 하지만 enum class로 정의된 열거형의 경우에는 위와 같은 문제가 발생하지 않는다.

 

#include <iostream>
using namespace std;

enum class EColor
{
    Red,
    Green,
    Blue,
};

class Red { };

int main()
{
    Red r;    // 컴파일 에러가 발생하지 않는다.
    EColor color = EColor::Red;    // 대신 접근지정자를 이용해 스코프를 지정해주어야 한다.

    return 0;
}

 

열거형을 표준 열거형이 아닌 enum class로 선언하면 열거형 내부에 존재하는 이름으로 클래스를 선언해도 문제가 발생하지 않는다. 하지만 표준 열거형에서는 Red라는 이름만으로 열거형 변수에 값을 넣어줄 수 있었지만 enum class 열거형에서는 열거형 변수에 값을 넣어주기 위해서는 반드시 접근지정자를 이용해서 스코프를 지정해주어야만 컴파일 에러 없이 값을 대입할 수 있게 된다. 이러한 형태의 이유 역시 MSDN에 설명되어 있는데 그것을 보면 왜 이런 형태를 가지는지 충분히 이해할 수 있게 된다.

 

enum class EColor
{
    Red,
    Green,
    Blue,
};

 

enum class로 선언된 EColor의 경우에는 다음과 같은 의미 체계를 가지게 된다.

 

class EColor
{
public:
    static const int Red = 0;
    static const int Green = 1;
    static const int Blue = 2;
};

 

위와 같은 형태를 가지게 된다는 것을 생각해보면 왜 enum class 열거형 변수에 스코프를 지정해 주어야하는지 명확하게 이해할 수 있다.

 

 

 

 

 

2. 표준 열거형은 타입 세이프(Type safe)하지 않다.

일반적으로 프로그래머들은 언어의 동작에서 프로그래머가 무의식적으로 실수를 유도하게 되는 동작을 좋아하지 않는다. 만약 프로그래머가 명시적으로 한 행동에서 문제가 발생하면 명백한 프로그래머의 실수이고 그 문제점을 찾아내기도 쉽지만, 프로그래머가 명시하지 않았는데 언어가 묵시적으로 처리해서 발생하는 문제는 찾아내기 어렵기 때문이다.

 

이러한 측면에서 표준 열거형이 발생시키는 문제가 몇 가지 있다.

 

첫 번째 문제로는 정수형의 변수에 곧바로 대입이 가능하다는 것이다(하지만 정수형에서 열거형으로의 대입은 명시적인 형 변환이 필요하다). 이것으로 인해서 발생하는 문제점에 대해서 경험이 많지 않아서 언급하지는 못하겠지만, 이런 묵시적인 동작으로 인해서 프로그래머가 실수를 할 가능성이 커지는 것은 부인할 수 없는 문제이다.

 

#include <iostream>
using namespace std;

enum EColor
{
    Red,
    Green,
    Blue,
};

int main()
{
    EColor color = Red;
    int i = color;    // 바로 대입이 가능하다.

    return 0;
}

 

하지만 열거형을 enum class로 선언할 경우, 다음과 같이 더 이상 명시적으로 형 변환을 거치지 않는 이상 열거형 변수나 값은 정수형에 대입할 수 없게 된다.

 

#include <iostream>
using namespace std;

enum class EColor
{
    Red,
    Green,
    Blue,
};

int main()
{
    EColor color = EColor::Red;
    int i = static_cast<int>(color);    // 명시적인 형 변환을 거쳐야만 대입이 가능하다.

    return 0;
}

 

두 번째 문제는 명시적인 형 변환 없이도 다른 열거형 간의 비교 연산이 가능하다는 것이다.

 

#include <iostream>
using namespace std;

enum EColor
{
    Red,
    Green,
    Blue,
};

enum EDirection
{
    Forward,
    Backward,
    Right,
    Left,
    Up,
    Down,
};

int main()
{
    EColor color = Red;
    EDirection dir = Up;
   
    if (color == dir)    // 문제없이 비교되고 컴파일 된다.
    {
        cout << "같다" << endl;
    }

    return 0;
}

 

이것이 무슨 문제가 되는지 이해하지 못할 수도 있겠지만, 위의 코드를 살펴보면 EColor 열거형과 EDirection 열거형의 종류와 내용, 그 의미는 명백하게 다르다. 이러한 상황을 프로그래머가 의도한 것이라면 모르겠지만, 이런 상황을 의도하지 않았고 실수로 전혀 다른 열거형을 비교하게 된 것이라면 문제가 될 것이고, 이러한 문제는 컴파일러가 프로그래머에게 알려주지 않기 때문에 찾아내기도 쉽지 않을 것이다.

 

하지만 열거형을 아래와 같이 enum class로 선언하면 명시적인 형 변환을 거치지 않는 이상 다른 열거형에 대한 비교 연산은 불가능해진다.

 

#include <iostream>
using namespace std;

enum class EColor
{
    Red,
    Green,
    Blue,
};

enum class EDirection
{
    Forward,
    Backward,
    Right,
    Left,
    Up,
    Down,
};

int main()
{
    EColor color = EColor::Red;
    EDirection dir = EDirection::Forward;
   
    if (color == (EColor)dir)    // 명시적으로 형 변환을 거친 후에야 비교 연산이 가능해진다.
    {
        cout << "같다" << endl;
    }

    return 0;
}

 

이러한 동작은 프로그래머의 무의식적인 실수로 인한 버그를 막아주는 것이고, 문제를 발생시킬 수 있는 동작의 경우에는 프로그래머가 의도적으로 코드를 작성해야 하고 그로인해 발생할 문제에 대한 대처와 책임을 충분히 해야한다는 것을 의미한다.

 

 

 

[유니티 어필리에이트 프로그램]

아래의 링크를 통해 에셋을 구매하시거나 유니티를 구독하시면 수익의 일부가 베르에게 수수료로 지급되어 채널의 운영에 도움이 됩니다.

 

에셋스토어

여러분의 작업에 필요한 베스트 에셋을 찾아보세요. 유니티 에셋스토어가 2D, 3D 모델, SDK, 템플릿, 툴 등 여러분의 콘텐츠 제작에 날개를 달아줄 다양한 에셋을 제공합니다.

assetstore.unity.com

 

Easy 2D, 3D, VR, & AR software for cross-platform development of games and mobile apps. - Unity Store

Have a 2D, 3D, VR, or AR project that needs cross-platform functionality? We can help. Take a look at the easy-to-use Unity Plus real-time dev platform!

store.unity.com

 

Create 2D & 3D Experiences With Unity's Game Engine | Unity Pro - Unity Store

Unity Pro software is a real-time 3D platform for teams who want to design cross-platform, 2D, 3D, VR, AR & mobile experiences with a full suite of advanced tools.

store.unity.com

[투네이션]

 

-

 

toon.at

[Patreon]

 

WER's GAME DEVELOP CHANNEL님이 Game making class videos 창작 중 | Patreon

WER's GAME DEVELOP CHANNEL의 후원자가 되어보세요. 아티스트와 크리에이터를 위한 세계 최대의 멤버십 플랫폼에서 멤버십 전용 콘텐츠와 체험을 즐길 수 있습니다.

www.patreon.com

[디스코드 채널]

 

Join the 베르의 게임 개발 채널 Discord Server!

Check out the 베르의 게임 개발 채널 community on Discord - hang out with 399 other members and enjoy free voice and text chat.

discord.com

 

반응형

'C++' 카테고리의 다른 글

[C++ 11] static_assert  (0) 2017.05.23
[C++ 11] Auto Vectorization  (1) 2016.11.01
[C++ 11] Range-Based For  (0) 2016.11.01
[C++ 11] Scoped Lock  (0) 2016.11.01

static_assert

 

프로그래밍을 하는 과정에서 버그의 발생과 디버그는 필연적이다. 아무리 설계가 완벽하다고 해도, 코드의 작성자가 인간인 이상 실수로 인하여 버그는 발생하기 때문에 코딩 이후에는 반드시 테스트와 디버그가 이루어져야 한다.

 

게임 프로그래밍의 경우에는 예외처리(Exception Handling)을 성능 상의 문제로 잘 사용하지 않고 개발 도중에 버그를 잡기 위해서 assert를 사용하는 경우가 많다. assert는 <assert.h> 헤더를 포함시키면 사용할 수 있으며 어떤 식이 참인지 거짓인지 판별해주고 그 식이 거짓이라면 에러 메시지 박스를 띄우고 어느 cpp의 몇 번째 줄에서 중단되었는지 알려주고 프로그램이 종료된다. 이 기능은 디버그 빌드에서만 작동하고 릴리즈 빌드에서는 작동하지 않는다.

 

기존의 assert 사용 예시

#include <assert.h>

 

class Player

{

// 플레이어에 대해 정의된 클래스

// ...

}

 

class GameManager

{

// 게임을 관리하는 매니저

// ...

 

static Player* GetPlayer(/*특정조건*/)

{

// 특정 조건에 맞는 플레이어를 반환한다.

// 만약 조건에 맞는 플레이어가 없다면 nullptr을 반환

}

}

 

int main()

{

Player * player = GetPlayer();

// 만약 player가 nullptr이라면 프로그램은 정지되고 에러 메시지 박스를 출력될 것이다.

assert(player != nullptr);

}

 

C++ 11에 들어서 새로 도입된 static_assert라는 것이 있는데 이것은 별도의 헤더를 포함시키지 않고도 사용할 수 있다. 이 static_assert가 기존의 assert와 다른 점은 기존의 assert는 런타임 도중에만 작동해서 해당 코드가 실행되기 전에는 에러가 발생하는지 알기 어려운 반면에 static_assert는 컴파일 타임에 발생하기 때문에 문제가 발생할 부분이라면 해당 코드가 작동하지 않을 확률이 높다고 하더라도 반드시 에러를 잡아낼 수 있다는 것이다. 다만 컴파일 타임에만 작동하는 static_assert의 특성 상 컴파일 타임에 결정되지 않았고 런타임이 되지 않으면 알 수 없는 부분에는 사용할 수 없다. 예를 들자면 위의 assert 예시 코드에서처럼 player 객체가 nullptr인지는 런타임 동안 GetPlayer()함수를 지나봐야만 결정되기 때문에 컴파일 타임에는 알 수 없어서 저런 곳에는 static_assert를 사용할 수 없다.

 

 

static_assert 사용 예시

/* 기존에 사용되던 구조체 a

struct a

{

int i

}

//*/

 

//* 수정된 구조체 a

struct a

{

int i;

float f;

}

//*/

 

int main()

{

static_assert(sizeof(a) == 8, "Old struct a used.");

}

 

위의 예시처럼 구조체 a가 수정된 이후에 실수로 이전 구조체를 사용하고 있는지 컴파일 타임에 확인해서 발생할 버그를 미리 막을 수 있게 된다. 다음은 각 상황에서 static_assert의 반응이다 :

 

실수로 이전의 구조체를 사용한 경우 컴파일 에러를 발생시킨다.

 

구조체를 제대로 사용하면 컴파일 에러를 발생시키지 않는다.

 

static_assert를 사용할 때, 주의할 점은 유니코드와 한글을 지원하지 않기 때문에 에러 메시지를 작성할 때, 멀티바이트 영어로 작성하는게 좋다.

 

 

 

[유니티 어필리에이트 프로그램]

아래의 링크를 통해 에셋을 구매하시거나 유니티를 구독하시면 수익의 일부가 베르에게 수수료로 지급되어 채널의 운영에 도움이 됩니다.

 

에셋스토어

여러분의 작업에 필요한 베스트 에셋을 찾아보세요. 유니티 에셋스토어가 2D, 3D 모델, SDK, 템플릿, 툴 등 여러분의 콘텐츠 제작에 날개를 달아줄 다양한 에셋을 제공합니다.

assetstore.unity.com

 

Easy 2D, 3D, VR, & AR software for cross-platform development of games and mobile apps. - Unity Store

Have a 2D, 3D, VR, or AR project that needs cross-platform functionality? We can help. Take a look at the easy-to-use Unity Plus real-time dev platform!

store.unity.com

 

Create 2D & 3D Experiences With Unity's Game Engine | Unity Pro - Unity Store

Unity Pro software is a real-time 3D platform for teams who want to design cross-platform, 2D, 3D, VR, AR & mobile experiences with a full suite of advanced tools.

store.unity.com

[투네이션]

 

-

 

toon.at

[Patreon]

 

WER's GAME DEVELOP CHANNEL님이 Game making class videos 창작 중 | Patreon

WER's GAME DEVELOP CHANNEL의 후원자가 되어보세요. 아티스트와 크리에이터를 위한 세계 최대의 멤버십 플랫폼에서 멤버십 전용 콘텐츠와 체험을 즐길 수 있습니다.

www.patreon.com

[디스코드 채널]

 

Join the 베르의 게임 개발 채널 Discord Server!

Check out the 베르의 게임 개발 채널 community on Discord - hang out with 399 other members and enjoy free voice and text chat.

discord.com

 

반응형

'C++' 카테고리의 다른 글

[C++11] enum class  (1) 2017.07.17
[C++ 11] Auto Vectorization  (1) 2016.11.01
[C++ 11] Range-Based For  (0) 2016.11.01
[C++ 11] Scoped Lock  (0) 2016.11.01

요즘 칩에는 벡터 레지스터라는 개념으로 메모리를 배치하는데, 이 벡터 레지스터 하나에 정수형이나 실수형 4개를 한꺼번에 담아둘 수 있다고 한다.

 

loop 작업시 float이나 int 형식의 4바이트 변수의 배열에 대해 하나씩 훑으며 작업할 때 굳이 하나씩 작업하지 않고 4개를 묶어서 16바이트를 벡터 형식으로 한 번에 처리할 수 있고 이것은 물론 스칼라 형식으로 하나씩 작업하는 것보다 빠르다(이론 상 최대 4배까지 빠를 수 있다).

 

이런 것을 Vectorization이라고 한다. 일반적으로 최신 컴파일러는 이런 Vectorization을 자동으로 수행해준다.

 

어떤 조건하에선 수행하지 못하는 경우가 있다. 수백만줄의 코드를 모두 체크하지는 못하기 때문에 이 체크에 실패하면 Auto Vectorization에 실패한다.

 

Auto Vectorization에 실패했을때 알아내는 방법은 프로그래머는 어셈블리어 코드를 확인하는 방법 밖에 없는데 이것은 어려운 일이다

그리고 Debug 빌드에서는 최종 코드를 내는 것이 아니기 때문에 최적화 기능의 상당부분이 꺼져있어서 Vectorization 또한 수행되지 않는다.

 

10000개 짜리 float 배열 두 개에 대한 loop 작업을 10000회를 수행했을 때의 결과물이다.

 

Release : x64

역으로 성능이 감소하는 케이스가 발생하기는 하지만 대부분 1%이하의 확률이다.

평균적으로 40~50%의 성능 향상을 기대할 수 있다.

 

Release : x86

x64 버전과 거의 비슷한 수준의 결과물을 얻을 수 있다.

반복적으로 실험한 결과 평균 성능 향상 비율을 2~10% 가량 떨어진다.

 

Debug

Debug 빌드에서는 최적화 기능이 꺼져있기 때문에 성능 향상이 거의 없음을 알 수 있다.

 

 

Vectorization 성능 비교 코드

#include <iostream>
#include <string>
#include <Windows.h>
using namespace std;
#define ARRSIZE 100000
class Vectorization
{
private:
    int mWorkCount;
    __int64 mTotalVectorTime;
    __int64 mTotalNoVectorTime;
    __int64 mNowVectorTime;
    __int64 mNowNoVectorTime;
    float mMinImproveTime;
    float mMaxImproveTime;
    int mPerformDecreseCase;
    int mPerformIncreseCase;
    int mTotalWorkCount;
public:
    Vectorization() : mWorkCount(0), mTotalVectorTime(0), mTotalNoVectorTime(0),
        mNowVectorTime(0), mNowNoVectorTime(0), mMinImproveTime(0.f), mMaxImproveTime(0.f),
        mPerformDecreseCase(0), mPerformIncreseCase(0), mTotalWorkCount(0)
    {}
    void CalculateWorkTime(float* a, float* b, int arrsize, bool isVector)
    {
        __int64 t1;
        __int64 t2;
        for (int i = 0; i < arrsize; i++)
        {
            a[i] = b[i] = i;
        }
        QueryPerformanceCounter((LARGE_INTEGER*)&t1);
        if (isVector)
        {
            #pragma loop(vector)
            for (int i = 0; i < arrsize; i++)
            {
                a[i] += b[i];
            }
        }
        else
        {
            #pragma loop(no_vector)
            for (int i = 0; i < arrsize; i++)
            {
                a[i] += b[i];
            }
        }
        QueryPerformanceCounter((LARGE_INTEGER*)&t2);
        if (isVector)
        {
            mTotalVectorTime += mNowVectorTime = t2 - t1;
        }
        else
        {
            mTotalNoVectorTime += mNowNoVectorTime = t2 - t1;
        }
    }
    void CalcNowWorkImprovePerform()
    {
        float ImproveRate = 100.0f - (float)mNowVectorTime / (float)mNowNoVectorTime * 100.0f;
        if (ImproveRate < mMinImproveTime)
        {
            mMinImproveTime = ImproveRate;
        }
        else if (ImproveRate > mMaxImproveTime)
        {
            mMaxImproveTime = ImproveRate;
        }
        if (ImproveRate < 0)
        {
            mPerformDecreseCase++;
        }
        else
        {
            mPerformIncreseCase++;
        }
        cout << "현재 벡터화 작업시간 :: " << mNowVectorTime << endl;
        cout << "현재 비벡터화 작업시간 :: " << mNowNoVectorTime << endl;
        printf("현재 성능 향상 비율 :: %.2f %%\n\n", ImproveRate);
        mTotalWorkCount++;
    }
    void CalcTotalWorkImprovePerform()
    {
        float improveRate = 100.0f - (float)mTotalVectorTime / (float)mTotalNoVectorTime * 100.0f;
        cout << "총 벡터화 작업시간 :: " << mTotalVectorTime << endl;
        cout << "총 비벡터화 작업시간 :: " << mTotalNoVectorTime << endl << endl;;
        printf("최저 성능 향상 비율 :: %.2f %%\n", mMinImproveTime);
        printf("최고 성능 향상 비율 :: %.2f %%\n", mMaxImproveTime);
        printf("평균 성능 향상 비율 :: %.2f %%\n\n", improveRate);
        cout << "성능 감소 케이스 :: " << mPerformDecreseCase << "회" << endl;
        printf("성능 감소 케이스 비율 :: %.2f %%\n\n", (float)mPerformDecreseCase / (float)mTotalWorkCount * 100.f);
        cout << "성능 향상 케이스 :: " << mPerformIncreseCase << "회" << endl;
        printf("성능 향상 케이스 비율 :: %.2f %%\n\n", (float)mPerformIncreseCase / (float)mTotalWorkCount * 100.f);
        cout << "총 성능 비교 횟수 :: " << mTotalWorkCount << "회" << endl;
    }
};
 
int main()
{
    Vectorization v;
    float fArr1[ARRSIZE];
    float fArr2[ARRSIZE];
    for (int i = 0; i < 10000; i++)
    {
        v.CalculateWorkTime(fArr1, fArr2, ARRSIZE, true);
        v.CalculateWorkTime(fArr1, fArr2, ARRSIZE, false);
        v.CalcNowWorkImprovePerform();
    }
    v.CalcTotalWorkImprovePerform();
}

 

 

 

[유니티 어필리에이트 프로그램]

아래의 링크를 통해 에셋을 구매하시거나 유니티를 구독하시면 수익의 일부가 베르에게 수수료로 지급되어 채널의 운영에 도움이 됩니다.

 

에셋스토어

여러분의 작업에 필요한 베스트 에셋을 찾아보세요. 유니티 에셋스토어가 2D, 3D 모델, SDK, 템플릿, 툴 등 여러분의 콘텐츠 제작에 날개를 달아줄 다양한 에셋을 제공합니다.

assetstore.unity.com

 

Easy 2D, 3D, VR, & AR software for cross-platform development of games and mobile apps. - Unity Store

Have a 2D, 3D, VR, or AR project that needs cross-platform functionality? We can help. Take a look at the easy-to-use Unity Plus real-time dev platform!

store.unity.com

 

Create 2D & 3D Experiences With Unity's Game Engine | Unity Pro - Unity Store

Unity Pro software is a real-time 3D platform for teams who want to design cross-platform, 2D, 3D, VR, AR & mobile experiences with a full suite of advanced tools.

store.unity.com

[투네이션]

 

-

 

toon.at

[Patreon]

 

WER's GAME DEVELOP CHANNEL님이 Game making class videos 창작 중 | Patreon

WER's GAME DEVELOP CHANNEL의 후원자가 되어보세요. 아티스트와 크리에이터를 위한 세계 최대의 멤버십 플랫폼에서 멤버십 전용 콘텐츠와 체험을 즐길 수 있습니다.

www.patreon.com

[디스코드 채널]

 

Join the 베르의 게임 개발 채널 Discord Server!

Check out the 베르의 게임 개발 채널 community on Discord - hang out with 399 other members and enjoy free voice and text chat.

discord.com

 

반응형

'C++' 카테고리의 다른 글

[C++11] enum class  (1) 2017.07.17
[C++ 11] static_assert  (0) 2017.05.23
[C++ 11] Range-Based For  (0) 2016.11.01
[C++ 11] Scoped Lock  (0) 2016.11.01

+ Recent posts