[UE5 C++] 직업 구분 및 Weapon 제작 - 1

LeeTaes·2024년 5월 2일
0

[UE_Project] MysticMaze

목록 보기
7/17

언리얼 엔진을 사용한 RPG 프로젝트 만들기

  • 직업 구분하기
    - 초보자, 전사, 궁수, 마법사
  • 무기 클래스 제작
    - 테스트용 무기를 플레이어에게 장착하기 (소지, 장착, 장착해제)
  • 전체 직업에 확장 가능한 구조를 만들고, 전사를 예시로 제작하기

직업 추가하기

이전까지 만든 작업물은 초보자(Beginner) 직군의 행동입니다.

  • 우선 직업을 구분하기 위한 열거형을 생성해주도록 합니다.
// MMEnums Header

#pragma once

#include "CoreMinimal.h"
#include "MMEnums.generated.h"

UENUM(BlueprintType)
enum class EClassType : uint8
{
    CT_None,
    CT_Beginner,    // 초보자
    CT_Warrior,     // 전사
    CT_Archer,      // 궁수
    CT_Mage,        // 마법사
};

플레이어 클래스에서 해당 헤더를 추가하고 직업에 관련된 변수/함수를 추가해주도록 하겠습니다.

// MMPlayerCharacter Header
void ChangeClass(EClassType Class);
EClassType ClassType;

// AMMPlayerCharacter Cpp
void AMMPlayerCharacter::ChangeClass(EClassType Class)
{
	ClassType = Class;
}

직업에 따른 입력 변경하기

  • 초보자는 무기를 착용할 수 없지만, 나머지 직군은 가능합니다.
  • 초보자는 기본 공격만 가능하지만, 나머지 직군은 특수 키가 존재합니다.
    - 전사 : Guard (방어)
    - 궁수 : Shoot (화살 발사)
    - 법사 : GatherMana (마나 충전)
  • 즉, 직업별로 InputMappingContext를 변경할 수 있도록 설정하도록 하겠습니다.

필요한 InputAction을 생성하고, 전사용 InputMappingContext를 추가합니다.

기본적으로 가지고 있는 IMC_BasicPlayer와 IMC_WarriorPlayer를 구분시켜줘야 합니다.

  • TMap<직업, IMC> 형식의 IMC_Array를 만들어 교체하는 로직을 구현해보도록 하겠습니다.
    - IA를 코드에 추가하는 부분은 생략하도록 하겠습니다.
// MMPlayerCharacter Header

// Input Section
protected:
	...
    
    // InputMappingContext
	TMap<EClassType, TObjectPtr<class UInputMappingContext>> IMC_Array;
// MMPlayerCharacter Cpp
AMMPlayerCharacter::AMMPlayerCharacter()
{
		// Basic Input
		static ConstructorHelpers::FObjectFinder<UInputMappingContext>IMC_BasicRef(TEXT("/Script/EnhancedInput.InputMappingContext'/Game/MysticMaze/Player/Control/IMC_BasicPlayer.IMC_BasicPlayer'"));
		if (IMC_BasicRef.Object)
		{
			IMC_Array.Add(EClassType::CT_None, IMC_BasicRef.Object);
		}
        
        // Warrior Input
		static ConstructorHelpers::FObjectFinder<UInputMappingContext>IMC_WarriorRef(TEXT("/Script/EnhancedInput.InputMappingContext'/Game/MysticMaze/Player/Control/IMC_WarriorPlayer.IMC_WarriorPlayer'"));
		if (IMC_WarriorRef.Object)
		{
			IMC_Array.Add(EClassType::CT_Warrior, IMC_WarriorRef.Object);
		}
}

