[UE5 C++] 궁수 - 활 및 화살(발사)

LeeTaes·2024년 5월 8일
0

[UE_Project] MysticMaze

목록 보기
10/17

언리얼 엔진을 사용한 RPG 프로젝트 만들기

  • 지난 시간까지 전사를 구현해보았습니다.
  • 궁수의 경우도 비슷하나 활 시위 당기기 및 화살 발사 로직을 추가로 구현해보도록 하겠습니다.

직업 - 궁수 구현하기

  • 기존 포스팅에서 구현했던 전사와 유사합니다.
  • 애니메이션을 추가하고, 기본 콤보 공격까지는 기존 틀에 맞춰 동일하게 제작해주도록 합니다.
  • 활 무기를 장착하기 위한 소켓 3가지를 추가해주도록 하겠습니다.


무기(활) 구현하기

  • 활은 기존 검과 달리 활 시위가 움직여야 하며, 화살을 스폰하고 발사하는 로직이 추가되어야 합니다.
  • 추가로 "Quiver" 라는 화살통을 부착해줘야 합니다.

실제로 Bow Weapon을 구현해보도록 하겠습니다.

MMBowWeapon Class

// MMBowWeapon Header

#pragma once

#include "CoreMinimal.h"
#include "Item/MMWeapon.h"
#include "MMBowWeapon.generated.h"

/**
 * 
 */
UCLASS()
class MYSTICMAZE_API AMMBowWeapon : public AMMWeapon
{
	GENERATED_BODY()
	
public:
	AMMBowWeapon();

protected:
	virtual void BeginPlay() override;
	virtual void Tick(float DeltaSeconds) override;

public:
	FORCEINLINE void SetIsHold(bool InValue) { bIsHold = InValue; }
	virtual void EquipWeapon() override;

	void SpawnArrow();
	void ShootArrow();
	void DestroyArrow();

protected:
	FVector GetArrowSocketLocation(USkeletalMeshComponent* Mesh);

	// 화살통 액터를 무기 장착과 동시에 스폰하기 위함
	UPROPERTY(EditAnywhere, Category = "Quiver", meta = (AllowPrivateAccess = "true"))
	TSubclassOf<AActor> QuiverClass;
	
    // 화살 발사에 필요한 클래스 정보
	UPROPERTY(VisibleAnywhere, Category = "Quiver", meta = (AllowPrivateAccess = "true"))
	TSubclassOf<AActor> ArrowClass;

	// 활 시위의 중앙 소켓 이름
	UPROPERTY(EditAnywhere, Category = "BaseSocketName", meta = (AllowPrivateAccess = "true"))
	FName StringSocketName;

	UPROPERTY()
	TObjectPtr<AActor> Quiver;

	UPROPERTY()
	TObjectPtr<class AMMArrow> TempArrow; 

	FName QuiverSocketName;
	FName ArrowSocketName;

private:
	// 활을 당기고 있는지 판별하기 위한 변수
	uint8 bIsHold : 1;
	
    // 활을 당기지 않은 경우의 StringSocket 위치
	FVector BaseLocation;
    // 활을 당기고 있는 경우의 StringSocket 위치
	FVector StringLocation;
};
// MMBowWeapon Cpp

#include "Item/MMBowWeapon.h"
#include "Item/MMArrow.h"
#include "Interface/MMPlayerVisualInterface.h"
#include "Collision/MMCollision.h"

#include "GameFramework/Character.h"
#include "Components/PoseableMeshComponent.h"
#include "Camera/CameraComponent.h"
#include "DrawDebugHelpers.h"

AMMBowWeapon::AMMBowWeapon()
{
	// Tick을 사용하겠다고 선언
	PrimaryActorTick.bCanEverTick = true;

	// 화살 클래스 등록
	static ConstructorHelpers::FClassFinder<AActor>ArrowRef(TEXT("/Script/Engine.Blueprint'/Game/MysticMaze/Items/Weapons/BP_BasicArrow.BP_BasicArrow_C'"));
	if (ArrowRef.Succeeded())
	{
		ArrowClass = ArrowRef.Class;
	}

	// 소켓 이름 및 무기 타입 저장
	WeaponType = EWeaponType::WT_Bow;
	BaseSocketName = TEXT("BowPosition");
	DrawSocketName = TEXT("BowSocket");
	QuiverSocketName = TEXT("QuiverPosition");
	ArrowSocketName = TEXT("ArrowSocket");
	
    // 변수 초기화
	bIsHold = false;
}

