데이터 테이블(Data Table) 사용하기

 

작성 기준 버전 :: 4.21.1

 

게임을 제작할 때 레벨업에 필요한 경험치량이나 스킬의 계수 등 추후에 밸런스 수정 작업이 필요한 값들은 함부로 코드에 상수로 넣어서는 안된다. 이런 부분은 기획자가 손쉽게 접근이 가능해야 하기 때문에, 기획자들이 주로 사용하는 엑셀이나 스프레드시트의 데이터를 언리얼 엔진으로 임포트해서 사용하는 방식을 지원한다. 이것을 데이터 주도형 접근법이라고 한다.

 

언리얼 엔진에서는 기획자들이 주로 사용하는 엑셀이나 스프레드시트에서 손쉽게 만들어낼 수 있는 .CSV 파일이나 서버 프로그램에서 주로 사용되는 JSON 파일을 손쉽게 임포트하는 기능을 제공한다.

 

 

데이터 테이블 임포트
 

데이터 테이블은 유용한 방식으로 짜여진 표를 의미한다. .CSV 파일을 임포트하기 위해서는 우선 프로그래머가 데이터를 엔진이 인식할 수 있게 Row 컨테이너를 만들어서 엔진에 데이터 해석 방식을 알려줘야 한다.

 

우리가 예시로 사용할 .CSV 파일은 다음 레벨업까지 필요한 경험치의 양에 대한 것이고 그 내용은 다음과 같다.

 

Name,ExpToNextLevel,TotalExp
1,0,0
2,100,100
3,200,300
4,300,500
5,400,700
6,500,900
7,600,1100
8,700,1300
9,800,1500
10,1600,2400

 

LevelUpTable.csv
다운로드

 

이런 컨테이너를 만드는 방법은 두 가지가 있는데 블루프린트를 이용하는 방식과 C++ 코드를 통해 만드는 방식이 있다.

 

 

블루프린트

 

데이터 테이블 로우를 만들기 위해서는 구조체를 생성해야 한다. 구조체의 이름은 BP_LevelUpTableRow로 한다.

 

 

블루프린트 구조체가 생성되면 더블클릭해서 블루프린트 구조체 에디터를 열고 변수를 추가한다. 추가하는 변수의 이름은 ExpToNextLevel과 TotalExp로 각 열의 이름과 순서가 일치해야 한다. 제일 첫 열인 Name은 게임 내에게 각 행에 접근하는 이름이 되는 것으로 따로 변수를 추가하지 않아도 된다.

 

 

변수를 모두 추가한 뒤에는 구조체를 저장하고 에디터를 닫는다. 그리고 콘텐츠 브라우저 패널에서 파일 창에 우클릭하여 /Game에 임포트... 를 선택한다.

 

 

CSV 파일을 임포트한다.

 

 

데이터 테이블 옵션 창이 뜨면 데이터 테이블 행 유형 선택을 방금 추가한 구조체로 설정하고 확인을 누른다.

 

 

추가된 데이터 테이블을 열어보면 .CSV 파일의 내용이 훌륭하게 임포트된 것을 확인할 수 있다.

 

 

 

C++ 코드

 

행 컨테이너를 블루프린트 구조체로 만들 경우, C++ 코드에서는 사용할 수 없다는 단점이 있다. C++ 코드에서 사용하기 위해서는 USTRUCT로 만들어야 되는데 언리얼 구조에 대한 설명은 C++ / USTRUCT 사용자 정의 구조체 만들기 문서에서 참고할 수 있다.

 

우선 Actor 클래스를 상속받아서 CustomDataTables라는 더미 클래스를 생성한다.

 

 

클래스가 생성되면 전처리기와 클래스 선언 사이에 구조체를 선언하는 코드를 추가해준다. 행 컨테이너로 사용되는 구조체는 FTableRowBase를 상속받아야만 한다.

 

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

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Engine/DataTable.h"
#include "CustomDataTables.generated.h"

USTRUCT(BlueprintType)
struct FLevelUpTableRow : public FTableRowBase
{
    GENERATED_BODY()

public:

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LevelUp")
        int32 ExpToNextLevel;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LevelUp")
        int32 TotalExp;
};

UCLASS()
class DATATABLETEST_API ACustomDataTables : public AActor
{
    GENERATED_BODY()
   
};

 

코드를 완성하고 프로젝트를 빌드한 뒤, 에디터로 돌아가서 .CSV 파일을 임포트해서 데이터 테이블 옵션의 데이터 테이블 행 유형 선택 드롭다운 메뉴를 열어보면 우리가 방금 추가한 LevelUpTableRow가 있는 것을 확인할 수 있다.

 

 

이를 통해서 .CSV 파일을 임포트하면 아까 블루프린트 구조체를 통해서 임포트했을 때와 동일하게 DataTable이 생성되는 것을 볼 수 있다.

 

 

 

 

 

데이터 테이블 사용하기

 

이번 파트에서는 작성한 데이터 테이블을 사용하는 방법에 대해서 알아보자.

 

블루프린트에서 데이터 테이블 사용하기

 

블루프린트에서 데이터 테이블을 사용하기 위해서는 블루프린트 그래프의 빈 자리에 우클릭해서 컨텍스트 메뉴를 열고 "데이터 테이블 행 구하기"를 검색해서 이 노드를 배치하면 된다.

 

 

GameModeBase를 상속받는 블루프린트 클래스를 하나 생성한다. 이벤트 그래프의 BeginPlay 이벤트로부터 다음과 같이 블루프린트 그래프를 구성하자.

 

 

위 그래프는 LevelUpTable에서 각 행을 가져와서 Total Exp 값을 화면에 출력하는 역할을 한다.

 

블루프린트를 저장하고 에디터로 가서 월드 세팅의 Game Mode를 방금 추가한 게임 모드로 바꿔준다.

 

 

그 다음 에디터에서 플레이 버튼을 눌러보면 화면에 각 레벨의 Total Exp가 연속으로 출력되는 것을 볼 수 있다.

 

 

 

C++ 코드에서 데이터 테이블 사용하기

 

C++ 코드에서 데이터 테이블을 사용하는 과정은 블루프린트에서 노드 하나만 생성하면 되는 것에 비해서는 조금 복잡하다.

 

우선 프로젝트에 DataTableTestGameModeBase라는 이름으로 새 게임 모드 클래스를 만들고 헤더에 다음 멤버 변수와 함수를 추가한다.

 

public:
    ADataTableTestGameModeBase();

    virtual void BeginPlay() override;

private:
    class UDataTable* LevelUpDataTable;

 

그리고 DataTableTestGameModeBase라는 .cpp로 가서 다음 전처리기를 추가한다.

 

#include "CustomDataTables.h"
#include "UObject/ConstructorHelpers.h"

 

ADataTableTestGameModeBase::ADataTableTestGameModeBase() 생성자 함수와 BeginPlay() 함수를 다음과 같이 구현한다.

 

ADataTableTestGameModeBase::ADataTableTestGameModeBase()
{
    static ConstructorHelpers::FObjectFinder<UDataTable> DataTable(TEXT("/Game/LevelUpTable"));
    if (DataTable.Succeeded())
    {
        LevelUpDataTable = DataTable.Object;
    }
}

void ADataTableTestGameModeBase::BeginPlay()
{
    Super::BeginPlay();

    if (LevelUpDataTable != nullptr)
    {
        for (int32 i = 1; i <= 10; i++)
        {
            FLevelUpTableRow* LevelUpTableRow = LevelUpDataTable->FindRow<FLevelUpTableRow>(FName(*(FString::FormatAsNumber(i))), FString(""));
            UE_LOG(LogTemp, Log, TEXT("Lv.%d :: ExpToNextLevel(%d) TotalExp(%d)"), i, (*LevelUpTableRow).ExpToNextLevel, (*LevelUpTableRow).TotalExp);
        }
    }
}

 

