[UE5 C++] 플레이어 콤보 공격

LeeTaes·2024년 4월 26일
0

[UE_Project] MysticMaze

목록 보기
5/17
post-thumbnail

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

  • 플레이어의 콤보 공격 구현
    - 플레이어와 몬스터가 사용할 오브젝트 채널 추가
    - 플레이어의 공격 체크에 사용할 트레이스 채널 추가
    - 플레이어의 캡슐 콜라이더에 사용할 프리셋 추가
    - 몽타주와 데이터 애셋을 활용한 콤보 공격 추가

콜리전 설정하기

  • 플레이어와 몬스터를 나타낼 오브젝트 채널 추가 및 프리셋 추가하기
  • 공격을 체크하기 위한 트레이스 채널 추가하기
  • [프로젝트 설정] - [콜리전]에서 오브젝트 채널 및 트레이스 채널을 추가합니다.
    - 오브젝트 채널 : MMCreature, 기본 반응 Block
    - 트레이스 채널 : MMAction, 기본 반응 Ignore
  • 새로 추가한 MMCreature의 경우 기존 Pawn과 동일하게 나머지 프리셋에서 설정을 변경해주도록 합니다.
  • 플레이어와 몬스터의 Capsule Component에 적용할 프리셋을 추가합니다.
    - 기존에 만든 MMAction을 Block으로 체크해주도록 합니다.

플레이어 캐릭터의 CapsuleComponent에 적용하기

C++ 코드로 콜리전 프리셋을 지정해주도록 하겠습니다.

// MMPlayerCharacter Cpp
AMMPlayerCharacter::AMMPlayerCharacter()
{
	// Collision 설정
	{
		GetCapsuleComponent()->InitCapsuleSize(35.0f, 90.0f);

		// 프리셋 지정
		GetCapsuleComponent()->SetCollisionProfileName(TEXT("MMCapsule"));
	}

	...
    
	// Mesh의 Collision 설정 (앞으로 모든 체크는 CapsuleComponent에서 할 것)
	GetMesh()->SetCollisionProfileName(TEXT("NoCollision"));
}
  • BP_Player에서 결과를 확인해보도록 합니다.

공격 액션 추가 및 몽타주 추가하기

  • IA_BaseAttack 액션을 추가하고, 코드상에서 매핑시켜주도록 합니다.

// MMPlayerCharacter CPP

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

	UEnhancedInputComponent* EnhancedInputComponent = CastChecked<UEnhancedInputComponent>(PlayerInputComponent);

	...
    
	EnhancedInputComponent->BindAction(IA_BasicAttack, ETriggerEvent::Triggered, this, &AMMPlayerCharacter::BasicAttack);
    
	...
}

공격에 사용할 몽타주를 생성해주도록 합니다.

  • 4가지 공격 애니메이션을 넣어줍니다.
  • 몽타주 섹션을 추가해 이름을 지정합니다. (BasicComboAttack 1 ~ 4)
  • 모든 섹션을 이어지지 않도록 분리합니다.


콤보 공격 구현하기

콤보 공격은 여러 방식으로 구현할 수 있습니다.

저는 기존에 콤보 체크 노티파이와 콤보 수를 체크하여 체크 노티파이가 발동되기 전에 입력이 들어온 경우 다음 콤보 수를 확인하여 가능하다면 섹션을 전환시켜주는 방법을 사용했습니다.

최근 콤보와 관련된 정보를 데이터 애셋에 저장하여 사용하는 방법을 공부하게 되어 해당 방법으로 구현해보도록 하겠습니다.

콤보 데이터 애셋에 저장할 내용

  • 섹션의 접두사(BasicComboAttack)
  • 최대 가능한 콤보의 수
  • 재생 속도 (프레임으로 콤보 가능여부를 체크하기 위함)
  • 다음 콤보로 넘어가기 위한 입력 체크용 프레임을 저장할 배열
  • 간단히 요약하자면 콤보 진행 순서는 다음과 같습니다.
    1. 공격 입력이 들어와 몽타주를 재생함과 동시에 타이머를 시작한다.
      (배열에 저장된 콤보 지속 가능한 시간)
    2. 해당 시간이 지나기 전에 다시 공격 입력이 들어오는 경우 다음 콤보 공격이 가능하다고 체크한다.
    3. (1)에서 지정한 타이머가 종료되면 (2)에서 들어온 입력이 있는지 체크하여, 있는 경우 다음 섹션의 몽타주를 재생하고 타이머를 재설정하며, 없는 경우 콤보를 종료한다.

