제대로 따라가기 (8) C++ 프로그래밍 튜토리얼 :: 일인칭 슈팅 C++ 튜토리얼 (3)

작성버전 :: 4.21.0

언리얼 엔진 튜토리얼인 일인칭 슈팅 C++ 튜토리얼에서는 C++ 코드 작업을 통해서 기본적인 일인칭 슈팅(FPS) 게임을 만드는 법을 배울 수 있다.

 

이번 튜토리얼은 각 하위 섹션들의 길이가 길어서 분할되어 작성된다.

 

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


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

 

수정

 

지난 섹션에서 VisibleDefaultOnly는 버전이 바뀌어서 사라진 지정자라고 했던 부분은 잘못된 부분입니다.

 

VisibleDefaultsOnly는 정상적으로 존재하는 UPROPERTY 지정자입니다. 제가 실수로 VisibleDefaultOnly로 오타를 내서 컴파일러가 지정자가 없다고 에러를 띄웠었습니다. 잘못된 정보로 혼동을 드린 점에 대해서 사과드립니다. 다음부터는 제대로된 확인을 거친 후, 글을 올리도록 하겠습니다.

 

지난 섹션의 잘못된 부분은 수정되었습니다.(지난 섹션 :: 제대로 따라가기 (7) C++ 프로그래밍 튜토리얼 :: 일인칭 슈팅 C++ 튜토리얼 (2))

 

3. 발사체 구현

 

이번 섹션에서는 일인칭 슈팅 게임에서 발사체를 구현하는 방법에 대해서 배울 수 있다.

 

3-1. 게임에 발사체 추가

 

이전 섹션에서 캐릭터 구성을 마쳤으니, 이제 발사체 무기를 구현하여 발사하면 단순한 수류탄 같은 발사체가 화면 중앙에서 발사되어 월드에 충돌할 때까지 날아가도록 만들어보자. 이번 단계에서는 발사체(Projectile)에 쓸 입력을 추가하고 새 코드 클래스를 만들 것이다.

 

발사 액션 매핑 추가

 

편집 메뉴에서 프로젝트 세팅 창을 연다. 그리고 엔진 섹션에서 입력을 선택한 뒤, 액션 매핑에 아래와 같이 "Fire" 라는 입력 세팅을 추가 한다.

 

 

발사체(Projectile) 클래스 추가

 

파일 메뉴에서 새로운 C++ 클래스... 를 선택하고 Actor 클래스를 부모 클래스로 선택하고 다음을 클릭한다.

 

 

새 클래스 이름을 "FPSProjectile"로 하고 클래스 생성을 클릭한다.

 

 

USphereComponent 추가

 

FPSProjectile.h로 가서 USphereComponent의 선언을 다음처럼 추가해준다.

 

UPROPERTY(VisibleDefaultsOnly, Category = "Projectile")
USphereComponent* CollisionComponent;

 

USphereComponenet가 정의되지 않았다고 에러가 발생한다면, USphereComponent 앞에 class 키워드를 붙여주자.

class USphereComponent* CollisionComponent;

 

그 다음엔 FPSProjectile.cpp의 AFPSProjectile::AFPSProjectile() 생성자 함수에서 다음 코드를 추가한다.

CollisionComponent = CreateDefaultSubobject<USphereComponent>(TEXT("SphereComponent"));
CollisionComponent->InitSphereRadius(15.0f);
RootComponent = CollisionComponent;

 

CollisionComponent에서 함수를 호출하려고 할 때, 불완전한 형식은 사용할 수 없다는 에러가 발생하면 "Engine/Classes/Components/SphereComponent.h"를 cpp의 전처리기에 추가해주자.

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

 

프로젝타일 무브먼트 컴포넌트(Projectile Movement Component) 추가

 

FPSProjectile.h의 하단에 다음 코드를 추가한다.

UPROPERTY(VisibleAnywhere, Category = "Movement")
UProjectileMovementComponent* ProjectileMovementComponent;

 

UProjectileMovementComponent가 정의되지 않았다고 에러가 발생한다면, UProjectileMovementComponent 앞에 class 키워드를 붙여주자.

class UProjectileMovementComponent* ProjectileMovementComponent;

 

다시, FPSProjectile.cpp의 AFPSProjectile::AFPSProjectile() 생성자 함수에서 다음 코드를 추가한다.

ProjectileMovementComponent = CreateDefaultSubobject<UProjectileMovementComponent>(TEXT("ProjectileMovementComponent"));
ProjectileMovementComponent->SetUpdatedComponent(CollisionComponent);
ProjectileMovementComponent->InitialSpeed = 3000.0f;
ProjectileMovementComponent->MaxSpeed = 3000.0f;
ProjectileMovementComponent->bRotationFollowsVelocity = true;
ProjectileMovementComponent->bShouldBounce = true;
ProjectileMovementComponent->Bounciness = 0.3f;

 

ProjectileMovementComponent에서 함수를 호출하려고 할 때, 불완전한 형식은 사용할 수 없다는 에러가 발생하면 "Engine/Classes/GameFramework/ProjectileMovementComponent.h"를 cpp의 전처리기에 추가해주자.

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

 

발사체 초기 속도 설정

 

FPSProjectile.h로 가서 다음의 함수 선언을 추가한다.

void FireInDirection(const FVector& ShootDirection);

 

이 함수가 발사체의 발사를 담당한다.

 

FPSProjectile.cpp에 함수의 정의부를 추가한다.

void AFPSProjectile::FireInDirection(const FVector& ShootDirection)
{
    ProjectileMovementComponent->Velocity = ShootDirection * ProjectileMovementComponent->InitialSpeed;
}

 

발사체의 속력은 ProjectileMovementComponent에 의해 정의되므로 발사 방향만 제공해주면 된다.

 

발사 입력 액션 바인딩

 

FPSCharacter.h로 가서 다음의 함수 선언을 추가한다.

UFUNCTION()
void Fire();

 

FPSCharacter.cpp의 SetupPlayerInputComponent() 함수에 다음 바인딩을 추가한다.

InputComponent->BindAction("Fire", IE_Pressed, this, &AFPSCharacter::Fire);

 

그리고, Fire() 함수의 정의부 역시 추가한다.

void AFPSCharacter::Fire()
{
}

 

발사체의 스폰 위치 정의

 

FPSProjectile 액터를 스폰하는 Fire() 함수 구현을 위해서는 발사체를 스폰할 위치와, 스폰해야할 발사체의 클래스를 고려해야 한다.

 

FPSCharacter.h에 다음 코드를 추가한다.

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = GamePlay)
FVector MuzzleOffset;

UPROPERTY(EditDefaultsOnly, Category = Projectile)
TSubclassOf<class AFPSProjectile> ProjectileClass;

 

코드 컴파일 및 검사

 

이제 새로 구현된 발사체 코드를 컴파일하고 검사할 차례다. 솔루션 탐색기에서 FPSProject에 우클릭하고 빌드(Build)를 선택하여 프로젝트를 컴파일한다.

 

 

 

3-2. 발사 구현

 

이번 단계에서는 캐릭터가 발사체 발사를 할 수 있도록 Fire() 함수를 구현해본다.

 

Fire 함수 구현

 

FPSCharacter.cpp 상단에 "FPSProjectile.h"를 포함시킨다.

#include "FPSProjectile.h"

 

FPSCharacter.cpp에 만들어둔 Fire() 함수의 바디를 다음과 같이 수정한다.

void AFPSCharacter::Fire()
{
    if (ProjectileClass)
    {
        FVector CameraLocation;
        FRotator CameraRotation;
        GetActorEyesViewPoint(CameraLocation, CameraRotation);

        FVector MuzzleLocation = CameraLocation + FTransform(CameraRotation).TransformVector(MuzzleOffset);
        FRotator MuzzleRotation = CameraRotation;

        MuzzleRotation.Pitch += 10.0f;
        UWorld* World = GetWorld();
        if (World)
        {
            FActorSpawnParameters SpawnParams;
            SpawnParams.Owner = this;
            SpawnParams.Instigator = Instigator;
            AFPSProjectile* Projectile = World->SpawnActor<AFPSProjectile>(ProjectileClass, MuzzleLocation, MuzzleRotation, SpawnParams);
            if (Projectile)
            {
                FVector LaunchDirection = MuzzleRotation.Vector();
                Projectile->FireInDirection(LaunchDirection);
            }
        }
    }
}

 

변경사항을 저장하고 프로젝트를 빌드한다.

 

 

프로젝타일 블루프린트 빌드하기

 

다음 링크에서 샘플 메시를 받아서 압축을 풀고 진행해야 한다. "프로젝타일 메시"

 

콘텐츠 브라우저의 파일 창에 우클릭해서 /Game에 임포트... 를 선택해서 임포트 대화창을 연다.

 

 

Sphere.fbx 메시 파일을 찾아서 선택하고 임포트를 선택해서 메시를 프로젝트에 추가하고, 저장 버튼을 클릭해서 임포트된 스태틱 메시를 저장한다.

 

콘텐츠 브라우저에서 Blueprints 폴더에 들어간다.

 

신규 추가 버튼을 클릭하고 블루프린트 클래스를 선택한다.

 

모든 클래스 드롭다운 메뉴를 펼친다음 검색창에 FPSProjectile을 입력하고 FPSProjectile 클래스를 선택한다.

 

 