코드 작성이 완료되면 프로젝트를 빌드하고 에디터로 넘어간다.

 

그 다음 월드 세팅에서 게임 모드를 방금 만든 것으로 교체한다.

 

 

플레이 버튼을 눌러보면 게임이 시작되면서 데이터 테이블의 값들을 가져와서 로그를 출력하는 것을 확인할 수 있다.

 

 

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

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

 

에셋스토어

여러분의 작업에 필요한 베스트 에셋을 찾아보세요. 유니티 에셋스토어가 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

 

반응형

다른 레벨로 이동하기

 

작성 기준 버전 :: 4.21.1

 

대규모 오픈 월드 게임이 아닌 경우 장소를 이동할 때나 대규모 오픈 월드 게임이더라도 특정한 장소로 이동하는 경우에도 레벨 혹은 씬을 전환하는 방식을 주로 사용한다. 이번 섹션에서는 다른 레벨로 이동하는 방법에 대해서 알아본다.

 

프로젝트 생성

 

LevelOpenTest라는 이름으로 C++ 삼인칭 프로젝트 하나를 새로 생성한다.

 

 

 

레벨 구성

 

프로젝트가 생성되면 레벨은 다음과 같이 꾸며져 있을 것이다.

 

 

그 레벨을 다음과 같이 수정한다. 새로 만든 구역에 들어가면 다른 레벨로 이동하도록 만들 예정이다.

 

 

이 다음에는 이동할 레벨을 추가한다.

 

 

그리고 새로 추가한 레벨을 다음과 같이 꾸민다.

 

 

이 맵을 SecondMap이라는 이름으로 저장한다.

 

 

 

 

 

 

LevelTransferVolume

 

이 다음에는 콜리전에 플레이어가 닿으면 다른 레벨로 전송시키는 볼륨을 만든다.

 

Actor 클래스를 상속받아서 LevelTransferVolume 클래스를 만든다.

 

 

LevelTransferVolume.h에서 다음 함수와 멤버 변수를 선언한다.

 

protected:
    virtual void NotifyActorBeginOverlap(AActor* OtherActor) override;

private:
    UPROPERTY(EditAnywhere, meta = (AllowPrivateAccess = "true"))
    FString TransferLevelName;

    UPROPERTY()
    class UBoxComponent* TransferVolume;

 

LevelTransferVolume.cpp로 넘어가서 박스 컴포넌트의 기능과 레벨 이동 함수를 사용하기 위해서 다음 전처리기를 추가한다.

 

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

#include "Kismet/GameplayStatics.h"

 

생성자 함수에 TransferVolume를 초기화 하는 코드를 추가한다.

 

TransferVolume = CreateDefaultSubobject<UBoxComponent>(TEXT("TransferVolume"));
RootComponent = TransferVolume;
TransferVolume->SetCollisionProfileName(TEXT("OverlapOnlyPawn"));

 

NotifyActorBeginOverlap() 함수를 구현한다.

 

void ALevelTransferVolume::NotifyActorBeginOverlap(AActor * OtherActor)
{
    APawn* Pawn = Cast<APawn>(OtherActor);
    if (Pawn != nullptr)
    {
        UGameplayStatics::OpenLevel(this, TransferLevelName);
    }
}

 

UGamePlayStatics 클래스의 OpenLevel() 함수가 이번 섹션의 주 목적이다. OpenLevel() 함수를 호출해서 이동하고자 하는 레벨의 이름을 넣어주면 원하는 레벨로 이동이 가능하다.

 

코드 작성이 완료되면 프로젝트를 빌드하고 언리얼 에디터로 돌아간다.

 

기본 맵의 새로 뚫어놓은 위치에 볼륨을 배치한 뒤 Transfer Level Name을 SecondMap으로 설정한다.

 

 

플레이해서 볼륨을 배치한 곳으로 캐릭터를 이동시키면 지정해준 다른 맵으로 이동하는 것을 확인할 수 있다.

 

 

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

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

 

에셋스토어

여러분의 작업에 필요한 베스트 에셋을 찾아보세요. 유니티 에셋스토어가 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++ / UENUM 사용자정의 열거형 만들기

 

작성 기준 버전 :: 4.21.1

 

C++ enum class 문서에서 확인할 수 있듯이, 열거형은 정수형 상수를 사람이 알아보기 쉽게 만들어준다. 이러한 열거형도 언리얼 구조체에서의 문제와 같이 표준 열거형은 코드 내부에서만 사용이 가능하고, 언리얼 에디터의 디테일 패널이나, 블루프린트에서 사용이 불가능하다는 문제가 있다.

 

이 열거형 역시 언리얼 구조체와 마찬가지로 언리얼 에디터에서 사용하고자 한다면 언리얼 열거형으로 만들어야만 한다.

 

언리얼 열거형 만들기

 

언리얼 전용의 열거형을 만드는 방법은 다음과 같다. 예시 코드와 같이 새로 만드는 열거형 앞에 UENUM() 매크로를 붙여주면 언리얼 에디터에서도 사용 가능한 언리얼 열거형이 만들어진다.

 

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

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "EnumTestActor.generated.h"

UENUM(BlueprintType)
enum class ETestEnum : uint8
{
    TE_OptionA UMETA(DisplayName = "Option A"),
    TE_OptionB UMETA(DisplayName = "Option B"),
};

UCLASS()
class ENUMTEST_API AEnumTestActor : public AActor
{
    GENERATED_BODY()
   
public:   
    // Sets default values for this actor's properties
    AEnumTestActor();

protected:
    // Called when the game starts or when spawned
    virtual void BeginPlay() override;

public:   
    // Called every frame
    virtual void Tick(float DeltaTime) override;

    UPROPERTY(EditAnywhere)
    ETestEnum TestEnum;
};

 

단, 여기서 주의해야할 점이 있는데, 언리얼 열거형을 만들때 반드시 일반적인 enum이 아닌 enum class로 만들어야 한다는 점이다. 만약 enum class로 만들지 않고 일반적인 enum으로 만들어서 UENUM() 매크로를 붙이고 컴파일을 하면 에러가 발생해서 컴파일에 실패한다. 그리고 UENUM은 uint8만을 지원하기 때문에 이 부분도 빠뜨리지 않고 넣어주어야 한다.

 

코드를 작성하고 컴파일한 후 레벨에 EnumTestActor를 배치하고 디테일 패널을 살펴보면 추가한 열거형이 보이는 것을 확인할 수 있다.

 

 

UENUM을 "BlueprintType"으로 선언하면 블루프린트에서도 사용할 수 있게 된다.

 

 

만약 새롭게 정의한 UENUM이 한 클래스에서 사용되는 것이 아니라 다른 코드 전반에서 사용되기를 원한다면, 언리얼 구조체 문서에서 설명한 것과 같이 열거형을 정의하기 위한 빈 클래스를 하나 추가해서 그 헤더에 UENUM을 선언하고 사용하고자 하는 곳에 그 헤더를 포함시키는 방법을 쓸 수 있다.

 

[투네이션]

 

-

 

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

 

 

반응형

캐스팅(Casting)

 

작성 기준 버전 :: 4.21 - 4.26

 

프로그래밍 작업을 할 때 캐스팅, 즉 형 변환은 상당히 중요하다. 특히 여러 클래스가 상속으로 엮여있는 상황이라면 더더욱 중요해진다. 언리얼 엔진에서는 대부분의 클래스가 AActor 클래스를 상속받고 있고, 개발자가 만들어내는 클래스 역시 상당수는 AActor를 상속받게 된다.

 

C++

 

그렇기 때문에 몇몇 함수들은 이렇게 메인이 되는 부모 클래스를 매개변수로 받거나 돌려준다. 간단한 예를 들자면 다음 함수가 있다.

 

void AActor::NotifyActorBeginOverlap(AActor* OtherActor)
{

}

 

