[UE5] 슈팅게임 제작하기-1

칼든개구리·2024년 12월 22일
0

[언리얼TO리얼]

목록 보기
42/42

게임 모드 제작과 맵 생성은 너무 기초인 이야기인지라 넘어가도록 하겠다

플레이어 클래스 생성하기

새 C++ 클래스를 폰을 부모로 하여 생성하고 이름은 PlayerPawn이라고 저장한다.
Pawn클래스도 Actor 클래스를 상속받은 클래스이기 때문에 Pawn 클래스를 상속받은 PlayerPawn클래스에도 접두어 A가 자동으로 붙는다. 블루프린트에서 플레이어를 생성시 가장 먼저 해야 했던 것은 플레이어를 눈으로 보고 충돌 처리를 할 수 있도록 외형(Mesh)와 충돌 영역(collider)설정에 필요한 컴포넌트를 설정하는 것이다. 액터에 부속으로 추가되는 컴포넌트 역시 클래스로 제작이 되어있다. 박스 콜라이더 컴포넌트는 UBoxComponent 클래스, 스태틱 메시 컴포넌트는 UStaticMeshComponent 클래스가 원형이다. 컴포넌트를 추가할려면 해당 클래스에 할당할 수 있는 포인터 변수부터 만들어야 한다.

class SHOOTINGGAME_API APlayerPawn : public APawn
{
	GENERATED_BODY()

public:
	APlayerPawn();

protected:
	virtual void BeginPlay() override;

public:
	virtual void Tick(float DeltaTime) override;

	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

	// 박스 충돌체 컴포넌트
	UPROPERTY(EditAnywhere)
	class UBoxComponent* boxComp;

	// 스태틱 메시 컴포넌트
	UPROPERTY(EditAnywhere)
	class UStaticMeshComponent* meshComp;
}

소스 파일로 이동해 충돌체 컴포넌틀르 생성하는 코드를 작성해본다. 충돌체나 박스 컴포넌트 같은 것은 에디터에서 플레이를 한 후 생성되게 하는 것보다는 단순히 플레이어를 월드 공간에 배치했을 때 같이 생성되는 것이 좋아보여 생성자 함수에서 구현해보도록 한다.