새 블루프린트의 이름을 BP_FPSProjectile로 정하고 아이콘을 더블클릭해서 블루프린트 에디터를 연다.

 

 

컴포넌트 탭에서 CollisionComponent를 클릭하고 컴포넌트 추가 드롭다운 목록에서 Static Mesh를 찾아서 추가하고 이름은 ProjectileMeshComponent로 한다.

 

 

디테일 탭의 Static Mesh 섹션에서 "없음"이라고 된 드롭다운 메뉴를 클릭해서 Sphere 스태틱 메시를 선택한다.

 

 

X, Y, Z 스케일 값을 0.09로 설정한다. 자물쇠 아이콘을 클릭하면 세 축을 모두 고정시켜 상대 비율이 유지된다.

 

 

ProjectileMeshComponent의 콜리전 프리셋 값을 NoCollision으로 설정한다. (콜리전에 이 스태틱 메시가 아닌 SphereComponent를 사용한다.)

 

 

블루프린트를 컴파일, 저장한 뒤 블루프린트 에디터를 닫는다.

 

BP_FPSCharacter를 더블클릭해서 블루프린트 에디터를 열고 Projectile Class 프로퍼티를 찾은 다음 BP_FPSProjectile로 설정한다.

 

 

그리고 Muzzle Offset 프로퍼티를 {100, 0, 0}으로 설정하여 발사체를 카메라 약간 앞에서 생성되게 한다.

 

 

블루프린트를 컴파일, 저장 후 블루프린트 에디터를 닫는다.

 

게임 내 발사체 발사 테스트

 

레벨 에디터에서 플레이 버튼을 클릭하여 PIE 모드에 들어가서 발사체를 발사해보자.

 

좌클릭하면 발사체가 월드로 발사된다.

 

 

 

Esc키를 누르거나 중지 버튼을 클릭하여 PIE 모드를 빠져나가자.

 

 

 

 

 

3-3. 발사체 콜리전 및 수명 구성

 

지금 만들어진 발사체는 아무리 긴 시간이 지나도 사라지지 안고, 월드의 다른 오브젝트와 충돌하지 않는 상태이다. 이번 단계에서는 발사체의 충돌과 수명을 구성한다.

 

발사체의 수명 기간 제한

 

FPSProjectile.cpp에서 AFPSProjectile::AFPSProjectile() 생성자 함수에 다음 코드를 추가하여 발사체의 수명을 설정한다.

InitialLifeSpan = 3.0f;

 

발사체의 충돌 세팅 편집

 

언리얼 엔진에는 여러가지 프리셋 콜리전 채널이 포함되어 있으나, 커스터마이징 프리셋 콜리전 채널을 만들 수도 있다.

 

프로젝트 세팅 창의 엔진 섹션에서 콜리전을 선택하면 콜리전 채널 프리셋들을 확인할 수 있다.

 

 

 

 

새 오브젝트 채널... 을 선택하여 콜리전 채널을 새로 만든다. 새로운 콜리전 채널 이름을 "Projectile"이라 하고 기본 반응(Default Response)를 Block으로 설정하여 수락을 누른다.

 

 

프리셋에 새... 버튼을 클릭해서 새 프로필의 이름을 "Projectile"로 하고, 다음 이미지를 참고해서 콜리전 프리셋을 설정하자.

 

 

새 콜리전 채널 세팅 사용

 

FPSProjectile.cpp의 생성자에서 CollisionComponent 설정 코드의 아래에 다음 줄을 추가한다.

CollisionComponent->BodyInstance.SetCollisionProfileName(TEXT("Projectile"));

 

변경사항을 저장하고 솔루션 탐색기에서 FPSProject를 우클릭해서 빌드를 선택해서 프로젝트를 컴파일한다.

 

 

 

3-4. 월드와 상호작용하는 프로젝타일

 

프로젝타일의 콜리전 상호작용을 감지할 수 있게 되었으니, 이제 그 콜리전에 어떻게 반응할지를 결정할 차례다. 이번 단계에서는 콜리전 이벤트에 반응하는 FPSProjectile에 OnHit() 함수를 추가할 것이다.

 

프로젝타일이 콜리전에 반응하도록 만들기

 

FPSProjectile.h를 열어서 클래스 정의에 다음 코드를 추가한다.

UFUNCTION()
void OnHit(UPrimitiveComponent* HitComponent, AActor* OtherActor, UPrimitiveComponent* OtherComponent, FVector NormalImpulse, const FHitResult& Hit);

 

FPSProjectile.cpp에서 OnHit() 함수의 동작을 구현한다.

void AFPSProjectile::OnHit(UPrimitiveComponent* HitComponent, AActor* OtherActor, UPrimitiveComponent* OtherComponent, FVector NormalImpulse, const FHitResult& Hit)
{
    if (OtherActor != this && OtherComponent->IsSimulatingPhysics())
    {
        OtherComponent->AddImpulseAtLocation(ProjectileMovementComponent->Velocity * 100.0f, Hit.ImpactPoint);
    }
}

 

그리고 FPSProjectile 생성자에 CollisionComponent 생성 코드 뒤에 다음 코드를 추가한다.

CollisionComponent->OnComponentHit.AddDynamic(this, &AFPSProjectile::OnHit);

 

변경사항들을 저장하고 솔루션 탐색기에서 FPSProject에 우클릭한 뒤, 빌드를 선택해서 프로젝트를 컴파일한다.

 

 

프로젝타일 콜리전 테스트

 

빌드 완료 후, 언리얼 에디터로 돌아가서 Floor 스태틱 메시를 복사 & 붙여넣기를 한다.

 

이름이 Floor2인 바작 메시 사본의 비율 고정이 풀렸는지 확인하고 스케일을 {0.2, 0.2, 3.0}으로 설정하고 위치를 {320, 0, 170}으로 조정한다.

 

 

Floor2의 디테일 패널에서 Physics 섹션을 찾은 뒤 Simulate Physics 옵션을 체크한다.

 

 

맵을 저장하고 BP_FPSProjectile을 더블클릭하여 블루프린트 에디터를 열고 컴포넌트 탭에서 ProjectileMeshComponent를 클릭한다.

 

디테일 패널에서 Collision 아래의 Collision Presets 프로퍼티를 Projectile로 설정한다.

 

 

블루프린트를 컴파일, 저장한 뒤 블루프린트 에디터를 닫는다.

 

플레이 버튼을 눌러 PIE 모드에 들어간다. 좌클릭으로 상자를 향해 발사체를 발사하면 상자가 발사체에 맞고 튕겨나가는 모습을 볼 수 있다.

 

 

레벨 에디터에서 중지 버튼을 눌러서 PIE 모드에서 빠져나간다.

 

 

3-5. 뷰포트에 조준선 추가

 

이번 단계에서는 게임에 조준선 HUD 요소를 추가하려 조준할 수 있도록 해보자.

 

조준선 애셋 임포트

 

시작하기에 앞서, 다음 링크에서 샘플 이미지를 다운로드하고 압축을 푼다. "샘플 조준선 이미지"

 

콘텐츠 브라우저의 파일 창에 우클릭하여 /Game에 임포트... 를 눌러서 임포트 대화 상자를 연다.

 

 

crosshair.TGA 이미지 파일을 찾아서 임포트한다.

 

새 HUD 클래스 추가

 

파일 메뉴에서 새로운 C++ 클래스... 을 선택한다.

 

부모 클래스 선택 메뉴가 열리면 HUD 클래스를 찾아서 부모 클래스로 선택하고 다음을 클릭한다.

 

 

클래스 이름에 "FPSHUD"를 입력하고 클래스를 생성한다.

 

 

FPSHUD.h에 다음 변수와 함수 코드를 추가한다.

protected:
    UPROPERTY(EditAnywhere)
    UTexture2D* CrosshairTexture;

public:
    virtual void DrawHUD() override;

 

FPSHUD.cpp에 DrawHUD() 함수를 구현한다.

void AFPSHUD::DrawHUD()
{
    Super::DrawHUD();
   
    if (CrosshairTexture)
    {
        FVector2D Center(Canvas->ClipX * 0.5f, Canvas->ClipY * 0.5f);

        FVector2D CrossHairDrawPosition(Center.X - (CrosshairTexture->GetSurfaceWidth() * 0.5f), Center.Y - (CrosshairTexture->GetSurfaceHeight() * 0.5f));

        FCanvasTileItem TileItem(CrossHairDrawPosition, CrosshairTexture->Resource, FLinearColor::White);
        TileItem.BlendMode = SE_BLEND_Translucent;
        Canvas->DrawItem(TileItem);
    }
}

 

DrawHUD() 함수를 구현할 때, Canvas 기능과 관련해서 불완전한 형식의 클래스에 대한 에러가 발생할 수 있다. 이 부분은 "Engine/Canvas.h"를 포함시켜주면 해결된다.

#include "Engine/Canvas.h"

 

변경사항을 저장하고 솔루션 탐색기에서 FPSProject를 우클릭해서 빌드를 선택하고 프로젝트를 컴파일한다.

 

 

C++ HUD 클래스를 블루프린트로 확장

 

콘텐츠 브라우저에서 FPSHUD 클래스에 우클릭해서 FPSHUD 기반 블루프린트 클래스 생성을 선택한다.

 

 

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

 

 