NotifyActorBeginOverlap() 함수는 액터의 콜리전에 콜리전을 가진 다른 액터가 들어오기 시작했을 때 호출되는 함수로, 매개변수를 통해서 자신의 콜리전과 접촉한 액터를 알려준다. 언리얼 엔진에서는 레벨에 배치되는 모든 오브젝트는 AActor 클래스를 상속받기 때문에, 콜리전과 접촉한 액터가 어떤 클래스던지 상관없이 무조건 AActor 클래스로 보내주는 것이다.

 

만약 콜리전 체크를 하는 액터가 겹침 이벤트가 발생할때마다 데미지를 입는 클래스인데 데미지를 입힐 수 있는 클래스가 AProjectile 클래스라고 가정했을 때, 위의 예시 코드처럼 별도의 검사를 하지 않는다면, 액터가 아무 물체에나 스칠 때마다 데미지를 입어버릴 것이다.

 

그래서 필요한 것이 바로 캐스팅이다. 언리얼 엔진에서는 Cast<T>() 라는 함수로 기본적인 캐스팅을 제공한다.

 

void AActor::NotifyActorBeginOverlap(AActor* OtherActor)
{
    AProjectile* Projectile = Cast<AProjectile>(OtherActor);
    if (Projectile)
    {
        // Damage Process
    }
}

 

바로 위의 예시 코드처럼 캐스팅을 진행하면 된다. 만약 콜리전에 검출된 액터가 AProjectile 클래스가 아니라면 캐스팅에 실패할 것이고 Projectile 변수의 값을 nullptr이 되기 때문에 if문 안으로 진행하지 못해서 Damage Process가 진행되지 않는다.

 

블루프린트

 

블루프린트 작업에서도 캐스팅이 가능하다.

 

 

 

블루프린트 컨텍스트 메뉴에서 "형변환"이나, 캐스팅하고자 하는 타입의 클래스 명을 검색하면 해당 클래스로 형변환할 수 있는 노드를 추가할 수 있다.

 

[투네이션]

 

-

 

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

 

반응형

RPG :: 캐릭터를 가리는 벽 투명하게 만들기

 

작성 기준 버전 :: 4.21.1

 

탑/다운에 가까운 쿼터뷰에서 내려보는 RPG에서는 건물이나 벽, 기둥 같은 오브젝트에 캐릭터가 가려지는 경우가 많다.

 

 

이런 경우 플레이어는 자신의 캐릭터의 위치를 찾기가 어려워지고 플레이에 매우 큰 지장을 준다. 그렇기 때문에 개발자들은 이런 문제를 해소하기 위해서 여러 가지 테크닉을 사용하는데 그 중 대표적인 것이 캐릭터를 가리고 있는 벽을 투명하게 만드는 것이다.

 

이 섹션에서는 캐릭터를 가리고 있는 벽을 투명하게 만드는 방법에 대해서 배워볼 것이다.

 

본격적인 내용에 들어가기에 앞서, 이 섹션은 진행하기 위해서는 다음과 같은 선행 지식이 필요하다.

 

머티리얼 인스턴싱 :: 머티리얼 파라미터와 머티리얼 인스턴스

C++ 코드에서 머티리얼 인스턴스 다이내믹 생성하고 다루기

콜리전과 콜리전 이벤트

 

이번 섹션은 지난 섹션 중 RPG :: 마우스 입력 이동 구현하기 섹션에 이어서 진행되는 섹션이다.

 

 

설계

 

이 기능을 구현하기 위한 설계는 다음과 같다.

 

1. 벽이나 기둥 같은 오브젝트가 가리는 위치에 캐릭터가 존재하고 있는지 확인하기 위한 콜리전을 깔아줄 SeeingThroughCollision 클래스

 

2. SeeingThroughCollision에게 신호를 받아서 투명하게 만들어질 SeeingThroughActor

 

 

이렇게 바닥에 콜리전을 까는 방법 이외에도 캐릭터와 카메라 사이에 콜리전을 두고 여기에 벽이나 기둥같은 오브젝트가 닿으면 투명하게 만드는 방법도 생각해볼 수 있다.

 

 

기능 구현

 

SeeingThroughActor

 

SeeingThroughActor 구현

 

새 C++ 클래스를 하나 추가하자. Actor 클래스를 상속방아서 SeeingThroughActor라는 이름으로 클래스를 생성한다.

 

 

이 클래스는 콜리전으로부터 통지를 받아서 자신의 머티리얼을 투명하게 하는 역할을 한다.

 

SeeingThroughActor.h에 다음 멤버 변수들을 추가한다.

 

private:
    UPROPERTY(EditAnywhere, meta = (AllowPrivateAccess = "true"))
    class UStaticMeshComponent* SeeingTroughMesh;

    UPROPERTY(EditAnywhere, meta = (AllowPrivateAccess = "true"))
    float SeeingThroughTime;

    bool bSeeingThrough;

    float RunningTimer;

 

먼저 SeeingThroughMesh는 메시가 가진 머티리얼을 다이내믹 인스턴스로 만들어서 투명화 작업을 하기 위한 변수이다.

 

SeeingThroughTime은 메시가 투명해지는데까지 걸리는 시간에 대한 변수이다. 이 변수는 외부에 공개해서 다른 개발자가 수정할 수 있게 하였다.

 

bSeeingThrough는 지금 메시가 투명해지는 중인지 아니면 불투명해지는 중인지에 대한 변수이다.

 

RunningTimer는 진행도를 위한 변수이다.

 

그 다음에는 SeeingThroughActor.cpp로 가서 스태틱 메시 컴포넌트의 기능을 사용하기 위해서 다음 전처리기를 추가한다.

 

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

 

그리고 ASeeingThroughActor::ASeeingThroughActor() 생성자 함수로 가서 멤버 변수들을 초기화하는 코드를 추가한다.

 

SeeingTroughMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("SeeingThroughMesh"));
RootComponent = SeeingTroughMesh;
SeeingTroughMesh->CreateDynamicMaterialInstance(0);

SeeingTroughMesh->SetCollisionProfileName(TEXT("InvisibleWall"));

SeeingThroughTime = 0.3f;
RunningTimer = 0.0f;
bSeeingThrough = false;

 

SeeingThroughMesh를 생성한 뒤, RootComponent로 설정해주고 다이내믹 머티리얼 인스턴스를 생성한다. 그리고 콜리전 프로필을 InvisibleWall로 설정해주는데, 이것은 벽이 물체는 가로막고 마우스를 클릭했을때 발생하는 트레이스는 통과시키기 위함이다.

 

그리고 투명해지는데 걸리는 시간은 기본으로 0.3초로 설정한다.

 

BeginPlay() 함수에 다음 코드를 추가한다.

 

PrimaryActorTick.SetTickFunctionEnable(false);

 

게임 시작했을때 Tick() 함수를 작동하지 않도록 만들어서 성능 낭비가 없도록 한다.

 

그 다음 Tick() 함수를 다음과 같이 구현한다.

 

void ASeeingThroughActor::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);

    RunningTimer += DeltaTime;

    float Opacity = 0.0f;
    if (bSeeingThrough)
    {
        Opacity = FMath::Lerp(1.0f, 0.0f, RunningTimer * (1.0f / SeeingThroughTime));
    }
    else
    {
        Opacity = FMath::Lerp(0.0f, 1.0f, RunningTimer * (1.0f / SeeingThroughTime));
    }

    SeeingTroughMesh->SetScalarParameterValueOnMaterials(TEXT("Opacity"), Opacity);

    if (RunningTimer > SeeingThroughTime)
    {
        RunningTimer = 0.0f;
        PrimaryActorTick.SetTickFunctionEnable(false);
    }
}

 