APlayerPawn::APlayerPawn()
{
	PrimaryActorTick.bCanEverTick = true;

	// 박스 콜라이더 컴포넌트를 생성한다.
	boxComp = CreateDefaultSubobject<UBoxComponent>(TEXT("My Box Component")); 

CreateDefaultSubobject()함수는 액터에게 컴포넌트를 추가할 때 사용되는 함수이다. 괄호 안에 매개변수에는 컴포넌트의 별명을 텍스트 형태로 기입하게 되어있다. < > 이 괄호는 템플린이라는 다양한 컴포넌트 클래스를 하나의 함수로 생성할 수 있게 해주는 문법 요소이다. < > 안에는 생성하려는 컴포넌트 클래스를 지정해주면 된다,

>>컴포넌트 생성 함수
CreateDefaultSubobject<클래스 이름>(TEXT("컴포넌트 별명"));

컴포넌트를 생성했으면 컴포넌트들 간의 부모 자식 관계를 설정해주어야 한다. 콜라이더 컴포넌트를 최상단 컴포넌트로 지정하고, 이어지는 메시 컴포넌트를 자식 컴포넌트로 지정해준다

APlayerPawn::APlayerPawn()
{
	PrimaryActorTick.bCanEverTick = true;

	// 박스 콜라이더 컴포넌트를 생성한다.
	boxComp = CreateDefaultSubobject<UBoxComponent>(TEXT("My Box Component"));

	// 생성한 박스 콜라이더 컴포넌트를 최상단 컴포넌트로 설정한다.
	SetRootComponent(boxComp);

같은 방식으로 스태틱 메시 컴포넌트도 추가해준다

APlayerPawn::APlayerPawn()
{
	PrimaryActorTick.bCanEverTick = true;

	// 박스 콜라이더 컴포넌트를 생성한다.
	boxComp = CreateDefaultSubobject<UBoxComponent>(TEXT("My Box Component"));

	// 생성한 박스 콜라이더 컴포넌트를 최상단 컴포넌트로 설정한다.
	SetRootComponent(boxComp);

	// 스태틱 메시 컴포넌트를 생성한다.
	meshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("My Static Mesh"));

	// 박스 콜라이더 컴포넌트의 자식 컴포넌트로 설정한다.
	meshComp->SetupAttachment(boxComp);

빌드 후 PlayerPawn을 부모로한 블루프린트 클래스를 생성해준다.

이후 컴포넌트 메시와 머티리얼을 설정해주는데, 충돌체(Box Collider)의 크기가 32x32x32로 되어 있어서 기본 큐브 사이즈에 맞게 50x50x50으로 확대해주어야 한다.

.cpp파일에서 생성자 함수 하단에 boxComp의 SetBoxExtent()함수를 호출한다. SetBoxExtent함수의 매개변수에는 x축, y축, z축의 값을 벡터의 형태로 입력받도록 되어있다. 벡터 변수의 자료형은 FVector이다. 각 축의 값이 50인 벡터 변수를 만들어 SetBoxExtent()의 매개변수로 넘겨준다.

APlayerPawn::APlayerPawn()
{
	PrimaryActorTick.bCanEverTick = true;

	// 박스 콜라이더 컴포넌트를 생성한다.
	boxComp = CreateDefaultSubobject<UBoxComponent>(TEXT("My Box Component"));

	// 생성한 박스 콜라이더 컴포넌트를 최상단 컴포넌트로 설정한다.
	SetRootComponent(boxComp);

	// 스태틱 메시 컴포넌트를 생성한다.
	meshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("My Static Mesh"));

	// 박스 콜라이더 컴포넌트의 자식 컴포넌트로 설정한다.
	meshComp->SetupAttachment(boxComp);

	// 박스 콜라이더의 크기를 50 x 50 x 50으로 설정한다.
	FVector boxSize = FVector(50.0f, 50.0f, 50.0f);
	boxComp->SetBoxExtent(boxSize);
}

사용자의 입력 키 바인딩하기


이렇게 만들어주고 Horizontal과 Vertical의 값 타입은 Axis1D로 설정해준다. 다음으로 매핑 컨텍스트에 상황에 따른 키보드 키와 모디파이어를 추가해준다.

Playerpawn.h로 돌아가 InputMappingContext와 inputAction파일을 할당 받기 위한 포인터 변수를 각각 설정한다.

// Input Mapping Context 파일의 포인터 변수
UPROPERTY(EditAnywhere)
class UInputMappingContext* imc_playerInput;

// Input Action 파일의 포인터 변수
UPROPERTY(EditAnywhere)
class UInputAction* ia_horizontal;

UPROPERTY(EditAnywhere)
class UInputAction* ia_vertical;

이제는 키 입력 이벤트가 발생했을 때 실행할 함수를 선언하도록 한다. 입력 바인딩을 위한 함수 선언은 InputAction 파일에서 설정된 값을 매개변수로 받아서 실행되기 때문에 반드시 FInputActionValue라는 구조체 변수를 매개변수로 선언해야 한다. 좌우 이동 시에는 OnInputHorizontal()과 상하 이동 시 OnInputVertical() 함수를 선언한다.
매개변수 자료형인 FInputActionValue가 선언된 헤더 파일을 읽어오기 위해 #include "InputActionValue.h"문구를 선언해준다. 주의할점은 헤더파일에서 다른 헤더파일을 include 할 때는 반드시 #include [파일 이름].generated.h 선언 위쪽에 선언해야 한다.

private:
	//사용자의 키 입력 값을 받을 변수
	void OnInputHorizontal(const struct FInputActionValue& value);
	void OnInputVertical(const struct FInputActionValue& value);

이어서 소스파일에서는 입력 서브 시스템을 imc 파일에 매핑하는 과정을 진행해보겠다. 우선 cpp 파일 상단에 우리가 사용할 EnhancedInputComponent.h와 EnhancedInputSubsystems.h 파일을 include 선언한다.

우리가 플레이어로 사용하는 폰 클래스에는 해당 폰을 조작하는 사용자의 실제 디바이스와 조작되는 폰 오브젝트를 중개하는 역할을 하는 APlayerController 클래스를 소유하고 있다. 사용자의 입력을 관리하는 서브 시스템 역시 현재 로컬 플레이어 폰이 소유하고 있는 컨트롤러 클래스가 있기 때문에 가장 먼저 플레이어 컨트롤러를 다음과 같이 다져와야 한다. 입력 맵핑은 플레이어가 생성된 후 최초 1 회만 하면 되기때문에 beginplay에서 구현해야 한다

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

	// 현재 플레이어가 소유한 컨트롤러를 가져온다.
	APlayerController* pc = GetWorld()->GetFirstPlayerController();

	// 만일, 플레이어 컨트롤러 변수에 값이 들어있다면…
	if (pc != nullptr)
	{

ULocalPlayer::GetSubsystem() 함수를 이용하면 플레이어 컨트롤러로부터 입력 서브 시스템 클래스에 대한 포인터 변수를 가져올 수 있다. 입력 서브 시스템 클래스에 구현된 AddMappingContext()함수를 통해 입력 서브 시스템과 Input Mappint Context를 연결하면 된다.

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

	// 현재 플레이어가 소유한 컨트롤러를 가져온다.
	APlayerController* pc = GetWorld()->GetFirstPlayerController();

	// 만일, 플레이어 컨트롤러 변수에 값이 들어있다면…
	if (pc != nullptr)
	{
		// 플레이어 컨트롤러로부터 입력 서브 시스템 정보를 가져온다.
		UEnhancedInputLocalPlayerSubsystem* subsys = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(pc->GetLocalPlayer());

		if (subsys != nullptr)
		{
			// 입력 서브 시스템에 IMC 파일 변수를 연결한다.
			subsys->AddMappingContext(imc_playerInput, 0);
		}
	}
}

언리얼 엔진 시스템에서 입력 처리 함수를 처리할 수 있도록 입력 처리 함수를 언리얼 엔진의 입력 시스템에 연결해주어야 한다. pawn이나 character클래스를 상속받는 클래스에는 항상 SetupPlayerInputComponent()함수가 존재한다. 이 함수는 beginplay()가 실행되기 전 1회 실행되는 입력 바인딩 함수이다. SetupPlayerInputComponent() 함수의 매개변수로 넘겨받는 변수를 보면 자료형이 UInputComponent 형태이다. 우선 언리얼 엔진4 버전 이하에서 사용되었던 UInputComponent 변수를 언리얼 5 버전에서 사용하는 UEnhancedInputComponent* 변수로 변환(Cast) 해야한다.

void APlayerPawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);

	UEnhancedInputComponent* enhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent);

