개발단에 가입하여 베르의 게임 개발 유튜브를 후원해주세요!

 

베르의 게임 개발 유튜브

안녕하세요! 여러분들과 함께 게임 개발을 공부하는 베르입니다! 게임 개발에 도움이 되는 강좌들을 올리는 채널입니다! [투네이션 후원] https://toon.at/donate/637735212761460238 [유니티 어필리에이트

www.youtube.com

 

안녕하세요! 여러분들과 함께 게임 개발을 공부하는 베르입니다!

베르의 게임 개발 유튜브의 디스코드 채널 개설 공지입니다!

 

타임라인

0:00 인트로

0:42 디스코드 채널 소개와 가입 방법

2:33 질문 채널 사용법

3:57 아웃트로

 

[디스코드 채널]

 

 

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

인트로

안녕하세요. 여러분들과 함께 게임 개발을 공부하는 베르입니다.

이번에는 베르의 게임 개발 유튜브의 디스코드 채널이 만들어졌다는 것을 알려드리려고 합니다.

사실 디스코드 채널 생성 공지는 얼마 전에 했던 라이브 스트리밍과 커뮤니티 글을 통해서 드렸었지만, 라이브 스트리밍과 커뮤니티 글은 접근성이 떨어져 못본 분들이 많은 듯해서 이렇게 영상을 만들게 되었습니다.

유튜브 채널을 계속 운영해오면서 강좌에 대한 질문이나 궁금하신 점들을 주로 댓글로 보고 답변을 드렸었는데 아무래도 유튜브 댓글로는 이미지나 영상을 첨부할 수 없어서 자세한 내용 파악이나 답변이 어려운 점이 많았습니다.

그래서 이렇게 이미지나 영상들을 포함해서 질문할 수 있는 사이트들을 여럿 고민하다 디스코드에 채널을 열기로 결정했습니다.

디스코드 채널 가입과 채널 소개

우선 영상 하단에 있는 디스코드 채널 링크를 통해 베르의 게임 개발 채널에 가입하실 수 있습니다.

채널에 가입하고 나면 이렇게 여러 개의 채팅 채널을 보실 수 있습니다.

먼저 공지 채널은 강좌가 업로드되거나 베르의 게임 개발 유튜브 채널에 이벤트가 있을 때 공지가 올라오는 채널입니다.

아직은 별다른 이벤트가 없어서 대부분은 강좌 업로드 공지만 올라오지만 언젠가 이벤트를 열게 되어서 이벤트 공지를 올릴 때가 오면 좋겠네요.

그 다음 일반 채널은 채널에 접속하신 유저들끼리 대화를 나누는 채널입니다.

욕설이나 어그로, 도배 등 나쁜 행동은 삼가해주시고 자유롭게 대화를 나눠주세요.

그 다음 질문 채널은 강좌나 개발에 있어서 궁금하거나 도움이 필요한 부분을 질문으로 올려주시면 되는 채널입니다.

질문 채널을 이용하는 방법은 잠시 후에 좀 더 자세하게 설명하기로 하고 다음 채널 설명으로 넘어가겠습니다.

그 다음 채널은 강의요청 채널입니다.

이 채널에서는 여러분들이 원하는 강좌를 요청해주시면 됩니다.

요청해주신 강좌는 제가 만들 수 있는 강좌라면 저의 강좌 목록에 올라가게 됩니다.

많이 어렵거나 제가 모르는 분야라서 제가 하기 어려운 내용이라면 요청을 받아들이지 않을 수도 있지만 가급적이면 리스트에 올리고 연구를 하는 방식으로 진행하려고 합니다.

그리고 이미 리스트에 있는 주제라면 리스트에서 우선 순위가 올라가게 됩니다.

물론 한 분이 여러 번 요청하는 건 기록해두고 한 계단만 올릴 예정이니 요청을 도배하지는 말아주세요.

그 다음 건의 채널은 유튜브 채널이나 디스코드 채널 운영과 관련하여 이렇게 했으면 좋겠다하는 사항을 제안하는 채널입니다.

마지막으로 프로젝트 자랑 채널은 여러분들이 만들거나 개발 중인 프로젝트를 자랑하기 위한 채널입니다.

자신의 프로젝트를 자랑하거나 다른 사람의 프로젝트를 보면서 칭찬과 격려를 아끼지 말아주세요.

그리고 이 채널에 올려주신 프로젝트는 제 유튜브 영상이나 스트리밍을 통해서 소개될 수 있습니다.

베르의 게임 개발 채널 디스코드는 현재 이렇게 운영되고 있습니다.

질문 채널 사용법

각 채널에 대한 설명을 끝마쳤으니 이제 질문 채널을 사용하는 법에 대해서 자세히 설명해보겠습니다.

먼저 질문을 하실 때는 하려고 하는 질문이 다른 유저가 한 적이 있는지 확인해보시면 좋습니다.

질문 채널의 상단에 #모양으로 된 스레드 버튼을 눌러보면 현재 해결 중인 질문과 이미 해결된 질문들을 볼 수 있습니다.