위의 코드는 틱이 작동하는 동안 bSeeingThrough의 상태에 따라서 다이내믹 머티리얼 인스턴스의 머티리얼 파라미터 "Opacity"를 0에서 1로 만들거나 1에서 0으로 만들고 작동이 끝나면 Tick() 함수의 작동을 멈추게 한다. 참고로 머티리얼 파라미터 "Opacity"는 이후 작업에서 추가한다.

 

다음 함수의 선언을 헤더에 추가한다.

 

public:

    void SetShowSeeingThrough(bool bThroughShow);

 

그리고 SeeingThroughActor.cpp로 가서 함수 구현을 완료한다.

 

void ASeeingThroughActor::SetShowSeeingThrough(bool bThroughShow)
{
    bSeeingThrough = bThroughShow;
    if (RunningTimer != 0.0f)
    {
        RunningTimer = SeeingThroughTime - RunningTimer;
    }
    PrimaryActorTick.SetTickFunctionEnable(true);
}

 

SetShowSeeingThrough() 함수는 bShow 변수를 받아서 액터가 투명해지기 시작하는지 불투명해지기 시작하는지 결정한 뒤 Tick() 함수를 작동시킨다.

 

코드 작업이 끝났다면 솔루션 탐색기에서 프로젝트를 빌드한 뒤, 에디터로 돌아간다.

 

투명해지는 만들기

 

이번에는 SeeingThroughActor가 사용할 머티리얼을 만들 차례이다. Props 폴더 안에 Materials 폴더를 만든 다음, 콘텐츠 브라우저 패널의 파일 창에 우클릭해서 머티리얼을 선택한다. 그리고 생성된 머티리얼의 이름을 M_SeeingThrough로 한다.

 

 

머티리얼을 더블클릭해서 머티리얼 에디터를 열고 디테일 패널에서 Material 카테고리의 Blend Mode를 Translucent로 설정한다.

 

 

그리고 TextureSamleParameter2D와 ScalarParameter를 추가하고 각각 이름을 Texture와 Opacity로 한다.

 

 

그리고 Opacity 파라미터 노드를 선택한 뒤, 디테일 패널에서 Default Value를 1로 설정한다.

 

그 다음, 적용과 저장을 하고 머티리얼 에디터를 닫는다.

 

언리얼 에디터로 돌아와서, 콘텐츠 브라우저 패널에서 방금 만든 머티리얼을 우클릭하고 머티리얼 인스턴스 생성을 선택한다.

 

 

머티리얼 인스턴스가 만들어지면 더블클릭해서 머티리얼 인스턴스 에디터를 열고 디테일 패널에서 Opacity를 체크해주고 머티리얼 인스턴스를 저장한 뒤, 머티리얼 인스턴스 에디터를 닫는다.

 

 

SeeingThroughActor 배치

 

이제 SeeingThroughActor를 배치할 차례이다. 콘텐츠 브라우저 패널에서 SeeingTroughActor를 찾아서 벽 토대 위에 배치한다.

 

 

SeeingThroughActor의 SeeingThroughMesh를 선택하고 Static Mesh에 Box를 할당한다.

 

 

그 다음, Material에 방금 전에 만든 M_SeeingThrough_Inst를 할당한다.

 

 

SeeingThroughActor의 스케일을 {1.0, 12.0, 5.0}으로 설정한다.

 

 

액터를 복사해서 다음과 같이 만든다.

 

 

 

 

 

SeeingThroughCollision

 

SeeingThroughCollision 구현

 

Actor 클래스를 상속받아서 SeeingThroughCollision 이라는 이름으로 클래스를 생성한다.

 

 

SeeingThroughCollision.h에 다음 멤버 변수 선언을 추가한다.

 

private:
    UPROPERTY()
    class UBoxComponent* SeeingThroughCollision;

    UPROPERTY(EditAnywhere, meta = (AllowPrivateAccess = "true"))
    TArray<class ASeeingThroughActor*> SeeingThroughActors;

 

SeeingThroughActor를 배열로 선언한 이유는 SeeingThroughCollision과 SeeingThroughActor를 1:1 매치를 시킬 수도 있지만, 1:N의 매치도 가능하게 유연성을 주기 위한 것이다.

 

SeeingThroughCollision.cpp로 가서 박스 컴포넌트와 SeeingThroughActor의 기능을 쓰기 위해서 다음 전처리기들을 추가한다.

 

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

#include "SeeingThroughActor.h"

 

ASeeingThroughCollision::ASeeingThroughCollision() 생성자 함수에 초기화 코드를 추가한다.

 

SeeingThroughCollision = CreateDefaultSubobject<UBoxComponent>(TEXT("SeeingThroughCollision"));
RootComponent = SeeingThroughCollision;
SeeingThroughCollision->SetCollisionProfileName(TEXT("OverlapOnlyPawn"));

 

콜리전의 프로필 네임을 OverlapOnlyPawn으로 설정해서 폰이 오버랩되었을 때만 반응하도록 만든다.

 

다시 SeeingThroughCollision.h로 가서 겹침 이벤트를 받아서 처리할 함수를 덮어씌우는 코드를 추가한다.

 

protected:
    virtual void NotifyActorBeginOverlap(AActor* OtherActor) override;

    virtual void NotifyActorEndOverlap(AActor* OtherActor) override;

 

그리고 SeeingThroughCollision.cpp로 가서 두 함수를 구현한다.

 

void ASeeingThroughCollision::NotifyActorBeginOverlap(AActor * OtherActor)
{
    ARpgCharacter* Character = Cast<ARpgCharacter>(OtherActor);
    if (Character)
    {
        for (auto SeeingThroughActor : SeeingThroughActors)
        {
            SeeingThroughActor->SetShowSeeingThrough(true);
        }
    }
}

void ASeeingThroughCollision::NotifyActorEndOverlap(AActor * OtherActor)
{
    ARpgCharacter* Character = Cast<ARpgCharacter>(OtherActor);
    if (Character)
    {
        for (auto SeeingThroughActor : SeeingThroughActors)
        {
            SeeingThroughActor->SetShowSeeingThrough(false);
        }
    }
}

 

코드 작업이 끝나면 솔루션 탐색기에서 프로젝트를 빌드하고 언리얼 에디터로 넘어간다.

 

SeeingThroughCollision 배치

 

콘텐츠 브라우저 패널에서 SeeingThroughCollision을 드래그해서 레벨에 배치한다.

 

 

배치한 SeeingThroughCollision의 위치를 {-420.0 -790.0 0.0}으로, 스케일을 {4.0, 22.0, 1.0}으로 수정한다. 그렇게 하면 벽 너머에 캐릭터가 들어갔을 때 가려지는 영역에 콜리전이 위치하게 된다.

 

 

그 다음, SeeingThroughActors 배열에 + 버튼을 눌러서 엘리먼트를 추가해준 다음, 투명해져야 하는 벽을 할당한다.

 

 

 

테스트

 

플레이 버튼을 누르고 캐릭터를 벽 뒤로 이동시켜보면 벽이 투명해지고 그 지역을 벗어나면 다시 벽이 불투명해지는 것을 확인할 수 있다.

 

 

나머지 기둥과 벽으로 가려지는 부분에도 적당하게 SeeingThroughCollision을 배치하고 SeeingThroughActor와 매칭시켜서 벽이 투명해지도록 만들어보자.

 

[투네이션]

 

-

 

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

 

반응형

RPG :: 마우스 입력 이동 구현하기

 

영상 기준 버전 : 4.27

 

작성 기준 버전 :: 4.21

 

이전 섹션에서는 C++ 내려보기 템플릿을 참고해서 일반적인 RPG처럼 마우스 클릭을 통해 캐릭터를 이동시키는 방법을 구현해보고 코드를 분석해본다.

 

프로젝트 세팅

 

RpgProject 라는 이름으로 새 프로젝트를 하나 만든다.

 

 

 

RpgProject를 생성한 뒤에는 내려보기 템플릿으로도 새 프로젝트를 하나 만드는데, 이것은 캐릭터의 메시와 애니메이션을 가져오기 위함이다.

 

