언리얼 자체를 많이 다루지 않아 익숙해지고 다루는데 꽤나 오랜 시간이 걸렸다.(지금도 잘 못다룸)
이번에는 공격 애니메이션을 몽타주로 설정하고 이를 효과적으로 관리하는 것과
화살 투사체를 발사하는 것을 만들어보자.
애니메이션 몽타주
애니메이션 몽타주는 여러개의 애니메이션을 효과적으로 한번에 관리 할 수 있다.
이를 이용해서 여러개의 공격 모션을 설정하고 입력한 값에 따라 모션을 다르게 재생해보자.
공격 모션 설정
공격은 총 4가지로 구성되어 있다.
마우스 왼쪽 클릭을 했을 경우 NormalAttack
Q 버튼을 누를 경우 SkillQ
E 버튼을 누를 경우 SkillE
R 버튼을 누를 경우 SkillR
그에 맞게 이름을 지어주고 각 모션에 할 것을 구분해서 애니메이션 몽타주를 설정해준다.
또한 이는 연속적인 모션이 아니라 하나의 동작으로서 사용할 것이기 때문에
이렇게 섹션을 나누어준다.
인풋
위에서 설정한대로 마우스나 키 입력을 받아주어야 한다.
4개의 Input Action을 만들어주고
모두 눌렀을 때 딱 한번만 반응하게 트리거를 설정해준다.
이제 Mapping Context에서 우리가 입력하고자 하는 값으로 연결해준다.
이제 코드를 작성해주자.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Input, meta = (AllowPrivateAccess = "true"))
TObjectPtr<class UInputAction> NormalAttackAction;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Input, meta = (AllowPrivateAccess = "true"))
TObjectPtr<class UInputAction> SkillQAction;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Input, meta = (AllowPrivateAccess = "true"))
TObjectPtr<class UInputAction> SkillEAction;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Input, meta = (AllowPrivateAccess = "true"))
TObjectPtr<class UInputAction> SkillRAction;
void PlayAttack(const struct FInputActionInstance& Instance);
void PlayNormalAttack();
void PlaySkillQ();
void PlaySkillE();
void PlaySkillR();
void APMPlayer::SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
UEnhancedInputComponent* EnhancedInput = CastChecked<UEnhancedInputComponent>(PlayerInputComponent);
EnhancedInput->BindAction(JumpAction, ETriggerEvent::Triggered, this, &APMPlayer::CheckJump);
EnhancedInput->BindAction(JumpAction, ETriggerEvent::Completed, this, &APMPlayer::JumpStop);
EnhancedInput->BindAction(MoveAction, ETriggerEvent::Triggered, this, &APMPlayer::Move);
EnhancedInput->BindAction(LookAction, ETriggerEvent::Triggered, this, &APMPlayer::Look);
// Attack
EnhancedInput->BindAction(NormalAttackAction, ETriggerEvent::Triggered, this, &APMPlayer::PlayAttack);
EnhancedInput->BindAction(SkillQAction, ETriggerEvent::Triggered, this, &APMPlayer::PlayAttack);
EnhancedInput->BindAction(SkillEAction, ETriggerEvent::Triggered, this, &APMPlayer::PlayAttack);
EnhancedInput->BindAction(SkillRAction, ETriggerEvent::Triggered, this, &APMPlayer::PlayAttack);
}
void APMPlayer::PlayAttack(const FInputActionInstance& Instance)
{
if (PMAnim == nullptr || AttackMontage == nullptr || bIsAttacking == true || GetMovementComponent()->IsFalling() == true) return;
const UInputAction* TriggeredAction = Instance.GetSourceAction();
bIsAttacking = true;
GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_None);
if (TriggeredAction == NormalAttackAction)
{
PlayNormalAttack();
}
else if (TriggeredAction == SkillQAction)
{
PlaySkillQ();
}
else if (TriggeredAction == SkillEAction)
{
PlaySkillE();
}
else if (TriggeredAction == SkillRAction)
{
PlaySkillR();
}
FOnMontageEnded EndDelegate;
EndDelegate.BindUObject(this, &APMPlayer::AttackEnd);
PMAnim->Montage_SetBlendingOutDelegate(EndDelegate, AttackMontage);
}
트리거된 액션, 내가 누른 버튼에 따라 어떤 함수를 실행해야하는지 결정해준다.
또한 애니메이션이 끝났을 때 할 행동을 델리게이트를 통해 설정해준다.
몽타주의 재생은 어렵지 않다.
void APMPlayer::PlayNormalAttack()
{
PMAnim->PlayAttackMontage(AttackMontage, EAttackTypes::NormalAttack);
}
void APMPlayer::PlaySkillQ()
{
PMAnim->PlayAttackMontage(AttackMontage, EAttackTypes::SkillQ);
}
void APMPlayer::PlaySkillE()
{
PMAnim->PlayAttackMontage(AttackMontage, EAttackTypes::SkillE);
}
void APMPlayer::PlaySkillR()
{
PMAnim->PlayAttackMontage(AttackMontage, EAttackTypes::SkillR);
}
void UPMPlayerAnimInstance::PlayAttackMontage(class UAnimMontage* AttackMontage, EAttackTypes AttackType, float InSpeed)
{
if (AttackMontage == nullptr) return;
if (Montage_IsPlaying(AttackMontage) == true) return;
Montage_Play(AttackMontage, InSpeed);
switch (AttackType)
{
case EAttackTypes::NormalAttack:
Montage_JumpToSection(TEXT("NormalAttack"), AttackMontage);
break;
case EAttackTypes::SkillQ:
Montage_JumpToSection(TEXT("SkillQ"), AttackMontage);
break;
case EAttackTypes::SkillE:
Montage_JumpToSection(TEXT("SkillE"), AttackMontage);
break;
case EAttackTypes::SkillR:
Montage_JumpToSection(TEXT("SkillR"), AttackMontage);
break;
default:
break;
}
}
이렇게 원하는 애니메이션을 재생해주면 된다.
애님 노티파이 스테이트
몽타주에서 노티파이를 이용해 공격을 할 때 끝날때를 지정할 수 있는데
이때 일반 노티파이가 아닌 노티파이 스테이트를 이용하면 특정 지점에서 매개변수와 시작과 끝을 효과적으로 관리할 수 있다.
먼저 FireArrow라고 하는 애님 노티파이 스테이트를만들고 이렇게 지정해 줄 수 있다.
노티파이 스테이트는 이렇게 구성할 수 있다.
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Animation/AnimNotifies/AnimNotifyState.h"
#include "Enums/PMEnums.h"
#include "AnimNotifyState_FireArrow.generated.h"
/**
*
*/
UCLASS()
class PROJECTM_API UAnimNotifyState_FireArrow : public UAnimNotifyState
{
GENERATED_BODY()
public:
UAnimNotifyState_FireArrow();
virtual void NotifyBegin(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, float TotalDuration) override;
virtual void NotifyEnd(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation) override;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Attack)
EAttackTypes AttackType;
};
// Fill out your copyright notice in the Description page of Project Settings.
#include "Animation/AnimNotifyState_FireArrow.h"
#include "Interface/PMFireArrowInterface.h"
UAnimNotifyState_FireArrow::UAnimNotifyState_FireArrow()
{
}
void UAnimNotifyState_FireArrow::NotifyBegin(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, float TotalDuration)
{
if (MeshComp)
{
IPMFireArrowInterface* FireArrowInterface = Cast<IPMFireArrowInterface>(MeshComp->GetOwner());
if (FireArrowInterface)
{
FireArrowInterface->FireArrowBegin(AttackType);
}
}
}
void UAnimNotifyState_FireArrow::NotifyEnd(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation)
{
if (MeshComp)
{
IPMFireArrowInterface* FireArrowInterface = Cast<IPMFireArrowInterface>(MeshComp->GetOwner());
if (FireArrowInterface)
{
FireArrowInterface->FireArrowEnd();
}
}
}
플레이어한테 인터페이스를 구현하게 하고 이 인터페이스를 가지고 있는지 확인하게 하여 의존성을 떨어뜨린다.
화살 가리기
현재 애니메이션을 재생하면 문제가 있는데 캐릭터의 스켈레탈 메쉬에 화살이 붙어있다는 것이다. 즉, 우리가 화살을 발사하는 모션을 했을 때, 화살은 안보여야 자연스러울텐데 메쉬에 계속 붙어있어 이상한 느낌을 준다.
따라서 스켈레톤에 붙어있는 화살의 본을 임시적으로 가린 후 애니메이션이 끝나면 화살의 모습을 다시 보여주게 하자.
void APMPlayer::FireArrowBegin(EAttackTypes AttackType)
{
USkeletalMeshComponent* SkeletalMesh = GetMesh();
// 화살 가리기
if (GetMesh())
{
FName BoneName(TEXT("arrow_nock"));
GetMesh()->HideBoneByName(BoneName, EPhysBodyOp::PBO_None);
}
}
void APMPlayer::FireArrowEnd()
{
if (GetMesh())
{
FName BoneName(TEXT("arrow_nock"));
GetMesh()->UnHideBoneByName(BoneName);
}
}
이렇게 해서 임시적으로 화살을 가릴 수 있다.
화살 투사체 만들기
화살 투사체는 Projectile Movement가 있기에 쉽게 만들 수 있다.
메쉬를 만들고 그 메쉬에 각종 Effect와 ProjectileMovement를 부착해 구현할 수 있다.
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "PMArrowProjectile.generated.h"
UCLASS()
class PROJECTM_API APMArrowProjectile : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
APMArrowProjectile();
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Mesh)
TObjectPtr<class UStaticMeshComponent> ArrowBody;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Move)
TObjectPtr<class UProjectileMovementComponent> ProjectileMovement;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Effect)
TObjectPtr<class UParticleSystemComponent> DestroyEffect;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Effect)
TObjectPtr<class UParticleSystemComponent> ArrowHeadEffect;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Effect)
TObjectPtr<class UParticleSystemComponent> ArrowBodyEffect;
FTimerHandle DestroyEffectHandle;
public:
void FireInDirection(const FVector& ShootDirection, AActor* InOwner);
/*OverlappedComponent: 이벤트를 발생시킨 컴포넌트(현재 객체의 컴포넌트).
OtherActor: 충돌한 다른 액터.
OtherComp : OtherActor에서의 충돌한 컴포넌트.
OtherBodyIndex : 충돌한 다른 액터의 본체 인덱스(본체가 여러 개인 경우).
bFromSweep : 스위프트 충돌 여부(충돌을 스위프트로 검출했는지 여부).
SweepResult : 충돌 정보를 제공하는 FHitResult 구조체.*/
UFUNCTION()
void OnArrowHitAnyWhere(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
void DestroyArrow();
public:
// Called every frame
virtual void Tick(float DeltaTime) override;
};
APMArrowProjectile::APMArrowProjectile()
{
PrimaryActorTick.bCanEverTick = true;
USceneComponent* SceneRoot = CreateDefaultSubobject<USceneComponent>(TEXT("SceneRoot"));
RootComponent = SceneRoot;
ArrowBody = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("ArrowBody"));
ArrowBody->SetupAttachment(SceneRoot);
ArrowHeadEffect = CreateDefaultSubobject<UParticleSystemComponent>(TEXT("ArrowHeadEffect"));
ArrowBodyEffect = CreateDefaultSubobject<UParticleSystemComponent>(TEXT("ArrowBodyEffect"));
DestroyEffect = CreateDefaultSubobject<UParticleSystemComponent>(TEXT("DestoryEffect"));
ArrowBodyEffect->SetupAttachment(ArrowBody);
ArrowHeadEffect->SetupAttachment(ArrowBody);
DestroyEffect->SetupAttachment(SceneRoot);
DestroyEffect->bAutoActivate = false;
DestroyEffect->SetVisibility(false);
static ConstructorHelpers::FObjectFinder<UStaticMesh> ArrowBodyRef(TEXT("/Script/Engine.StaticMesh'/Game/ParagonSparrow/FX/Meshes/Heroes/Sparrow/Abilities/SM_Sparrow_Arrow.SM_Sparrow_Arrow'"));
if (ArrowBodyRef.Object)
{
ArrowBody->SetStaticMesh(ArrowBodyRef.Object);
}
static ConstructorHelpers::FObjectFinder<UMaterial> ArrowMaterialRef(TEXT("/Script/Engine.Material'/Game/ParagonSparrow/Characters/Heroes/Sparrow/Skins/Rogue/Materials/M_Sparrow_Rogue_Arrow.M_Sparrow_Rogue_Arrow'"));
if (ArrowMaterialRef.Object)
{
ArrowBody->SetMaterial(0, ArrowMaterialRef.Object);
}
ProjectileMovement = CreateDefaultSubobject<UProjectileMovementComponent>(TEXT("ArrowMovement"));
ProjectileMovement->SetUpdatedComponent(ArrowBody);
ProjectileMovement->InitialSpeed = 3000.f; // Set initial speed
ProjectileMovement->MaxSpeed = 3000.f; // Set maximum speed
ProjectileMovement->bRotationFollowsVelocity = false; // Rotate based on velocity
ProjectileMovement->bShouldBounce = false; // Disable bouncing
ProjectileMovement->ProjectileGravityScale = 0.0f; // Add some gravity
ArrowBody->SetGenerateOverlapEvents(true);
ArrowBody->OnComponentBeginOverlap.AddDynamic(this, &APMArrowProjectile::OnArrowHitAnyWhere);
InitialLifeSpan = 5.f;
}
화살의 충돌 판정
화살의 충돌을 판별하는 방법이 여러개가 있지만 그 중 하나인 Collision을 Overlap으로 화살을 만들고 그 뒤에 Overlap 이벤트를 발생해 확인하는 작업을 할 것이다.
그리고 화살끼리의 충돌을 방지하기 위해 ObjectChannel을 하나 추가하고 그 경우는 Ignore 처리하자.
void APMArrowProjectile::OnArrowHitAnyWhere(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
if (OtherActor && Owner && OtherActor != this && OtherActor != Owner && OtherComp)
{
// 데미지를 주기
UE_LOG(LogTemp, Log, TEXT("%s"), *OtherActor->GetName());
// 이펙트 재생
DestroyArrow();
}
}
void APMArrowProjectile::DestroyArrow()
{
ProjectileMovement->StopMovementImmediately();
ProjectileMovement->SetComponentTickEnabled(false);
ArrowBody->SetVisibility(false);
ArrowBodyEffect->SetVisibility(false);
ArrowHeadEffect->SetVisibility(false);
if (DestroyEffect && DestroyEffect->Template)
{
DestroyEffect->SetRelativeLocation(ArrowBody->GetRelativeLocation());
DestroyEffect->SetVisibility(true);
DestroyEffect->ActivateSystem();
// 파티클 시스템 Duration 계산
float ParticleDuration = PMUtils::GetParticleSystemDuration(DestroyEffect->Template);
// 만약 무한 루프(== Max())면 즉시 Destroy하는 등 로직 분기
if (ParticleDuration >= TNumericLimits<float>::Max())
{
// Emitter 중 하나라도 무한 루프면, 파티클 끄고 바로 Destroy
DestroyEffect->DeactivateSystem();
Destroy();
}
else
{
GetWorld()->GetTimerManager().SetTimer(DestroyEffectHandle,
FTimerDelegate::CreateLambda(
[this]()
{
Destroy();
}
), ParticleDuration, false
);
}
}
else
{
Destroy();
}
}
void APMPlayer::FireArrowBegin(EAttackTypes AttackType)
{
USkeletalMeshComponent* SkeletalMesh = GetMesh();
// 화살 가리기
if (GetMesh())
{
FName BoneName(TEXT("arrow_nock"));
GetMesh()->HideBoneByName(BoneName, EPhysBodyOp::PBO_None);
switch (AttackType)
{
case EAttackTypes::NormalAttack:
if (NormalArrow)
{
FVector SpawnLocation = Center->GetComponentLocation();
FRotator SpawnRotation = Center->GetComponentRotation();
FActorSpawnParameters SpawnParams;
SpawnParams.Owner = this;
SpawnParams.Instigator = GetInstigator();
APMArrowProjectile* Arrow = Cast<APMArrowProjectile>(GetWorld()->SpawnActor<APMArrowProjectile>(NormalArrow, SpawnLocation, SpawnRotation, SpawnParams));
if (Arrow)
{
FVector ShootDirection = SpawnRotation.Vector();
Arrow->FireInDirection(ShootDirection, this);
}
else
{
UE_LOG(LogTemp, Log, TEXT("asd"));
}
}
break;
case EAttackTypes::SkillQ:
break;
case EAttackTypes::SkillE:
break;
case EAttackTypes::SkillR:
break;
default:
break;
}
}
}
플레이어 화살 발사 부분에서 액터를 소환해주면 된다.
결과
'Unreal Engine 5 > ProjectM' 카테고리의 다른 글
[UE5] ProjectM - 점프와 타이머, 카메라 범위 제한 (1) | 2024.12.24 |
---|---|
[UE5] ProjectM - 캐릭터 움직임과 애니메이션 (1) | 2024.12.23 |