스레드에서 활성화를 선택해서 나오는 스레드들은 현재 해결 중이거나, 해결된지 얼마 안 된 질문입니다.

그리고 그 옆에 보관됨을 선택하면 해결된지 시간이 꽤 지난 질문들을 볼 수 있습니다.

여기서 궁금한 내용을 검색해서 찾아보고 원하는 해결책이 아니라면 질문을 새로 올려주시면 됩니다.

질문을 올리실 때 버그와 관련된 내용이라면 스크린샷이나 GIF, 영상, 로그 등 해당 문제와 관련된 자료를 첨부하시면 좀 더 원활한 답변이 가능합니다.

그리고 이 질문 채널에서는 저 뿐만 아니라 여러분들도 알고 계시는 내용의 질문에 답변을 함께 달아주시면 모두에게 도움이 됩니다.

질문에 답변을 남기시는 방법은 해당 질문 채팅에 마우스 커서를 올리고 스레드 만들기 버튼을 클릭하면 답변을 남길 스레드를 만들 수 있습니다.

스레드를 만들 때는 스레드의 제목을 정해야 하는데 이 제목은 [작성자의닉네임]을 적고 유니티와 관련된 내용이라면 [유니티], 언리얼4 엔진과 관련된 내용이면 [언리얼4], 그 외의 내용이라면 [기타]처럼 [질문 카테고리]를 적어주고, 본 제목으로 질문 내용을 요약해서 적어주시면 됩니다.

아웃트로

이번 영상에서는 베르의 게임 개발 유튜브의 디스코드 채널 생성 공지를 해드렸습니다.

앞으로 질문을 디스코드 채널을 통해서 해주시느라 영상의 댓글이 줄어들 수 있을 것 같습니다.

그래도 유튜브 영상에 댓글 많이 남겨주세요.

이 채널의 강좌들은 시청자 여러분들의 시청과 후원으로 제작되며 채널의 운영에 큰 도움이 됩니다.

이상 베르의 게임 개발 유튜브였습니다. 감사합니다.

 

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

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

 

에셋스토어

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

 

반응형

Physics

-

디스트럭터블 메시 사용하기(기초)

 

작성기준버전 :: 4.22.3

 

게임에서 배경 오브젝트를 상호작용하여 파괴하는 기능은 사실 그렇게 쓸모있어 보이지는 않지만, 플레이어에게 자신이 이 게임 속의 세상과 상호작용을 하고 있다는 체감을 더 강하게 느끼게 만들어준다.

 

 

이렇게 파괴가능한 오브젝트를 만드는 전통적인 방법은 오브젝트를 모델링 할 때, 온전한 모델 하나와 잘게 쪼개진 모델들을 만들어서 우선 온전한 오브젝트를 배치해 두었다가 상호작용이 발생하면 잘게 쪼개진 모델들로 바꿔치기해서 각각의 조각들에 물리효과를 주는 것이었다. 이러한 방법은 작업자의 역량에 따라서 더 자연스럽게 오브젝트를 쪼갤 수 있지만 작업 시간이 많이 소요된다는 단점을 가지고 있었다.

 

 

디스트럭터블 메시(Destructable Mesh)

 

메시를 쪼개는 작업 시간을 줄이기 위해서 온전한 메시를 자동으로 쪼개주는 기능이 바로 언리얼 엔진 4의 디스트럭터블 메시(Destructable Mesh)이다. 참고로 이 기능은 초기 버전의 언리얼 엔진 4에서는 기본적으로 활성화 되어 있는 상태였지만 최근의 버전에서는 기본적으로 비활성화되어 있으며 해당 기능을 사용하기 위해서는 플러그인을 활성화 시켜야 한다.

 

 

플러그인 활성화

 

플러그인을 활성화시키기 위해서는 상단의 메뉴에서 [편집>플러그인] 항목을 선택한다.

 

 

플러그인 창이 열리면 검색창에 "APEX"를 입력하면 Apex Destruction 플러그인이 검색된다. 활성화 체크박스를 체크하고 지금 재시작 버튼을 누르면 언리얼 엔진이 재시작되면서 플러그인이 활성화 된다.

 

 

 

 

 

 

디스트럭터블 메시 생성 및 설정

 

플러그인이 활성화 되었으면 콘텐츠 브라우저 패널에서 디스트럭터블 메시를 생성하고자 하는 스태틱 메시를 찾아서 우클릭한 뒤 [디스트럭터블 메시 생성] 항목을 선택한다.

 

 

그렇게 하면 선택한 스태틱 메시에 대한 디스트럭터블 메시가 생성되고, 생성된 디스트럭터블 메시를 편집할 수 있는 에디터 창이 열린다.

 

 

열린 에디터 창에서 [프랙처 메시] 버튼을 누르면 플러그인이 자동으로 스태틱 메시를 쪼개서 파편을 만들어 준다.

 

 

기본적으로 에디터의 우측에 있는 디스트럭터블 세팅 패널과 프랙처 세팅 패널을 통해서 디스트럭터블 메시를 설정할 수 있다.

 

 