제일 먼저 할 일은 내려보기 프로젝트에서 캐릭터의 메시와 애니메이션을 가져올 것이다. 방금 만든 내려보기 프로젝트의 콘텐츠 패널의 콘텐츠 폴더 하위에 Mannequin 폴더가 보일 것이다. 이 안에 캐릭터의 메시와 애니메이션이 들어있다. 이 폴더의 내용물들을 RpgProject로 옮겨야 한다. 이렇게 프로젝트에 포함된 애셋들을 다른 프로젝트로 옮기는 작업을 이주(Migrate)라고 한다. 직접 파일을 옮기지 않아도 언리얼 엔진에서는 이것을 도와주는 기능을 제공한다.

 

 

Mannequin 폴더에 우클릭을 하고, 이주... 를 선택한다. 애셋 리포트 창이 뜨면 리스트를 체크하고 확인 버튼을 누른다.

 

 

그 다음 대상 콘텐츠 폴더 선택 대화상자가 열리면 RpgProject 프로젝트의 Content 폴더를 찾아서 폴더 선택을 한다.

 

 

이주 작업이 끝난 뒤에 RpgProject로 가서 콘텐츠 브라우저 패널을 확인하면 내려보기 프로젝트에 있던 Mannequin 폴더와 그 안의 애셋들이 RpgProject에 성공적으로 옮겨진 것을 확인할 수 있다.

 

 

캐릭터의 메시와 애니메이션을 모두 이주시켰으면 그 다음은, 비주얼 스튜티오를 열고 솔루션 탐색기에서 RpgProject.Build.cs를 찾아서 소스파일을 연다.

 

 

Build.cs에서는 게임을 개발하면서 사용할 모듈을 추가하거나 뺄 수 있는데, 여기서는 두 가지 모듈을 추가할 것이다. 아래의 예시 코드와 같이 PublicDependencyModuleNames에 "NavigationSystem"과 "AIModule"을 추가해주자. NavigationSystem 모듈은 네비게이션 메시와 관련된 기능에 도움을 주는 모듈이고 AIModule 은 이름 그대로 AI 기능에 관련된 모듈이다. 클릭된 위치로 캐릭터를 이동시킬 때, 처리하는 코드를 일일이 만드는 대신, 이 두 모듈의 기능의 도움으로 내비게이션된 경로를 따라서 움직이도록 만들 예정이다.

 

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

 

 

맵 세팅하기

 

프로젝트에 대한 세팅이 끝났다면, 캐릭터가 움직일 맵을 구성해보자.

 

맵을 세팅하기 이전에 맵에 배치할 기둥이나 벽을 대신할 메시 파일을 다운받는다.

 

Box.zip
다운로드

 

그리고 콘텐츠 브라우저 패널에서 Props 폴더를 생성하고 그 폴더에 방금 다운받은 Box.fbx를 임포트(Import)한다.

 

 

그 다음 RpgProject의 레벨 에디터를 보면 빈 평면만 있는 것을 볼 수 있다. 여기에 방금 전에 받은 박스를 이용해서 유닛의 이동을 방해할 수 있게 적절하게 배치해주도록 하자. 아래의 예시와 같이 배치하여도 되고 원하는 대로 편하게 배치해도 된다.

 

 

맵에 장애물들을 모두 배치했다면, 모드 패널의 볼륨에서 내비 메시 바운드 볼륨을 선택해서 레벨의 정중앙에 배치한다. 이 내비 매시 바운드 볼륨은 볼륨의 영역 내에 있는 오브젝트들을 찾아서 계산한 뒤 이동 경로를 찾아줄 내비 메시를 만들어내는 역할을 한다.

 

 

내비 메시 바운드 볼륨을 맵 중앙에 배치한 뒤, 이것의 스케일을 맵을 충분히 덮을 만큼 키워준 다음, P[각주:1]를 눌러보면 캐릭터가 이동할 수 있는 범위가 초록색으로 표시되는 것을 볼 수 있다.

 

 

모든 맵 세팅을 마쳤다면 콘텐츠 브라우저에서 Maps 폴더를 만들고 Ctrl + S를 눌러서 Maps 폴더에 지금 만든 맵을 RpgTestMap 이라는 이름으로 저장한다.

 

 

맵을 저장한 뒤에는 프로젝트 세팅 창을 열고 맵 & 모드에서 Editor Startup Map을 방금 저장한 RpgTestMap으로 설정해준다. 이렇게 하면 다음에 프로젝트를 열었을 때, 지정한 멥이 제일 먼저 열릴 것이다.

 

 

 

Player Controller로 마우스 입력 받기

 

이 다음 작업은 플레이어 컨트롤러로 클릭 입력을 받아서 컨트롤러가 소유한 폰을 클릭한 위치로 이동시키는 코드를 작성한다.

 

코드 작성 이전에 프로젝트 세팅 창의 입력에서 다음과 같이 입력 매핑을 세팅해준다. 이번 섹션에서는 클릭 지점으로 캐릭터를 이동시키는 것만을 목표로 할 것이기 때문에, 입력 매핑은 InputClick을 왼쪽 마우스 버튼으로 하는 것으로 충분하다.

 

 

입력 환경설정을 마쳤다면, 콘텐츠 브라우저 패널에서 신규 추가 버튼을 눌러서 새 C++ 클래스를 추가한다. 부모 클래스로는 Player Controller를 선택하고 다음 버튼을 누른다.

 

 

클래스의 이름은 RpgPlayerController로 하고 클래스 생성 버튼을 누른다.

 

 

클래스가 생성되고 비주얼 스튜디오가 열리면, 우선 다음 코드를 추가해서 생성자를 선언한다.

 

public:
    ARpgPlayerController();

 

RpgPlayerController.cpp에 생성자를 구현한다.

 

ARpgPlayerController::ARpgPlayerController()
{
    bShowMouseCursor = true;
}

 

bShowMouseCursor 프로퍼티를 true로 설정하면 게임 내에서 마우스 커서가 보이도록 만들어준다. 우리는 마우스로 캐릭터를 이동시킬 계획이기 때문에 반드시 필요한 코드이다.

 

RpgPlayerController.h에 다음 변수를 추가한다.

 

protected:
    bool bClickMouse;

 

이 변수는 왼쪽 마우스 버튼을 누를 때 true가 되고, 뗄 때 false가 될 것이다.

 

그 아래에 다음 함수들을 선언을 추가한다.

 

void InputClickPressed();

void InputClickReleased();

 

이 함수들은 입력 매핑과 바인딩되어서 입력을 받으면 bClickMouse 변수의 값을 바꿔주는 역할을 한다. RpgPlayerController.cpp에 위 두 함수를 구현한다.

 

void ARpgPlayerController::InputClickPressed()
{
    bClickMouse = true;
}

void ARpgPlayerController::InputClickReleased()
{
    bClickMouse = false;
}

 

SetupInputComponent() 함수를 덮어씌워서 구현할 차례다. 헤더에 다음 선언을 추가한다.

 

virtual void SetupInputComponent() override;

 

다시 RpgPlayerController.cpp로 가서 SetupInputComponent() 함수를 구현한다.

 

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

    InputComponent->BindAction("InputClick", IE_Pressed, this, &ARpgPlayerController::InputClickPressed);
    InputComponent->BindAction("InputClick", IE_Released, this, &ARpgPlayerController::InputClickReleased);
}

 

왼쪽 마우스 버튼을 누르면 InputClickPressed() 함수가 호출되서 bClickMouse 변수가 true가 되고, 왼쪽 마우스 버튼을 떼면 InputClickReleased() 함수가 호출되서 bClickMouse 변수가 false가 되도록 세팅되었다.

 

이 다음 작업은 마우스를 클릭하면 클릭한 위치로 캐릭터를 이동시키는 코드를 작성하는 작업이다.

 