void AMMBowWeapon::BeginPlay()
{
	Super::BeginPlay();
	
	// 화살 줄의 로컬 위치를 구해줍니다.
	// * GetTransform().InverseTransformPosition(소켓 위치) : 월드 상의 소켓 위치를 월드변환행렬의 역함수를 곱해 로컬 위치를 구합니다.
	BaseLocation = GetTransform().InverseTransformPosition(WeaponMesh->GetSocketLocation(StringSocketName));
}

void AMMBowWeapon::Tick(float DeltaSeconds)
{
	Super::Tick(DeltaSeconds);

	// 활을 장전한 상태이면
	if (bIsHold)
	{
		ACharacter* PlayerCharacter = Cast<ACharacter>(GetOwner());
		if (PlayerCharacter)
		{	
			// 화살 위치를 전달받아 StringLocation에 저장합니다.
			StringLocation = GetArrowSocketLocation(PlayerCharacter->GetMesh());
			// 화살 줄 Bone의 위치를 당겨진 상태로 설정합니다.
			WeaponMesh->SetBoneLocationByName(StringSocketName, StringLocation, EBoneSpaces::WorldSpace);
		}
	}
	else
	{
		// 화살 줄 Bone의 위치를 기본 위치로 설정합니다.
		WeaponMesh->SetBoneLocationByName(StringSocketName, BaseLocation, EBoneSpaces::ComponentSpace);
	}
}

void AMMBowWeapon::EquipWeapon()
{
	Super::EquipWeapon();

	// 기존 무기 장착 로직 + 화살통 장착 로직
	if (QuiverClass)
	{
		FActorSpawnParameters Params;
		Quiver = GetWorld()->SpawnActor(QuiverClass);

		if (Quiver)
		{
			ACharacter* PlayerCharacter = Cast<ACharacter>(GetOwner());
			if (PlayerCharacter)
			{
				Quiver->AttachToComponent(PlayerCharacter->GetMesh(), FAttachmentTransformRules::KeepRelativeTransform, QuiverSocketName);
			}
		}
	}
}

void AMMBowWeapon::SpawnArrow()
{
	// 화살을 스폰하여 잠시 저장합니다.
	TempArrow = Cast<AMMArrow>(GetWorld()->SpawnActor(ArrowClass));

	if (TempArrow)
	{
		// 화살을 화살 소켓에 부착합니다.
		ACharacter* PlayerCharacter = Cast<ACharacter>(GetOwner());
		if (PlayerCharacter)
		{
			TempArrow->SetOwner(Owner);
			TempArrow->AttachToComponent(PlayerCharacter->GetMesh(), FAttachmentTransformRules::KeepRelativeTransform, ArrowSocketName);
		}
	}
}

void AMMBowWeapon::ShootArrow()
{
	if (TempArrow)
	{
		// 플레이어의 카메라에서 화면의 중앙으로 LineTrace를 진행합니다.
		IMMPlayerVisualInterface* PlayerCharacter = Cast<IMMPlayerVisualInterface>(GetOwner());
		if (PlayerCharacter)
		{
			// 충돌 결과 반환용
			FHitResult HitResult;
			// 시작 지점 (카메라의 위치)
			FVector Start = PlayerCharacter->GetPlayerCamera()->GetComponentLocation();
			// 종료 지점 (카메라 위치 + 카메라 전방벡터 * 20000)
			float Distance = 20000;
			FVector End = Start + (PlayerCharacter->GetPlayerCamera()->GetForwardVector() * Distance);
			// 파라미터 설정
			FCollisionQueryParams Params(SCENE_QUERY_STAT(Shoot), false, Owner);
		
			// 충돌 탐지
			bool bHasHit = GetWorld()->LineTraceSingleByChannel(
				HitResult,
				Start,
				End,
				CHANNEL_VISIBILITY,
				Params
			);
			
			// 부모 액터로부터 부착 해제
			TempArrow.Get()->DetachFromActor(FDetachmentTransformRules::KeepWorldTransform);

			if (bHasHit)
			{
				// TODO : 해당 방향으로 화살 발사
				TempArrow.Get()->Fire(HitResult.ImpactPoint);
				TempArrow = nullptr;
			}
			else
			{
				TempArrow.Get()->Fire(End);
				TempArrow = nullptr;
			}

			// 디버깅
			FColor DrawColor = bHasHit ? FColor::Green : FColor::Red;
			DrawDebugLine(GetWorld(), Start, End, DrawColor, false, 3.0f);
		}
	}
}