변환한 inputComponent의 멤버 함수인 BindAction()함수를 사용해 입력 키와 실행할 함수를 연결한다.

BindAction(연결할 ia 파일, 입력 이벤트, 연결할 함수가 있는 클래스,연결할 함수 주소값)

아래와 같이 구현해준다

void APlayerPawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);

	UEnhancedInputComponent* enhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent);

	if (enhancedInputComponent != nullptr)
	{
		enhancedInputComponent->BindAction(ia_horizontal, ETriggerEvent::Triggered, this, &APlayerPawn::OnInputHorizontal);
		enhancedInputComponent->BindAction(ia_horizontal, ETriggerEvent::Completed, this, &APlayerPawn::OnInputHorizontal);
		enhancedInputComponent->BindAction(ia_vertical, ETriggerEvent::Triggered, this, &APlayerPawn::OnInputVertical);
		enhancedInputComponent->BindAction(ia_vertical, ETriggerEvent::Completed, this, &APlayerPawn::OnInputVertical);
		enhancedInputComponent->BindAction(ia_fire, ETriggerEvent::Started, this, &APlayerPawn::Fire);

	}
}

구현된 코드를 보니 APlayerPawn 클래스 대신 this를 적고 있다. this 라는 키워드는 "현재 코드가 작성된 클래스, 즉 자신의 클래스"를 의미합니다. 함수의 주소 값은 포인터 변수의 주소값을 가져올 때와 마찬가지로 함수 이름 앞에 &를 붙여 주면 된다. 이렇게 함수의 주소값을 기반으로 해당 함수를 찾아가는 함수 형식을 함수 포인터라고 한다. 주의할 점은 함수의 주소 값을 호출할 때에는 함수 자체를 호출하는 경우와 달리 매개변수를 입력하기 위한 소괄호를 사용하지 않는다.

다음으로 사용자가 지정된 키 입력 시 실행할 이벤트 함수들의 구현부를 구현할 차례이다. 매개변수로 받은 FInputActionValue클래스는 InputAction 파일에 설정한 자료형으로 값을 받아올 수 있다. 입력으로 들어온 값을 가져올 때는 Get<자료형>() 함수를 사용한다.

// 좌우축 입력 처리 함수
void APlayerPawn::OnInputHorizontal(const FInputActionValue& value)
{
	float hor = value.Get<float>();
	UE_LOG(LogTemp, Warning, TEXT("Horizontal: %.2f"), hor);

}

// 상하축 입력 처리 함수
void APlayerPawn::OnInputVertical(const FInputActionValue& value)
{
	float ver = value.Get<float>();
	UE_LOG(LogTemp, Warning, TEXT("Vertical: %.2f"), ver);

}

