게임/UnReal_MakeMyGameStudy

일인칭 슈팅 C++ 튜토리얼

CMS419 2021. 5. 7. 17:43

목적

이 튜토리얼은 C++ 를 사용해서 기본적인 일인칭 슈팅 (FPS) 게임을 만드는 법을 보여드립니다.

목표

이 튜토리얼을 마칠 때 쯤이면 다음과 같은 작업이 가능할 것입니다:

  • 프로젝트 구성
  • 캐릭터 구현
  • 프로젝타일 구현
  • 캐릭터 애니메이션

본문

 

일인칭 슈팅 C++ 튜토리얼

일인칭 슈팅 게임 메커니즘 구현 방법을 배워봅니다.

docs.unrealengine.com

코드

FPSCharacter

FPSCharacter.cpp

/*
	마우스 감도나 축 반전과 같은 추가 처리를 해 주려거든,
	입력 값을 함수에 전달하기 전 별도의 조정을 가하는 함수를 추가해 주면 되지만,
	여기서는 입력을 바로 AddControllerYawInput 과 AddControllerPitchInput 함수에 바인딩하도록 하겠습니다.
*/

#include "FPSCharacter.h"
#include <Engine/Classes/Components/CapsuleComponent.h>

// Sets default values
AFPSCharacter::AFPSCharacter()
{
	// 매 프레임 Tick() 호출을 위해 이 캐릭터를 설정합니다. 필요치 않은 경우 꺼 주면 퍼포먼스가 향상됩니다.
	PrimaryActorTick.bCanEverTick = true;

	// 일인칭 카메라 컴포넌트 생성합니다
	FPSCameraComponent = CreateDefaultSubobject<UCameraComponent>(TEXT("FirstPersonCamera"));
	// 카메라 컴포턴넌트를 캡슐 컴포턴트에 붙입니다.
	FPSCameraComponent->SetupAttachment(GetCapsuleComponent());
	//카메라 위치를 눈 살짝 위쪽으로 잡습니다.
	FPSCameraComponent->SetRelativeLocation(FVector(0.0f, 0.0f, 50.0f + BaseEyeHeight));
	// 폰의 카메라 로테이션 제어를 혀용합니다.
	FPSCameraComponent->bUsePawnControlRotation = true;

	//일인칭 메시 컴포넌트
	FPSMesh = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("FirstPersonMesh"));
	// 소유 플레이어만 이 메시를 볼 수 있다.
	FPSMesh->SetOnlyOwnerSee(true);
	// FPS 메시를 FPS 카메라에 붙입니다.
	FPSMesh->SetupAttachment(FPSCameraComponent);
	//일부 환경 섀도잉을 꺼 메시가 하나인 듯 보이는 느낌을 유지합니다.
	FPSMesh->bCastDynamicShadow = false;
	FPSMesh->CastShadow = false;

	// 자신 이외 모두가 일반 몸통 메시를 볼 수 있습니다.
	GetMesh()->SetOwnerNoSee(true);
}

// 게임 시작시 또는 스폰시 호출됩니다.
void AFPSCharacter::BeginPlay()
{
	Super::BeginPlay();
	
	if (GEngine)
	{
		// 5초간 디버그 메세지 표시 첫 인수"-1 "Key" 값은 이 메시직를 업데이트 또는 새로고칠 필요가 없음을 나타냅니다.
		GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Red, TEXT("We are using FPSCaracter!!"));
	}

}

// 매 프레임 호출됩니다.
void AFPSCharacter::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

}

// 입력을 위한 함수성 바인딩을 위해 호출됩니다.
void AFPSCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);


	//"movement" 바인딩 구성
	//InputComponent 는 입력 데이터 처리 방식을 정의하는 컴포넌트입니다. 
	//InputComponent 는 입력을 받기 원하는 액터에 붙이면 됩니다.
	PlayerInputComponent->BindAxis("MoveForward", this, &AFPSCharacter::MoveForward);
	PlayerInputComponent->BindAxis("MoveRight", this, &AFPSCharacter::MoveRight);

	//"Look" 바인딩을 구성합니다.
	PlayerInputComponent->BindAxis("Turn", this, &AFPSCharacter::AddControllerYawInput);
	PlayerInputComponent->BindAxis("LookUp", this, &AFPSCharacter::AddControllerPitchInput);

	//"action" 바인딩을 구성합니다.
	PlayerInputComponent->BindAction("Jump", IE_Pressed, this, &AFPSCharacter::StartJump);
	PlayerInputComponent->BindAction("Jump", IE_Released, this, &AFPSCharacter::StopJump);

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

void AFPSCharacter::MoveForward(float Value)
{
	// 어는 쪽이 전방인지 알아내어, 플레이어가 그 방향으로 이동하고자 한다고 기록합니다.
	FVector Direction = FRotationMatrix(Controller->GetControlRotation()).GetScaledAxis(EAxis::X);
	AddMovementInput(Direction, Value);
}