void AMMBowWeapon::DestroyArrow()
{
	if (TempArrow)
	{
    	// 발사되지 않은 화살을 소멸시키기
		TempArrow->Destroy();
	}
}

FVector AMMBowWeapon::GetArrowSocketLocation(USkeletalMeshComponent* Mesh)
{
	// 화살 소켓 위치를 반환합니다.
	return Mesh->GetSocketLocation(ArrowSocketName);
}

화살 구현하기

  • 화살은 생성 후 잠시 플레이어 손에 부착되었다가 발사되어야 합니다.
  • ProjectileMovement Component를 사용해 발사를 구현하도록 하겠습니다.
// MMArrow Header

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "MMArrow.generated.h"

UCLASS()
class MYSTICMAZE_API AMMArrow : public AActor
{
	GENERATED_BODY()
	
public:	
	AMMArrow();

protected:
	virtual void BeginPlay() override;
	virtual void PostInitializeComponents() override;

public:
	void Fire(FVector TargetLocation);

protected:
	// 충돌 탐지용 함수
	UFUNCTION()
	void OnBeginOverlap(class UPrimitiveComponent* OverlappedComp, class AActor* OtherActor, class UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);

	// 깃털
	UPROPERTY(VisibleAnywhere, Category = "Arrow", meta = (AllowPrivateAccess = "true"))
	TObjectPtr<UStaticMeshComponent> FeatherComponent;
	
    // 화살대
	UPROPERTY(VisibleAnywhere, Category = "Arrow", meta = (AllowPrivateAccess = "true"))
	TObjectPtr<UStaticMeshComponent> ShaftComponent;

	// 화살촉
	UPROPERTY(VisibleAnywhere, Category = "Arrow", meta = (AllowPrivateAccess = "true"))
	TObjectPtr<UStaticMeshComponent> IronComponent;

	// 충돌체
	UPROPERTY(VisibleAnywhere, Category = "Arrow", meta = (AllowPrivateAccess = "true"))
	TObjectPtr<class USphereComponent> ArrowCollision;

	// Movement Component
	UPROPERTY(VisibleAnywhere, Category = "Movement", meta = (AllowPrivateAccess = "true"))
	TObjectPtr<class UProjectileMovementComponent> MovementComponent;

private:
	// 초기 속력
	float Speed = 5000.0f;
};
// MMArrow Cpp

#include "Item/MMArrow.h"
#include "Collision/MMCollision.h"

#include "Components/SphereComponent.h"
#include "GameFramework/ProjectileMovementComponent.h"
#include "DrawDebugHelpers.h"

AMMArrow::AMMArrow()
{
	// 컴포넌트를 생성하고 개별 충돌 설정을 진행합니다.
	FeatherComponent = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Feather"));
	FeatherComponent->SetCollisionEnabled(ECollisionEnabled::NoCollision);
	RootComponent = FeatherComponent;

	ShaftComponent = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Shaft"));
	ShaftComponent->SetupAttachment(FeatherComponent);
	ShaftComponent->SetCollisionEnabled(ECollisionEnabled::NoCollision);

	IronComponent = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Iron"));
	IronComponent->SetupAttachment(ShaftComponent);
	IronComponent->SetCollisionEnabled(ECollisionEnabled::NoCollision);

	ArrowCollision = CreateDefaultSubobject<USphereComponent>(TEXT("ArrowCollision"));
	ArrowCollision->SetupAttachment(IronComponent);
	ArrowCollision->SetCollisionProfileName(MMWEAPON);

	// MovementComponent를 추가합니다.
	MovementComponent = CreateDefaultSubobject<UProjectileMovementComponent>(TEXT("MovementComponent"));
    // * 발사체의 회전이 Velocity에 종속됩니다.
	MovementComponent->bRotationFollowsVelocity = true;
    // * 초기 속도를 지정합니다.
	MovementComponent->InitialSpeed = Speed;
}