MMComboActionData Class

  • 콤보 데이터 저장용 클래스를 구현합니다.
UCLASS()
class MYSTICMAZE_API UMMComboActionData : public UDataAsset
{
	GENERATED_BODY()
	
public:
	UMMComboActionData();

public:
	// 몽타주 섹션 이름 (접두사)
	UPROPERTY(EditAnywhere, Category = Name)
	FString SectionPrefix;

	// 재생 속도
	UPROPERTY(EditAnywhere, Category = ComboData)
	float FrameRate;

	// 최대 가능 콤보 수
	UPROPERTY(EditAnywhere, Category = ComboData)
	uint8 MaxComboCount;

	// 콤보별 다음 콤보로 넘어가기 위한 입력 프레임 정보
	UPROPERTY(EditAnywhere, Category = ComboData)
	TArray<float> ComboFrame;
};
  • Basic 콤보 공격에 대한 데이터 에셋을 완성시켜주도록 합니다.

  • 플레이어에 넣어줄 수 있도록 슬롯을 코드로 생성해주도록 합니다.

  • 추가적으로 재생할 몽타주 또한 슬롯으로 생성해주도록 합니다.

// Combo
// Montage
protected:
	UPROPERTY(EditAnywhere, Category = Montage, Meta = (AllowPrivateAccess = "true"))
	TObjectPtr<class UAnimMontage> BasicComboMontage;

// Combo
protected:
	UPROPERTY(EditAnywhere, Category = ComboData, Meta = (AllowPrivateAccess = "true"))
	TObjectPtr<class UMMComboActionData> BasicComboData;
  • 위에서 생성한 콤보 액션 데이터와 몽타주를 플레이어에 추가해주도록 하겠습니다.

콤보 로직 구현

MMPlayerCharacter Class

  • ComboStart() : 콤보를 시작하기 위한 함수
  • ComboEnd() : 콤보 종료를 알리는 함수
  • ComboCheck() : 콤보를 이어 진행할 수 있는지 체크하는 함수
  • SetComboTimer() : 콤보 체크 호출 시간을 설정하기 위한 함수
// MMPlayerCharacter Header
protected:
	void ComboStart();
	void ComboEnd(class UAnimMontage* Montage, bool IsEnded);
	void ComboCheck();
	void SetComboTimer();

	// 콤보에 사용될 타이머 변수
	FTimerHandle ComboTimerHandle;
	// 현재 콤보 진행 수
	int32 CurrentComboCount;
	// 콤보 입력 판별
	uint8 bHasComboInput : 1;
    
    ...
    
protected:
	// 공격 중 구르기, 구르기 중 공격, 데쉬 중 공격을 막기 위해 추가
	uint8 bIsAttacking : 1;
// MMPlayerCharacter Cpp
void AMMPlayerCharacter::BasicAttack()
{
	// 구르기 상태일 때 공격 불가
	if (bIsRoll) return;

	// 콤보 시작
	if (CurrentComboCount == 0)
	{
		ComboStart();
		bIsAttacking = true;
		return;
	}

	// 중간 입력 체크
	// * 콤보 타이머가 종료되지 않은 상태라면 콤보 입력 체크
	if (ComboTimerHandle.IsValid())
	{
		bHasComboInput = true;
	}
	// * 콤보 타이머가 유효하지 않은(종료) 상태라면 콤보 입력 체크 해제
	else
	{
		bHasComboInput = false;
	}
}

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(BasicComboMontage, AttackSpeedRate);

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

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

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

void AMMPlayerCharacter::ComboEnd(UAnimMontage* Montage, bool IsEnded)
{
	// 콤보 수 초기화
	CurrentComboCount = 0;

	// 콤보 입력 판별 초기화
	bHasComboInput = false;
	
	// 공격 종료
	bIsAttacking = false;

	// 플레이어 이동 가능하도록 설정
	GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_Walking);
}

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

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

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

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

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

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

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

		// 타이머 설정하기
		if (ComboAvailableTime > 0.0f)
		{
			// ComboAvailableTime시간이 지나면 ComboCheck() 함수 호출
			GetWorld()->GetTimerManager().SetTimer(ComboTimerHandle, this, &AMMPlayerCharacter::ComboCheck, ComboAvailableTime, false);
		}
	}
}
  • 결과 확인
profile
클라이언트 프로그래머 지망생

0개의 댓글