2-5강 캐릭터 콤보 액션

Ryan Ham·2024년 6월 28일
0

이득우 Unreal

목록 보기
7/23
post-thumbnail

강의 목표

  • Unreal Animation Montage 시스템의 활용 방법
  • Data Asset과 Delegate를 활용한 콤보 공격

Delegate에 대해 선행 공부를 하고 싶다면 이 링크를 참고하자


Montage기능이란?

여러개의 animation clip을 모아둔 다수의 section으로 구성. (중요 !!) AnimInstance 함수를 통해서 원하는 section으로 건너뛰게 할 수 있다.

Animation Montage 만드는 방법

1. BP 부분

  1. Animation -> Animation Montage로 생성
  2. Combo Attack에 해당하는 Animation Clip들을 Montage에 배치하고 각 부분을 Section으로 나눈다.

2. C++ 부분

  1. Attack IA만들고 IMC에 등록, Character header와 cpp에도 등록 과정 완료하기
  2. Character.cpp에 attack() 함수 로직 작성하기
  3. Montage를 재생하기 위해서는 AnimInstace에 대한 포인터를 가져온다. 캐릭터에서 GetMesh() -> GetAnimInstance() -> Montage_Play(몽타주 애셋)으로 특정 Montage를 재생할 수 있다.

3. Anim Graph 부분

  1. 최종적으로 캐릭터 Mesh가 가르키고 있는 AnimInstance의 Anim Graph에 Montage를 연결해주어야 한다. 최종 Output Pose로 들어가기 전에 Montage Animation 안에 Combo애 해당하는 slot이라는 노드를 중간에 연결해준다. 이 slot은 Montage가 실행되면 기존 애니메이션에 덮어쓰여서 효과를 적용하게 된다.

Montage로 구현하는 Combo 공격의 원리

  • Combo Frame 전에 입력이 들어오면 다음 Montage section 이어서 재생(1번 케이스에 해당)
  • Combo Frame보다 입력이 늦으면 해당 section을 마저 플레이하고 종료(2번 케이스에 해당)

Data Asset만들고 콤보 정보를 저장

  • 각 콤보마다 입력을 테스트하는 frame을 지정

  • 지정된 frame 이전에 입력이 들어오면 자동으로 다음 montage section 플레이하게 연결

Data Asset 만드는 방법

  1. C++ 클래스 추가탭에서 DataAsset->PrimaryDataAsset 추가
  2. BP에서 Miscellaneous(기타)->DataTable하고 1에서 만든 cpp 클래스 부모로 지정하기

DataAsset.h

    // ComboActionData.h
    ...
	// 몽타주 section 이름
	UPROPERTY(EditAnywhere, Category = Name)
	FString MontageSectionNamePrefix;

	// 총 몇개의 combo가 존재하는지
	UPROPERTY(EditAnywhere, Category = Name)
	uint8 MaxComboCount;

	UPROPERTY(EditAnywhere, Category = Name)
	float FrameRate;

	UPROPERTY(EditAnywhere, Category = ComboData)
	TArray<float> EffectiveFrameCount;


코드분석

처음 시작은 Character.cpp부분에 Attack함수를 만들고 이를 EnhancedInputComponent에 binding한다.

EnhancedInputComponent->BindAction(AttackAction, ETriggerEvent::Triggered, this, &AMyRyanCharacter::Attack);

Attack함수는 ProcessComboCommand라는 함수를 부르게 되고 여기서부터는 CharacterBase.cpp에서 모든 로직이 처리되게 된다.

// 처음(CurrentCombo가 0)에만 시작 함수를 따로 구현하고
// 나머지에 대해서는 timer로 처리하게 한다. 
// 타이머가 돌면서 HasNextComboCommand값을 확인하는 방식.
void ARyanCharacterBase::ProcessComboCommand()
{
	if (CurrentCombo == 0)
	{
		ComboActionBegin();
		return;
	}

	if (!ComboTimerHandle.IsValid())
	{
		HasNextComboCommand = false;
	}
	else
	{
		HasNextComboCommand = true;
	}
}

ProcessComboCommand는 CharacterBase에서 처음 Attack 관련해서 시작되는 함수이다. 처음에 키를 눌렀을때(CurrentCombo가 0)와 아닌 상태의 로직이 다르다. 0일때는 ComboActionBegin()을 실행하고 아닐때는 HasNextComboCommand의 bool 값만 조절하고 Timer가 expire되기 전에 이 값을 체크하면서 콤보 처리를 구현한다.

void ARyanCharacterBase::ComboActionBegin()
{
	// Combo Status
	CurrentCombo = 1;

	// Movement Setting
	// MOVE_None으로 setting하면 캐릭터의 이동기능이 상실된다. 
	GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_None);

	// Animation Setting
	const float AttackSpeedRate = 1.0f;
	UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
	AnimInstance->Montage_Play(ComboActionMontage, AttackSpeedRate);

	// Montage_SetEndDelegate에 ComboActionEnd라는 함수를 등록하고 Montage가 끝날때 자동으로 실행될 수 있게 한다. 
	FOnMontageEnded EndDelegate;
	EndDelegate.BindUObject(this, &ARyanCharacterBase::ComboActionEnd);
	AnimInstance->Montage_SetEndDelegate(EndDelegate, ComboActionMontage);

	ComboTimerHandle.Invalidate();
	SetComboCheckTimer();
}

void ARyanCharacterBase::ComboActionEnd(UAnimMontage* TargetMontage, bool IsProperlyEnded)
{
	ensure(CurrentCombo != 0);
	CurrentCombo = 0;
	GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_Walking);
}