void AMMPlayerCharacter::ChangeClass(EClassType Class)
{
	APlayerController* PlayerController = Cast<APlayerController>(GetController());
	if (PlayerController)
	{
		if (UEnhancedInputLocalPlayerSubsystem* SubSystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
		{
			// 기존에 저장된 IMC 초기화
			SubSystem->ClearAllMappings();

			// 새로운 매핑 컨텍스트 연동
			UInputMappingContext* NewMappingContext = IMC_Array[Class];
			SubSystem->AddMappingContext(NewMappingContext, 0);
		}

		ClassType = Class;
	}
}

직업에 따른 공격, 특수 입력 설정하기

현재는 BasicAttack이 초보자를 기준으로 설정되어 있습니다.

  • IMC_Array와 유사하게 ComboMontage를 TMap으로 만들어 구현해주도록 하겠습니다.

우선 사용할 몽타주를 제작해주도록 하겠습니다.

전사의 콤보 공격 데이터를 추가합니다.

코드에서 구현 내용을 수정해주도록 하겠습니다.

  1. 블루프린트 상에서 데이터를 추가할 수 있도록 TMap을 생성합니다.
// MMPlayerCharacter Header
protected:
	...
    
	UPROPERTY(EditAnywhere, Category = Montage, Meta = (AllowPrivateAccess = "true"))
	TMap<EClassType, TObjectPtr<class UAnimMontage>> ComboMontage;
    
    UPROPERTY(EditAnywhere, Category = ComboData, Meta = (AllowPrivateAccess = "true"))
	TMap<EClassType, TObjectPtr<class UMMComboActionData>> ComboData;
  1. 블루프린트 상에서 콤보에 관련된 데이터를 추가해주도록 합니다.

  1. 공격 관련 함수들을 수정해주도록 하겠습니다.
  • 콤보 공격 몽타주와 콤보 공격 데이터를 직업별로 구분(TMap)
// MMPlayerCharacter Cpp
void AMMPlayerCharacter::ComboStart()
{
	// 현재 콤보 수 1로 증가
	CurrentComboCount = 1;

	// 공격 시 플레이어 이동 불가
	GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_None);

	// TODO : 공격 속도가 추가되면 값 가져와 지정하기
	const float AttackSpeedRate = 1.0f;


	// 애님 인스턴스 가져오기
	UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
	if (AnimInstance)
	{
		// 몽타주 재생
		AnimInstance->Montage_Play(ComboMontage[ClassType], AttackSpeedRate);

		// 몽타주 재생 종료 바인딩
		FOnMontageEnded EndDelegate;
		EndDelegate.BindUObject(this, &AMMPlayerCharacter::ComboEnd);

		// ComboMontage가 종료되면 EndDelegate에 연동된 ComboEnd함수 호출
		AnimInstance->Montage_SetEndDelegate(EndDelegate, ComboMontage[ClassType]);

		// 타이머 초기화
		ComboTimerHandle.Invalidate();
		// 타이머 설정
		SetComboTimer();
	}
}

void AMMPlayerCharacter::ComboCheck()
{
	// 타이머 핸들 초기화
	ComboTimerHandle.Invalidate();

	// 콤보에 대한 입력이 들어온 상황이라면?
	if (bHasComboInput)
	{
		// 콤보 수 증가
		CurrentComboCount = FMath::Clamp(CurrentComboCount + 1, 1, ComboData[ClassType]->MaxComboCount);
		
		// 애님 인스턴스 가져오기
		UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
		if (AnimInstance)
		{
			// 다음 섹션의 이름 만들기
			FName SectionName = *FString::Printf(TEXT("%s%d"), *ComboData[ClassType]->SectionPrefix, CurrentComboCount);

			// 다음 섹션으로 이동하기
			AnimInstance->Montage_JumpToSection(SectionName, ComboMontage[ClassType]);

			// 타이머 재설정
			SetComboTimer();
			// 콤보 입력 판별 초기화
			bHasComboInput = false;
		}
	}
}

void AMMPlayerCharacter::SetComboTimer()
{
	// 인덱스 조정
	// * 콤보 인덱스 : 1, 2, 3, 4
	// * 배열 인덱스 : 0, 1, 2, 3
	int32 ComboIndex = CurrentComboCount - 1;

	// 인덱스가 유효한지 체크
	if (ComboData[ClassType]->ComboFrame.IsValidIndex(ComboIndex))
	{
		// TODO : 공격 속도가 추가되면 값 가져와 지정하기
		const float AttackSpeedRate = 1.0f;

		// 실제 콤보가 입력될 수 있는 시간 구하기
		float ComboAvailableTime = (ComboData[ClassType]->ComboFrame[ComboIndex] / ComboData[ClassType]->FrameRate) / AttackSpeedRate;

		// 타이머 설정하기
		if (ComboAvailableTime > 0.0f)
		{
			// ComboAvailableTime시간이 지나면 ComboCheck() 함수 호출
			GetWorld()->GetTimerManager().SetTimer(ComboTimerHandle, this, &AMMPlayerCharacter::ComboCheck, ComboAvailableTime, false);
		}
	}
}