기본 HUD 클래스 설정

 

프로젝트 세팅의 맵 & 모드에서 HUD를 새로 생성한 BP_FPSHUD로 설정해준다.

 

 

세팅이 끝났으면 BP_FPSHUD 블루프린트 에디터를 열고 Crosshair Texture를 crosshair로 설정해준다.

 

 

마지막으로, 블루프린트를 저장하고 블루프린트 에디터를 닫는다.

 

HUD 확인

 

레벨 에디터에서 플레이 해보면 새로 추가한 조준선이 보일 것이다.

 

 

레벨 에디터의 중지 버튼을 눌러서 PIE 모드를 빠져나오자.

 

 

 

 

 

 


 

이번 섹션에서 배운 것

 

1. UProjectileMovementComponent

UProjectileMovementComponent* ProjectileMovementComponent;

발사체의 이동을 처리해주는 컴포넌트

ProjectileMovementComponent->InitialSpeed = 3000.0f;

발사체의 초기 속도 변수. 이 값이 0이면 Velocity 값을 속도로 사용하고, 0이 아니면 Velocity 값을 무시한다.

ProjectileMovementComponent->MaxSpeed = 3000.0f;

발사체에 허용되는 최대 속력

ProjectileMovementComponent->bRotationFollowsVelocity = true;

이 값이 참이면, 발사체의 회전이 이동 방향에 맞춰 매 프레임 업데이트된다.

ProjectileMovementComponent->bShouldBounce = true;

이 값이 참이면, 간단한 바운스가 시뮬레이션 된다. 접촉 시뮬레이션을 중지하려면 이 값을 false로 설정한다.

ProjectileMovementComponent->Bounciness = 0.3f;

충돌의 법선 방향으로 반발이 발생한 이후에 유지되는 속도의 비율

ProjectileMovementComponent->Velocity;

발사체의 속도

 

2. AActor

FVector EyeLocation;
FRotator EyeRotation;

GetActorEyesViewPoint(EyeLocation, EyeRotation);

액터의 시점을 가져오는 함수. 이 시점은 액터의 붙은 카메라의 위치와 회전이 아니라 실제 액터의 눈에 해당하는 위치와 회전이다.

InitialLifeSpan = 3.0f;

액터의 수명을 결정하는 변수. 0으로 설정하면 시간이 지나도 소멸하지 않는다. 이 값은 초기 값이기 때문에, 플레이가 시작된 이후에는 수정해서는 안된다.

 

3. FActorSpawnParameters

FActorSpawnParameters SpawnParams;

액터를 스폰할 때 사용되는 매개변수들을 담는 구조체

SpawnParams.Owner;

이 액터를 생성한 액터. 생성한 액터가 따로 없다면 NULL로 둘 수 있다.

 

4. UWorld

GetWorld()->SpawnActor<T>(Class, Location, Rotation, SpawnParams);

월드에 액터를 스폰하는 함수이다.

 

5. UPrimitiveComponent

Target->AddImpulseAtLocation(Impulse, Location);

대상 물체에 충돌 효과를 주는 함수.

 


 

일인칭 슈팅 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

 

반응형

제대로 따라가기 (7) C++ 프로그래밍 튜토리얼 :: 일인칭 슈팅 C++ 튜토리얼 (2)


작성버전 :: 4.21.0

언리얼 엔진 튜토리얼인 일인칭 슈팅 C++ 튜토리얼에서는 C++ 코드 작업을 통해서 기본적인 일인칭 슈팅(FPS) 게임을 만드는 법을 배울 수 있다.

 

이번 튜토리얼은 각 하위 섹션들의 길이가 길어서 분할되어 작성된다.

 

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


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

 

VisibleDefaultsOnly는 정상적으로 존재하는 UPROPERTY 지정자입니다. 제가 실수로 VisibleDefaultOnly로 오타를 내서 컴파일러가 지정자가 없다고 에러를 띄웠었습니다. 잘못된 정보로 혼동을 드린 점에 대해서 사과드립니다.

 

 

2. 캐릭터 임포트

 

이번 섹션의 목표는 일인칭 슈팅 캐릭터 구현법을 배우는 것이다.

 

 

2-1. 새 캐릭터 만들기(문서)

 

이번 단계에서는 엔진의 Character[각주:1] 베이스 클래스를 사용해서 새 캐릭터를 만들어보자. Character 클래스에는 걷기, 달리기, 점프와 같은 이족보행 동작이 기본 내장되어 있다.

 

캐릭터 클래스 추가

 

파일 드롭다운 메뉴에서 새로운 C++ 클래스... 를 선택해서 새 부모 클래스를 선택한다.

 

 

부모 클래스 선책 창이 열리면 Character 클래스를 부모로 선택하고 다음 버튼을 누른다.

 

 

새 클래스 이름을 "FPSCharacter"라 하고 클래스 생성을 클릭한다.

 

 

FPSCharacter 클래스의 생성이 완료되었다면 FPSCharacter.cpp의 BeginPlay() 함수에 FPSCharacter 클래스가 사용중인지 확인하는 다음 코드를 추가한다.

 

if (GEngine)
{
    GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Red, 
        TEXT("We are using FPSCharacter!"));
}

 

이전 섹션에서도 언급했듯이 전역에 선언된 GEngine 변수를 사용하기 위해서는 "Engine.h"를 포함시켜야 한다. FPSCharacter.cpp의 전처리기 리스트에 "Engine.h" 포함 선언을 추가해주자.

 

#include "Engine.h"

만약 지난 섹션에서 언급한 다른 방식인 "FPSProject.h"에 선언을 추가해두었다면 "FPSProject.h"를 포함시켜주면 된다.

 

#include "FPSProject.h"

 

코드 작성이 끝났다면 변경사항을 저장하고 에디터로 돌아가 컴파일을 진행한다.

 

C++ FPS 캐릭터 클래스를 블루프린트로 확장

 

콘텐츠 브라우저에서 생성된 FPSCharacter 클래스를 찾아서 우클릭한 뒤 FPSCharacter 기반 블루프린트 클래스 생성을 클릭한다.

 

 

BP_FPSCharacter라고 블루프린트 이름을 정하고 Blueprints 폴더를 선택한 뒤, 블루프린트 생성 버튼을 클릭한다.

 

 

기본 폰 클래스 설정

 

새로운 캐릭터 클래스를 블루프린트로 확장했으니, 이제 이 BP_FPSCharacter를 기본 폰으로 사용하도록 설정할 차례다.

 

프로젝트 세칭 창을 열고 프로젝트 섹셩에 맵 & 모드에서 Default Pawn Class 항목을 BP_FPSCharacter로 설정한다.

 

 

 

세팅이 끝났다면 프로젝트 세팅 창을 닫고 레벨 에디터에서 플레이 버튼을 클릭해서 PIE 모드로 들어가보자. 뷰포트 좌상단에 원래의 로그 메시지와 함께 새로 추가한 "We are using FPSCharacter!"라는 문구가 빨간색으로 5초간 표시될 것이다.

 

 

아직 이동에 대한 기능을 전혀 만들지 않았기 때문에 WASD를 사용해도 움직이지 않는 것이 정상적이며 FPSCharacter가 제대로 적용된 것이다.

 

다음 단계로 넘어가기 전에 Esc키를 눌러 PIE 모드에서 빠져나오자.

 

 

2-2. 축 매핑 구성(문서)

 

일반적으로 축 매핑(Axis Mappings)을 통해서 키보드, 마우스, 컨트롤러 입력을 "친근한 이름"으로 매핑시킨뒤 나중에 이동 등의 게임 동작에 바인딩할 수 있다. 축 매핑은 지속적으로 폴링되어, 부드러운 전환 및 게임 동작이 가능하다. 컨트롤러의 조이스틱 같은 하드웨어 축은 "눌렸다", "안눌렸다" 같은 식의 구분되는 입력이 아닌 연속적인 입력 수치를 제공한다. 컨트롤러 조이스틱 입력 방법이 스케일식 동작 입력을 제공해 주기는 하지만, 축 매핑으로 WASD 처럼 지속적 폴링되는 게임 동작을 위한 일반 이동 키 매핑도 가능하다.

 

프로젝트 세팅 창을 열고 엔진 섹션의 입력을 선택한다. 그리고 입력 매핑 세팅을 다음처럼 구성한다.

 

 

 

2-3. 캐릭터 동작 함수 구현(문서)

 

이번 단계에서는 Player Input Component를 구성하고, FPSCharacter 클래스에 다음 함수들을 구현한다.

 

MoveForward();

MoveRight();

 

무브먼트 함수 인터페이스

 

에디터에서 축 매핑을 구성했으니, 매핑에 바인딩할 함수들을 구현하자.

 

FPSCharacter.h에서 클래스 하단부에 다음 함수들의 선언을 추가한다.

 

UFUNCTION()
void MoveForward(float AxisValue);

UFUNCTION()
void MoveRight(float AxisValue);

 

함수 위에 붙여준 UFUNCTION() 매크로는 엔진에게 이 함수들을 인식시켜 직렬화(Serialization), 최적화, 기타 엔진 함수성에 포함될 수 있도록 해준다.

 

동작 함수 구현

 

