프로젝트 세팅 - 콜리전에서 오브젝트 채널, 트레이스 채널 및 프리셋을 지정해 줄 수 있다

오브젝트 채널: 콜리전 영역에 지정하는 콜리전 채널 (WorldStatic, WorldDynamic, Pawn, PhysicsBody 등)

트레이스 채널: 어떤 행동에 설정하는 콜리전 채널 (Visibility, Camera)

 

 

// MyAnimInstance.h
DECLARE_MULTICAST_DELEGATE(FOnAttackHit); // 델리게이트 타입 생성

UCLASS()
class TESTUNREALENGINE_API UMyAnimInstance : public UAnimInstance
{
public: // private으로 은닉해야 하지만 번거로우므로 public으로 열어둠
    FOnAttackHit OnAttackHit;
}

// MyAnimInstance.cpp
void UMyAnimInstance::AnimNotify_AttackHit()
{
    UE_LOG(LogTemp, Log, TEXT("AnimNotify_AttackHit"));
    // 노티파이 발생시 OnAttackHit을 구독하는 객체들에게 메시지를 뿌림
    OnAttackHit.Broadcast();
}

// ---------------

// MyCharacter.h
protected:
    // 생명주기상 BeginPlay보다는 모든 컴포넌트들이 초기화가 된 이후에
    // 서로 연동시켜주는 부분을 넣는것이 맞기 때문에 해당 함수 오버라이딩
    virtual void PostInitializeComponents() override;
public:
    void AttackCheck();

// MyCharacter.cpp
void AMyCharacter::PostInitializeComponents()
{
    Super::PostInitializeComponents();

    // 기존에 BeginPlay에 있던 내용들도 옮겨준다
    AnimInstance = Cast<UMyAnimInstance>(GetMesh()->GetAnimInstance());
    if (AnimInstance)
    {
        AnimInstance->OnMontageEnded.AddDynamic(this, &AMyCharacter::OnAttackMontageEnded);
        //OnAttackHit을 구독하여 Broadcast를 받을 때 실행할 메소드 등록
        AnimInstance->OnAttackHit.AddUObject(this, &AMyCharacter::AttackCheck);
    }
}

void AMyCharacter::AttackCheck()
{
	FHitResult HitResult;
	FCollisionQueryParams Params(NAME_None, false, this);

	float AttackRange = 100.f;
	float AttackRadius = 50.f;

	bool bResult = GetWorld()->SweepSingleByChannel(
		OUT HitResult, GetActorLocation(),
		GetActorLocation() + GetActorForwardVector() * AttackRange,
		FQuat::Identity,
		ECollisionChannel::ECC_GameTraceChannel2,
		FCollisionShape::MakeSphere(AttackRadius),
		Params);

	if (bResult && HitResult.Actor.IsValid())
	{
		UE_LOG(LogTemp, Log, TEXT("Hit Actor : %s"), *HitResult.Actor->GetName());
	}
}

 

노티파이가 발생할 때마다 OnAttackHit에 등록된(구독하는) MyCharacter의 AttackCheck가 실행되는데, 내부적으로 충돌 체크를 연산하여 실제로 충돌중일때만 로그를 띄우게 된다.

 

// MyCharacter.cpp
#include "DrawDebugHelpers.h"

FVector Vec = GetActorForwardVector() * AttackRange;
FVector Center = GetActorLocation() + Vec * 0.5f;
float HalfHeight = AttackRange * 0.5f + AttackRadius;
FQuat Rotation = FRotationMatrix::MakeFromZ(Vec).ToQuat();
FColor DrawColor;
if (bResult)
    DrawColor = FColor::Green;
else
    DrawColor = FColor::Red;

DrawDebugCapsule(GetWorld(), Center, HalfHeight, AttackRadius,
    Rotation, DrawColor, false, 2.f);

 

AttackCheck에 해당 내용을 추가로 넣어주면 시각적으로 공격의 실패, 성공 여부를 확인할 수 있다.

 

충돌 처리는 온갖 부분에서 다 쓰이므로 매우 중요한 내용이다.

애니메이션을 섞는다.

 

 

생성시 스켈레톤을 선택한다.

 

 

 

각각의 애니메이션을 축에 맞게 배치 후, Shift를 눌러서 이동시켜보면 배치한 애니메이션들이 적절하게 섞이는것을 확인할 수 있다.

 

 

// MyAnimInstance.h
private:
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Pawn, Meta = (AllowPrivateAccess = true))
    float Horizontal;

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Pawn, Meta = (AllowPrivateAccess = true))
    float Vertical;