프랙처 세팅 패널의 Voronoi 카테고리의 프로퍼티인 Cell Site Count 값을 조절하여 메시가 쪼개지는 갯수를 설정할 수 있다.

 

 

 

 

 

간단한 사용법

 

게임 내에서 실제로 이 디스트럭터블 메시가 부숴지는 모습을 확인해보자.

 

 

우선 디스트럭터블 세팅 패널에서 Enable Impact Damage를 true로 세팅하고 저장한 뒤 디스트럭터블 메시 에디터를 닫는다.

 

 

생성한 디스트럭터블 메시를 레벨에 배치한다.

 

 

배치된 디스트럭터블 메시를 선택하고 Pysics 카테고리에서 Simulate Physics 프로퍼티를 체크한다.

 

 

 

그 다음 플레이 버튼을 누르고 캐릭터를 움직여서 배치된 의자에 부딪히면 의자가 산산조각 나서 부숴지는 것을 확인할 수 있다.

 

[투네이션]

 

-

 

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

 

반응형

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

 

반응형
  1. qqq 2020.01.12 16:14

    그대로 따라했는데 발사체가 사라지질 않습니다... 뭐가 문제일까요?
    혹시나 해서 공식문서의 완성 코드로 덮어 씌워봤으나 여전히 사라지지 않습니다.
    언리얼 엔진 4.23 버전입니다.

    • qqq 2020.01.15 21:32

      처음부터 다시 했더니 또 되네요...
      혼란을 드려 죄송합니다.

    • wergia 2020.01.22 10:22 신고

      차근차근 따라가다보면 잘 되는 경우가 많습니다. ㅎㅎ

  2. NogameNoHope 2020.09.25 03:25

    다른 버전은 모르겠는데 4.25에서는
    SpawnParams.Instigator = Instigator;
    이 부분에서 에러가 뜹니다.
    확인 결과 Actor.h 내부에 Instigator 가 private로 지정되있어서 직접 접근을 금지시켰는데
    SpawnParams.Instigator = GetInstigator();
    이렇게 바꿔주니까 해결되는 것 같습니다.

    항상 잘 보고 있습니다.

    • wergia 2020.10.20 00:10 신고

      언리얼은 자꾸 버전마다 바뀌는 부분이 심하네요 ㅜㅜ

  3. Kkk 2022.07.18 23:26

    Fire함수를 호출할때, if(ProjectileClass)문을 통과하지 못하는것으로 보아, ProjectileClass가 명시되지 않는거 같은데, 이거 어떻게 해결해야할까요?

    • wergia 2022.07.18 23:47 신고

      https://youtu.be/qDrQ1NSuq_Y?t=291

      영상 이부분에서 ProjectilClass 프로퍼티에 할당해주는 부분을 수행하셨나요?

  4. Kkk 2022.07.19 17:00

    네 수행하였는데도 SpawnActor failed because no class was specified라고 뜹니다

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

 

반응형

제대로 따라가기 (2) C++ 프로그래밍 튜토리얼 :: 플레이어 입력 및 폰

 

작성버전 :: 4.20.3

 

언리얼 엔진 튜토리얼인 플레이어 입력 및 폰 문서에서는 폰(Pawn)[각주:1] 클래스를 확장해서 플레이어의 입력에 반응하도록 하는 법을 배울 수 있다.

 

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

 

 

1. 폰 커스터마이즈(Pawn Customize)(튜토리얼)

 

프로젝트를 생성하고 Pawn 클래스를 상속받는 MyPawn 클래스를 생성해보자.

 

 

 

 

MyPawn 클래스의 생성이 성공적으로 끝났다면, 게임이 시작되었을 때 MyPawn이 자동으로 플레이어의 입력에 반응하도록 설정해보자. Pawn 클래스에는 초기화 중에 자동으로 플레이어의 입력에 반응하도록 설정해주는 변수를 제공한다. MyPawn.cpp의 AMyPawn::AMyPawn() 생성자를 다음과 같이 수정하자.

 

AMyPawn::AMyPawn()
{
     // Set this pawn to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
    PrimaryActorTick.bCanEverTick = true;

    AutoPossessPlayer = EAutoReceiveInput::Player0;
}

 

컴포넌트의 기록 유지를 위해서[각주:2] 다음의 코드를 MyPawn.h 의 클래스 정의 하단부에 추가하자.

 

UPROPERTY(EditAnywhere)
USceneComponent* OurVisibleComponent;

 

그리고 MyPawn.cpp로 돌아와서 폰에 카메라를 붙이고 위치와 회전을 설정하기 위해 다음과 같이 코드를 수정한다.

 

AMyPawn::AMyPawn()
{
     // Set this pawn to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
    PrimaryActorTick.bCanEverTick = true;

    AutoPossessPlayer = EAutoReceiveInput::Player0;

    RootComponent = CreateDefaultSubobject(TEXT("RootComponent"));
    UCameraComponent* OurCamera = CreateDefaultSubobject(TEXT("OurCamera"));
    OurVisibleComponent = CreateDefaultSubobject(TEXT("OurVisibleComponent"));
    OurCamera->SetupAttachment(RootComponent);
    OurCamera->SetRelativeLocation(FVector(-250.0f, 0.0f, 250.0f));
    OurCamera->SetRelativeRotation(FRotator(-45.0f, 0.0f, 0.0f));
    OurVisibleComponent->SetupAttachment(RootComponent);
}

 

