언리얼 C++ 게임 개발의 정석/7. 애니메이션 시스템 활용

7.2 델리게이트 및 애니메이션 노티파이

ftftgop3 2024. 3. 29. 20:19

델리 게이트

특정 객체가 해야 할 로직을 다른 객체가 대신 처리

등록된 함수를 특정 이벤트에 맞게 실행 가능

 

사용 예시

애닝 인스턴스 제공 되는 델리게이트 사용

몽타주 재생 종료 시 발생하는 OnMontageEnded(UAnimMontage*, bool )라는 델리게이트 제공 

게임 캐릭터에서 위의 델리게이트에 등록 해 몽타주 애니메이션 끝나는 시점 파악

 

언리얼 델리게이트 종류

델리게이트  : C++ 에서만 사용 가능, 블루 프린트 오브젝틑는 직렬화 매커니즘 때문에 사용 불가

다이나믹 델리게이트 : C++와 블루 프린트 객체에 모두 사용 할 수 있는 델리게이트, UFUNCTION 매크로 

 

UCLASS()
class ARENABATTLE_API AABCharacter : public ACharacter
{
	GENERATED_BODY()
public:
	//애닝 인스턴스에 OnAttackMontageEnded 델리게이트에 등록
	virtual void PostInitializeComponents() override;
private:
	//몽타주 애니메이션 종료에 등록할 함수
	UFUNCTION()
	void OnAttackMontageEnded(UAnimMontage* Montage, bool bInterrupted);

private:
	//몽타주 애니메이션 종료 확인하는 bool 변수
	UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = Attack, Meta = (AllowPrivateAccess = true))
		bool IsAttacking;
        
}

 

ABCHECK 매크로 사용 생성

조건이 참이 아닐 경우 에러 로그 발생

 

OnMontageEnded 은 멀티 캐스트 델리게이트라고 부를 수 있고

여러개의 함수를 등록, 헹동이 끝나면 모든 등록 함수에 알려주는 기능 제공

 

OnMontageEnded 선언 시 정의된 델리게이트 형식을 시그니처 라고 한다

void AABCharacter::Attack()
{
	if (IsAttacking)
		return;
        
	auto AnimInstance = Cast<UABAnimInstance>(GetMesh()->GetAnimInstance());
	ABCHECK(AnimInstance != nullptr)
	AnimInstance->PlayAttackMontage();
	IsAttacking = true;
}

void AABCharacter::PostInitializeComponents()
{
	Super::PostInitializeComponents();

	auto AnimInstance = Cast<UABAnimInstance>(GetMesh()->GetAnimInstance());
	ABCHECK(AnimInstance != nullptr);
	//OnMontageEnded 델리게이트에 함수 등록
	//AddDynamic 은 델리게이트 매크로 제공
	AnimInstance->OnMontageEnded.AddDynamic(this, &AABCharacter::OnAttackMontageEnded);

}
void AABCharacter::OnAttackMontageEnded(UAnimMontage* Montage, bool bInterrupted)
{
	//IsAttacking 이 false 면 return
	ABCHECK(IsAttacking);
	IsAttacking = false;
}

 

애닝 인스턴스를 맴버 변수로 구조 변경

class ARENABATTLE_API AABCharacter : public ACharacter
{
	GENERATED_BODY()
  
 prevate:
 	//코드 중복 소스 제거 및 최적화
    UPROPERTY()
		class UABAnimInstance* ABAnim;
 }
 
 void AABCharacter::PostInitializeComponents()
{
	Super::PostInitializeComponents();
	//ABAnim 초기화
	ABAnim = Cast<UABAnimInstance>(GetMesh()->GetAnimInstance());
	ABCHECK(ABAnim != nullptr);
	//OnMontageEnded 델리게이트에 함수 등록
	//AddDynamic 은 델리게이트 매크로 제공
	
	ABAnim->OnMontageEnded.AddDynamic(this, &AABCharacter::OnAttackMontageEnded);

}
void AABCharacter::Attack()
{
	if (IsAttacking)
		return;
	//Cast 부분 제거
	ABAnim->PlayAttackMontage();
	IsAttacking = true;
}

 