먼저 RpgPlayerController.h에 다음 함수를 정의한다.

 

void SetNewDestination(const FVector DestLocation);

 

이 함수의 역할은 새로운 목표 위치를 받아서 컨트롤러가 소유한 폰을 그 위치로 이동시키는 역할을 할 것이다.

 

SetNewDestination() 함수에서 내비 메시 위에서 움직이기 위한 작업을 처리하기 위해 다음 전처리기를 추가한다.

 

#include "Blueprint/AIBlueprintHelperLibrary.h"

 

이제 RpgPlayerController.cpp에 SetNewDestination() 함수를 구현해보자.

 

void ARpgPlayerController::SetNewDestination(const FVector DestLocation)
{
    APawn* const MyPawn = GetPawn();
    if (MyPawn)
    {
        float const Distance = FVector::Dist(DestLocation, MyPawn->GetActorLocation());

        if (Distance > 120.0f)
        {
            UAIBlueprintHelperLibrary::SimpleMoveToLocation(this, DestLocation);
        }
    }
}

 

이 함수에서는 우선 컨트롤러가 소유하고 있는 폰을 가져와서 폰과 목적지 사이의 거리를 측정해서, 그 거리가 120 언리얼 유닛보다 크면 폰을 목적지로 이동시킨다. UAIBlueprintHelperLibrary클래스의 SimpleMoveToLocation() 함수는 프로그래머가 목적지로 폰을 이동시키기 위한 처리를 하는 모든 코드를 일일이 작성하는 대신에 간단한 함수 호출로 그 모든 일을 할 수 있도록 도와준다. 아까 전 프로젝트 세팅 단계에 모듈을 추가한 것은 이 기능을 사용하기 위해서 였다.

 

헤더 파일로 돌아가서 다음 함수를 정의한다.

 

void MoveToMouseCursor();

 

그리고 cpp파일에 MoveToMouseCursor() 함수를 구현한다.

 

void ARpgPlayerController::MoveToMouseCursor()
{
    FHitResult Hit;
    GetHitResultUnderCursor(ECC_Visibility, false, Hit);

    if (Hit.bBlockingHit)
    {
        SetNewDestination(Hit.ImpactPoint);
    }
}

 

MoveToMouseCursor() 함수는 GetHitResultUnderCursor() 함수를 통해 마우스 커서 아래에 레이 트레이스를 쏴서 그 위치를 SetNewDestination() 함수에 전달하는 역할을 한다.

 

PlayerTick() 함수를 덮어쓸 차례이다. 헤더에 다음 함수 선언을 추가한다.

 

virtual void PlayerTick(float DeltaTime) override;

 

Cpp 파일에 함수 구현 코드를 추가한다.

 

void ARpgPlayerController::PlayerTick(float DeltaTime)
{
    Super::PlayerTick(DeltaTime);

    if (bClickMouse)
    {
        MoveToMouseCursor();
    }
}

 

코드 추가가 끝났다면 솔루션 탐색기에서 RpgProject를 우클릭 해서 빌드하고 에디터로 돌아간다.

 

 

캐릭터 구현

 

캐릭터가 움직일 맵과 캐릭터를 컨트롤할 플레이어 컨트롤러를 모두 만들었으니 이제 맵 위에서 움직일 캐릭터를 만들 차례이다.

 

콘텐츠 브라우저 패널에서 신규 추가 버튼을 누르고 새 C++ 클래스를 추가한다. 부모 클래스로는 Character 클래스를 선택한다.

 

 

클래스의 이름은 RpgCharacter로 한다.

 

 

RpgCharacter 클래스가 생성되면 RpgCharacter.h로 가서 다음 변수들을 추가한다.

 

private:
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
        class UCameraComponent* RpgCameraComponent;

    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
        class USpringArmComponent* RpgCameraSpringArmComponent;

 

이 변수들은 카메라의 위치를 Rpg 게임에 알맞은 위치로 맞춰주는 역할을 할 것이다. 카메라 컴포너트와 스프링 암 컴포넌트를 초기화 시켜주기 전에, 필요한 컴포넌트들을 사용하기 위한 헤더들을 포함시키는 전처리기들을 RpgCharacter.cpp에 추가해주자.

 

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

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

 

그리고 ARpgCharacter::ARpgCharacter() 생성자 함수로 가서 다음 코드들을 차례로 추가한다.

 

GetCapsuleComponent()->InitCapsuleSize(42.0f, 96.0f);

 

이 코드는 캐릭터 클래스가 기본적으로 가지고 있는 캡슐 콜라이더의 크기를 초기화한다.

 

bUseControllerRotationPitch = false;
bUseControllerRotationYaw = false;
bUseControllerRotationRoll = false;

 

이 코드는 캐릭터가 카메라의 회전을 따라서 회전하지 않도록 한다.

 

GetCharacterMovement()->bOrientRotationToMovement = true;
GetCharacterMovement()->RotationRate = FRotator(0.0f, 640.0f, 0.0f);
GetCharacterMovement()->bConstrainToPlane = true;
GetCharacterMovement()->bSnapToPlaneAtStart = true;

 

이 코드는 캐릭터의 무브먼트를 규정하는 코드로, 캐릭터를 이동시키기 전에 이동 방향과 현재 캐릭터의 방향이 다르면 캐릭터를 이동 방향으로 초당 640도의 회전 속도로 회전시킨다음 이동시킨다. 그리고 캐릭터의 이동을 평면으로 제한하고, 시작할 때 캐릭터의 위치가 평면을 벗어난 상태라면 가까운 평면으로 붙여서 시작되도록 한다. 여기서 평면이란 내비게이션 메시를 의미한다.

 

RpgCameraSpringArmComponent = CreateDefaultSubobject<USpringArmComponent>(TEXT("RpgCameraSpringArm"));
RpgCameraSpringArmComponent->SetupAttachment(RootComponent);
RpgCameraSpringArmComponent->SetUsingAbsoluteRotation(false);
RpgCameraSpringArmComponent->TargetArmLength = 800.0f;
RpgCameraSpringArmComponent->RelativeRotation = FRotator(-60.0f, 45.0f, 0.0f);
RpgCameraSpringArmComponent->bDoCollisionTest = false;

 

이 코드는 카메라를 캐릭터에게서 적절한 위지를 잡도록 도와주는 스프링 암 컴포넌트를 생성하고 설정한다.

 

bAbsoluteRotation은 스프링 암의 회전이 루트 컴포넌트와 상위 컴포넌트를 따르지 않고 월드 좌표계의 회전을 따르도록 한다.

 

TargetArmLength는 카메라와 캐릭터의 거리를 800으로 설정하고 ReleativeRotation은 스프링 암을 회전시켜 위에서 캐릭터를 내려다보도록 설정한다.

 

bDoCollisionTest는 카메라가 벽에 닿으면 충돌 계산을 통해 카메라와 캐릭터의 거리를 좁혀 카메라가 벽을 뚫지 않게 만들어주는 프로퍼티이지만, Rpg게임에서는 사용되지 않는 옵션이기 때문에 false로 설정한다.

 

RpgCameraComponent = CreateDefaultSubobject<UCameraComponent>(TEXT("RpgCamera"));
RpgCameraComponent->SetupAttachment(RpgCameraSpringArmComponent, USpringArmComponent::SocketName);
RpgCameraComponent->bUsePawnControlRotation = false;

 

이 코드는 카메라 컴포넌트를 생성하고 스프링 암 컴포넌트에 붙이는 작업을 한다.

 

PrimaryActorTick.bCanEverTick = true;
PrimaryActorTick.bStartWithTickEnabled = true;

 

그리고 마지막으로 틱 함수가 동작하도록 설정한다.

 

모든 코드 작업이 끝났다면 솔루션 탐색기에서 RpgProject를 우클릭해서 프로젝트를 빌드하고 에디터로 돌아간다.

 