하지만 이 구간에서 튜토리얼을 제대로 따라갈 수 없는 문제가 다시 발생한다.

 

 

 

1) 제대로 따라가기 (1) 섹션에서도 보았듯이 CreateDefaultSubobject() 함수에 템플릿 인자가 들어가 있지 않아서 어떤 오브젝트를 생성해야되는지 몰라서 신텍스 에러가 발생한다.

 

해결 :: CreateDefaultSubobject() 함수를 다음과 같이 수정하자.

 

RootComponent = CreateDefaultSubobject<USceneComponent>(TEXT("RootComponent"));
UCameraComponent* OurCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("OurCamera"));
OurVisibleComponent = CreateDefaultSubobject<USceneComponent>(TEXT("OurVisibleComponent"));

 

2) UCameraComponent가 정의되어 있지 않다고 신텍스 에러가 발생한다.

 

해결 :: MyPawn.cpp의 헤더 포함 전처리기 아래에 "Engine/Classes/Camera/CameraComponent.h"를 포함시키자.

 

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

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

 

이 두 가지를 모두 적용하고 나면 신텍스 에러가 더 이상 발생하지 않음을 볼 수 있다.

 

 

 

코드 수정이 모두 끝났다면 변경사항을 모두 저장하고 에디터로 돌아가서 컴파일을 해보자.

 

 

 

 

 

2. 게임 입력 환경설정(튜토리얼)

 

게임에서 특정한 키를 눌렀을 때, 특정 동작을 하도록 만드는 것을 언리얼에서는 입력 매핑이라고 한다. 이러한 입력 매핑에는 두 가지 종류가 있다.

 

액션 매핑(Action Mapping) - 마우스나 조이스틱, 패드, 키보드 버튼처럼 누르거나, 떼거나, 더블 클릭하거나, 특정 시간동안 누르고 있을 때 보고한다. 점프, 공격, 상호작용 등이 액션 매핑의 예시이며, X를 눌러서 조이를 표하는 것도 액션 매핑에 속한다.

 

축 매핑(Axis Mapping) - 연속적인 것으로 마우스의 위치나 조이스틱 막대의 기울기 같은 것으로 "일정량"의 입력으로 생각하면 된다. 움직이지 않더라도 매 프레임 값을 보고한다. 걷기, 달리기, 둘러보기, 탈 것의 방향조절 같은 것들이 주로 축 매핑으로 처리된다.

 

코드에서도 직접 입력 매핑을 할 수 있지만, 일반적으로는 에디터에서 정의하는 경우가 많으니, 이 튜토리얼에서는 그 방식을 따른다.

 

1. 언리얼 엔진 에디터에서 편집 드롭다운 메뉴에서 프로젝트 세팅 옵션을 선택한다.

 

 

2. 왼쪽의 엔진 섹션의 입력 항목을 선택하고 바인딩(Binding) 카테고리에 다음과 같이 하나의 액션 매핑과 두 개의 축 매핑을 추가한다.

 

 

3. 입력 환경 설정이 모두 끝났다면, 레벨에 MyPawn을 배치한다. 콘텐츠 브라우저에 있는 MyPawn 클래스를 레벨 에디터에 끌어다 놓으면 된다.

 

 

 

4. 레벨에 MyPawn을 배치한 뒤에는, 우리가 배치한 Pawn이 움직이는 것을 볼 수 있게 하기 위해서 OurVisibleComponent의 스태틱 메시(Static Mesh) 카테고리에 "Shape_Cylinder"를 넣어야 한다고 언리얼 튜토리얼 문서에 나와있다.

 

 

 

하지만 우리가 배치한 MyPawn의 OurVisibleComponent에서는 스태틱 메시 카테고리가 보이지 않는 것을 알 수 있다.

 

 

 

이 문제의 원인을 추측해보자면 언리얼 튜토리얼의 예시 코드에는 CreateDefaultSubobject() 함수로 컴포넌트를 생성할 때, 명시적인 컴포넌트 타입이 없었기 때문에 헤더에 추가한 OurVisibleComponent의 타입에 맞춰서 USceneComponent로 생성했기 때문에 발생한 문제로 보인다.

 

언리얼 튜토리얼의 예시 코드

OurVisibleComponent = CreateDefaultSubobject(TEXT("OurVisibleComponent"));

 

수정한 예시코드

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

 

그렇다면 스태틱 메시 카테고리가 나오도록 하려면 어떻게 해야할까? 바로 CreateDefaultSubobject() 함수로 UStaticMeshComponent를 생성해서 OurVisibleComponent에 대입시켜 주면 될 것 같다. 언리얼 엔진 문서에 따르면 UStaticMeshComponent는 USceneComponent를 상속받고 있기 때문에 충분히 가능한 코드이다. 여기까지 유추했다면 코드를 다음과 같이 수정해보자.

 