void AFPSCharacter::MoveRight(float Value)
{
	// 어는 쪽이 오른쪽인지 알아내어, 플레이어가 그 방향으로 이동하고자 한다고 기록합니다.
	FVector Direction = FRotationMatrix(Controller->GetControlRotation()).GetScaledAxis(EAxis::Y);
	AddMovementInput(Direction, Value);
}

/*
Character 베이스 클래스에 대한 인터페이스 (*.h) 파일 안을 보면,
캐릭터 점프에 대한 지원이 내장되어 있는 것을 볼 수 있습니다.
캐릭터 점프는 `bPressedJump` 변수에 묶여 있어서, 점프 동작을 누르면 부울을 `true` 로, 떼면 `false` 로 설정
그렇게 하기 위해 다음의 두 함수를 추가해야 합니다:
*/

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

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


/*
	고려 해야할게 2가지 있다
	1) 발사체 스폰 위치
	2) 프로젝타일 클래스 (FPSCharacter 와 그 파생 블루프린트가 스폰할 발사체를 알도록 하기 위한것이다.)

	이유: 카메라 스페이스 오프셋 벡터를 사용하여 프로젝타일의 스폰 위치를 결정하게 됩니다. 
	이 파라미터를 편집가능하게 만들어야 BP_FPSCharacter 블루프린트에서 설정 및 조정할 수 있습니다. 
	궁극적으로 이 데이터를 토대로 발사체에 대한 초기 위치를 계산할 수 있을 것입니다.

	참고로 멀티플레이어 게임을 만드는 경우,
	MovementComp 컴포넌트의 Initial Velocity in Local Space (로컬 스페이스의 초기 속도) 
	옵션 체크를 해제해 줘야 이 발사체가 서버에 제대로 리플리케이트됩니다.

*/
void AFPSCharacter::Fire()
{
	//프로젝타일 발사를 시도합니다
	if (ProjectileClass)
	{

		//카메라 트랜스폼을 구합니다.
		FVector CameraLocation;
		FRotator CameraRotation;
		GetActorEyesViewPoint(CameraLocation, CameraRotation);

		//MuzzleOffset 을 카메라 스페이스에서 월드 스페이스로 변환합니다.
		FVector MuzzleLocation = CameraLocation + FTransform(CameraRotation).TransformVector(MuzzleLocation);
		FRotator MuzzleRotation = CameraRotation;

		// 조준선을 약간 윗쪽으로 조준합니다.
		MuzzleRotation.Pitch += 10.0f;
		UWorld* World = GetWorld();

		if (World)
		{
			FActorSpawnParameters SpawnParams;
			SpawnParams.Owner = this;
			SpawnParams.Instigator = GetInstigator();
			//총구 위치에 발사체를 스폰시킵니다.
			AFPSProjectile* Projectile = World->SpawnActor<AFPSProjectile>(ProjectileClass, MuzzleLocation, MuzzleRotation, SpawnParams);

			if (Projectile)
			{
				//발사 방향을 알아냅니다.
				FVector LaunchDirection = MuzzleRotation.Vector();
				Projectile->FireInDirection(LaunchDirection);
			}
		}
	}
}

FPSCharacter.h

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "Camera/CameraComponent.h"
#include "FpsProjectile.h"
#include "FPSCharacter.generated.h"

UCLASS()
class FPSPROJECT_API AFPSCharacter : public ACharacter
{
	GENERATED_BODY()

public:
	// 이 캐릭터의 프로퍼티 기본값을 설정합니다.
	AFPSCharacter();

protected:
	// 게임 시작 또는 스폰시 호출됩니다.
	virtual void BeginPlay() override;

public:	
	// 매 프레임 호출됩니다.
	virtual void Tick(float DeltaTime) override;

	// 입력에 함수성 바인딩을 위해 호출됩니다.
	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
	
	// 전후 이동 처리
	UFUNCTION()
	void MoveForward(float Value);
	
	//좌우 이동 처리
	UFUNCTION()
	void MoveRight(float Value);

	// 키를 누르면 점프 플래그를 설정합니다.
	UFUNCTION()
	void StartJump();

	// 키를 떼면 점프 플래그를 지웁니다.
	UFUNCTION()
	void StopJump();

	// FPS 카메라
	UPROPERTY(VisibleAnywhere)
	UCameraComponent* FPSCameraComponent;

	//일인칭 메시 (팔) 소유 플레이어에게만 보이기
	UPROPERTY(VisibleDefaultsOnly, Category = "Mesh")
	USkeletalMeshComponent* FPSMesh;

	UFUNCTION()
	void Fire();