// ------------
// MyAnimInstance.cpp
void UMyAnimInstance::NativeUpdateAnimation(float DeltaSeconds)
{
		// .. 생략
		auto Character = Cast<AMyCharacter>(Pawn); // ACharacter에서 AMyCharacter로 변경
		if (Character)
		{
			IsFalling = Character->GetMovementComponent()->IsFalling();

			Vertical = Character->UpDownValue; // 추가된 코드
			Horizontal = Character->LeftRightValue; // 추가된 코드
		}
}

// ------------
// MyCharacter.h
public: // 따로 Get 메소드를 만들기 귀찮아서 public으로 열어둠
	UPROPERTY()
	float UpDownValue = 0;

	UPROPERTY()
	float LeftRightValue = 0;


// ------------

// MyCharacter.cpp
void AMyCharacter::UpDown(float Value)
{
	//UE_LOG(LogTemp, Warning, TEXT("UpDown %f"), Value)

	UpDownValue = Value;
	AddMovementInput(GetActorForwardVector(), Value);
}

void AMyCharacter::LeftRight(float Value)
{
	//UE_LOG(LogTemp, Warning, TEXT("LeftRight %f"), Value)

	LeftRightValue = Value;
	AddMovementInput(GetActorRightVector(), Value);
}

 

 

블렌드 스페이스, 코드 추가, 스테이트 수정까지 완료하고 나면 이동 애니메이션이 방향에 따라 적절하게 섞인것을 볼 수 있다.

ADAD 연타를 하거나 대각선끼리 이동시 부자연스러움이 느껴지는데 이부분은 Interpolation Time을 0.1정도 줘서 자연스럽게 만들어 줄 수 있다.

 

 

애니메이션 처리는 서버에 넘길 필요없이 클라에서 처리하면 되는 기술이다.

 

참고로 블렌드 스페이스 1D는 축이 하나이다.

애니메이션 재생중 특정 시점이 되었을 때 코드에 알람(노티파이)을 주는 개념

애니메이션, 몽타주 둘다 설정 가능하다. 유니티에도 거의 동일한 기능이 있다.

 

 

노티파이 설정

 

// MyAnimInstance.h
class TESTUNREALENGINE_API UMyAnimInstance : public UAnimInstance
{
	GENERATED_BODY()

private:
	UFUNCTION()
	void AnimNotify_AttackHit();
}

// -------

// MyAnimInstance.cpp
void UMyAnimInstance::AnimNotify_AttackHit()
{
	UE_LOG(LogTemp, Log, TEXT("AnimNotify_AttackHit"));
}

 

코드에서도 추가해 주는데 메소드명을 막 짓는것이 아니라 AnimNotify_"노티파이명" 으로 지정해 주어야 한다.

 

 

 

실행 후 공격 시 애니메이션의 노티파이 때마다 한 번씩 호출되는 것을 확인할 수 있다.

 

 

 

 

 

 

지금까지는 한번의 공격으로 평타 세번이 연속해서 나갔지만 각각의 모션을 끊어서 재생하고 싶다면 몽타주 섹션으로 구분하고 링크를 끊어주면 된다. 최상단이 몽타주 섹션이고 우클릭으로 추가할 수 있다.

 

// MyAnimInstance.h
class TESTUNREALENGINE_API UMyAnimInstance : public UAnimInstance
{
// .. 생략
public:
	void JumpToSection(int32 SectionIndex);

	FName GetAttackMontageName(int32 SectionIndex);
};

// ----------

// MyAnimInstance.cpp
void UMyAnimInstance::JumpToSection(int32 SectionIndex)
{
	FName Name = GetAttackMontageName(SectionIndex); // 섹션 이름 변환
	Montage_JumpToSection(Name, AttackMontage); // 해당 섹션 재생
}

FName UMyAnimInstance::GetAttackMontageName(int32 SectionIndex)
{
	// 언리얼에서 사용하는 이름 타입이라고 생각됨
	return FName(*FString::Printf(TEXT("Attack%d"), SectionIndex));
}

// ---------

// MyCharacter.h

// ---------

// MyCharacter.cpp
void AMyCharacter::Attack()
{
	if (IsAttacking)
		return;

	AnimInstance->PlayAttackMontage();

	AnimInstance->JumpToSection(AttackIndex); // 공격시마다 0, 1, 2, 0.. 을 넘겨줌
	AttackIndex = (AttackIndex + 1) % 3;

	IsAttacking = true;
}

 