OurVisibleComponent = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("OurVisibleComponent"));

 

UStaticMeshComponent가 USceneComponent를 상속받고 있기 때문에 충분히 대입이 가능할거라고 생각했는데 할당할 수 없다는 에러가 발생한다.

 

 

 

이 경우는 타이머를 배울 때, GetWorldTimerManager() 함수를 호출해서 기능을 사용하려고 했을 때를 생각해보자. 그 때 불완전한 형식은 사용할 수 없다는 에러가 떴었던 것과 그 문제를 해결하기 위해서 "TimerManager.h"를 포함시켜주었던 것을 기억할 수 있다.

 

그와 같이 MyPawn.cpp의 헤더 포함 전처리기 부분에 "Engine/Classes/Components/StaticMeshComponent.h"를 포함시키면 CreateDefaultSubobject()로 생성한 UStaticMeshComponent가 성공적으로 OurVisibleComponent에 대입되는 것을 확인할 수 있다.

 

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

#include "MyPawn.h"
#include "Engine/Classes/Camera/CameraComponent.h"
#include "Engine/Classes/Components/StaticMeshComponent.h"

 

코드를 모두 수정하고 에디터로 돌아가서 컴파일을 진행하면 아까 전까지는 보이지 않았던 OurVisibleComponent의 스태틱 메시 카테고리가 보이는 것을 확인할 수 있다.

 

그럼 이제 Static Mesh에 Shape_Cylinder를 넣어주자.

 

 

 

 

 

 

3. 게임 액션 프로그래밍 및 바인딩(튜토리얼)

 

게임 입력 환경설정 파트에서 매핑한 입력 매핑과 코드의 함수 동작을 묶어서 입력이 들어오면 입력 매핑에 묶어준 함수가 실행되도록 하는 것을 바인딩(Binding)이라고 한다.

 

입력 매핑에 바인딩할 함수들과 동작에 관련된 변수들을 MyPawn.h에 추가해보도록 하자.

 

void Move_XAxis(float AxisValue);
void Move_YAxis(float AxisValue);
void StartGrowing();
void StopGrowing();

FVector CurrentVelocity;
bool bGrowing;

 

헤더에 함수들을 모두 정의했다면 MyPawn.cpp에서 함수들을 구현해야 한다.

 

void AMyPawn::Move_XAxis(float AxisValue)
{
    CurrentVelocity.X = FMath::Clamp(AxisValue, -1.0f, 1.0f) * 100.0f;
}

void AMyPawn::Move_YAxis(float AxisValue)
{
    CurrentVelocity.Y = FMath::Clamp(AxisValue, -1.0f, 1.0f) * 100.0f;
}

void AMyPawn::StartGrowing()
{
    bGrowing = true;
}

void AMyPawn::StopGrowing()
{
    bGrowing = false;
}

 

축 입력 매핑에 대한 동작을 구현할 때, FMath::Clamp()함수를 사용했는데 이것은 입력된 값이 -1.0과 1.0 사이를 벗어나지 않도록 만들어 준다. 전 파트에서 우리가 축 매핑을 추가할 때, MoveX의 입력을 W와 S만을 추가했는데 만약 다른 입력 방식도 사용하기 위해서 위쪽 화살표와 아래쪽 화살표로도 MoveX 입력을 받도록 만들었을 때, 만약 Clamp로 입력의 범위를 제한하지 않았다면 W와 위쪽 화살표를 동시에 누른다면 캐릭터가 두 배의 속도로 빠르게 움직이는 버그가 발생할 것이다.

 

입력 함수의 정의와 구현을 모두 끝냈으니, 적합한 입력에 반응하도록 바인딩을 진행할 차례다. AMyPawn::SetupPlayerInputComponent() 함수 안에 다음 코드를 작성하자.

 

// Called to bind functionality to input
void AMyPawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
    Super::SetupPlayerInputComponent(PlayerInputComponent);

    InputComponent->BindAction("Grow", IE_Pressed, this, &AMyPawn::StartGrowing);
    InputComponent->BindAction("Grow", IE_Released, this, &AMyPawn::StopGrowing);

    InputComponent->BindAxis("MoveX", this, &AMyPawn::Move_XAxis);
    InputComponent->BindAxis("MoveY", this, &AMyPawn::Move_YAxis);
}

 

InputComponent의 함수를 호출해서 사용하려고 할 때 여기서도 불완전한 형식을 사용할 수 없다는 에러가 발생할 것이다.

 

MyPawn.cpp의 전처리기 파트 아래쪽에 "Engine/Classes/Components/InputComponent.h"를 포함시켜주자.

 

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

#include "MyPawn.h"
#include "Engine/Classes/Camera/CameraComponent.h"
#include "Engine/Classes/Components/StaticMeshComponent.h"
#include "Engine/Classes/Components/InputComponent.h"

 

