개발 이야기 안하는 개발자

언리얼5_2_2 : 캐릭터의 애니메이션 설정 본문

Unreal/이득우의 언리얼 프로그래밍

언리얼5_2_2 : 캐릭터의 애니메이션 설정

07e 2024. 3. 25. 10:57
반응형

캐릭터에는 사용하는 애니메이션을 지정해주는 컴포넌트가 있다.

캐릭터 매쉬에 Animation을 보면 사용할 모드와 애님 클래스를 지정할 수 있다.

 

블루프린트를 사용할 꺼고, 애님 클래스는 ABP_ABCharacter_C 를 사용한다고 적혀있다.

적혀져 있는 ABP_ABCharacter_C는 따로 제작한 클래스이다.

 

AnimInstance를 상속받은 클래스를 하나 제작한다.

그리고 이것을 토대로 블루프린트를 제작하고 지정하면 된다.

타겟할 스켈레톤과 만들었떤 Anim Instance를 지정해주고 제작하면된다.

 

캐릭터는 GetAnimInstance()를 통해 AnimInstance를 가져올 수 있고,

AnimInstance는 GetOwningActor()를 통해 캐릭터를 가져올 수 있다.

 

 

애니메이션 블루프린트는 이벤트 그래프 영역과 애님 그래프 영역으로 나뉜다.

NativeInitializeAnimation은 최초 초기화 될때, NativeUpdateAnimation은 매프레임 호출된다.

AnimInstance를 상속받아서 virtual을 override하는 코드이다.

 

NativeInit 에서는 Owner를 지정 하고, Movement를 지정할 것이다.

그리고 매 프레임 변경되는 데이터를 업데이트 할 것이다.

	...
    
보호됨 :
	 가상  무효  NativeInitializeAnimation () 재정의 ;

	가상  무효  NativeUpdateAnimation ( float DeltaSeconds) 재정의 ;
    
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, 카테고리 = 캐릭터)
	TObjectPtr< class  ACharacter > 소유자 ;

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, 카테고리 = 캐릭터)
	TObjectPtr< class  UCharacterMovementComponent > 이동 ;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, 카테고리 = 캐릭터)
	FVector 속도;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, 카테고리 = 캐릭터)
	플로트 지면 속도;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, 카테고리 = 캐릭터)
	uint8 bIsIdle : 1 ;
    
    ...
    
}
void UABAnimInstance::NativeInitializeAnimation()
{
	슈퍼::NativeInitializeAnimation();

	소유자 = Cast<ACharacter>(GetOwningActor());
	if (소유자)
	{
		이동 = 소유자->GetCharacterMovement();
	}
}

void UABAnimInstance::NativeUpdateAnimation( float DeltaSeconds)
{
	슈퍼::NativeUpdateAnimation(DeltaSeconds);

	if (이동)
	{
		속도 = 이동->속도;
		GroundSpeed ​​= Velocity.Size2D();
		bIsIdle = GroundSpeed ​​< MovingThreshold;
		bIsFalling = 이동->IsFalling();
		bIsJumping = bIsFalling & (Velocity.Z > JumpingThreshould);
	}
}

 

이후 제작한 Anim Instance 블루프린트로 가서 Show Inherited Variables를 누르면 상속받은 변수들을 볼 수 있다.

보면 제작했던 변수들이 모두 보이는 것을 확인할 수 있다.

 

 

애님 그래프는 저장된 변수로 부터 지정된 상태의 애니메이션을 재생한다.

이때 복잡해지는 상태를 State Alias 로 쉽게 분리해서 제작이 가능하다.

 

제작할 애니메이션이 복잡하기 때문에 StateMachine을 만들어서 내부에서 구현한다.

더블클릭하면 제작한 StateMachine을 볼 수 있다.

 

로코모션부터 보면 캐릭터의 이동에 따라 애니메이션을 변경할 때 보여주는 내용이다.

로코모션에 들어가서 Add State를 눌러 상태를 추가한다.

Idle 상태와 IdleWalkRun 상태를 만들어 가만히 있을때와 이동할때를 분리한다.

이동할때 움직임은 속도에 비례해서 애니메이션 속도가 변경되어야 자연스럽기 때문에 BlendSpace를 제작한다.

블렌드 스페이스는 특정 변수에 따라 여러 애니메이션의 가중치를 조절하는 기능이다.

 

가중치에 원하는 애니메이션을 올린다.

적당한 속도에 걷기, 빠른 속도에 달리기를 넣어놓고, 컨트롤키를 눌러 드래그하면 미리볼수 있다.

 

제작한 해당 블렌드 스페이스를 제작했던 로코모션 IdleWalkRun에 대입한다.

변경에 적용될 변수는 속도이다.

제작한 로코모션은 캐쉬에 저장된다.