애니메이션 노티파이

애니메이션 재생 동안 특정 타이밍에 애님 인스턴스에 신호 보내는 기능

공격 애니메이션의 특정 타이밍에 공격 판정 할 수 있게 몽타주 시스템에 추가 

 

몽타주에 노티파이 추가 

Attack1의 8프레임에 AttackHitCheck 노티파이 추가

 

노티파이 호출 시 애님 인스턴스 클래스의 AnimNotify_노티파이명 맴버 함수 호출

애님 인스턴스 클래스의 AnimNotify_노티파이명 맴버 함수 구현

class ARENABATTLE_API UABAnimInstance : public UAnimInstance
{
	GENERATED_BODY()
    
    private:
    //몽타주에 추가한 노티파이 이벤트 함수 
	UFUNCTION()
		void AnimNotify_AttackHitCheck();
}
void UABAnimInstance::AnimNotify_AttackHitCheck()
{
	ABLOG_S(Warning);
}

 

콤보 공격의 구현

일정 시간 동안 공격 명령 시 다음 동작으로 이동하는 콤보 기능

이어 지지 않으면 IDLE 애니메이션으로 전환

몽타주 공격 섹션 분리

attack1, attack2~4 등으로 섹션 분리

4개의 섹션 확인 및 미리보기 

 

NextAttackCheck 노티파이 추가

각 공격 애니메이션 사이에 공격이 이어지는 타이밍 체크  

 

노티파이 카테고리의 Montage Tick Type 변경

Queued type : 비동기 방식으로 신호 처리, 타이밍에 민감하지 않는 사운드나 이펙트

Branching Point tpye : 해당 프레임에 즉각적으로 반응 하는 방식, 타이밍에 민감할 때 사용

 

 

ABCharacter 에서 콤보 관련 변수 추가

class ARENABATTLE_API AABCharacter : public ACharacter
{
	GENERATED_BODY()
private:
  	//콤보 시작 시 관련 속성 지정 하는 함수 추가
	void AttackStartComboState();
	void AttackEndComboState();
    
    private:
	//다음 콤보로 이동 가능 여부 
	UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = Attack, Meta = (AllowPrivateAccess = true))
		bool CanNextCombo;
	//콤보 입력 여부
	UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = Attack, Meta = (AllowPrivateAccess = true))
		bool IsComboInputOn;
	//현재 콤보 횟수
	UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = Attack, Meta = (AllowPrivateAccess = true))
		int32 CurrentCombo;
	//최대 콤보 횟수
	UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = Attack, Meta = (AllowPrivateAccess = true))
		int32 MaxCombo;
}

AABCharacter::AABCharacter()
{
	MaxCombo = 4;
	AttackEndComboState();
}
void AABCharacter::AttackStartComboState()
{
	CanNextCombo = true;
	IsComboInputOn = false;
	//MaxCombo - 1 >= CurrentCombo >= 0 시 true
	ABCHECK(FMath::IsWithinInclusive<int32>(CurrentCombo, 0, MaxCombo - 1));
	//현재 CurrentCombo 값 증가 후, 1 ~ maxcombo 값 사이로 지정
	CurrentCombo = FMath::Clamp<int32>(CurrentCombo + 1, 1, MaxCombo);

}

void AABCharacter::AttackEndComboState()
{
	IsComboInputOn = false;
	CanNextCombo = false;
	CurrentCombo = 0;
}

 

ABAnimInstance 

콤보 카운트를 받아 해당 몽타주 섹션 실행

각각의 애니메이션 노티 파이 이벤트 마다 델리게이트 함수 호출 및 사용

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once


#include "ArenaBattle.h"
#include "Animation/AnimInstance.h"
#include "ABAnimInstance.generated.h"