이동 공식 적용하기

사용자의 키 입력을 -1,0,1의 값으로 변환한 값을 변수에 저장해본다.

	float h;
	float v;

cpp파일에서도 수정해준다

// 좌우축 입력 처리 함수
void APlayerPawn::OnInputHorizontal(const FInputActionValue& value)
{
	//float hor = value.Get<float>();
	//UE_LOG(LogTemp, Warning, TEXT("Horizontal: %.2f"), hor);
	h = value.Get<float>();
}

// 상하축 입력 처리 함수
void APlayerPawn::OnInputVertical(const FInputActionValue& value)
{
	//float ver = value.Get<float>();
	//UE_LOG(LogTemp, Warning, TEXT("Vertical: %.2f"), ver);
	v = value.Get<float>();
}

사용자가 wasd를 누를 때마다 그 값이 h,v 변수에 저장된다. 이렇게 받아온 값을 이용해 액터의 위치를 변경해주도록 한다. 이동 기능은 프레임마다 계속 반영되어야 하기 때문에 Tick()함수에서 구현해야 한다. 구현 방식은 1. 읽어온 값을 이용해서 방향 벡터를 만들고 2. 방향 벡터의 길이가 1이 되도록 정규화 해준다음 3. 현재 위치에 속도(방향x속력)와 시간 보간 값을 이동된 값을 더해서 새로운 위치로 설정한다.

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

	// 사용자의 입력 키를 이용해서 
	// 1. 상하 입력 값과 좌우 입력 값을 이용해서 방향 벡터를 만든다.
	FVector dir = FVector(0, h, v);

	// 2. 방향 벡터의 길이가 1이 되도록 벡터를 정규화한다.
	dir.Normalize();

	// 3. 이동할 위치 좌표를 구한다(p = p0 + vt).
	FVector newLocation = GetActorLocation() + dir * moveSpeed * DeltaTime;

	// 4. 현재 액터의 위치 좌표를 앞에서 구한 새 좌표로 갱신한다.
	SetActorLocation(newLocation, true);

}

액터의 현재 위치 벡터를 구할 때는 GetActorLocation()함수를 사용하면 손 쉽게 좌표를 구할 수 있다. 현재 위치에서 방향 x 속력 x 프레임 시간을 더하면 이동해야 할 위치 좌표가 된다. 이 위치 좌표를 액터의 위치 벡터로 설정할 때는 SetActorLocation()함수를 사용하면 된다.


총알 제작하기

새로운 c++ 클래스인 bullet을 만들어준다. 총알에도 외형과 충돌체가 있어야하므로 boxComponent와 UstaticMeshComponent를 생성해준다

class SHOOTINGGAME_API ABullet : public AActor
{
	GENERATED_BODY()
	
public:	
	// Sets default values for this actor's properties
	ABullet();

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)
	class UBoxComponent* boxComp;

	UPROPERTY(EditAnywhere)
	class UStaticMeshComponent* meshComp;

소스파일 역시 플레이어와 동일하다. 컴포넌트 클래스 생성하려면 상단에 헤더파일을 추가해야하는 점을 잊지말자!

#include "Bullet.h"
#include "Components/BoxComponent.h"
#include "Components/StaticMeshComponent.h"
// Sets default values
ABullet::ABullet()
{
 	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;

	boxComp = CreateDefaultSubobject<UBoxComponent>(TEXT("Box Collider"));
	SetRootComponent(boxComp);
	boxComp->SetBoxExtent(FVector(50.0f, 50.0f, 50.0f));

	//박스 컴포넌트 크기 변경
	boxComp->SetWorldScale3D(FVector(0.75f, 0.25f, 1.0f));

	meshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Static Mesh Component"));
	meshComp->SetupAttachment(boxComp);
}

총알의 경우 플레이어와 달리 정육면체가 아닌 총알에 가깝게 x축과 y축의 스케일을 조정해야 한다. 박스 컴포넌트의 멤버 함수 중 SetWorldScale3D()함수를 사용하면 매개변수로 입력받은 벡터 값으로 박스 컴포넌트 크기를 조절할 수 있다.

	//박스 컴포넌트 크기 변경
	boxComp->SetWorldScale3D(FVector(0.75f, 0.25f, 1.0f));

이제 BP_BULLET을 상속받는 블루프린트인 BULLET을 만들고 메시를 넣고 설정해준다.
총알을 전방으로 움직이기 위해 속력변수를 800F로 설정해준다.

profile
메타쏭이

0개의 댓글