이후, Main AnimMachine에서 이를 사용해서 애니메이션을 변경할 것이다.

 

매인 움직임에서는 아래와 같이 제작하는데, Locomotion은 방금 위에서 제작한 Cache 데이터를 사용함으로서 적용할 것이다.

 

 

점프상태 일때 애니메이션을 적용해본다.

ToJump는 State Alias이다.

이는 동시에 호출되면서 여러 상태를 보기 쉽게 만들어 준다.

지금 상태가 Locomotion이 호출되고 있는 상태라면 ToJump도 같이 호출되어 상태를 비교하게 만들어주는 것이다.

 

Jump상태로 이동할때는 제작한 Is Jumping상태일때 호출되고, Falling 상태는 Is Falling 상태일때 호출된다.

위 선택로에는 우선순위를 지정할 수 있는데, IsJumping 이면서 IsFalling인 경우 (두가지 모두 만족하는 경우)에는 우선순위가 더 높은 (숫자가 더 낮은) 조건으로 이동하게 된다.

점프애니메이션에서 Falling으로 전환되는 기준은 점프의 남은 애니메이션 0.1 기준으로 자연스럽게 넘어가게 된다.

즉, 점프 애니메이션이 0.1 비율 남으면 Falling 애니메이션으로 전환된다.

 

지금 Falling중이거나 Jumping 중이라면 ToLand를 호출하도록 지정했다.

Land로 가는 경로는 캐릭터가 땅에 닿았는지 (IsFalling이 false인지)를 체크해서 Land로 이동한다.

Land는 착지하는 애니메이션이 포함되어 있다.

계속 연산중인 Cache 데이터와 착지 애니메이션을 적절히 석어서 Land애니메이션에 포함한다.

이것도 애니메이션이 0.1정도 남으면 Locomotion 상태로 돌아가도록 로직을 구현한다.

 

 

애니메이션 몽타주를 활용해서 전투 콤보를 제작한다.

몽타주는 이미지 일부를 잘라내 한 화면에서 합성하는 회화 기법으로 애니메이션 클릭을 진행중에 잘라내어 다른 애니메이션을 합성해서 재생하는 기법으로 보면 된다.

초록칸은 각각 애니메이션이다.

보라색 칸은 각 섹션을 나누는 칸이다.

각 몽타주섹션은 분리되어 있어야 관리하기 편하기 때문에 섹션 순서를 분리한다(우측 Montage Sections에서 각 개별로 둔다)

이제 공격키를 만든다.

Input Action을 하나 제작해 주고,

해당 Input Action은 Input MappingContext에 넣어준다.

코드에서도 액션을 바인드 해준다.

 

void AABCharacterBase::ProcessComboCommand()
{
	if (현재 콤보 == 0 )
	{
		ComboActionBegin();
		반품 ;
	}

	if (!ComboTimerHandle.IsValid())
	{
		HasNextComboCommand = false ;
	}
	또 다른
	{
		HasNextComboCommand = true ;
	}
}

공격버튼을 누르면 위 메소드가 호출된다.

CurrentCombo 변수를 하나 제작하고 지금 현재 몇번째 콤보 공격이 호출되는지 체크한다.

공격 타이밍에 맞춰서 다음 공격버튼을 눌러야 함으로 해당 타이머를 체크한다.

타이머가 돌고 있을때, 공격키를 눌렀다면 HasNextComboCommand가 true로 변경된다.

 

void AABCharacterBase::ComboActionBegin()
{
	// 콤보 상태 
	CurrentCombo = 1 ;

	// 움직임 설정
	GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_None);

	// 애니메이션 설정 
	const  float AttackSpeedRate = 1.0f ;
	UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
	AnimInstance->Montage_Play(ComboActionMontage, AttackSpeedRate);

	FOnMontageEnded EndDelegate;
	EndDelegate.BindUObject( this , &AABCharacterBase::ComboActionEnd);
	AnimInstance->Montage_SetEndDelegate(EndDelegate, ComboActionMontage);

	ComboTimerHandle.Invalidate();
	SetComboCheckTimer();
}

콤보없이 최초 공격인 경우

제작한 몽타주를 실행한다. (Montage_Play())

각 몽타주에는 끝부분을 체크할 수 있다. (FOnMonageEnded)

끝부분에 왔는지 델리게이트를 바인드해서 끝부분에 왔다면 ComoActionEnd()를 호출하도록 한다.

 

void AABCharacterBase::ComboActionEnd(UAnimMontage* TargetMontage, bool IsProperlyEnded)
{
	verify(CurrentCombo != 0 );
	현재콤보 = 0 ;
	GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_Walking);
}

끝부분에 왔다면 콤보를 실행할 타이밍을 놓친것이다.

초기화해주도록 한다.

 