전형적인 FPS 조작법에서, 캐릭터의 동작 축은 카메라에 상대적이다. "전방"이란 "카메라가 향하는 방향"을, "우측"이란 "카메라가 향하는 방향의 우측"을 뜻한다. 캐릭터의 제어 방향을 구하는 데는 PlayerController를 사용할 것이다. 또한 MoveForward() 함수는 제어 회전의 피치 컴포넌트를 무시하고 입력을 XY 면으로 제한시켜 위아래를 쳐다보더라도 캐릭터는 땅과 평행으로 움직일 수 있도록 한다.

 

FPSCharacter.cpp에서 AFPSCharacter::SetupPlayerInputComponent() 함수의 하단에 다음 코드를

 

InputComponent->BindAxis("MoveForward", this, &AFPSCharacter::MoveForward);
InputComponent->BindAxis("MoveRight", this, &AFPSCharacter::MoveRight);

 

그리고 MoveForward()함수와 MoveRight()함수를 구현한다.

 

void AFPSCharacter::MoveForward(float AxisValue)
{
    FVector Direction = FRotationMatrix(Controller->GetControlRotation()).GetScaledAxis(EAxis::X);
    AddMovementInput(Direction, AxisValue);
}

void AFPSCharacter::MoveRight(float AxisValue)
{
    FVector Direction = FRotationMatrix(Controller->GetControlRotation()).GetScaledAxis(EAxis::Y);
    AddMovementInput(Direction, AxisValue);
}

 

캐릭터 동작 테스트하기

 

구현이 모두 끝났다면, 변경사항들을 저장하고 에디터로 넘어가서 컴파일을 한다.

 

컴파일이 끝나면 플레이 버튼을 눌러서 PIE 모드를 실행한 뒤, WASD 키를 눌러서 캐릭터가 전후좌우로 움직이는지 확인해보자.

 

성공적으로 움직인다면 Esc를 눌러서 PIE 모드를 빠져나오고 다음 파트로 넘어간다.

 

 

 

 

 

2-4. 마우스 카메라 컨트롤 구현(문서)

 

이번 단계에서는 캐릭터의 시야 및 이동 방향을 마우스로 조정하는 기능을 추가한다.

 

마우스 축 매핑 추가

 

프로젝트 세팅 창에서 입력 매핑을 다음과 같이 추가한다.

 

 

입력 처리 구현

 

회전과 쳐다보기에 대한 마우스 입력 처리를 하는 코드를 추가할 차례이다. Character 베이스 클래스는 카메라 회전 컨트롤에 대해서 다음과 같은 필수 함수 둘을 제공한다. 그렇기 때문에 FPSCharacter 클래스에 별도의 함수를 정의하고 구현할 필요없이 바로 AFPSCharacter::SetupPlayerInputComponent() 함수에 바인딩하는 코드를 추가하면 된다.

 

InputComponent->BindAxis("Turn", this, &AFPSCharacter::AddControllerYawInput);
InputComponent->BindAxis("LookUp", this, &AFPSCharacter::AddControllerPitchInput);

 

마우스 카메라 컨트롤 테스트

 

변경 사항들을 저장하고 에디터로 넘어가서 컴파일을 진행한 뒤, 플레이 버튼을 누르고 카메라를 움직여서 카메라가 회전하는 것을 확인해보자.

 

확인을 마쳤다면 Esc를 눌러서 PIE 모드에서 빠져나온다.

 

 

2-5. 캐릭터 점프 구현(문서)

 

액션 매핑은 별도의 이벤트에 대한 입력을 다루며, "친근한 이름"에 매핑시켜 나중에 이벤트 주도형 동작에 바인딩시킬 수 있도록 해준다. 최종 효과는 키나 마우스 버튼, 혹은 키패드 버튼에 대해서 누르기/떼기를 통해서 게임 동작을 실행시키도록 하는 거이다.

 

이번 단계에서는, 스페이스 바에 대한 액션 매핑을 구성하여 캐릭터에 점프 능력을 추가하는 것이다.

 

점프 액션 매핑

 

프로젝트 세팅 창을 열고 엔진 섹션에서 입력을 선택한다. 그리고 액션 매핑을 다음과 같이 추가한다.

 

 

입력 처리 구현

 

Character 베이스 클래스의 인터페이스(*.h) 파일 안을 보면, 캐릭터 점프에 대한 지원이 내장되어 있는 것을 볼 수 있다. 캐릭터 점프는 bPressedJump 변수에 묶여 있어서, 점프 키를 누르면, 이 변수를 true로, 떼면 false로 설정해주기만 하면 된다.

 

FPSChararcter.h에 다음 두 함수의 선언을 추가한다.

 

UFUNCTION()
void StartJump();

UFUNCTION()
void StopJump();

 

그리고 FPSCharacter.cpp에 함수의 구현을 추가해준다.

 

void AFPSCharacter::StartJump()
{
    bPressedJump = true;
}

void AFPSCharacter::StopJump()
{
    bPressedJump = false;
}

 

구현이 끝났으면 SetupPlayerInputComponent() 함수에 바인딩 코드를 추가한다.

 

InputComponent->BindAction("Jump", IE_Pressed, this, &AFPSCharacter::StartJump);
InputComponent->BindAction("Jump", IE_Released, this, &AFPSCharacter::StopJump);

 

캐릭터 점프 테스트

 

변경 사항을 저장하고, 에디터로 돌아가서 컴파일 한다. 그리고 플레이 버튼을 눌러서 PIE에 들어가서 스페이스 바를 눌러서 캐릭터의 점프가 정상적으로 동작하는지 확인해보자.

 

점프가 정상적으로 동작한다면 Esc키를 눌러서 PIE 모드에서 빠져나온다.

 

 

2-6. 캐릭터에 메시 추가(문서)

 

이번 단계에서는 캐릭터에 스켈레탈 메시를 추가한다. Character 클래스는 기본적으로 우리가 쓸 SkeletalMeshComponent를 생성해 주므로, 어떤 스켈레탈 메시를 사용할지 알려주기만 하면 된다.

 

이 튜토리얼에서 사용되는 기본 스켈레탈 메시는 파트 제목 옆에 문서 링크를 통해서 들어가면 샘플 메시라는 링크에서 받을 수 있다.

 

스켈레탈 메시 임포트

 

콘텐츠 브라우저의 콘텐츠 폴더에 들어가서 파일 창에 우클릭하여 /Game에 임포트... 를 선택해서 임포트 대화창을 연다.

 

 

아까 다운받아서 압축해제한 GenericMale.fbx 메시 파일을 찾아 선택하고 열기를 클릭하여 메시를 프로젝트에 임포트 시작한다.

 

콘텐츠 브라우저에서 FBX 임포트 옵션 대화창이 뜬다. 임포트를 클릭하면 프로젝트에 메시가 추가된다.

 

저장 버튼을 클릭해서 임포트된 메시를 저장한다.

 

삼인칭 메시 구성

 

BP_FPSCharacter 아이콘을 더블클릭해서 블루프린트 에디터를 열고 컴포넌트 탭에서 Mesh 컴포넌트를 선택한다.

 

 

디테일 패널의 메시 섹션으로 스크롤해 내려가서 없음이라고 되어있는 드롭다운 메뉴를 클릭해서 GenericMale 스켈레탈 메시를 선택해서 넣는다.

 

 

Z 위치를 -88.0으로 설정해서 SkeletalMeshComponent를 CapsuleComponent에 정렬시킨다.

 

 

SkeletalMeshComponent는 다음과 같은 상태일 것이다.

 

 

 

일반적으로 CapsuleComponent안에 SkeletalMeshComponent이 포함되고, ArrowComponent와의 방향이 일치해야 캐릭터가 월드에서 정상적으로 돌아다닐 수 있다.

 

스켈레탈 메시 설정이 끝났다면 BP_FPSCharacter 블루프린트를 컴파일하고 저장한 뒤에 블루프린트 에디터를 닫자.

 

PIE 모드에서 새 메시 확인

 

레벨 에디터에서 플레이 버튼을 클릭하면 캐릭터를 따라오는 메시의 그림자가 보일 것이다.

 

 

Esc를 눌러 PIE 모드를 빠져나오자.

 

 

 

 

 

2-7. 카메라 뷰 변경(문서)

 

이전 단계에서 살펴보았을 때, 별도의 설정을 하지 않은 기본 카메라의 위치는 메시의 목 안쪽에 위치했었다. 이번 단계에서는 카메라(위치나 시야같은) 프로퍼티 조정에 사용할 수 있는 적합한 FPS 카메라를 구성해보자.

 

카메라 컴포넌트 붙이기

 

FPSCharacter.h에 다음 코드를 추가한다.

 

UPROPERTY(VisibleAnywhere)
UCameraComponent* FPSCameraComponent;

 

그리고 FPSCharacter.cpp의 AFPSCharacter::AFPSCharacter() 생성자 함수에 다음 코드를 추가해준다.

 

FPSCameraComponent = CreateDefaultSubobject<UCameraComponent>(TEXT("FirstPersonCamera"));
FPSCameraComponent->SetupAttachment(GetCapsuleComponent());
FPSCameraComponent->SetRelativeLocation(FVector(0.0f, 0.0f, 50.0f + BaseEyeHeight));
FPSCameraComponent->bUsePawnControlRotation = true;

 