입력 매핑과 바인딩을 모두 끝냈으니, 입력으로 변하는 변수를 통해서 동작하는 코드를 작성해보자. AMyPawn::Tick() 함수를 다음과 같이 수정하자.

 

// Called every frame
void AMyPawn::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);

    {
        float CurrentScale = OurVisibleComponent->GetComponentScale().X;
        if (bGrowing)
        {
            CurrentScale += DeltaTime;
        }
        else
        {
            CurrentScale -= (DeltaTime * 0.5f);
        }

        CurrentScale = FMath::Clamp(CurrentScale, 1.0f, 2.0f);
        OurVisibleComponent->SetWorldScale3D(FVector(CurrentScale));
    }

    {
        if (!CurrentVelocity.IsZero())
        {
            FVector NewLocation = GetActorLocation() + (CurrentVelocity * DeltaTime);
            SetActorLocation(NewLocation);
        }
    }
}

 

마지막으로 수정한 코드를 저장하고, 에디터로 돌아와서 컴파일을 한 뒤에 플레이해보면 WASD를 입력하면 배치한 MyPawn이 움직이고 스페이스바를 누르면 커지고 손을 떼면 다시 작아지는 것을 볼 수 있다.

 

 

 

 

 

 


 

 

이번 섹션에서 배운 것

 

1. Pawn(언리얼 엔진 문서)

 

Pawn 클래스는 플레이어나 AI가 컨트롤할 수 있는 모든 액터의 베이스 클래스다.

 

2. APawn::AutoPossessPlayer

 

레벨이 시작되거나 폰이 생성되었을 때, 플레이어 컨트롤러가 있다면 어떤 플레이어 컨트롤러가 자동으로 이 폰을 소유해야 되는지에 대한 변수다.

 

3. USceneComponent

 

USceneComponent* RootComponent;

USceneComponent* SubComponent;

 

USceneComponent는 트랜스폼을 가지고 있고 다른 컴포넌트를 이 컴포넌트에 덧붙이는(Attachment) 것을 지원하지만 충돌 같은 물리적 효과를 지원하지 않고 렌더링 기능이 없다. 계층 구조에서 더미로 활용하기 좋다.

 

SubComponent->SetupAttachment(RootComponent);

 

SetupAttachment() 함수는 컴포넌트를 다른 컴포넌트의 아래 계층으로 붙이는데 사용된다. 위의 예시 코드에 따르면 SubComponent는 계층적으로 자식 컴포넌트가 되고 RootComponent는 부모 컴포넌트가 되는 것이다.

 

SubComponent->SetRelativeLocation(FVector(-250.0f, 0.0f, 250.0f));

 

SetRelativeLocation() 함수는 현재 컴포넌트가 상위 계층의 컴포넌트나 오브젝트로부터 얼마나 떨어진 위치에 있을지 정한다.

 

SubComponent->SetRelativeRotation(FRotator(-45.0f, 0.0f, 0.0f));

 

SetRelativeRotation() 함수는 현재 컴포넌트가 부모를 기준으로 얼마나 회전된 상태인지 정한다.

 

SubComponent->GetComponentScale();

 

GetComponentScale() 함수는 월드 스페이스에서의 컴포넌트 크기를 가져온다.

 

SubComponent->SetWorldScale3D(FVector(0.0f, 0.0f, 0.0f));

 

SetWorldScale3D() 함수는 월드 스페이스에서의 컴포넌트 크기를 수정한다.

 

4. UCameraComponent

 

액터에 덧붙일 수 있는 카메라 컴포넌트이다.

 

5. UStaticMeshComponent

 

엑터에 덧붙일 수 있는 스태틱 메시 컴포넌트이다. 월드에 렌더링된다.

 

6. AActor::InputComponent

 

입력이 활성화된 액터에 대한 입력을 처리하는 컴포넌트이다.

 

InputComponent->BindAction("Action", IE_Pressed, this, &AMyActor::ActionProcess);

 

액션 매핑에 처리 함수를 바인딩하는 함수다.

 

첫 번째 매개변수는 바인딩할 액션 매핑의 이름이다.

 

두 번째 매개변수는 처리할 키 이벤트다. 기본적으로 사용되는 이벤트는 키가 눌렸을 때를 뜻하는 IE_Pressed와 눌린 키가 떼졌을 때를 뜻하는 IE_Released가 있다.

 

세 번째 매개변수는 입력을 바인딩하는 오브젝트이다.

 

네 번째 매개변수는 입력이 들어왔을 때 입력을 처리하는 함수이다.

 

InputComponent->BindAxis("Axis", this, &AMyPawn::AxisProcess);

 

축 매핑에 처리 함수를 바인딩하는 함수다.

 

첫 번째 매개변수는 바인딩할 축 매핑의 이름이다.

 

두 번째 매개변수는 입력을 바인딩하는 오브젝트이다.

 

세 번째 매개변수는 입력이 들어왔을 때 입력을 처리하는 함수이다.

 

7. AActor::GetActorLocation()

 

GetActorLocation();

 

액터의 월드 스페이스 상의 위치를 가져오는 함수이다.

 

8. AActor::SetActorLocation()

 