//멀티 캐스트 델리게이트 선언
//애니메이션의 노티파이 발생 시 델리게이트 호출
DECLARE_MULTICAST_DELEGATE(FOnNextAttackCheckDelegate);
DECLARE_MULTICAST_DELEGATE(FOnAttackHitCheckDelegate);
/**
 * 
 */
UCLASS()
class ARENABATTLE_API UABAnimInstance : public UAnimInstance
{
	GENERATED_BODY()
	
public:
	//NewSection 맞는 섹션 실행
	void JumpToAttackMontageSection(int32 NewSection);

public:
	//델리게이트 선언
	FOnNextAttackCheckDelegate OnNextAttackCheck;
	FOnAttackHitCheckDelegate OnAttackHitCheck;

private:
	//AttackHitCheck 노트파이 이벤트 실행
	UFUNCTION()
		void AnimNotify_AttackHitCheck();

	//NextAttackCheck 노트파이 함수 호출
	UFUNCTION()
		void AnimNotify_NextAttackCheck();

	//섹션 넘버를 받아 센셕 이름으로 변환 후 리턴
	FName GetAttackMontageSectionName(int32 Section);

};
void UABAnimInstance::JumpToAttackMontageSection(int32 NewSection)
{
	//현재 몽타주 실행 중 인지 체크
	ABCHECK(Montage_IsPlaying(AttackMontage));
	//해당 몽타주의 NewSection 섹션으로 실행
	Montage_JumpToSection(GetAttackMontageSectionName(NewSection), AttackMontage);
}

void UABAnimInstance::AnimNotify_AttackHitCheck()
{
	OnAttackHitCheck.Broadcast();
}

void UABAnimInstance::AnimNotify_NextAttackCheck()
{
	OnNextAttackCheck.Broadcast();
}

FName UABAnimInstance::GetAttackMontageSectionName(int32 Section)
{
	//Section 값이 1 ~ 4 범위 검사
	ABCHECK(FMath::IsWithinInclusive<int32>(Section, 1, 4), NAME_None);
	//FName : Attack1~4 리턴
	return FName(*FString::Printf(TEXT("Attack%d"), Section));
}

 

ABCharacter 플레이어 콤보 

1. 공격 명령 시 콤보가 가능 한지 파악 후 대응

2. 공격 시작 후 NextAttackCheck 타이밍 확인

3. 공격 명령이 들어오면 CanNextCombo 속성 변경

 

OnNextAttackCheck 델리게이트에 등록할 로직을 람다식 사용

void AABCharacter::PostInitializeComponents()
{

	//노티파이 이벤트에 델리게이트 함수 등록
	ABAnim->OnNextAttackCheck.AddLambda([this]() -> void {

		ABLOG(Warning, TEXT("OnNextAttackCheck"));
		CanNextCombo = false;
		//콤보 인풋 확인
		if (IsComboInputOn)
		{
			AttackStartComboState();
			ABAnim->JumpToAttackMontageSection(CurrentCombo);
		}
		});
 }
 
 void AABCharacter::Attack()
{
	//공격 상태 확인
	if (IsAttacking)
	{
		//콤보 공격 입력
		ABCHECK(FMath::IsWithinInclusive<int32>(CurrentCombo, 1, MaxCombo));
		if (CanNextCombo)
		{
			IsComboInputOn = true;
		}
	}
	else
	{
		//첫번 째 공격 입력
		ABCHECK(CurrentCombo == 0);
		AttackStartComboState();
		ABAnim->PlayAttackMontage();
		ABAnim->JumpToAttackMontageSection(CurrentCombo);
		IsAttacking = true;
	}
}

void AABCharacter::OnAttackMontageEnded(UAnimMontage* Montage, bool bInterrupted)
{
	//IsAttacking, CurrentCombo > 0 일때 동작
	ABCHECK(IsAttacking);
	ABCHECK(CurrentCombo > 0);
	IsAttacking = false;
	AttackEndComboState();
}

void AABCharacter::AttackEndComboState()
{
	IsComboInputOn = false;
	CanNextCombo = false;
	CurrentCombo = 0;
}