에디터로 돌아왔다면, 콘텐츠 브라우저 패널에서 Bluprints 폴더를 생성한 뒤, RpgCharacter 클래스를 찾아서 우클릭하고 RpgCharacter 기반 블루프린트 클래스 생성을 선택한다.

 

 

 

 

그리고 Blueprints 폴더에 BP_RpgCharacter라는 이름으로 블루프린트 클래스를 생성한다.

 

 

블루프린트가 생성되면, 생성된 블루프린트 클래스를 더블클릭해서 블루프린트 에디터를 열고 컴포넌트 패널에서 Mesh 컴포넌트를 선택한다.

 

 

그리고 디테일 패널에서 Mesh 카테고리를 찾아 Skeletal Mesh 프로퍼티를 내려보기 프로젝트에서 이주시킨 SK_Mannequin으로 설정한다.

 

 

애니메이션 역시 내려보기 프로젝트에서 가져온 ThirdPerson_AnimBP로 설정한다.

 

 

위 작업을 하고 나서 블루프린트 에디터의 뷰포트 패널을 보면 캐릭터의 메시가 캡슐 콜라이더를 벗어나고 방향 역시 다르게 되어 있을 것이다.

 

 

이를 일치시키기 위해서 메시 컴포넌트의 위치를 {0.0, 0.0, -90.0}으로 회전을 {0.0, 0.0, -90.0}으로 수정해주자.

 

 

세팅이 모두 끝났다면 블루프린트 클래스를 컴파일하고 저장한다.

 

 

게임 모드 설정

 

플레이어 컨트롤러와 캐릭터의 설정이 모두 끝났으니, 이제 게임이 우리가 만든 플레이어 컨트롤러와 캐릭터를 사용하도록 할 차례이다.

 

콘텐츠 브라우저 패널에서 RpgProjectGameModeBase 클래스를 찾아서 우클릭하여 RpgProjectGameModeBase 기반 블루프린트를 생성한다.

 

 

BP_RpgProjectGameModeBase라는 이름으로 Blueprints 폴더에 블루프린트 클래스를 생성한다.

 

 

게임 모드 블루프린트가 생성되면 더블클릭하여 블루프린트 에디터를 열고, 디테일 패널에서 Player Controller Class를 RpgPlayerController로, Default Pawn Class를 BP_RpgCharacter로 설정한다. 그리고 블루프린트를 컴파일하고 저장한 뒤 블루프린트 에디터를 닫는다.

 

 

레벨 에디터 상단 메뉴바에서 세팅>월드 세팅을 선택하면 월드 세팅 패널이 열린다.

 

 

월드 세팅 패널에서 Game Mode Override를 방금 만든 BP_RpgProjectGameMode로 설정한다.

 

 

모든 과정을 마친 뒤 레벨 에디터에서 플레이 버튼을 눌러서 PIE 모드로 들어가면 캐릭터가 마우스 클릭 지점으로 이동하고, 그 과정에서 적절하게 장애물을 회피하는 것을 볼 수 있다.

 

 

  1. 언리얼 엔진에서 생성된 내비 메시를 보여주는 단축키이다. [본문으로]

 

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

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

 

에셋스토어

여러분의 작업에 필요한 베스트 에셋을 찾아보세요. 유니티 에셋스토어가 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

 

반응형

Player Controller

 

작성 기준 버전 :: 4.21.1

 

언리얼 엔진에서 Player Controller는 Pawn과 그것을 제어하려는 사람 사이의 인터페이스로서, 플레이어의 의지를 대변하는 클래스이다. Player Controller는 Controller 클래스를 상속받는데, Possess() 함수로 Pawn의 제어권을 획득하고, UnPossess() 함수로 제어권을 포기한다.

 

컨트롤러에 대한 언리얼 문서

 

플레이어 컨트롤러에 대한 언리얼 문서

 

이러한 설명이 언리얼 문서 상에서의 Player Controller에 대한 내용의 대부분이고, 실제적인 C++ 코드에서의 사용법은 레퍼런스에서 찾아서 멤버 변수와 함수 등을 직접 찾아서 읽어야 하는 불편함이 있기 때문에 입문자는 Player Controller의 사용법을 제대로 익히기가 쉽지 않다.

 

그럼 이제부터 Player Controller의 기본적인 기능과 사용법에 대해서 알아보도록 하자.

 

 

마우스 인터페이스

 

플레이어 컨트롤러가 기본적으로 제공하는 기능 중에는 마우스 인터페이스에 관련된 기능이 있다.

 

플레이어 컨트롤러를 상속받는 블루프린트 클래스를 만들고 에디터를 열어보면 디테일 패널의 Mouse Interface 카테고리에서 플레이어 컨트롤러가 기본적으로 제공하는 마우스 인터페이스와 관련된 기능들을 한눈에 볼 수 있다. 디테일 패널에서 보이는 것들은 아주 기본적인 내용으로 이보다 심도깊은 기능에 대해서 배우고자 한다면 APlayerController 클래스의 언리얼 레퍼런스를 확인하는게 좋다.

 

 

bEnableClickEvents = true;
bEnableTouchEvents = true;
bEnableMouseOverEvents = true;
bEnableTouchOverEvents = true;

 

C++ 코드 상에서는 UPlayerController 클래스의 위와 같은 프로퍼티들을 통해서 옵션을 바꿀 수 있다.

 

Show Mouse Cursor

 

Show Mouse Cursor 옵션은 말 그대로 게임 도중에 마우스 커서를 보이게 할 것인가에 대한 옵션이다. 예를 들어보자면, 전통적인 PC 플랫폼에서의 탑다운뷰 RPG 게임은 캐릭터를 이동시킬 때 주로 마우스를 이용한다. 때문에 커서가 항상 보이도록 이 옵션을 true로 해줘야 한다. 하지만 FPS 게임은 화면 중앙의 크로스헤어에 적을 일치시키고 공격하는 방식이기 때문에 커서가 보일 필요가 없다. 혹은 위 두 가지 사례가 융합된 장르들은 전투나 이동 같은 플레이 중일 때는 커서를 비활성화시킨 상태로 크로스헤어를 사용하고, 인벤토리나 지도를 열면 커서를 활성화시키는 방식으로 사용하기도 한다.

 

bShowMouseCursor = true;

bShowMouseCursor = false;

 

C++ 코드 상에서는 UPlayerController 클래스의 bShowMouseCursor 프로퍼티를 통해서 옵션을 바꿀 수 있다.

 

Event

 

그 다음 네 가지 옵션은 월드에 배치된 액터/컴포넌트가 마우스나 터치에 대해서 이벤트를 발생시킬지에 대한 프로퍼티이다.

 

Click Event / Touch Event는 액터/컴포넌트에 대한 클릭/터치 이벤트이고, Mouse Over Event / Touch Over Event는 액터/컴포넌트에 커서나 터치가 올라가 있는 상태에 대한 이벤트이다.

 

간단하게 예시를 보자면 언리얼 프로젝트에 테스트용 C++ 클래스를 하나 만들고 NotifyActorOnClicked() 함수를 덮어씌워서 다음과 같이 구현하고 프로젝트를 다시 빌드한다.

 

virtual void NotifyActorOnClicked(FKey PressedButton = EKeys::LeftMouseButton) override;

 

void AClickEventTestActor::NotifyActorOnClicked(FKey PressedButton)
{
    UE_LOG(LogTemp, Log, TEXT("NotifyActorOnClicked"));
}

 

그리고 새 Player Controller를 하나 만들어서 Show Mouse Cursor와 Enable Click Event를 true로 설정한 뒤, 프로젝트 설정의 맵 & 모드에서 기본 플레이어 컨트롤러로 설정한다.

 

그 다음 아까 만든 액터를 레벨에 배치하고 클릭할 수 있게 스태틱 메시 컴포넌트를 추가해주자.

 