void AMMArrow::BeginPlay()
{
	Super::BeginPlay();

	// Projectile Movement Component 비활성화
	MovementComponent->SetActive(false);
}

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

	// Event Mapping
	ArrowCollision->OnComponentBeginOverlap.AddDynamic(this, &AMMArrow::OnBeginOverlap);
}

void AMMArrow::Fire(FVector TargetLocation)
{
	// 방향 구하기
	FVector LaunchDirection = (TargetLocation - GetActorLocation()).GetSafeNormal();
	
	// 방향 지정 및 Projectile Movement Component 활성화
	MovementComponent->Velocity = LaunchDirection * MovementComponent->InitialSpeed;
	MovementComponent->Activate();

	// 3초 후 자동 삭제
	SetLifeSpan(3.0f);
}

void AMMArrow::OnBeginOverlap(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	if (Owner == OtherActor) return;

	// TODO : 데미지 전달
	UE_LOG(LogTemp, Warning, TEXT("%s"), *OtherActor->GetName());
	UE_LOG(LogTemp, Warning, TEXT("%s"), *OverlappedComp->GetName());

	// Projectile Movement Component 비활성화
	MovementComponent->SetActive(false);

	// 맞은 물체에 화살 부착
	FAttachmentTransformRules AttachmentRules(EAttachmentRule::KeepWorld, EAttachmentRule::KeepRelative, EAttachmentRule::KeepRelative, true);
	this->AttachToActor(OtherActor, AttachmentRules);
}
  • 제작한 클래스를 상속받는 블루프린트를 생성합니다.

화살 조준/발사 애니메이션 구성하기

  • 제가 구현하는 궁수는 우클릭 시 조준하며, 조준 상태일 경우 좌클릭으로 연속하여 화살을 발사할 수 있도록 하고 싶습니다.
  • 우선 활을 조준하는 몽타주와, 발사하는 몽타주를 추가해주도록 하겠습니다.
  • 활 조준하기
    - 활을 조준하며 화살을 스폰하고 줄을 당겨야 합니다.
    - 2개의 AnimNotify를 추가해주도록 하겠습니다.
// AnimNotify_MMArrowSpawn Cpp

#include "Animation/AnimNotify_MMArrowSpawn.h"
#include "Interface/MMAnimationWeaponInterface.h"
#include "Item/MMBowWeapon.h"

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

	if (MeshComp)
	{
		// 화살을 스폰합니다.
		IMMAnimationWeaponInterface* WeaponPawn = Cast<IMMAnimationWeaponInterface>(MeshComp->GetOwner());
		if (WeaponPawn)
		{
			AMMBowWeapon* BowWeapon = Cast<AMMBowWeapon>(WeaponPawn->GetWeapon());
			if (BowWeapon)
			{
				BowWeapon->SpawnArrow();
			}
		}
	}
}
// AnimNotify_MMPullString Cpp

#include "Animation/AnimNotify_MMPullString.h"
#include "Interface/MMAnimationWeaponInterface.h"
#include "Item/MMBowWeapon.h"

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

	if (MeshComp)
	{
		// 활을 당겼다는 것을 전달합니다.
		IMMAnimationWeaponInterface* WeaponPawn = Cast<IMMAnimationWeaponInterface>(MeshComp->GetOwner());
		if (WeaponPawn)
		{
			AMMBowWeapon* BowWeapon = Cast<AMMBowWeapon>(WeaponPawn->GetWeapon());
			if (BowWeapon)
			{
				BowWeapon->SetIsHold(true);
			}
		}
	}
}
  • 위 2가지 함수에 따라 Bow는 Arrow를 스폰하고 플레이어의 손에 부착시키며, SetIsHold(true) 함수에 의해 활 시위가 손에 부착되는 결과를 얻을 수 있습니다.

  • 활을 발사하는 몽타주를 추가해주도록 하겠습니다.
  • 특정 시점에 활 시위를 놓아주는 AnimNotify를 추가해주도록 합니다.