SetActorLocation(FVector(0.0f, 0.0f, 0.0f));

 

액터의 월드 스페이스 상의 위치를 정하는 함수이다.

 

9. FMath::Clamp()

 

FMath 클래스는 수학적인 기능들을 제공한다.

 

FMath::Clamp(Value, Min, Max);

 

Clamp() 함수는 Value의 값이 Min보다 값이 작으면 Min 값을, Max보다 크면 Max 값을 돌려주고, 그 사잇값이라면 Value를 돌려주는 함수이다. 값이 특정한 범위를 벗어나면 안되는 경우에 사용하면 좋다.

  1. 폰(Pawn)이란 플레이어나 AI의 컨트롤러가 빙의(연결)되어 제어받을 수 있도록 설계된 클래스이다. [본문으로]
  2. UPROPERTY() 매크로가 적용된 변수는 언리얼 에디터에서 볼 수 있고, 게임이 실행되거나, 프로젝트나 레벨을 닫고 다시 불러와도 변수가 리셋되지 않는다. [본문으로]

 

[투네이션]

 

-

 

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. Artsdayo 2019.02.04 03:09

    정말 이 블로그 밖에 없네요.... 정말 고맙습니다 ㅠㅠㅠ
    튜토리얼에서 3시간동안 삽질 했네요... 정말... 고맙습니다 ㅠㅠㅠ

    • wergia 2019.02.04 11:25 신고

      저도 언리얼 처음 공부할 때 이 부분에서 시간을 많이 썼습니다 ㅎㅎ

  2. Teoun 2020.04.29 22:05

    적어 놓으신 강의를 쭉 보고 있는데 너무 도움됩니다...ㅠㅠ
    감사합니다..ㅠㅠㅠ

  3. NogameNoHope 2020.09.20 02:02

    많은 블로그를 둘러보았지만 이 곳 만큼 쉬운 해설과 오류 발생시의 대처법 등을 상세히 적어놓은 곳을 보지 못했습니다.
    언리얼 공식 튜토리얼보다도 훨씬 이해가 쉽고 따라하기 좋습니다. 감사합니다.

    • wergia 2020.10.20 00:08 신고

      저도 공식 튜토리얼 보다가 어려워서 적어봤습니다 ㅎㅎ

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

 

작성버전 :: 4.20.3

 

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

 

 

 

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

 

 

 

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

 

 

 

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

 

 

 

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

 

 

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

 

 

 

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

 

 

주의사항

 

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

 

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

 

 

[투네이션]

 

-

 

toon.at

[Patreon]

 

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

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

www.patreon.com

[디스코드 채널]

 

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

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

discord.com

 

반응형
  1. 2022.02.05 16:12 신고

    유익한 포스팅 감사합니다~

C++ / USTRUCT 사용자 정의 구조체 만들기

 

작성버전 :: 4.20.3

 

구조체는 기존에 존재는 데이터 타입을 조합하여 새로운 데이터 타입을 만들어내는 유용한 개념이다.

 

struct UserDefinedStruct

{

public:

    int i;

    float f;

};

 

일반적인 C++ 프로젝트에서는 구조체를 위와 같이 정의하고 사용하게 된다.

 

하지만 언리얼 엔진 프로젝트에서 이러한 정규 구조체는 C++ 코드 내부에서는 사용될 수 있지만, 에디터의 디테일 패널에 노출되지 않고, 블루프린트에서도 사용이 불가능하다.

 

에디터에서 사용가능한 구조체를 만들고자 한다면 언리얼 구조체 즉, USTRUCT를 만들어야 한다.

 

 

블루프린트에서만 사용할 구조체라면 위의 이미지와 같은 방법으로 구조체를 생성할 수 있는데, 블루프린트 구조체는 C++ 코드에서는 사용할 수 없다. 하지만 C++ 코드에서 만든 구조체는 C++ 코드는 물론 블루프린트에서도 사용할 수 있다는 장점이 있다.

 

C++ 언리얼 구조체는 간단한 블루프린트 구조체 생성 방법과 비교했을 때, 엔진 내부에서 명시적인 생성 방법이 없기 때문에 생성 과정이 조금 복잡하다.

 

 

언리얼 구조체 만들기

 

언리얼 구조체를 만드는 과정을 따라가보도록 하자.

 

 

 

 

 

우선 사용자가 정의한 UStruct를 담을 헤더를 만들어야 한다. 만약 구조체가 특정한 클래스에서만 자주 사용될 것이라면 그 클래스의 헤더 파일 하단에 구조체를 정의하는 편이 좋지만, 범용적으로 여러 곳에서 사용될 구조체라면 사용자가 정의한 헤더에 몰아서 정의하는 편이 좋다.

 

CustomStruct00 클래스의 추가가 끝났다면 아래의 예시 코드와 같이 클래스 정의 아래 쪽에 커스텀 구조체를 정의해보자.

 

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

#pragma once

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

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

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

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


