개요

  • 캐릭터는 마우스 우클릭을 통해 조준을, 좌클릭을 통해 던지는 동작을 수행한다.
  • 던지는 동작 수행 중에는 조준 설정/해제가 불가능하다.
  • 조준 시간에 따라 차징이 가능하다. 차징 정도에 따라 던지는 거리에 가중치가 곱해진다.
  • AnimNotify를 사용하여 몽타주 재생이 끝날 때 지정 함수를 호출한다.
  • 각각의 동작은 몽타주를 사용한다.
  • 코드 조각만 가져왔다. 전체 코드는 Github-Explosion 둘러보기.

1) 몽타주 생성 및 호출

몽타주 생성

Untitled

ParagonDrongo 에셋의 애니메이션을 사용하여 조준 / 던지기-재장전의 두 몽타주를 생성했다.

  • Ability_Grenade_Throw(던지는 동작)
  • Ablility_Grenade_Prep(허리에서 꺼내는 동작)
  • Idle_Grenade(조준 상태)

위 3개의 애니메이션은 리타게팅 이후 시퀸서를 사용해 좌우 반전을 진행했다.

AnimationBlueprint에서 몽타주 세팅하기(슬롯, 상하체 분리)

몽타주는 슬롯을 사용하여 일회성 애니메이션을 재생한다. 이를 위해서 슬롯의 생성, 지정 작업을 진행했다.

  1. 몽타주→슬롯 매니저→새 슬롯 생성

    Untitled

    상체 애니메이션만을 적용할 예정이기에 ‘UpperBody’로 이름지었다.

  2. 기본 슬롯을 새로 생성한 슬롯으로 변경

    Untitled

    폭탄을 던지기-재장전, 조준 동작 모두 UpperBody 슬롯에 속한다.

  3. ABP에 슬롯 추가

    Untitled

    추가한 슬롯 노드의 디테일 패널에서 앞서 만든 슬롯인 ‘UpperBody’를 선택했다.

    Untitled

  4. 상하체 구분 - Layered blend per bond

    몽타주가 재생되었을 때, 상체는 몽타주를 재생하고 하체는 일반 동작을 수행한다. 이를 위한 ‘Layered blend per bond’ 노드이다. 해당 노드는 특정 본을 기준으로 애니메이션을 블랜딩 한다. 기준이 되는 본은 spine_01(가장 아래 척추)를 지정했다.

    Untitled

C++에서 몽타주 재생

AnimInstance->Montage_Play(AimingMontage);
AnimInstance->Montage_Stop(0.3f, AimingMontage);

간단하게 재생과 정지가 가능하다! AnimInstance.h에는 몽타주와 관련된 다양한 함수가 존재하므로 필요한 함수를 찾아서 사용하자.

2) AnimNotify 사용하기

AnimNotify는 무엇인가?

Untitled

애니메이션 시퀀스에 동기화하여 특정 타이밍에 반복적으로 이벤트를 생성하는 방법이다. 발이 땅에 닿을 때마다 소리나 이펙트를 재생하는 것이 대표적인 예시이다. 기본적인 노티파이 몇 개는 언리얼에서 제공하지만, 원하는 기능이 따로 있다면 AnimNotify를 상속받아서 동작을 구현할 수 있다. 생성한 노티파이는 몽타주에서 사용 가능하다.

나는 3개의 노티파이를 사용했다.

  • Throwing
    • 폭탄을 던질 때, 손과 폭탄이 분리되는 시점에 호출.
    • 폭탄을 캐릭터의 소켓에서 분리하여 특정 방향으로 던진다.
    • 폭탄이 캐릭터에 부착되어 있을 때는 물리 작용을 하지 않지만, 이 시점부터 물리 작용을 시작한다.
  • Reloading
    • 폭탄을 스폰하고 캐릭터의 소켓에 부착한다.
  • Throwing_End
    • 폭탄을 던지는 몽타주가 재생중일 때, 조준의 변경이나 새롭게 폭탄을 던지는 동작을 금지한다. 이는 bool 변수를 사용하여 제어한다.
    • 몽타주의 재생이 끝났을 때, 해당 변수를 제어하고 조준 여부를 확인하여 조준 몽타주를 재생한다.

이들은 어떻게 동작하는가?

Untitled

Throwing, Reloading은 대리자를 사용한다. 이벤트가 발생했을 때 캐릭터 베이스의 대리자를 실행시키고, 해당 대리자에 바인딩 된 폭탄과 캐릭터 플레이어의 함수들이 실행된다. 그저 알리는 역할에만 충실하면 되기에 대리자를 사용했다.