// AnimNotify_MMReleaseString Cpp

#include "Animation/AnimNotify_MMReleaseString.h"
#include "Interface/MMAnimationWeaponInterface.h"
#include "Item/MMBowWeapon.h"

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

	if (MeshComp)
	{
		// 활을 놓았다는 것을 전달합니다.
		IMMAnimationWeaponInterface* WeaponPawn = Cast<IMMAnimationWeaponInterface>(MeshComp->GetOwner());
		if (WeaponPawn)
		{
			AMMBowWeapon* BowWeapon = Cast<AMMBowWeapon>(WeaponPawn->GetWeapon());
			if (BowWeapon)
			{
				BowWeapon->SetIsHold(false);
			}
		}
	}
}


발사 로직 추가하기

  • 플레이어는 우클릭으로 활을 조준할 수 있습니다.
    - 조준 시 시야가 확대되며, 폰의 회전은 컨트롤러를 따르게 됩니다.
    - InputAction을 추가하는 부분은 생략하도록 하겠습니다.

  • 활을 조준하는 로직입니다.
    - bIsHold를 통해 활이 당겨지고 있는지 없는지를 체크하도록 합니다.
void AMMPlayerCharacter::DrawArrow()
{
	// 장전 중인 경우 반환
	if (bIsHold) return;
	// 구르기중인 경우 반환
	if (bIsRoll) return;
	// 무기 스왑중인 경우 반환
	if (bIsChange) return;
	// 무기를 장착하지 않은 경우 반환
	if (!bIsEquip) return;

	UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
	if (!AnimInstance) return;
	if (AnimInstance->Montage_IsPlaying(ReleaseArrowMontage) || AnimInstance->Montage_IsPlaying(DrawArrowMontage)) return;

	if (CurrentWeapon)
	{
		// 장전시 플레이어 이동 불가
		GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_None);
		// 플레이어 달리기 취소
		if (bIsDash)
		{
			GetCharacterMovement()->MaxWalkSpeed = WalkSpeed;
			bIsDash = false;
		}
		
		bIsHold = true;
		bIsStop = false;

		// 움직임 설정
		GetCharacterMovement()->bOrientRotationToMovement = false;
		GetCharacterMovement()->bUseControllerDesiredRotation = true;

		// 몽타주 재생
		AnimInstance->Montage_Play(DrawArrowMontage, 1.0f);

		// 몽타주 재생 종료 바인딩
		FOnMontageEnded EndDelegate;
		EndDelegate.BindUObject(this, &AMMPlayerCharacter::DrawArrowEnd);

		// DrawArrowMontage가 종료되면 EndDelegate에 연동된 DrawArrowEnd함수 호출
		AnimInstance->Montage_SetEndDelegate(EndDelegate, DrawArrowMontage);
	}
}

void AMMPlayerCharacter::DrawArrowEnd(UAnimMontage* Montage, bool IsEnded)
{
	// 움직임 설정
	GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_Walking);
	bCanShoot = true;
}
  • bIsHold 변수에 따라 Tick() 함수 내부에서 화면을 조정합니다.
    - 부드럽게 조정하기 위해 FInterpTo, RInterpTo 함수를 사용하였습니다.
void AMMPlayerCharacter::Tick(float DeltaSeconds)
{
	Super::Tick(DeltaSeconds);

	if (bIsHold)
	{
		// Spring Arm 길이 조정
		SpringArm->TargetArmLength = FMath::FInterpTo(SpringArm->TargetArmLength, 100.0f, DeltaSeconds, 2.0f);

		// Character Rotation 조정
		FRotator StartRot = GetActorRotation();
		FRotator TargetRot = FRotator(GetActorRotation().Pitch, GetControlRotation().Yaw, GetActorRotation().Roll);
		SetActorRotation(FMath::RInterpTo(StartRot, TargetRot, DeltaSeconds, 4.0f));

		// Camera Position 조정
		Camera->SetRelativeLocation(FVector(0.0f, FMath::FInterpTo(Camera->GetRelativeLocation().Y, 50.0f, DeltaSeconds, 3.0f), FMath::FInterpTo(Camera->GetRelativeLocation().Z, 100.0f, DeltaSeconds, 3.0f)));
	}
}
  • 조준 상태에서 해제하는 로직을 구현해보도록 하겠습니다.
    - 재생중이던 애니메이션을 종료하고 화살이 존재하는 경우 소멸시켜줍니다.
    - 기본적인 움직임/카메라 설정을 원상태로 복원합니다.