이 코드는 카메라의 위치를 캐릭터의 눈 살짝 위쪽으로 잡으면서 폰이 카메라 로테이션을 제어할 수 있도록 해준다.

 

 

이대로 빌드하면 Camera Component에서 에러가 발생해서 컴파일에 실패할 수 있다. 코드를 작성할 때는 에러가 뜨지 않아서 방심했지만 이 에러는 충분이 아는 에러일 것이다. 지금 비주얼 스튜디오가 한글 버전이라 로그가 깨져있지만 튜토리얼을 진행하면서 생긴 경험으로 미루어 짐작하건데, 헤더의 30라인에서 발생하는 에러는 UCameraComponent가 정의되지 않았다는 에러일 것이다. UCameraComponent 선언 앞에 class를 붙여주자.

 

UPROPERTY(VisibleAnywhere)
class UCameraComponent* FPSCameraComponent;

 

새 카메라 테스트

 

변경사항을 저장하고, 에디터에서 컴파일한 후, 플레이 버튼을 눌러서 테스트 해보자.

 

 

2-8. 캐릭터에 일인칭 메시 추가(문서)

 

FPS에서 흔히 사용되는 방법은, 전신 바디 메시 하나, 무기와 손 메시하나, 이렇게 별개의 캐릭터 메시 두 개를 사용하는 것이다. 전신 메시는 삼인칭 시첨에서 캐릭터를 보거나 다른 캐릭터를 볼대 사용되고, 플레이어가 일인칭 시점에서 게임을 볼 때는 이 전신 메시를 숨긴다. 그리고 "무기와 손" 메시는 전형적으로 카메라에 붙여 플레이어가 일인칭 시점에서 맵을 볼 때 플레이어에게만 보이는 것이다. 이 파트에서는 캐릭터에 일인칭 메시를 추가해보자.

 

일인칭 캐릭터 메시 추가

 

비주얼 스튜디오로 이동해서 FPSCharacter.h를 열고 다음 코드를 추가한다.

 

UPROPERTY(VisibleDefaultsOnly, Category = "Mesh")
USkeletalMeshComponent* FPSMesh;

 

그리고 FPSCharacter.cpp의 AFPSCharacter::AFPSCharacter() 생성자 함수에 다음 코드를 추가하여 일인칭 메시를 생성하고 설정해준다.

 

FPSMesh = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("FirstPersonMesh"));
FPSMesh->SetOnlyOwnerSee(true);
FPSMesh->SetupAttachment(FPSCameraComponent);
FPSMesh->bCastDynamicShadow = false;
FPSMesh->CastShadow = false;

 

SetOnlyOwnerSee는 이 메시가 이 Character에 빙의(Possession)한 PlayerController에게만 보인다는 의미다. 이 코드는 메시를 카메라에 붙이고 배경 그림자를 끈다. 만약 카메라에 붙은 팔의 그림자가 보이면 기괴한 모습이 될 것이다.

 

그 아래쪽에 다음 코드를 추가하여 소유 캐릭터에서 기존 삼인칭 메시를 숨긴다.

 

GetMesh()->SetOwnerNoSee(true);

 

이제 코드의 변경 사항을 저장하고 에디터로 돌아가서 컴파일을 진행한다.

 

만약 에디터 컴파일로 곧바로 변경 사항이 반영되지 않는다면 에디터를 종료하고 솔루션 탐색기에서 FPSProject를 찾아서 우클릭하고 빌드(Build)를 선택해서 프로젝트를 새로 빌드하고 다시 실행하면 변경 사항이 반영된다.

 

 

빌드 완료 후, PIE 모드에서 살펴보면 캐릭터의 메시가 더 이상 보이지 않는 것을 알 수 있다.

 

 

메시 블루프린트 빌드

 

계속하기 전에, 다음 링크에서 샘플 메시를 다운로드해서 압축을 풀어야 한다. "일인칭 스켈레탈 메시"

 

콘텐츠 브라우저의 콘텐츠 파일창에서 우클릭해서 /Game에 임포트를 선택한다.

 

 

HeroFPP.fbx 메시 파일을 찾아 임포트 한다.

 

콘텐츠 브라우저에서 Blueprints 폴더로 들어가서 BP_FPSCharacter 아이콘을 더블클릭해서 블루프린트 에디터를 연다.

 

컴포넌트 탭에서 FPSMesh 컴포넌트를 찾는다.

 

 

FPSMesh 컴포넌트는 FPSCameraComponent의 자손이라, 카메라에 항상 붙어있다.

 

컴포넌트 탭에서 FPSMesh를 클릭한다.

 

디테일 탭에서 Mesh 섹션으로 스크롤해 내려가서 "없음"이라는 드롭다운 메뉴를 클릭해서 HeroFPP 스켈레탈 메시를 선택하여 뷰포트에 팔을 추가한다.

 

 

 

새로 추가된 HeroFPP 스켈레탈 메시는 뷰포트 안에서 다음과 같이 보일 것이다.

 

 

새로 추가된 메시의 트랜스폼이 카메라 앞에 오도록 조절한다. 위치를 로, 회전을 {-180, 50, -180}으로 설정한다.

 

 

블루프린트 에디터를 닫기 전에 BP_FPSCharacter 블루프린트를 반드시 컴파일하고 저장하자.

 

게임 내 새 메시 확인

 

레벨 에디터에서 플레이 버튼을 클릭해서 게임 내에서 새 메시를 확인한다.

 

 

Esc키를 눌러서 PIE 모드에서 빠져나오자.

 

다음 섹션에서는 발사체(Projectile) 구현법을 배워보자.

 


 

이번 섹션에서 배운 것

 

1. FRotatorMatrix

 

FRotatorMatrix RotatorMatrix;

 

행렬에서 이동 행렬을 뺀 순수하게 회전에 대한 행렬이다.

 

RotatorMatrix.GetScaledAxis(EAxis::X);

 

행렬의 크기에 의해서 스케일링된 행렬의 축을 가져오는 함수

 

2. ACharacter

 

Controller;

 

캐릭터를 제어하는 컨트롤러에 대한 변수

 

Controller->GetControlRotation();

 

컨트롤 회전을 가져오는 함수. 이것은 카메라 방향(예 : 3 인칭보기)과 다를 수 있는 완전한 목표 회전이며 제어된 Pawn (시각적으로 피치를 두거나 롤하지 않도록 선택할 수 있음)의 회전과 다를 수 있다.

 

AddControllerYawInput();

 

캐릭터 클래스에서 기본적으로 지원하는 Yaw 회전 처리 함수. 이 함수를 마우스 좌우 이동 입력과 바인딩하면 카메라 좌우 회전하는 기능을 만들 수 있다.

 

AddControllerPitchInput();

 

캐릭터 클래스에서 기본적으로 지원하는 Pitch 회전 처리 함수.

 

bPressedJump = true;

 

캐릭터 클래스에서 기본적으로 지원하는 점프 처리 변수. true가 되면 캐릭터 클래스에서 자동으로 점프를 처리해준다.

 

3. UCameraComponent

 

CameraComponent->bUsePawnControlRotation = true;

 

이 카메라 컴포넌트가 폰의 컨트롤 회전을 따라서 회전할 지를 결정하는 변수

 

4. USkeletalMeshComponent

 

USkeletalMeshComponent* SkeletalMeshComponent;

 

애니메이션을 사용할 수 있는 스켈레탈 메시 애셋의 인스턴스를 만드는데 사용되는 컴포넌트.

 

SkeletalMeshComponent->SetOnlyOwnerSee(true);

 

UPrimitiveComponent 클래스에서 상속받은 함수로 이 메시를 소유한 플레이어만 이 메시를 볼 수 있게 하는 함수

 

SkeletalMeshComponent->bCastDynamicShadow = false;

 

사전에 계산되지 않은 실시간 그림자를 그려야하는지에 대한 변수. CastShadow가 true일 때만 작동한다.

 

SkeletalMeshComponent->CastShadow = false;

 

그림자를 그려야하는지에 대한 변수

 

SkeletalMeshComponent->SetOwnerNoSee(true);

 

이 메시를 소유한 플레이어에게만 보이지 않도록 하는 함수

 

  1. Pawn 클래스에서 파생되었다. [본문으로]

[투네이션]

 

-

 

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

 

반응형

제대로 따라가기 (6) C++ 프로그래밍 튜토리얼 :: 일인칭 슈팅 C++ 튜토리얼 (1)

작성버전 :: 4.21.0

 


언리얼 엔진 튜토리얼인 일인칭 슈팅 C++ 튜토리얼에서는 C++ 코드 작업을 통해서 기본적인 일인칭 슈팅(FPS) 게임을 만드는 법을 배울 수 있다.

 

이번 튜토리얼은 각 하위 섹션들의 길이가 길어서 분할되어 작성된다.

 

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


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

 

1. 프로젝트 구성

 

1-1. 프로젝트 구성(문서)

 

언리얼 엔진을 실행하고 프로젝트 브라우저에서 새 프로젝트 탭에서 C++탭을 선택한다.

 

기본 코드(Basic Code)를 선택하고 프로젝트 이름을 "FPSProject"[각주:1]로 입력한다. "시작용 콘텐츠 없음"으로 하여 프로젝트를 생성한다.

 

 

