언리얼 엔진 5/개발 일지

[UE5] 블루아카이브 TPS게임 개발일지 (4) - EActionState 추가, 근접 공격 추가

ciel45 2023. 12. 3. 13:45

캐릭터가 어떤 무기를 들고있는지를 나타내는 ECharacterState에 더해, 현재 어떤 액션을 취하고 있는지를 나타내는 EActionState 열거형을 만들었다. 현재 EActionState 값에 따라 플레이어가 할 수 있는 행동을 정해줄 수 있다.

예를 들어, 캐릭터가 구르는 중에는 조준을 할 수 없고, 근접공격 중에는 조준을 할 수 없도록 해야 한다.

 

EActionState: 현재 캐릭터의 액션 상태를 나타냄

#pragma once

UENUM(BlueprintType)
enum class ECharacterState : uint8
{
	ECS_Unarmed UMETA(DisplayName = "Unarmed"),
	ECS_ArmedWithPistol UMETA(DisplayName = "Armed With Pistol"),
	ECS_ArmedWithRifle UMETA(DisplayName = "Armed With Rifle")
};

UENUM(BlueprintType)
enum class EActionState : uint8
{
	EAS_Neutral UMETA(DisplayName = "Neutral"),
	EAS_Attacking UMETA(DisplayName = "Attacking"),
	EAS_Rolling UMETA(DisplayName = "Rolling"),
	EAS_Aiming UMETA(DisplayName = "Aiming"),
};

 

 

UENUM(BlueprintType)은 이 enum을 리플렉션 시스템에 포함시키며, 블루프린트에서 열거형의 형태로 사용할 수 있게 한다는 의미이다.

 

우선 중립, 공격(근접 공격), 구르기, 조준의 4가지 상태를 만들었다. 개발이 진행됨에 따라 더 추가될 수 있다.

그리고 캐릭터의 조준, 중립 상태를 IsAiming 플래그가 아닌 EActionState를 토대로 전환하도록 하였다.

 

void AKannaCharacter::Aim()
{
	if(CharacterState == ECharacterState::ECS_Unarmed) return; //비무장 상태일 시 Action State 바꾸지 않음, 카메라만 줌 인

	ActionState = EActionState::EAS_Aiming;
}

void AKannaCharacter::ReleaseAim()
{
	ActionState = EActionState::EAS_Neutral;
}

Aim과 ReleaseAim은 각각 마우스 오른쪽을 홀드할 때, 뗄 때의 콜백 함수이다. 함수는 실행되면서 캐릭터의 액션 상태를 전환한다.

 

 

애니메이션 블루프린트에서 IsAiming 플래그를 삭제하고, 캐릭터의 EActionState를 읽어서 사용하도록 변경하였다.

 

 

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


#include "Character/KannaAnimInstance.h"
#include "Character/KannaCharacter.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "Kismet/KismetMathLibrary.h"

void UKannaAnimInstance::NativeInitializeAnimation()
{
	Super::NativeInitializeAnimation();

	KannaCharacter = Cast<AKannaCharacter>(TryGetPawnOwner());
	if (KannaCharacter)
	{
		KannaCharacterMovement = KannaCharacter->GetCharacterMovement();
	}
}

void UKannaAnimInstance::NativeUpdateAnimation(float DeltaTime)
{
	Super::NativeUpdateAnimation(DeltaTime);

	if (KannaCharacterMovement)
	{
		GroundSpeed = UKismetMathLibrary::VSizeXY(KannaCharacterMovement->Velocity);
		IsFalling = KannaCharacterMovement->IsFalling();
		CharacterState = KannaCharacter->GetCharacterState();
		ActionState = KannaCharacter->GetActionState();
	}
}

애니메이션 블루프린트의 부모 C++ 클래스. 매 틱마다 CharacterState, ActionState를 불러오고 있다.

 

 

 

 

 

이제 근접공격 기능을 구현할 차례이다. 현재 공격 모션은 두 가지가 있다.

AS_Punch
AS_Kick

 

 

 

 

이 두 애니메이션 시퀀스를 합쳐, AM_Attack이라는 애니메이션 몽타주를 만들었다.

 

 

 

 

애니메이션 몽타주를 이용하면 여러 애니메이션 시퀀스를 하나로 합친 에셋을 만들어, C++ 코드나 블루프린트 노드를 통해 재생을 컨트롤할 수 있다.

 

 

 

자세한 내용은 https://docs.unrealengine.com/5.3/ko/animation-montage-in-unreal-engine/

 

애니메이션 몽타주

블루프린트를 사용하여 애니메이션을 단일 에셋으로 결합하고 재생을 제어하도록 지원하는 애니메이션 에셋인 애니메이션 몽타주를 살펴봅니다.

docs.unrealengine.com

 

 

 