Throwing_End는 AnimInstance에 정의된 대리자를 사용한다. 몽타주 실행이 끝나기 전에 바인딩 된 함수가 실행된다.

AnimNotify_Throwing, Relaoding

// AnimNotify_ThrwoingBomb.cpp
void UAnimNotify_ThrowingBomb::Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, const FAnimNotifyEventReference& EventReference)
{
	Super::Notify(MeshComp, Animation, EventReference);

	AEPCharacterBase* Character = Cast<AEPCharacterBase>(MeshComp->GetOwner());
	if (Character)
	{
		Character->OnThrowingBombDelegate.Broadcast();
	}
}

// AnimNotify_Reloading.cpp
...
Character->OnReloadingBombDelegate.Execute();
... 

Notify는 AnimNotify에서 상속받았다. 스케레탈 메시를 통해 캐릭터에 접근하고, 캐릭터 베이스에 선언된 대리자를 실행한다.

Throwing은 폭탄과 캐릭터의 메소드를 등록해야 하므로 멀티캐스트 델리게이트로 선언했다. 그래서 Broadcast()를 사용한다. Reloading은 캐릭터의 메소드 하나만 실행하면 되므로 일반 델리케이트로 선언했다. 따라서 Execute()를 사용한다.

CharacterBase, CharacterPlayer

// CharacterBase.h
DECLARE_MULTICAST_DELEGATE(FOnThrowingBombDelegate);
DECLARE_DELEGATE(FOnReloadingBombDelegate);
...

public:
	FOnThrowingBombDelegate OnThrowingBombDelegate;
	FOnReloadingBombDelegate OnReloadingBombDelegate;
	
protected:
	UPROPERTY(EditAnywhere, Category = "Bomb", Meta = (AllowPrivateAccess = "true"))
	TSubclassOf<class AEPBombBase> BP_Bomb;
	
	virtual void OnThrowingBomb();
	virtual void OnReloadingBomb();

AI, Player 모두 폭탄을 보유하며 던지고 재장전하는 과정이 필요하다. 이렇게 공통되는 요소들은 CharacterBase에 선언되었다.

// CharacterPlayer.h
protected:
	UPROPERTY(EditAnyWhere, BlueprintReadWrite, Category = "Animation")
	TObjectPtr<class UAnimMontage> AimingMontage;

	UPROPERTY(EditAnyWhere, BlueprintReadWrite, Category = "Animation")
	TObjectPtr<class UAnimMontage> ThrowingMontage;
...
protected:
	UFUNCTION()
	void Throwing_OnMontageEnded(class UAnimMontage* TargetMontage, bool IsProperlyEnded);
	UFUNCTION()
	virtual void OnReloadingBomb() override;
	UFUNCTION()
	virtual void OnThrowingBomb() override;
...
protected:
	UPROPERTY(VisibleAnyWhere, Category = "Bomb")
	TObjectPtr<class AEPBombBase> BombInstance;

이를 상속받은 CharacterPlayer에서 세부 내용을 구현했다. 몽타주 에셋의 로딩, 상속받은 함수의 구현.

// CharacterPlayer.cpp
void AEPCharacterPlayer::BeginPlay()
{
	...
	AnimInstance = GetMesh()->GetAnimInstance();

	// 폭탄을 던지고 재장전 할 때의 대리자 연결(순서주의)
	OnThrowingBombDelegate.AddUObject(this, &AEPCharacterPlayer::OnThrowingBomb);
	OnReloadingBomb();
	OnReloadingBombDelegate.BindUObject(this, &AEPCharacterPlayer::OnReloadingBomb);
}

BeginPlay에서 CharacterBase에 선언한 대리자와 CharactertPlayer의 함수를 바인딩한다. 폭탄에도 OnThrowingBomb이 있는데, 폭탄 인스턴스를 생성했을 때 바인딩 하기 위하여 OnReloadingBomb에서 이를 진행한다.

void AEPCharacterPlayer::OnReloadingBomb()
{
	BombInstance = GetWorld()->SpawnActor<AEPBombBase>(BP_Bomb, FVector::ZeroVector, FRotator::ZeroRotator);
	if (BombInstance)
	{
		OnThrowingBombDelegate.AddUObject(BombInstance, &AEPBombBase::OnThrowingBomb);
		FName BombSocket(TEXT("BombHolder"));
		BombInstance->AttachToComponent(
			GetMesh(),
			FAttachmentTransformRules::SnapToTargetIncludingScale,
			BombSocket
		);
	}
}