무기 추가하기

  • 무기는 3가지 종류가 존재합니다.
    - 전사(검)
    - 궁수(활)
    - 마법사(지팡이)
  • 무기의 기본이 될 "MMWeapon" 클래스를 생성하고, 해당 클래스를 상속받은 개별 무기 클래스를 생성해주도록 하겠습니다.

무기를 추가하기 이전에 무기는 스탯을 가지고 있으며, 장착한 플레이어에게 도움을 주도록 구현할 것입니다.

스탯을 저장하기 위한 구조체를 생성해주도록 하겠습니다.

FMMCharacterStat Class

  • 스탯은 STR, DEX, CON, INT의 4가지를 사용할 것입니다.
  • 구조체를 쉽게 연산하기 위해 + 연산자를 오버로딩하여 구조체 간의 계산이 가능하도록 설정해주도록 하겠습니다.
// FMMCharacterStat Header
#pragma once

#include "CoreMinimal.h"
#include "Engine/DataTable.h"
#include "MMCharacterStat.generated.h"

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

public:
    FMMCharacterStat() : STR(0), DEX(0), CON(0), INT(0) {}

public:
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Stat)
        int32 STR;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Stat)
        int32 DEX;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Stat)
        int32 CON;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Stat)
        int32 INT;

    FMMCharacterStat operator+(const FMMCharacterStat& other) const
    {
        FMMCharacterStat Result;
      
        Result.STR = this->STR + other.STR;
        Result.DEX = this->DEX + other.DEX;
        Result.CON = this->CON + other.CON;
        Result.INT = this->INT + other.INT;

        return Result;
    }
};

MMWeapon Class

  • 실제 월드상에 스폰되어야 하므로 AActor를 상속받아 제작합니다.
  • 직업별 착용 무기 제한이 있으므로, 무기의 타입을 구분해줍니다.
  • 무기를 착용할 위치와, 장착할 위치의 소켓 이름을 저장해줍니다.
  • 무기의 추가 스탯을 저장하기 위한 변수를 추가해주도록 합니다.

무기를 장착할 소켓은 다음과 같이 설정하였습니다.

// MMWeapon Header
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "GameData/MMCharacterStat.h"
#include "MMWeapon.generated.h"

UENUM(BlueprintType)
enum class EWeaponType : uint8
{
	WT_Sword,	// 검
	WT_Bow,		// 활
	WT_Staff,	// 지팡이
};

UCLASS()
class MYSTICMAZE_API AMMWeapon : public AActor
{
	GENERATED_BODY()
	
public:	
	AMMWeapon();

public:
	FORCEINLINE EWeaponType GetWeaponType() { return WeaponType; }
	
public:
	void EquipWeapon(ACharacter* Player);
	void DrawWeapon(USkeletalMeshComponent* Mesh);
	void SheatheWeapon(USkeletalMeshComponent* Mesh);

protected:
	virtual void BeginPlay() override;

protected:
	UPROPERTY(VisibleAnywhere, Category = "Weapon", meta = (AllowPrivateAccess = "true"))
	TObjectPtr<class UPoseableMeshComponent> WeaponMesh;

	UPROPERTY(VisibleAnywhere, Category = "Weapon", meta = (AllowPrivateAccess = "true"))
	TObjectPtr<class USphereComponent> WeaponCollision;

	EWeaponType WeaponType;

	FMMCharacterStat WeaponStat;

	FName BaseSocketName;
	FName DrawSocketName;
};
// MMWeapon Cpp
// Fill out your copyright notice in the Description page of Project Settings.


#include "Item/MMWeapon.h"
#include "Collision/MMCollision.h"

#include "GameFramework/Character.h"
#include "Components/PoseableMeshComponent.h"
#include "Components/SphereComponent.h"

