- 플레이어의 콤보 공격 구현
- 플레이어와 몬스터가 사용할 오브젝트 채널 추가
- 플레이어의 공격 체크에 사용할 트레이스 채널 추가
- 플레이어의 캡슐 콜라이더에 사용할 프리셋 추가
- 몽타주와 데이터 애셋을 활용한 콤보 공격 추가
- 플레이어와 몬스터를 나타낼 오브젝트 채널 추가 및 프리셋 추가하기
- 공격을 체크하기 위한 트레이스 채널 추가하기
C++ 코드로 콜리전 프리셋을 지정해주도록 하겠습니다.
// MMPlayerCharacter Cpp
AMMPlayerCharacter::AMMPlayerCharacter()
{
// Collision 설정
{
GetCapsuleComponent()->InitCapsuleSize(35.0f, 90.0f);
// 프리셋 지정
GetCapsuleComponent()->SetCollisionProfileName(TEXT("MMCapsule"));
}
...
// Mesh의 Collision 설정 (앞으로 모든 체크는 CapsuleComponent에서 할 것)
GetMesh()->SetCollisionProfileName(TEXT("NoCollision"));
}
- 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)
- 최대 가능한 콤보의 수
- 재생 속도 (프레임으로 콤보 가능여부를 체크하기 위함)
- 다음 콤보로 넘어가기 위한 입력 체크용 프레임을 저장할 배열
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);
}
}
}