언리얼 에디터에서 프로젝트가 열리면, 플레이 버튼을 눌러서 에디터에서 플레이(PIE, Play In Editor) 모드로 들어가본다. WASD키를 이용해서 이동하거나 마우스를 이용해서 화면의 방향을 전환할 수 있다.

 

 

Esc키를 누르거나, 중지 버튼을 눌러서 PIE 모드를 빠져나간다.

 

 

레벨 탐험을 마쳤다면 Contents 폴더 안에 Maps 폴더를 생성한다.

 

 

파일 메뉴에서 현재 레벨을 다른 이름으로 저장... 을 선택하여 레벨을 Maps 폴더에 "FPSMap"으로 저장한다.

 

 

편집 메뉴에서 프로젝트 세팅을 클릭한다.

 

 

프로젝트 세팅 창에서 프로젝트 섹션의 맵 & 모드에 Editor Startup Map을 우리가 만든 FPSMap으로 설정해준다.

 

 

프로젝트 세팅 창을 닫고, 프로젝트를 저장한 뒤, 다음 단계로 이동하자.

 

 

1-2. Visual Studio에서 프로젝트 열기(문서)

 

Game Mode는 게임의 규칙과 승리 조건등을 정의하는 클래스로, 프로젝트를 구성할 때, 언리얼 엔진이 기본 Game Mode 클래스를 생성해주었다. 우리는 이 Game Mode에 기본 게임플레이 프레임워크 유형에 사용될 기본 클래스를, Pawn, PlayerController, HUD 등을 포함해서 설정할 계획이다. 이 파트에서는 에디터를 통해서 비주얼 스튜디오를 열고 거기서 프로젝트의 Game Mode 클래스를 확인해 볼 것이다.

 

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

 

 

비주얼 스튜디오가 열리면, 솔루션 탐색기를 통해서 프로젝트에 포함된 소스파일(.cpp)과 헤더파일(.h)가 보인다.

 

 

FPSProjectGameMode.cpp 안에 다음과 같은 코드가 있을 것이다.

 

#include "FPSProject.h"
#include "FPSProjectGameMode.h"

 

언리얼 엔진이 버전업 되면서 프로젝트에 자동 생성되는 Game Mode 클래스명 끝에 Base가 붙도록 변경되었다. 그래서 열어봐야할 소스파일의 이름은 "FPSProjectGameModeBase.cpp가 된다. 이것은 헤더파일에도 포함되는 것이다.

 

그리고 처음에 불필요한 빌드 및 컴파일 시간은 줄이기 위해서 기본적으로 필요한 헤더를 제외한 헤더의 포함을 최소화 하도록 변경되었기 때문에 FPSProjectGameModeBase.cpp 파일의 전처리기에 "FPSProject.h"가 포함되어 있지 않을 것이다. 그렇기 때문에 소스파일의 내용은 다음과 같을 것이다.

 

#include "FPSProjectGameModeBase.h"

 

다음 부분 부터는 FPSProjectGameModeBase를 기준으로 설명할 것이다.

 

FPSProjectGameModeBase.h 안에는 다음과 같은 코드가 있다.

 

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/GameModeBase.h"
#include "FPSProjectGameModeBase.generated.h"

/**
 *
 */
UCLASS()
class FPSPROJECT_API AFPSProjectGameModeBase : public AGameModeBase
{
    GENERATED_BODY()
};

 

이제 프로젝트에 코드를 추가할 준비가 완료되었다.

 

 

 

 

 

1-3. 로그 메시징 추가(문서)

 

이 파트에서는 로그 메시지를 사용해서 언리얼 엔진에서 제공되는 기본 Game Mode가 아닌 FPSProjectGameModeBase가 실제로 사용되는지 점검해볼 것이다. 로그 메시지는 개발 도중 코드를 점검하고 디버깅하는데 쓰이는 유용한 도구이다.

 

FPSProjectGameModeBase.h의 클래스 선언 하단에 다음 함수 선언을 추가하자.

 

public:
    virtual void StartPlay() override;

 

그리고 FPSProjectGameModeBase.cpp로 가서 함수의 구현을 작성한다.

 

void AFPSProjectGameModeBase::StartPlay()
{
    Super::StartPlay();

    if (GEngine)
    {
        GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Yellow, TEXT("Hello World, this is FPSGameMode!"));
    }
}

 

이 부분에서는 GEngine이 정의되지 않았다는 문제가 발생할 것이다. 이 문제는 FPSProjectGameModeBase.cpp의 전처리기 파트에 "Engine.h"를 포함시켜주면 해결된다.

 

참고로 이 Engine 헤더는 덩치가 꽤나 큰 헤더기 때문에 필요할 때마다 이곳 저곳에서 포함시켜서 사용하는 것보다는 "FPSProject.h" 같은 헤더에 미리 선언해두고 이 "FPSProject.h"를 포함시켜서 사용하는 편이 좋을 수 있다.

 

#include "Engine.h"

 

코드 작성이 모두 끝났다면 변경사항들을 저장하고 에디터로 넘어간다.

 

 

1-4. 프로젝트 컴파일(문서)

 

이 파트에서는 프로젝트를 컴파일하여 코드 변경사항을 게임에 반영시켜볼 것이다.

 

에디터로 돌아왔다면, 컴파일 버튼을 클릭해서 코드를 컴파일한다.

 

 

플레이 버튼을 눌러서 PIE모드에 들어가서 화면에 로그가 뜨는지 확인해보자.

 

 

하지만 PIE 모드에 들어가도, 화면에 로그가 나타나지 않을 것이다. 그 이유는 아직 기본 Game Mode를 사용하고 우리가 만든 FPS Game Mode를 적용하지 않았기 때문이다.

 

그럼 이제, Esc를 눌러 PIE 모드를 중지하고 다음 단계로 넘어가자.

 

CPP Game Mode 클래스를 블루프린트로 확장

 

먼저, 콘텐트 브라우저의 콘텐츠 폴더 안에 Blueprints 폴더를 만든다.

 

 

C++클래스/FPSProject 폴더 안의 FPSGameModeBase 클래스를 우클릭해서 "FPSProjectGameModeBase 기반 블루프린트 클래스 생성"을 선택한다.

 

 

새 블루프린트 클래스의 이름을 BP_FPSProjectGameModeBase로 짓고, Blueprints 폴더를 선택한 뒤, 블루프린트 클래스 생성 버튼을 누른다.

 

 

새로 생성한 블루프린트 클래스가 Blueprints 폴더에 생긴 것을 볼 수 있다. 블루프린트를 저장하고 블루프린트 에디터를 닫자.

 

 

 

1-5. 디폴트 게임 모드 설정(문서)

 

새로 만든 게임 모드를 블루프린트로 확장시키는데 성공했으니, 이번 파트에서는 프로젝트가 FPSProjectGameModeBase를 기본 게임 모드로 사용하도록 설정해줘야 한다.

 

편집 메뉴에서 프로젝트 세팅창을 연다.

 

프로젝트 세팅 창이 열리면 프로젝트 섹션 아래의 맵 & 모드를 선택하고 Default GameMode 드롭다운에서 BP_FPSProjectGameModeBase를 선택한다.

 

 

프로젝트 세팅 메뉴를 닫고 레벨 에디터의 플레이 버튼을 클릭하면 뷰포트 좌상단 구성에서 "Hello World, this is FPSGameMode!" 라는 문구가 5초간 노란색으로 표시되는 것을 볼 수 있다.

 

 

 

 

이제 프로젝트 구성이 끝났다. 다음 섹션에서부터는 캐릭터를 구현하는 방법을 배우게 될 것이다.

 

 

 

 

 

 


 

이번 섹션에서 배운 것

 

1. AGameMode

 

StartPlay();

 

플레이가 시작되었을 때, 게임 모드에서 액터들의 BeginPlay() 함수를 호출하는 역할.

 

2. GEngine

 

GEngine;

 

전역에 선언되어 있는 엔진 포인터. 사용하기 위해서는 "Engine.h"를 포함해줘야 한다. 엔진에서 실행되는 경우가 아닐 경우, 값이 유효하지 않을 수 있으니 사용하기 전에 반드시 GEngine이 유효한 상태인지 체크하고 사용해야한다.

 

GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Yellow, TEXT("Hello World, this is FPSGameMode!"));

 

뷰포트에 디버그 메시지를 띄우는데 사용되는 함수

 

3. FColor

 

FColor Color;

 

언리얼 엔진에서 색상으로 표현하는데 사용되는 구조체

 

FColor::Red;
FColor::Blue;
FColor::Green;
FColor::Black;
FColor::White;
FColor::Cyan;
FColor::Emerald;
FColor::Magenta;
FColor::Orange;
FColor::Purple;
FColor::Silver;
FColor::Transparent;
FColor::Turquoise;
FColor::Yellow;

 

언리얼 엔진에서는 기본적인 색상을 스태틱으로 미리 만들어서 사용자가 일일이 구조체를 생성해서 색상을 만들 필요가 없게 만들어 두었다.

 

 

  1. 다른 이름을 사용해도 상관없지만, 만약 다른 이름을 사용한다면 몇몇 코드 샘플에서의 이름이 달라질 것이다. [본문으로]

[투네이션]

 

-

 

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

 

반응형