void AMMPlayerCharacter::ReleaseArrow()
{
	if (!bIsHold) return;
	// 무기를 장착하지 않은 경우 반환
	if (!bIsEquip) return;

	UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
	if (!AnimInstance) return;
	if (AnimInstance->Montage_IsPlaying(DrawArrowMontage))
	{
		AnimInstance->Montage_Stop(0.1f, DrawArrowMontage);
	}

	if (AnimInstance->Montage_IsPlaying(ReleaseArrowMontage))
	{
		AnimInstance->Montage_Stop(0.1f, ReleaseArrowMontage);
	}

	AMMBowWeapon* BowWeapon = Cast<AMMBowWeapon>(CurrentWeapon);
	if (BowWeapon)
	{
		BowWeapon->SetIsHold(false);
		BowWeapon->DestroyArrow();
	}

	bIsHold = false;
	bCanShoot = false;
	bIsStop = true;

	// 움직임 설정
	GetCharacterMovement()->bOrientRotationToMovement = true;
	GetCharacterMovement()->bUseControllerDesiredRotation = false;
	GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_Walking);

	// 카메라 설정
	Camera->SetRelativeLocation(FVector(0.0f, 0.0f, 0.0f));
	SpringArm->TargetArmLength = 500.0f;
}
  • 공격 로직을 추가해보도록 하겠습니다.
    - 조준이 완료되면 bCanShoot 변수가 True로 전환됩니다.
    - 화살 조준 중에 좌클릭으로 화살을 발사합니다.
    - 연사를 위해 화살 발사가 종료되면 자동으로 조준 애니메이션을 재생합니다.
void AMMPlayerCharacter::BasicAttack()
{
	...
    
	// 화살 장전 중에는 화살 발사 애니메이션 재생 후 종료
	if (bIsHold)
	{
		if (bCanShoot)
		{
			bCanShoot = false;
			ShootArrow();
		}
		return;
	}
    
    ...
}

void AMMPlayerCharacter::ShootArrow()
{
	if (!bIsHold) return;
	if (CurrentWeapon)
	{
		// 공격 시 플레이어 이동 불가
		GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_None);

		UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
		if (AnimInstance)
		{
			// 몽타주 재생
			AnimInstance->Montage_Play(ReleaseArrowMontage);

			// 몽타주 재생 종료 바인딩
			FOnMontageEnded EndDelegate;
			EndDelegate.BindUObject(this, &AMMPlayerCharacter::ReleaseArrowEnd);

			// ReleaseArrowMontage가 종료되면 EndDelegate에 연동된 ReleaseArrowEnd함수 호출
			AnimInstance->Montage_SetEndDelegate(EndDelegate, ReleaseArrowMontage);
		}

		// 화살을 발사합니다.
		AMMBowWeapon* BowWeapon = Cast<AMMBowWeapon>(CurrentWeapon);
		if (BowWeapon)
		{
			BowWeapon->ShootArrow();
		}
	}
}

void AMMPlayerCharacter::ReleaseArrowEnd(UAnimMontage* Montage, bool IsEnded)
{
	if (bIsStop) return;

	UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
	if (!AnimInstance) return;

	// 몽타주 재생
	AnimInstance->Montage_Play(DrawArrowMontage, 1.5f);

	// 몽타주 재생 종료 바인딩
	FOnMontageEnded EndDelegate;
	EndDelegate.BindUObject(this, &AMMPlayerCharacter::DrawArrowEnd);

	// DrawArrowMontage가 종료되면 EndDelegate에 연동된 DrawArrowEnd함수 호출
	AnimInstance->Montage_SetEndDelegate(EndDelegate, DrawArrowMontage);
}

구현 결과

profile
클라이언트 프로그래머 지망생

0개의 댓글