애니메이션 몽타주 AM_Attack. 펀치와 킥이 하나로 합쳐져 있다. 각각의 애니메이션에 Attack1, Attack2라는 섹션 이름을 붙였다.

 

 

위의 이미지에서 AttackEnd라는 하얀 라벨은 Notify로, 애니메이션 블루프린트의 이벤트 그래프에서 Event 노드로서 사용할 수 있다.

 

언리얼의 Notify는 유니티의 Animation Event와 동일하다.

 

먼저 KannaCharacter.h에 UAnimMontage 포인터 타입 변수를 선언하고, 위에서 만든 몽타주를 할당했다.

 

다음은 이 애니메이션 몽타주를 재생하는 코드이다.

void AKannaCharacter::PlayAttackMontage()
{
	UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();

	if (AnimInstance && AttackMontage) //null check
	{
		AnimInstance->Montage_Play(AttackMontage);
		const int32 RandNum = FMath::RandRange(1, 2);
		FName SectionName = FName();

		switch (RandNum)
		{
		case 1:
			SectionName = FName("Attack1");
			break;
		case 2:
			SectionName = FName("Attack2");
			break;
		default:
			break;
		}
		AnimInstance->Montage_JumpToSection(SectionName, AttackMontage);
	}
}

먼저 Mesh를 통해 애니메이션 인스턴스를 참조해온다.

 

애니메이션 인스턴스와 AttackMontage의 널 체크 후, AnimInstance->Montage_Play를 통해 몽타주를 재생한다.

 

유니티의 Animation.Play랑 같은 기능이지만, 언리얼에서는 자동으로 전후 애니메이션과 블렌딩이 되어 더 자연스럽다.

 

RandNum는 1~2 범위 난수이다. switch문을 통해 재생할 섹션의 이름을 정한다.

 

AnimInstance->Montage_JumpToSection(SectionName, AttackMontage)은 함수 이름 그대로 애니메이션 몽타주 안에서 파라미터로 받은 섹션으로 점프한다. 

 

결과적으로 펀치와 킥 중 하나가 랜덤으로 재생된다.

 

 

 

다음은 PlayAttackMontage()를 호출하는 코드이다.

void AKannaCharacter::Attack()
{
	if (ActionState != EActionState::EAS_Neutral || CharacterState == ECharacterState::ECS_Unarmed) return;

	//GetCharacterMovement()->DisableMovement(); - 수정(2023.12.20)
	Controller->SetIgnoreMoveInput(true); //공격 중에는 움직이지 않도록

	PlayAttackMontage();
	ActionState = EActionState::EAS_Attacking;
}

void AKannaCharacter::AttackEnd()
{
	ActionState = EActionState::EAS_Neutral;
	//GetCharacterMovement()->MovementMode = EMovementMode::MOVE_Walking;  - 수정(2023.12.20)
	Controller->SetIgnoreMoveInput(false); //공격 완료 후 Movement Mode 초기화 
}

Attack(), AttackEnd() 함수이다. Attack()은 근접공격 인풋의 콜백 함수로 바인딩되어있고, AttackEnd()는 위의 애니메이션 몽타주의 Notify에 바인딩되어있다.

 

공격은 중립상태일 때에만 가능하며, 함수 안에서 자동으로 캐릭터의 EActionState를 전환한다.

 

공격을 시작하면 공격이 끝날 때 까지 EActionState가 EAS_Attacking으로 유지되어, 빠르게 또 다시 근접공격 키를 눌러도 Attack()의 if문에서 걸러진다.

 

만약 이렇게 하지 않으면, 근접공격 키를 연타하면 계속 근접공격이 시작되어 칸나가 부자연스럽게 덜덜거리는 것을 볼 수 있다.

 

 

 

또한, 근접공격중에는 캐릭터의 움직임을 없애주어야 한다. 그렇지 않으면 공격하면서 미끄러지는 것 처럼 보이게 된다.

 

GetCharacterMovement()의 멤버함수로 DisableMovement()는 있는데, EnableMovement()는 없어 의아했었다.

검색 결과 실제로 해당하는 함수는 따로 없는 것이 맞고, MovementMode를 다시 할당해주면 된다고 한다.

 

따라서, 사실상의 기본값인 EMovementMode::MOVE_Walking을 할당해주었다.

(나머지는 Flying, Falling, Swimming, ... 이런거다.)

 

 

 

2023.12.20 수정)

EMovement를 한번 None으로 했다가 바꿀 경우 근접 공격 후 구르기 시 캐릭터가 안움직이는 버그가 있었다.

따라서 MovementMode를 만지는 대신 Controller->SetIgnoreMoveInput 함수를 사용하는 것으로 바꿨다.

 

 

KannaCharacter.cpp에서 만들어둔 AttackEnd 함수도, 애니메이션 블루프린트 이벤트 그래프에서 Notify에 잘 바인딩해 두었다.

 

 

 

결과:

https://www.youtube.com/watch?v=ATc_mRC9rP8