// Sets default values
AMMWeapon::AMMWeapon()
{
	// Component Setting
	{
		WeaponMesh = CreateDefaultSubobject<UPoseableMeshComponent>(TEXT("WeaponMesh"));
		RootComponent = WeaponMesh;
		WeaponMesh->SetCollisionProfileName(TEXT("NoCollision"));

		WeaponCollision = CreateDefaultSubobject<USphereComponent>(TEXT("WeaponCollision"));
		WeaponCollision->SetupAttachment(RootComponent);
		WeaponCollision->SetCollisionProfileName(MMWEAPON);
		WeaponCollision->bHiddenInGame = false;
	}
}

// Called when the game starts or when spawned
void AMMWeapon::BeginPlay()
{
	Super::BeginPlay();
	
	// 시작과 동시에 충돌 체크 해제
	WeaponCollision->SetCollisionEnabled(ECollisionEnabled::NoCollision);
}

void AMMWeapon::EquipWeapon(ACharacter* Player)
{
	if (Player)
	{
		USkeletalMeshComponent* PlayerMesh = Player->GetMesh();

		WeaponMesh->AttachToComponent(PlayerMesh, FAttachmentTransformRules::KeepRelativeTransform, BaseSocketName);
	}
}

void AMMWeapon::DrawWeapon(USkeletalMeshComponent* Mesh)
{
	if (Mesh)
	{
		WeaponMesh->AttachToComponent(Mesh, FAttachmentTransformRules::KeepRelativeTransform, DrawSocketName);
	}
}

void AMMWeapon::SheatheWeapon(USkeletalMeshComponent* Mesh)
{
	if (Mesh)
	{
		WeaponMesh->AttachToComponent(Mesh, FAttachmentTransformRules::KeepRelativeTransform, BaseSocketName);
	}
}

MMSwordWeapon Class

  • MMWeapon을 상속받는 검 클래스를 생성해주도록 하겠습니다.
  • 우선은 기본적으로 소켓과 무기의 타입을 추가해주도록 하겠습니다.
// MMSwordWeapon Cpp
#include "Item/MMSwordWeapon.h"

AMMSwordWeapon::AMMSwordWeapon()
{
	WeaponType = EWeaponType::WT_Sword;
	BaseSocketName = TEXT("SwordPosition");
	DrawSocketName = TEXT("SwordSocket");
}

MMSwordWeapon 클래스를 상속받은 테스트용 블루프린트를 만들고, 플레이어에게 착용시켜보도록 하겠습니다.

  • 무기 SkeletalMesh를 추가하고, 충돌체의 위치만 수정하였습니다.

  • 플레이어 클래스에서 임시로 무기를 장착하는 로직을 추가해보도록 하겠습니다.

// MMPlayerCharacter Header
	void EquipWeapon(class AMMWeapon* Weapon);

	// TEST
	UPROPERTY(EditAnywhere, Category = Weapon, meta = (AllowPrivateAccess = "true"))
	TSubclassOf<class AMMWeapon> WeaponClass;

	UPROPERTY(VisibleAnywhere, Category = Weapon, meta = (AllowPrivateAccess = "true"))
	TObjectPtr<class AMMWeapon> CurrentWeapon;
// MMPlayerCharacter Cpp

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

	//ChangeClass(EClassType::CT_Beginner);

	// TEST
	{
		ChangeClass(EClassType::CT_Warrior);

		if (GetWorld())
		{
			CurrentWeapon = Cast<AMMWeapon>(GetWorld()->SpawnActor<AMMWeapon>(WeaponClass));
			if (CurrentWeapon)
			{
				UE_LOG(LogTemp, Warning, TEXT("Weapon Spawned"));
				EquipWeapon(CurrentWeapon);
			}
		}
	}
}

void AMMPlayerCharacter::EquipWeapon(AMMWeapon* Weapon)
{
	Weapon->EquipWeapon(this);
}

결과

  • 실행과 동시에 무기를 장착한 플레이어를 볼 수 있습니다.
  • 다음 포스트에서 직업별 애니메이션 적용 방법에 대해 다루도록 하겠습니다.
profile
클라이언트 프로그래머 지망생

0개의 댓글