구분된 섹션을 코드로 제어할 수 있다.

여태까지 InputComponent를 받는 방식이 델리게이트 라고 볼수 있다. (바인딩)

'어떤 행동이 끝났을 때, ~~을 호출해줘' -> 콜백처리

 

// MyCharacter.h
class TESTUNREALENGINE_API AMyCharacter : public ACharacter
{
	
public:	
	// .. 생략
	UFUNCTION()
	void OnAttackMontageEnded(UAnimMontage* Montage, bool bInterrupted);

private:
	// .. 생략
    UPROPERTY(VisibleAnywhere)
	bool IsAttacking = false;
    
	UPROPERTY()
	class UMyAnimInstance* AnimInstance;
};

// --------

// MyCharacter.cpp
void AMyCharacter::BeginPlay()
{
	Super::BeginPlay();

	AnimInstance = Cast<UMyAnimInstance>(GetMesh()->GetAnimInstance());
    // 몽타주 재생이 끝나면 OnAttackMontageEnded를 실행해 달라고 요청
	AnimInstance->OnMontageEnded.AddDynamic(this, &AMyCharacter::OnAttackMontageEnded);
}

void AMyCharacter::Attack()
{
	if (IsAttacking)
		return;

	AnimInstance->PlayAttackMontage();

	IsAttacking = true;
}

// 델리게이트에 등록된 메소드
void AMyCharacter::OnAttackMontageEnded(UAnimMontage* Montage, bool bInterrupted)
{
	IsAttacking = false;
}

// ----------

// MyAnimInstance.cpp
void UMyAnimInstance::PlayAttackMontage()
{
	// MyCharacter에서 조건을 모두 검사하기 때문에 이제 여기서 검사할 필요는 없다
	Montage_Play(AttackMontage, 1.f);
}

 

이런 패턴이 정말 자주 일어난다.

애니메이션 편집기라고 보면 된다.

 

 

메시를 더블클릭해서 들어온 후 해당 메뉴에서 애님 몽타주를 만들면 편집 창으로 들어올 수 있다.

애셋 브라우저에서 애니메이션 몇 개를 끌어오면 연속되어 재생되는것을 확인할 수 있다.

원한다면 애니메이션 세그먼트에서 재생 시간, 재생 속도, 루프 횟수를 조절할 수 있다.

 

 

점프같은 경우는 AnimInstance에서 캐릭터의 상태를 매 틱 체크하는 방식이었지만 반대로 공격은 캐릭터에서 발생 시 AnimInstance에 접근해서 Attack Montage를 실행 시킬 것이다.

 

// MyAnimInstance.h
class TESTUNREALENGINE_API UMyAnimInstance : public UAnimInstance
{
	GENERATED_BODY()

public:
	UMyAnimInstance(); // 생성자 추가

	virtual void NativeUpdateAnimation(float DeltaSeconds) override;

	void PlayAttackMontage(); // 메소드 추가

private:
	// .. 생략
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Pawn, Meta = (AllowPrivateAccess = true))
	UAnimMontage* AttackMontage; // Montage 추가
};

// ----------

// MyAnimInstance.cpp
UMyAnimInstance::UMyAnimInstance()
{
	static ConstructorHelpers::FObjectFinder<UAnimMontage> AM(TEXT("AnimMontage'/Game/Animations/Greystone_Skeleton_Montage.Greystone_Skeleton_Montage'"));

	if (AM.Succeeded())
	{
		AttackMontage = AM.Object;
	}
}

void UMyAnimInstance::PlayAttackMontage() // 실제 Montage 실행부
{
	if (!Montage_IsPlaying(AttackMontage))
	{
		Montage_Play(AttackMontage, 1.f);
	}
}

// ------------

// MyCharacter.cpp
void AMyCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	// .. 생략
	PlayerInputComponent->BindAction(TEXT("Attack"), EInputEvent::IE_Pressed, this, &AMyCharacter::Attack);
}

void AMyCharacter::Attack()
{
		// MyAnimInstance에 접근
	auto AnimInstance = Cast<UMyAnimInstance>(GetMesh()->GetAnimInstance());

	if (AnimInstance)
	{
    		// 메소드 직접 실행
		AnimInstance->PlayAttackMontage();
	}
}

 

Attack을 바인딩 했으니 당연히 입력에서 Attack을 마우스 왼클릭으로 설정해주면 된다.

 

 

코드 및 AnimGraph를 완성하면 좌클릭시 설정한 애니메이션 몽타주가 재생된다.

+ Recent posts