	/*
	EditAnywhere 지정자를 통해 총구 오프셋 값을 블루프린트 에디터의 디폴트 모드나 
	캐릭터의 아무 인스턴스에 대한 디테일 탭에서 변경할 수 있습니다. 
	BlueprintReadWrite 지정자를 통해서는 블루프린트 안에서 총구 오프셋의 값을 구하고 설정할 수 있습니다.
	*/

	// 카메라 위치에서의 총구 오프셋
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Gameplay)
	FVector MuzzleOffset;

	/*
	EditDefaultsOnly 지정자는 프로젝타일 클래스를 블루프린트의 
	각 인스턴스 상에서가 아니라 블루프린트의 디폴트로만 설정할 수 있다는 뜻입니다.
	*/

	// 스폰시킬 프로젝타일 클래스
	UPROPERTY(EditDefaultsOnly, Category = Projectile)
	TSubclassOf<AFPSProjectile> ProjectileClass;
};

FPSHUD

FPSHUD.cpp

#include <Engine/Canvas.h>
#include "FPSHUD.h"

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

FPSHUD.h

#include "CoreMinimal.h"
#include "GameFramework/HUD.h"
#include "FPSHUD.generated.h"

/**
 * 
 */
UCLASS()
class FPSPROJECT_API AFPSHUD : public AHUD
{
	GENERATED_BODY()

protected:
	// 화면 중앙에 그려질 것입니다.
	UPROPERTY(EditAnywhere)
	UTexture2D* CrosshairTexture;

public:
	// HUD에 대한 주요 드로 콜입니다.
	virtual void DrawHUD() override;

};

FPSProjectGameModeBase

FPSProjectGameModeBase.cpp

// Copyright Epic Games, Inc. All Rights Reserved.


#include "FPSProjectGameModeBase.h"

void AFPSProjectGameModeBase::StartPlay()
{

	Super::StartPlay();


	if (GEngine)
	{
		//디버그 메시지를 5초간 표시합니다
		//"키" (첫 번쨰 인수) 값을 -1 로 하면 이 메시지를 절대 업데이트하거나 새로고칠 필요가 없음을 나타냅니다.
		GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Yellow, TEXT("Hello World, this is FPSGameMode"));
	}
}

FPSProjectGameModeBase.h

// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

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

/**
 * 
 */
UCLASS()
class FPSPROJECT_API AFPSProjectGameModeBase : public AGameModeBase
{
	GENERATED_BODY()
	virtual void StartPlay() override;

};

FPSProjectile

FPSProjectile.cpp

#include "FPSProjectile.h"

// Sets default values
AFPSProjectile::AFPSProjectile()
{
 	// 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;

	// 시뮬레이션으로 구동시켜줄 것이므로
	// CollisionComponent 를 RootComponent 로 만듭니다.

	// 구체를 단순 콜리전 표현으로 사용합니다.
	CollisionComponent = CreateDefaultSubobject<USphereComponent>(TEXT("SphereComponent"));
	// 구체의 콜리전 반경을 설정합니다.
	CollisionComponent->InitSphereRadius(15.0f);
	// 루트 컴포넌트를 콜리전 컴포넌트로 설정합니다.
	RootComponent = CollisionComponent;

	// ProjectileMovementComponent 를 사용하여 이 발사체의 운동을 관장합니다.
	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;

	// 3초 뒤에 죽는다.
	InitialLifeSpan = 3.0f;

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

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

// Called when the game starts or when spawned
void AFPSProjectile::BeginPlay()
{
	Super::BeginPlay();
	
}

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

}

// 프로젝타일의 속도를 발사 방향으로 초기화시키는 함수입니다.
void AFPSProjectile::FireInDirection(const FVector& ShootDirection)
{
	ProjectileMovementComponent->Velocity = ShootDirection * ProjectileMovementComponent->InitialSpeed;
}

// 프로젝타일에 무언가 맞으면 호출되는 함수입니다.
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.h

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include <Engine/Classes/Components/SphereComponent.h>
#include <Engine/Classes/GameFramework/ProjectileMovementComponent.h>
#include "FPSProjectile.generated.h"

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

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

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

	// 구체 콜리전 컴포넌트
	UPROPERTY(VisibleDefaultsOnly, Category = "Projectile")
	USphereComponent* CollisionComponent;

	// 프로젝타일 무브먼트 컴포넌트
	UPROPERTY(VisibleAnywhere, Category = "Movement")
	UProjectileMovementComponent* ProjectileMovementComponent;

	// 프로젝타일이 무언가에 맞으면 호출되는 함수입니다.
	UFUNCTION()
	void OnHit(UPrimitiveComponent* HitComponent, 
				AActor* OtherActor, 
				UPrimitiveComponent* OtherComponent, 
				FVector NormalImpulse, 
				const FHitResult& Hit
				);


	// 발사체의 속도를 발사 방향으로 초기화시킵니다.
	void FireInDirection(const FVector& ShootDirection);

};