먼저 처음 콤보가 시작될때 어떻게 되는지를 확인해보자. ComboActionBegin 함수에서는 skeletal mesh에서 AnimInstance를 가져오고, 이를 통해 첫번째 Attack Montage section을 play한다. 그리고 Montage가 종료될때 사용하라고 만든 FOnMontageEnded 구조체를 선언하고, Montage_SetEndDelegate 델리게이트에 ComboActionEnd라는 함수를 등록한다. 이 함수를 Montage가 최종적으로 끝날때 실행되게 된다. 델리게이트에 바인딩 된 ComboActionEnd 함수는 단순히 Montage가 끝났을때 Combo 값을 0으로 돌려놓고 Character Movement를 해제시키는 함수이다.

void ARyanCharacterBase::SetComboCheckTimer()
{
	int32 ComboIndex = CurrentCombo - 1;

	//FString FormattedString = FString::Printf(TEXT("ComboIndex: %d, CurrentCombo: %d"), ComboIndex, CurrentCombo);
	//GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Green, FormattedString);
	ensure(ComboActionData->EffectiveFrameCount.IsValidIndex(ComboIndex));

	const float AttackSpeedRate = 1.0f;
	// frame수를 frame rate로 나누면 play된 시간이 나오게 된다. 
	// EffectiveFrameCount 배열에는 AttackMontage의 각 section 공격에 대한 frame 구간이 있다. [17,17,20,0] 이렇게 구성되어 있음.
	// ComboTimerHandle이라는 타이머를 만들어서 ComboEffectiveTime 후에 ComboCheck이라는 함수를 call하게 한다. 
	float ComboEffectiveTime = (ComboActionData->EffectiveFrameCount[ComboIndex] / ComboActionData->FrameRate) / AttackSpeedRate;
	if (ComboEffectiveTime > 0.0f)
	{
		GetWorld()->GetTimerManager().SetTimer(ComboTimerHandle, this, &ARyanCharacterBase::ComboCheck, ComboEffectiveTime, false);
	}
}

SetComboCheckTimerComboTimerHandle을 통해 언리얼 Timer 시스템을 만들어 주는 함수이다. 이 타이머는 우리가 사전에 설정한 DataAsset에서 Montage의 유효클릭이 가능한 프레임을 ComboEffectiveTime라는 변수에 저장한다. 타이머는 ComboEffectiveTime의 시간이 지난 후에 ComboCheck이라는 함수를 call하고 사라진다.

void ARyanCharacterBase::ComboCheck()
{
	ComboTimerHandle.Invalidate();
	// 만약 정해진 시간안에 다시 attack key를 눌렀다면 HasNextComboCommand가 true로 설정되어 있을것. 
	if (HasNextComboCommand)
	{
		UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();

		// 지정한 콤보 index값을 넘어가면 안되기 때문에 Clamp로 
		// 여기서 CurrentCombo 값을 업데이트한다. 
		CurrentCombo = FMath::Clamp(CurrentCombo + 1, 1, ComboActionData->MaxComboCount);
		// Montage에서 이동할 section 이름값 구하기.
		// FString::Printf로 string concatenation을 한다.  
		FName NextSection = *FString::Printf(TEXT("%s%d"), *ComboActionData->MontageSectionNamePrefix, CurrentCombo);
		AnimInstance->Montage_JumpToSection(NextSection, ComboActionMontage);
		//다음 montage를 play하므로 타이머는 다시 초기화
		SetComboCheckTimer();
		HasNextComboCommand = false;
	}
}

마지막 부분을 보면, 처음에 ProcessComboCommand()의 아리송 했던 부분을 이해할 수 있다. ComboCheck함수는 HasNextComboCommand라는 변수값을 체크하면서 true일때만 동작한다. 위에서 타이머가 만료되려고 할때 HasNextComboCommand 값이 true이면 Montage_JumpToSection을 통해 다음 Montage를 재생하고 다시 SetComboCheckTimer을 불러 timer를 초기화시킨다. 만약 HasNextComboCommand 값이 false라면, 타이머를 invalidate 시킨다.

최종정리(중요!!)

복잡한 내용을 정리하자면, 처음에 Attack키를 눌렀을때 ProcessComboCommand 함수가 호출되게 되는데 여기서 CurrentCombo가 0일때와 아닐때 로직을 나누어서 본다. 0일때는 직관적으로 ComboActionBegin이라는 함수를 실행하고 첫번째 Montage section을 play한다. 여기서 ComboTimerHandle라는 Timer을 작동시키게 된다.

이 Timer은 자기에게 생성될때 주어진 시간이 지나면 ComboCheck라는 함수를 call하고 expire가 되게 되는데, 이때 HasNextComboCommand라는 변수를 확인해보게 된다. 이 변수는 사용자가 combo에 알맞는 시간에 다시 attack 키를 누르면 true로 바뀌게 되고 아니면 false로 값이 설정된다. 이렇게 처음 키를 누르는 경우를 제외하고는 다 timer 방식으로 다음 montage section을 play하여 attack combo를 구현하게 된다.


Montage 관련 함수 모아보기!

##############################################

AnimInstance->함수()식으로 사용

  • Montage_Play()
  • Montage_JumpToSection()
  • FOnMontageEnded
  • Montage_SetEndDelegate()

##############################################


최종화면

타이머가 expire되기 전에 적절한 타이밍에 attack 버튼을 누르면 Animation Montage에서 Attack1, Attack2, Attack3, Attack4에 해당하는 animation section을 차례대로 보여주는 것을 알 수 있다.

profile
🏦KAIST EE | 🏦SNU AI(빅데이터 핀테크 전문가 과정) | 📙CryptoHipsters 저자

0개의 댓글