USTRUCT(Atomic, BlueprintType)
struct FCustomStruct
{
    GENERATED_BODY()
public:
    UPROPERTY(EditAnywhere, BlueprintReadWrite)
        AActor* actor;
    UPROPERTY(EditAnywhere, BlueprintReadWrite)
        float f;
    UPROPERTY(EditAnywhere, BlueprintReadWrite)
        int32 i;
};

 

클래스에는 UCLASS() 매크로가 붙지만 구조체의 경우에는 USTRUCT() 매크로가 붙는다. 그리고 구조체 지정자는 Atomic과 BlueprintType으로 지정해뒀는데 Atomic은 이 구조체가 항상 하나의 단위로 직렬화(Serialize)됨을 의미하고 BlueprintType은 이 구조체가 블루프린트에서 사용될 수 있음을 의미한다.

 

만약 이 구조체가 에디터의 디테일 창에서 표시되고 수정 가능하기만 원한다면 지정자를 Atomic으로만 설정하기를 권한다. 또한 모든 멤버 변수의 UPROPERTY() 매크로의 지정자를 EditAnywhere로 설정해야 한다.

 

혹은 구조체가 디테일 창에서는 보이지 않고 코드 내부나 블루프린트에서만 사용되기를 원한다면 USTRUCT() 매크로의 지정자를 BlueprintType으로, UPROPERTY() 매크로의 지정자를 BlueprintReadWrite로 설정해야 한다.

그리고 구조체의 이름은 F로 시작되게 작성해야 하며, 댕글링(Dangling) 포인터 문제에 대해서 보호받기 위해서 구조체의 모든 멤버 변수들에 UPROPERTY() 매크로를 붙이는 것을 권장한다.

 

또한 구조체의 멤버 변수에 포인터를 사용한다면 깊은 복사 얕은 복사 문제에 주의를 기울여야 한다.

 

사용할 구조체를 모두 정의했다면, 이 구조체를 사용할 코드의 헤더에 CustomStruct00.h를 포함시켜준다.

 

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

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "CustomStruct00.h"
#include "TestActor.generated.h"

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

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)
    FCustomStruct st;
};

 

 

 

이렇게 구조체를 테스트 액터의 멤버 변수로 추가시킨 후 에디터로 돌아가서 컴파일을 해주고, 액터를 레벨에 배치하고 선택해보면 위의 이미지처럼 구조체가 디테일 패널에서 수정가능하록 노출된 것을 확인할 수 있다.

 

Tip :: 이후에 구조체의 멤버 변수 종류를 수정하고 컴파일했을 때, 디테일 패널에 곧바로 적용이 되지 않는 문제가 가끔있는데 이런 경우 해당 구조체를 가진 클래스의 멤버 변수에 임시 변수 하나를 추가하고 컴파일하면 적용이 된다.

 

 

 

 

 

생성한 구조체 블루프린트에서 사용하기(Use Custom Struct at Blueprint)

 

C++ 코드에서 정의한 구조체를 블루프린트에서 사용하는 방법은 간단하다.

 

 

 

블루프린트에서 정의한 CustomStruct 변수 유형으로 변수를 추가할 수 있다.

 

 

 

이미지와 같이 이벤트 그래프에서 우클릭을 한 뒤 정의한 구조체의 이름을 검색하면 이벤트 플로우 도중에 CustomStruct를 만들거나 구조체를 분해해서 구조체의 변수를 따로 뽑아내서 사용할 수도 있다.

 


 

참고

 

Unreal Engine 4 Wiki :: Structs, USTRUCTS(), They're Awesome(https://wiki.unrealengine.com/Structs,_USTRUCTS(),_They%27re_Awesome)

Unreal Engine 4 Wiki :: How To Make UStruct(https://wiki.unrealengine.com/How_To_Make_UStruct)

 

 

[투네이션]

 

-

 

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. 교육생 2020.12.03 16:53

    안녕하세요 언리얼4 입문하게된 교육생입니다
    영상하고 항상 잘 보고있습니다
    C++로 언리얼을 접근하려는데
    어려움이 많아서 도움을 요청하고 싶어서 댓글달아 봅니다
    몬스터를 UEnum으로 이름의 목록을 구성하여
    구조체로 그에맞는 몬스터를 뽑아오려고 생각하여
    USTRUCT 에서 폰을 추가하였고
    editanywhrer로 둘다 사용할수있게 하였는데
    에디터창에서 사용할 폰을 삽입했을시
    블루프린트에서 지정된 폰이 어떤건지 인식이 안되던데 도움좀 받고싶습니다
    너무 설명이 어려운거같은데
    게임개발자로 거듭나기위해 도움을 많이 받고싶어요

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

 

반응형
  1. 지나갑니다. 2018.12.01 23:08

    오타있어서 말씀드립니다.

    함수명 AdvanceTimer로 변경 필요합니다.

  2. 왕초보진화중 2019.12.23 14:48

    막막 했는데 많은 도움이 되었습니다. 감사합니다.

  3. 가는길 2020.06.14 13:10

    블루프린트쓰다가
    cpp로 작성하고 싶어 배우는중에
    큰 도움받고 갑니다

+ Recent posts