제대로 따라가기 (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

 

반응형

제대로 따라가기 (1) C++ 프로그래밍 튜토리얼 :: 변수, 타이머, 이벤트 (타이머를 사용하는 액터 만들기)

 

작성버전 :: 4.20.3

 

언리얼 엔진은 다양한 기능을 제공하며, 그 기능에 대한 튜토리얼들이 문서에 존재한다. 언리얼 엔진을 공부하기 위해선 필수적으로 이러한 튜토리얼들을 첫걸음으로 따라가게 되는데, 언리얼 튜토리얼 문서는 가끔 따라가다보면 제대로 진행이 안되고 막히는 부분이 존재한다. 튜토리얼은 배우는 단계인데 아직 엔진에 전혀 숙련되지 못한 사람이 이런 문제에 부딪히면 생각보다 많은 시간은 잡아먹게 된다. 제대로 따라가기는 이런 튜토리얼 도중에 막히는 부분을 빠르게 해소하고 따라가기 위해 제작되었다.

 

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

 

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

 

 

변수, 타이머, 이벤트 (1. 타이머를 사용하는 액터 만들기)

 

변수, 타이머, 이벤트 튜토리얼은 변수와 함수를 에디터에 노출시키는 법, 타이머를 사용하여 코드 실행을 지연 또는 반복시키는 법, 이벤트를 사용하여 액터 사이의 통신을 하는 법을 알려주는 튜토리얼이다.

 

Countdown 클래스 추가

 

 

 

우선 C++ 프로젝트에서 Actor 클래스를 상속받는 Countdown 클래스를 생성하도록 한다.

 

 

카운트다운 진행 상황을 보여주기 위한 기능 추가

 

클래스가 생성되었다면 비주얼 스튜디오를 열어서 생성된 클래스에 카운트다운할 시간 변수와 카운트다운 진행 상황을 보여줄 텍스트 렌더 컴포넌트와 함수를 추가해야 한다. 그 예시 코드는 다음과 같다.

#pragma once

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

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

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

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

    int32 CountdownTime;
   
    UTextRenderComponent* CountdownText;

    void UpdateTimerDisplay();
};

 

추가된 것은 int32 CountdownTime, UTextRenderComponent* CountdownText, void UpdateTimerDisplay()이다.

 

바로 이 부분에서 막히는 사람들이 꽤 많을 거라고 생각한다.

 

 

바로 UTextRenderComponent가 정의되어 있지 않다고 신텍스 에러가 뜨기 때문이다. 이 문제를 해결하기 위해서는 UTextRenderComponent가 정의된 헤더를 포함시켜줘야 한다. UTextRenderComponent 클래스는 Engine/Classes/Components/TextRenderComponent.h 에 정의되어 있다.
 
하지만 이 TextRenderComponent.h를 추가해야 된다는 걸 깨달았다고 모든 문제가 해결되지는 않았다. 바로 헤더 포함 순서 문제가 남아있기 때문이다. 습관적으로 새로 추가하는 헤더를 가장 뒤에 추가하는 프로그래머들이 많을텐데 언리얼 C++프로그래밍에서는 헤더를 포함할 때 순서를 지켜야 한다. 새로 추가되는 헤더는 무조건 generated.h보다 위쪽에서 추가되어야 한다.
#pragma once

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

#include "Engine/Classes/Components/TextRenderComponent.h"
#include "Countdown.generated.h"

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

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

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

    int32 CountdownTime;
   
    UTextRenderComponent* CountdownText;

    void UpdateTimerDisplay();
};

위의 예시 코드처럼 generated.h 위의 적당한 위치에 TextRenderComponent.h를 포함시켜주면 신텍스 에러가 발생하지 않는다.

 

그 다음 작업은 ACountdown 클래스의 생성자에서 액터의 프로퍼티 값들을 초기화해주는 것이다. 언리얼 엔진 문서에서 제공하는 예시코드는 다음과 같다.

// Sets default values
ACountdown::ACountdown()
{
     // Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
    PrimaryActorTick.bCanEverTick = false;

    CountdownText = CreateDefaultSubobject(TEXT("CountdownNumber"));
    CountdownText->SetHorizontalAlignment(EHTA_Center);
    CountdownText->SetWorldSize(150.0f);
    RootComponent = CountdownText;

    CountdownTime = 3;
}

 

이 클래스에서 Tick 기능은 사용하지 않기 때문에 bCanEverTick은 false로 하고 CountdownText에 TextRenderComponent를 생성해서 루트 컴포넌트에 붙여주고 CountdownTime을 3초로 설정한다.

 

하지만 코드가 과거버전 기준으로 만들어지고 문서가 업데이트되지 않은 문제인지, CreateDefaultSubobject()함수를 호출하는 부분에서 신텍스 에러가 발생한다. 그래서 CreateDefaultSubobject() 함수를 살펴보면 템플릿 함수임을 알 수 있다.

CountdownText = CreateDefaultSubobject<UTextRenderComponent>(TEXT("CountdownNumber"));

CountdownText 변수가 받아야하는 UTextRenderComponent를 템플릿 파라미터에 넣어주면 문제없이 신텍스 에러가 사라진다.

그 다음은 아까 정의해둔 UpdateTimerDisplay() 함수를 구현하는 것이다. 이 함수는 남은 시간을 TextRenderComponent에 업데이트하고 시간이 다되면 0을 표시하도록 한다.

void ACountdown::UpdateTimerDisplay()
{
    CountdownText->SetText(FString::FromInt(FMath::Max(CountdownTime, 0)));
}

 

 

 

 

 

타이머(Timer)

 

화면에 대한 준비를 끝냈다면 이번에는 시간을 체크할 타이머를 추가할 차례다. 타이머란 사용자가 정의한 시간마다 사용자가 지정한 동작이 실행되도록 하는 것이다. 이러한 동작은 물론 Tick() 함수에서 DeltaTime 값을 받아서 같은 동작을 수행하도록 할 수는 있지만, 사용자가 지정한 동작이 지속적으로 실행될 필요가 없이 특정한 순간에만 몇 번 실행되면 되거나 실행될 텀이 1초를 넘는 경우라면 Tick() 함수에서 시간을 재서 실행하는 것보다는 타이머를 이용하는 편이 좋다.

 

타이머에 대해 이해가 되었다면 이제 타이머에 필요한 멤버 변수와 함수들을 Countdown.h의 Countdown 클래스의 하단에 추가해보자.

void AdvanceTimer();

void CountdownHasFinished();

FTimerHandle CountdownTimerHandle;

 

AdvanceTimer() 함수는 Timer가 돌아가면서 호출될 함수이다.

 

CountdownHasFinished() 타이머가 사용자가 의도한 만큼 돌아간 뒤의 처리를 위한 함수이다.

 

차량에 달린 핸들이 차량의 이동 방향을 컨트롤하기 위한 것이듯, FTimerHandle 역시 타이머를 컨트롤하기 위한 구조체로서 CountdownTimerHandle 변수는 카운트다운이 끝났을 때, 타이머가 계속해서 돌아가지 않도록 종료하기 위해서 필요하다.

 

AdvanceTimer() 함수와 CountdownHasFinished() 함수를 모두 정의했다면 이번에는 각 함수를 구현해보자.

void ACountdown::AdvanceTimer()
{
    --CountdownTime;
    UpdateTimerDisplay();
    if (CountdownTime < 1)
    {

        // 카운트다운이 완료되면 타이머를 중지
        GetWorldTimerManager().ClearTimer(CountdownTimerHandle);
        CountdownHasFinished();
    }
}

 

AdvanceTimer() 함수의 예시 코드는 위와 같은데 이 함수를 구현하면서 문제가 다시 발생한다. 이번에는 GetWorldTimerManager() 함수에서 ClearTimer() 함수를 호출할 때 "불완전한 형식은 사용할 수 없습니다." (E0070 :: Incomplete type is not allowed.) 라는 에러가 발생한다.

 

이 문제는 아래의 예시 코드와 같이 Countdown.cpp의 상단에 TimerManager.h를 포함시켜주면 해결된다.

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

#include "Countdown.h"
#include "TimerManager.h"

 

CountdownHasFinished() 함수의 코드는 다음과 같다.

void ACountdown::CountdownHasFinished()
{
    CountdownText->SetText(TEXT("Go!"));
}

 

다음 작업은 BeginPlay() 함수에서 텍스트 표시를 초기화하고 타이머를 동작시키는 것이다.

void ACountdown::BeginPlay()
{
    Super::BeginPlay();
   
    UpdateTimerDisplay();
    GetWorldTimerManager().SetTimer(CountdownTimerHandle, this, &ACountdown::AdvanceTimer, 1.0f, true);
}

 

 

에디터 컴파일과 레벨 배치 그리고 테스트 실행

 

 

 

모든 코드 작업이 끝났다면 이제 언리얼 에디터로 돌아가서 컴파일 버튼을 눌러보자. 

 

 

 

만약 컴파일 에러 없이 컴파일에 성공했다면 위의 이미지와 같이 컴파일 완료라고 에디터의 오른쪽 하단에 출력될 것이다.

 

 

 

컴파일이 완료된 다음에 우리가 작성한 Countdown 클래스를 레벨 에디터에 드래그 앤 드롭해서 배치할 수 있다.

 

 

 