void AABCharacterBase::SetComboCheckTimer()
{
	int32 ComboIndex = CurrentCombo - 1 ;
	verify(ComboActionData->EffectiveFrameCount.IsValidIndex(ComboIndex));

	const  float AttackSpeedRate = 1.0f ;
	 float ComboEffectiveTime = (ComboActionData->EffectiveFrameCount[ComboIndex] / ComboActionData->FrameRate) / AttackSpeedRate;
	 if (ComboEffectiveTime > 0.0f )
	{
		GetWorld()->GetTimerManager().SetTimer(ComboTimerHandle, this , &AABCharacterBase::ComboCheck, ComboEffectiveTime, false );
	}
}

활성화한 타이머이다.

ComboEffectiveTime은 활성화 되는 시간을 체크한 것이다.

	UPROPERTY(EditAnywhere, BlueprintReadOnly, 범주 = 공격, 메타 = (AllowPrivateAccess = "true" ))
	TObjectPtr< class  UABComboActionData > ComboActionData ;

ABComboActionData는 UPrimaryDataAsset를 상속받아 제작되었다.

ComboActionData에는 각 공격마다 콤보 가능 타이밍(프레임)을 저장해 두었고, 이를 수식으로 가져와 시간으로 변경했다.

 

각 시간이 되었을때 입력이 있었는지를 체크하는 타이머이다.

즉, 위 데이터 테이블에 있는 프레임이 되었을때 유저가 공격키를 입력했었는지 체크하는 것이다. (키 입력 예약)

타이머가 되기 전에 유저가 공격키를 눌렀었다면 ComboCheck를 호출한다.

 

void AABCharacterBase::ComboCheck()
{
	ComboTimerHandle.Invalidate();
	if (HasNextComboCommand)
	{
		UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();

		CurrentCombo = FMath::Clamp(CurrentCombo + 1 , 1 , ComboActionData->MaxComboCount);
		FName NextSection = *FString::Printf(TEXT( "%s%d" ), *ComboActionData->MontageSectionNamePrefix, CurrentCombo);
		AnimInstance->Montage_JumpToSection(NextSection, ComboActionMontage);
		SetComboCheckTimer();
		HasNextComboCommand = false ;
	}
}

다음 공격 콤보로 섹션을 이동해서 플레이하고 초기화이후 다시 타이머를 가동한다 (Montage_JumpToSection())

이때 사용되는 HasNextComboCommand는 맨 처음 공격키를 눌렀을때 타이머가 있는지 체크하던 메소드인 processComboCommand()에 있다.

 

애니메이션에서 몽타주를 추가해 주어야 한다. (Add Slot)

공격 몽타주의 우선순위가 높은 것이다.

여기서 DefaultSlot인 이유는 제작한 몽타주의 그룹이름이 Default Slot이기 때문이다.

 

캐릭터 블루프린트로 넘어가 제작한 몽타주를 넣는다.

 

 

 

 

이번엔 공격 판정을 제작한다.

충돌체들끼리의 충돌 판정을 월드가 제공한다.

충돌 판정을 체크하기 위한 채널을 제작한다. Ignore로 지정함으로써 모든 충돌체가 아닌 지정한 충돌체에만 반응하도록 한다.

 

ABCapsule을 제작하는데 Trace에서 ABAction을 block함으로서 이제 충돌체크할 수 있다.

Overlap은 충돌 판정이고 Block은 막는내용이다.

ABTrigger도 제작한다.

제작한 위 내용들은 모두 preset이다. 액터에게서 직접 수정도 가능하다 (custom)

제작한 프리셋말고도 Custom으로 여기서 직접 수정할 수 있다.

제작한 프리셋들은 DefaultEngine.ini에 모두 저장된다.

코드에서 이 채널을 사용하기 위해선 ECC_GameTraceChannel1 을 사용하는 것이 더 수월하다.

 

공격하는 일정 부분에서부터 공격 가능 판정을 내릴 것이다.

그 부분을 지정하기 위해서 몽타주에선 Notify를 사용한다.

이 Notify를 스크립트로 제작하고 (AnimNotify를 상속) 몽타주에 추가한다.

void UAnimNotify_AttackHitCheck::Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, const FAnimNotifyEventReference& EventReference)
{
	Super::Notify(MeshComp, Animation, EventReference);

	if (MeshComp)
	{
		IABAnimationAttackInterface* AttackPawn = Cast<IABAnimationAttackInterface>(MeshComp->GetOwner());
		if (AttackPawn)
		{
			AttackPawn->AttackHitCheck();
		}
	}
}

몽타주 플레이가 노티파이까지 도달할 경우 해당 내용이 호출된다.

위 메소드는 override 받은 메소드이다.

 

