- 지난 시간까지 전사를 구현해보았습니다.
- 궁수의 경우도 비슷하나 활 시위 당기기 및 화살 발사 로직을 추가로 구현해보도록 하겠습니다.
- 기존 포스팅에서 구현했던 전사와 유사합니다.
- 애니메이션을 추가하고, 기본 콤보 공격까지는 기존 틀에 맞춰 동일하게 제작해주도록 합니다.
- 활 무기를 장착하기 위한 소켓 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);
}
- 제가 구현하는 궁수는 우클릭 시 조준하며, 조준 상태일 경우 좌클릭으로 연속하여 화살을 발사할 수 있도록 하고 싶습니다.
- 우선 활을 조준하는 몽타주와, 발사하는 몽타주를 추가해주도록 하겠습니다.
// 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);
}
}
}
}
- 활을 발사하는 몽타주를 추가해주도록 하겠습니다.
- 특정 시점에 활 시위를 놓아주는 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을 추가하는 부분은 생략하도록 하겠습니다.
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;
}
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;
}
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);
}