레벨 에디터에서 플레이 버튼을 누르고 추가한 액터를 클릭해보면 출력 로그 패널에서 "NotifyActorOnClicked"라고 출력되는 것을 확인할 수 있다.

 

 

액터나 컴포넌트에 Click Event를 추가했더라도 Player Controller에서 Enable Click Event를 false로 설정했다면, NotifyActorOnClicked() 이벤트는 호출되지 않는다.

 

Default Mouse Cursor

 

Default Mouse Cursor 프로퍼티는 기본적인 마우스 커서 모양을 정하는 프로퍼티이다. 커서 모양의 종류는 언리얼 문서에서 확인할 수 있다.

 

DefaultMouseCursor = EMouseCursor::Default;

 

C++ 코드 상에서는 UPlayerController 클래스의 DefaultMouseCursor 프로퍼티를 통해서 옵션을 바꿀 수 있다.

 

Default Click Trace Channel

 

Default Click Trace Channel은 클릭 이벤트가 발생했을 때, 플레이어가 클릭한 대상을 3D 공간에서 찾기 위해서 트레이스를 통해 광선 같은 개념의 선을 쏘는데 어떤 채널에 속하는 객체가 맞았을 때 찾았다고 판정할 지를 정하는 프로퍼티이다. "Visibility"가 기본값으로 되어 있는데, 이는 화면에 보이는 객체가 맞으면 무조건 맞았다고 판정한다는 의미이다.

 

이 값의 종류에는 Visibility 이 외에도, Pawn만 찾아내는 Pawn, 월드에 스태틱으로 배치된 액터만 찾아내는 WorldStatic 등이 있다.

 

DefaultClickTraceChannel = ECollisionChannel::ECC_Visibility;

 

C++ 코드 상에서는 UPlayerController 클래스의 DefaultClickTraceChannel 프로퍼티를 통해서 옵션을 바꿀 수 있다.

 

Trace Distance

 

Trace Distance 프로퍼티는 클릭 이벤트 등이 발생했을 때, 화면으로부터 얼마나 멀리 떨어진 지점까지 대상을 탐색할 것인가에 대한 것이다.

 

HitResultTraceDistance = 10000.0f;

 

그 외의 유용한 마우스 관련 함수

 

1. GetHitResultUnderCursor()

 

GetHitResultUnderCursor() 함수는 함수 이름 그대로 화면에서 마우스 커서 위치에 트레이스를 쏴서 그 결과를 가져오는 함수이다. 일반적인 함수의 사용법은 아래와 같으며, 이 함수는 레벨에 배치된 오브젝트를 선택하거나, RPG 식으로 클릭한 위치로 캐릭터를 이동시킬 때, 유용하게 사용할 수 있다.

 

FHitResult Hit;
GetHitResultUnderCursor(ECC_Visibility, false, Hit);

if (Hit.bBlockingHit)
{
    // 충돌 결과가 있을 때의 처리
}

 

 

플레이어 틱(Player Tick)

 

플레이어 컨트롤러는 액터 클래스에서 상속받는 Tick() 함수 외에 PlayerTick() 이라는 별도의 틱 함수를 하나 더 가진다. 일반 Tick() 함수는 어디서나 작동하는 반면에, PlayerTick() 함수는 Player Controller에 PlayerInput 객체가 있는 경우에만 호출된다. 따라서 로컬로 제어되는 Player Controller에서만 플레이어 틱이 호출된다. 이 말인 즉슨, 만약 멀티플레이 게임이라면 자기 자신의 플레이어 컨트롤러에서만 플레이어 틱이 호출된다는 것이다.

 

virtual void PlayerTick(float DeltaTime) override;

 

void ATestPlayerController::PlayerTick(float DeltaTime)
{
    Super::PlayerTick(DeltaTime);

}

 

플레이어 틱 함수를 덮어쓰기 위해서는 위와 같이 코드를 작성하면 된다.

 

 

폰(Pawn)

 

플레이어 컨트롤러는 기본적으로 자신이 빙의(Possess)하여 컨트롤하는 폰을 레퍼런스로 가지고 있게 된다. 일반적으로 프로젝트 세팅의 맵 & 모드나 월드 세팅에서 디폴트 폰을 None이 아닌 특정 폰으로 설정해둔 상태라면, 게임이 시작되면 폰이 생성되고 플레이어 컨트롤러는 생성된 폰에 자동으로 빙의된다.

 

디폴트 폰이 None이어서 자동으로 빙의되는 상황이 아니라면, Possess() 함수를 이용하여 빙의하고자 하는 폰에 컨트롤러를 빙의시키면된다.

 

Possess(TargetPawn);

 

이 반대의 경우로 현재 빙의하고 있는 폰에서 제어권을 포기하고 탈출하려고 한다면 UnPossess() 함수를 사용하면 된다.

 

UnPossess();

 

컨트롤러가 빙의 중인 폰을 가져와서 사용하기 위해서는 다음과 같이 GetPawn() 함수를 사용하면 된다.

 

APawn* MyPawn = GetPawn();

 

만약에 컨트롤러가 현재 컨트롤 중인 폰이 없는 상태라면 GetPawn() 함수는 nullptr를 반환한다.

 


 

[투네이션]

 

-

 

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

 

반응형

TSubclassOf<T>

 

TSubclassOf 클래스는 UClass 타입 안정성을 제공하는 템플릿 클래스이다.

 

예를 들어, 데미지 타입을 지정할 수 있는 발사체 클래스를 만든다고 가정했을 때, 다음 코드처럼 UClass 타입의 UPROPERTY를 만들어서 에디터에 노출시킨 뒤, 에디터 작업자에게 이 프로퍼티에 UDamageType의 파생 클래스만 할당해 달라고 한다면 어떻게 될까?

 

UPROPERTY(EditDefaultsOnly, Category = Damage)
UClass* DamageType;

 

블루프린트 에디터의 디테일 패널에서 Damage Type 프로퍼티에 클래스를 할당하기 위해서 드롭다운 메뉴를 확장해보면 클래스 타입에 상관없이 모든 클래스가 표시되고 있음을 볼 수 있다. 이런 상황에서 UDamageType의 파생 클래스만 할당해달라고 한다면, 낮은 확률으로라도 언젠가는 잘못된 클래스를 할당하는 일이 분명 생길 수 밖에 없다.

 

 

이러한 문제를 예방하기 위해서 존재하는 것이 바로 TSubclssOf 클래스이다. 다음 코드와 같이 TSubclassOf<UDamageType>으로 UPROPERTY를 만든다.

 

UPROPERTY(EditDefaultsOnly, Category = Damage)
TSubclassOf<UDamageType> DamageType;

 

그렇게 하면, 블루프린트 창의 디테일 패널에서 Damage Type의 드롭다운 메뉴에서는 UDamageType의 파생 클래스만 표시된다. 이렇게 되면 개발자가 가끔 잘못된 데미지 타입을 골라서 넣는 실수는 할 수 있겠지만, 애초에 잘못된 클래스를 선택하는 문제는 발생하지 않을 것이다.

 

 

또한 TSubclassOf 클래스는 이런 UPROPERTY에 대한 안정성 이외에 C++ 수준의 타입 안정성 역시 제공하기 때문에 서로 호환되지 않는 TSubclassOf 타입을 서로 할당하려고 하면 컴파일 오류가 발생하고, UClass 타입을 할당하려고 하면 할당을 수행할 수 있는지 런타임중에 검사한다. 런타임 검사에 실패하면 결과값은 nullptr이 된다.

 

UClass* ClassA = UDamageType::StaticClass();

TSubclassOf<UDamageType> ClassB;

ClassB = ClassA; // 런타임 중에 체크.

TSubclassOf<UFireDamageType> ClassC;

ClassB = ClassC; // 컴파일 타임 중에 체크

 

[투네이션]

 

-

 

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

 

반응형

+ Recent posts