Notify에서 OnReloadingBombDelegate를 실행했을 때 호출되는 함수이다. BombBase를 상속받아 만든 폭탄 BP를 하나 스폰한다. 그리고 스폰된 인스턴스는 폭탄을 던졌을 때 Notify에 반응할 수 있도록 OnThrowingBombDelegate에 등록한다. 캐릭터가 아닌 폭탄의 OnThrowingBomb을 등록한다.

캐릭터의 손에 위치한 소켓에 폭탄을 부착하는 것으로 마무리.

void AEPCharacterPlayer::OnThrowingBomb()
{
	if (BombInstance)
	{
		// 바라보는 방향
		FRotator Rotation = GetControlRotation();
		FVector Direction = Rotation.Vector();

		// 던지기(임시)
		BombInstance->GetBombMeshComponent()->AddImpulse(Direction * ThrowingDistanceMultiplier * ThrowingDistance);
		
		OnThrowingBombDelegate.RemoveAll(BombInstance);
		
		BombInstance->DetachFromActor(FDetachmentTransformRules::KeepWorldTransform);
		BombInstance = nullptr;
	}
}

Notify에서 OnThrowingBombDelegate를 실행했을 때 호출되는 함수이다. 캐릭터 컨트롤러가 바라보는 방향으로 폭탄을 던진다.

한 번 던진 폭탄은 더이상 대리자에 남겨둘 필요가 없으므로 삭제한다. RemoveAll이 오브젝트 기반 동작이라 해당 폭탄 인스턴스의 대리자만 전부 삭제된다. 새로운 폭탄을 꺼내면 BombInstance는 덧씌워진다.

플레이어의 소켓으로부터 폭탄을 분리한다.

// 던지는 함수
void AEPCharacterPlayer::Throwing()
{
	...
		if (AnimInstance)
	{
		AnimInstance->Montage_Play(ThrowingMontage);

		// 폭탄 던지기가 끝났을 때의 대리자 연결
		FOnMontageBlendingOutStarted BlendingOutDelegate;
		BlendingOutDelegate.BindUObject(this, &AEPCharacterPlayer::Throwing_OnMontageEnded);
		AnimInstance->Montage_SetBlendingOutDelegate(BlendingOutDelegate, ThrowingMontage);
	}
	...
}

// 던지는 동작이 끝날 때 즈음 호출되는 함수
void AEPCharacterPlayer::Throwing_OnMontageEnded(class UAnimMontage* TargetMontage, bool IsProperlyEnded)
{
	bIsThrowing = false;
	if (bIsAiming)
	{
		if (AnimInstance)
		{
			AnimInstance->Montage_Play(AimingMontage);
		}
		...
	}
}

폭탄을 던지는 함수와 던지기-재장던 몽타주가 끝날 때 즈음 호출되는 함수이다. 왜 끝날 때 즈음이냐면 몽타주 재생이 완전히 끝나고 다른 몽타주를 재생하면 서로 전혀 연결되지 않기 때문이다. 블랜드를 위해서 조금 빨리 호출해주는 Montage_SetBlendingOutDelegate를 사용했다.

왜 BeginPlay에서 대리자를 등록하지 않고 폭탄을 던질 때 마다 대리자를 등록하는가? 폭탄을 던질 때마다 몽타주 에셋으로부터 인스턴스가 새롭게 생성되기 때문이다. 한참 헤맸다(출처)

BombBase

// BombBase.cpp
void AEPBombBase::OnThrowingBomb()
{
	...
}

캐릭터 플레이어의 OnReloadingBomb에서 OnThrowingBombDelegate에 등록한 함수이다. 폭탄의 물리 제어 관련 내용이 들어간다.

3) 최종 결과

Untitled

던지는 타이밍, 재장전 타이밍에 맞추어 노티파이가 실행되고 관련 함수들까지 제대로 호출이 되는 모습이다.

+추가1) 기본 포즈로 돌아가는 문제

애니메이션을 편집하다보니 마지막 부분에서 기본 포즈로 돌아가는 문제가 발생했다. 언리얼 엔진에서 가끔씩 발생하는 문제라고한다. 애니메이션의 타임라인에서 우클릭→해당 프레임을 제거하는 것으로 해결했다.

Untitled

+추가2) 몽타주 루프 문제

조준은 마우스 오른쪽 버튼을 누르는 동안 계속 유지되어야 한다. 몽타주의 루프가 필요한 것이다.

Untitled

몽타주 섹션에서 다음 섹션을 본인으로 지정하면 알아서 반복한다.

참고자료