배치를 완료했다면 플레이 버튼을 눌러서 실행해보자. 그러면 화면의 Text 글자가 3, 2, 1, Go!로 바뀌는 것을 확인할 수 있다.

 

 

 

 

 

 


 

 

이번 섹션에서 배운 것

 

 

1. CreateDefaultSubobject<T>() (언리얼 엔진 문서)

 

UObject 클래스를 상속받는 모든 클래스에서 사용가능한 함수이다. 하위 오브젝트나 컴포넌트를 생성할 때 사용되는 함수로 2번의 UTextRenderComponent를 생성하는 예시와 같이 사용된다. 이 함수는 T의 포인터(T*) 타입을 반환한다.

 

 

2. UTextRenderComponent(언리얼 엔진 문서)

 

UTextRenderComponent* TextRenderComponent;

 

설정된 텍스트를 3D 공간 상에 렌더링하는 컴포넌트이다. 글자 색, 크기, 폰트, 정렬 등을 설정할 수 있으며 액터 등에 컴포넌트로 덧붙여서 사용할 수 있다. 이 컴포넌트를 사용하기 위해서는 "Engine/Classes/Components/TextRenderComponent.h"를 포함해야 한다.

 

TextRenderComponent = CreateDefaultSubobject<UTextRenderComponent>(TEXT("TextRenderComponent"));

 

코드 상에서 UTextRenderComponent를 생성하는 방법은 위와 같다.

 

TextRenderComponent->SetHorizontalAlignment(EHTA_Center);

 

렌더링되는 텍스트의 수평 정렬을 설정하는 함수이다. 정렬 방식은 EHTA_Center, EHTA_Left, EHTA_Right가 있다.

 

TextRenderComponent->SetWorldSize(100.0f);

 

렌더링되는 텍스트의 월드에서의 크기를 설정하는 함수이다.

 

TextRenderComponent->SetText(TEXT("TEXT"));

 

렌더링되는 텍스트의 문자열 내용을 설정하는 함수이다.

 

 

3. Timer

 

타이머는 사용자가 정의한 시간마다 사용자가 지정한 동작이 실행되도록 만든다.

 

1) FTimerHandle (언리얼 엔진 문서)

 

FTimerHandle TimerHandle;

 

FTimerHandle은 타이머를 구별할 수 있는 유일한 핸들이다. 타이머를 생성하는 함수는 타이머를 생성할 때, 타이머의 핸들을 돌려주는데, 이 핸들을 가지고 있어야 생성한 타이머를 중지시킬 수 있다.

 

2) GetWorldTimerManager() (언리얼 엔진 문서)

 

AActor 클래스를 상속받는 모든 클래스에서 호출가능한 함수이다. 월드 타이머 매니저를 반환한다. GetWorldTimerManager()의 호출이 정상적으로 되지 않을 경우 "TimerManager.h"를 포함시키면 된다.

 

GetWorldTimerManager().SetTimer(TimerHandle, this, &ACountdown::AdvenceTimer, 1.0f, true);

 

SetTimer() 함수는 타이머를 생성하고 시작시키는 함수로 여러가지 오버로드가 존재하지만 우선은 위의 오버로드 형식만 살펴보자.

 

첫 번째 매개변수는 지금 생성되는 타이머의 핸들이다. 위에서 설명했듯이 이 핸들을 가지고 있어야 나중에 타이머를 종료할 수 있다.

 

두 번째 매개변수는 타이머 함수를 호출하는 오브젝트이다.

 

세 번째 매개변수는 타이머가 발동할 때마다 호출될 함수이다.

 

네 번째 매개변수는 타이머가 호출될 시간이다. 만약 값을 1로 두면 1초에 한 번씩 함수가 호출된다.

 

다섯 번째 매개변수는 타이머의 반복 여부이다. 만약 값이 false라면 타이머는 반복되지 않고 정해진 시간에 한 번만 호출된다.

 

GetWorldTimerManager().ClearTimer(TimerHandle);

 

ClearTimer() 함수는 돌아가고 있는 타이머를 중지시키고 해당 핸들을 무효화시키는 함수이다.

 

 

[투네이션]

 

-

 

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

 

반응형

악마의 문법, goto

 

프로그래밍을 공부하면 여러가지 프로그래밍 문법과 기능과 그것을 다루는 기술들을 배우게 된다. 그렇게 배우는 문법들의 중의 하나가 바로 goto 인데, 이 goto 문에 대해서는 여러 프로그래머들의 부정적인 시각이 강하다. 오죽하면 이 goto 문을 알려주고 나서 하는 제일 첫 마디가 "가급적 사용하지 말라"이겠는가?

 

이 goto 문의 사용을 권장하지 않는 이유는 하나다. 코드의 가독성을 심각하게 해친다는 것이다. 물론 goto를 사용한다고 해서 무조건 코드의 가독성을 해치는 것은 아니지만 다음처럼 사용하게 된다면 많은 문제가 발생하게 될 것이다 :

 

void f()
{
    int i = 10;

    if (i > 100)
    {
        I100:
        if (i < 200)
        {
            goto I200;
        }
        else if (i < 300)
        {
            goto I300;
        }
    }
    goto I100;

I200:
    cout << 1 << endl;
    return;
I300:
    cout << 2 << endl;
    return;
}

 

위의 예시처럼 코드 이곳저곳을 뛰어넘게 되는 goto 문이 많을 경우나 goto I100; 부분처럼 조건에 맞지 않는 상황에서 조건문 안으로 강제 진입하게 만드는 goto 문이 있을 경우는 심각한 문제가 된다. 다른 작업자가 작업하게 되거나 작성자 자신이 재작업할 때, 코드의 흐름을 읽어내기가 어려워지고 조건문과 코드를 신뢰할 수 없게 될 것이다.

 

그렇다면 적절한 goto 문의 사용법이란 무엇인가? 사실 모든 프로그램은 goto 문 없이도 작성이 가능하다. 그렇기 때문에 goto문을 사용하지 않도록 권장하는 것이기도 하다. 하지만 이 goto 문 역시 적절하게 사용하면 기존의 방식보다 편하게 코드를 작성이 가능하다.

 

그 첫 번째 예시는 2중 이상의 반복문에서 탈출할 때이다. 반복문에서 탈출할 때 주로 사용되는 문법은 break인데, 이 break는 한 번에 단 하나의 반복문만을 탈출할 수 있다. 그래서 break로 2중 이상의 반복문을 탈출할 때는 다음과 같이 코드를 작성해야 한다 :

 

int main()
{
    bool isBreak = false;
    for (int i = 0; i < 10; i++)
    {
        for (int j = 0; i < 10; j++)
        {
            if (/*탈출조건*/)
            {
                isBreak = true;
                break;
            }
        }
        if (isBreak)
            break;
    }
}

 

위의 예제처럼 goto 문을 사용하지 않아도 2중 반복문을 탈출할 수 있지만, 하나의 논리 변수를 추가로 사용하고 분기문 역시 추가로 사용해야 한다. 그리고 이것은 반복문의 깊이가 깊어질 수록 사용되는 분기문의 숫자 역시 늘어나게 될 것이다(흔하지 않은 경우이기는 하다). 하지만 다음과 같이 goto 문을 사용하면 논리 변수와 반복문을 추가로 사용하지 않고도 단번에 다중 반복문을 탈출할 수 있게 된다 :

 

int main()
{
    for (int i = 0; i < 10; i++)
    {
        for (int j = 0; i < 10; j++)
        {
            if (/*탈출조건*/)
            {
                goto BREAK;
            }
        }
    }
BREAK:
}

 

 

두 번째 예시는 재입력 처리이다. 만약 1-3 값 만을 입력해야 하는 프로그램이 있다면,  그 이외의 값이 들어온다면 사용자가 값을 다시 입력하도록 프로그래밍해야할 것이다. 여기에 사용되는 일반적인 방법은 do-while 문을 사용하는 방법과 무한 loop에 진입시킨 이후에 옳은 값을 입력했다면 루프에서 탈출시키는 방법이다.

 

int main()
{
    int i = 0;

    // do-while 문을 사용하는 방법
    do
    {
        cout << "값을 입력하세요(1~3)(do-while) :: ";
        cin >> i;
    } while (i < 1 || i > 3);

    // 무한 loop를 사용하는 방법
    while (true)
    {
        cout << "값을 입력하세요(1~3)(무한루프) :: ";
        cin >> i;
        if (i > 0 && i < 4)break;
    }
}

 

이 경우에도 충분히 goto 문이 사용될 수 있다.

 

int main()
{
    int i = 0;

WRONGINPUT:
    cout << "값을 입력하세요(1~3)(무한루프) :: ";
    cin >> i;
    if (i < 1 || i > 3)
        goto WRONGINPUT;
}

 

이렇듯 충분히 가독성을 해치지 않는 선을 지키면서도 goto 문을 사용할 수 있다. 프로그래밍을 하면서 중요한 점은 바로 이것이다. 프로그래밍에 필요한 모든 것을 최대한 활용하되 과하게 사용하지 말고 적절한 위치에 적절하게 사용하라는 것이다. 그것만으로도 코드는 충분히 깔끔해지고 작업 효율성이 상승하게 될 것이다.

 

 

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

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

 

에셋스토어

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

 

반응형

+ Recent posts