ABAnimationAttackInterface는 캐릭터에 구현이 된다. 이는 나중에 적군 캐릭터에게도 사용하기 위해서 인터페이스로 제작했다.

//다른 스크립트
#define CPROFILE_ABCAPSULE TEXT("ABCapsule")
#define CPROFILE_ABTRIGGER TEXT("ABTrigger")
#define CCHANNEL_ABACTION ECC_GameTraceChannel1

...

void AABCharacterBase::AttackHitCheck()
{
	FHitResult OutHitResult;
	FCollisionQueryParams Params(SCENE_QUERY_STAT(Attack), false, this);

	const float AttackRange = 40.0f;
	const float AttackRadius = 50.0f;
	const float AttackDamage = 30.0f;
	const FVector Start = GetActorLocation() + GetActorForwardVector() * GetCapsuleComponent()->GetScaledCapsuleRadius();
	const FVector End = Start + GetActorForwardVector() * AttackRange;

	bool HitDetected = GetWorld()->SweepSingleByChannel(OutHitResult, Start, End, FQuat::Identity, CCHANNEL_ABACTION, FCollisionShape::MakeSphere(AttackRadius), Params);
	if (HitDetected)
	{
		FDamageEvent DamageEvent;
		OutHitResult.GetActor()->TakeDamage(AttackDamage, DamageEvent, GetController(), this);
	}

#if ENABLE_DRAW_DEBUG

	FVector CapsuleOrigin = Start + (End - Start) * 0.5f;
	float CapsuleHalfHeight = AttackRange * 0.5f;
	FColor DrawColor = HitDetected ? FColor::Green : FColor::Red;

	DrawDebugCapsule(GetWorld(), CapsuleOrigin, CapsuleHalfHeight, AttackRadius, FRotationMatrix::MakeFromZ(GetActorForwardVector()).ToQuat(), DrawColor, false, 5.0f);

#endif
}

콜라이더 충돌 방식에 있어서 함수를 선택해야 한다.

1. 처리방법(LineTrace, Sweep, OverLap)

2. 대상 (Test_무언가 감지되었는지 테스트, Single_감지된 단일 물체 정보 반환, Multi_감지된 모든 물체 정보 배열로)

3. 처리 설정 (ByChannel_채널 정보를 사용, ByObjectType_물체 지정된 물리 타입 정보, ByProfile_프로필 정보 사용)

위 3개로 {처리방법}{대상}{처리설정} 로 함수를 사용하면 된다.

 

위에선 SweepSingleByChannel을 사용해서 충돌 체크를 한다.

 

사용한 마지막 Params는 충돌체에 대한 정보를 가져오기위해서 정보를 담는다고 보면 된다.

SCENE_QUERY_STAT(Attack) 은 언리얼 엔진이 정보를 얻어오기 편하게 하기 위해 넣어둔 정보이다.

Attack은 태그이다.

 

이후 디버그를 드로잉해서 공격 하는 범위를 시각화 했다.

데미지를 받으면 초록색으로 변경될 것이다.

float AABCharacterBase::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
	Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);

	SetDead();

	return DamageAmount;
}

데미지를 받아 죽는 로직이다.

TakeDamage는 Pawn이 제작한 메소드라서 Override한다.

 

void AABCharacterBase::SetDead()
{
	GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_None);
	PlayDeadAnimation();
	SetActorEnableCollision(false);
}

void AABCharacterBase::PlayDeadAnimation()
{
	UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
	AnimInstance->StopAllMontages(0.0f);
	AnimInstance->Montage_Play(DeadMontage, 1.0f);
}

죽으면 움직임을 멈추고 콜리전을 비활성화 한다.

이후 죽은 애니메이션을 실행한다.

죽는 애니메이션은 새로운 몽타주를 제작해서 Slot의 이름을 변경했다.

죽게 된다면 Dead Mongtage를 실행하도록 한다.

 

이제 공격을 받는 NPC를 제작한다.

void AABCharacterNonPlayer::SetDead()
{
	Super::SetDead();

	FTimerHandle DeadTimerHandle;
	GetWorld()->GetTimerManager().SetTimer(DeadTimerHandle, FTimerDelegate::CreateLambda(
		[&]()
		{
			Destroy();
		}
	), DeadEventDelayTime, false);
}

NPC캐릭터가 죽으면 일정시간 이후 캐릭터가 없어지도록 람다 타이머를 추가했다.

 

공격을 진행하며 중간에 호출된 Notify를 통해 콜라이더 활성화 로직을 구현했다.

빨간색 Draw Debug Line을 이용해 빗맞은 것을 표현된다.

초록색은 맞은 내용이고 적 NPC가 맞아 바로 죽는 몽타주 애니메이션이 실행되었다.

5초뒤 없어지는것을 볼 수 있다.

 

 

 

 

 

 

 

 

 

 

 

 

 

반응형