지난번엔 하늘에서 떨어지는 애로우 레인 과
4발을 연속적으로 발사하는 스트레이프를 만들어 봤다.
실제 데미지까지 입히는 것은 추후 몬스터까지 만든 후 제작하겠다.
이번에는 궁극기인 스나이핑과 골든 호크라는 소환수 스킬을 제작해보겠다.
Skill 3 - Sniping
스나이핑은 말 그대로 저격하는 스킬로 만들것이다. 물론 실제로 저격하는 것은 아니지만 아주 쎈 한 발의 화살을 날린다. 라는 개념으로 만들 것이다.
차지 시간이 길지만 그만큼 큰 데미지와 주변에 스플레시 데미지를 입힐 것이다.
Sniping Actor
PMSniping 이라는 Actor class를 만들고 이를 BP로 상속받아서 사용한다.
결국 이 Sniping도 기본공격이나 스트레이프와 같이 화살을 발사하는 것은 동일하기에 override를 해서 구현한다.
다만 게임 특성상 판정을 쉽게하기 위해 CapsuleComponent를 가로로 눕힌다음 Width와 Height를 적절히 조절해 타격 포인트를 만든다.
그 아래 자식으로는 화살 이펙트들을 달아준다.
#pragma once
#include "CoreMinimal.h"
#include "Projectile/PMProjectile.h"
#include "PMSniping.generated.h"
/**
*
*/
UCLASS()
class PROJECTM_API APMSniping : public APMProjectile
{
GENERATED_BODY()
public:
APMSniping();
protected:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Collision)
TObjectPtr<class UCapsuleComponent> Body;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Particle)
TObjectPtr<class UParticleSystemComponent> ArrowBodyEffect;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Particle)
TObjectPtr<class UParticleSystemComponent> DestroyEffect;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Movement)
TObjectPtr<class UProjectileMovementComponent> ProjectileMovement;
FTimerHandle DestroyEffectHandle;
/*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();
virtual void FireArrow(AActor* InOwner, FVector InShootLocation, FRotator InShootRotation, int32 InDamage = 0) override;
};
Construct
위에서 생각한대로 Construct를 작성해준다.
APMSniping::APMSniping()
{
USceneComponent* SceneRoot = CreateDefaultSubobject<USceneComponent>(TEXT("SceneRoot"));
RootComponent = SceneRoot;
Body = CreateDefaultSubobject<UCapsuleComponent>(TEXT("BodyCollision"));
Body->SetupAttachment(SceneRoot);
ArrowBodyEffect = CreateDefaultSubobject<UParticleSystemComponent>(TEXT("ArrowBodyEffect"));
ArrowBodyEffect->SetupAttachment(Body);
ArrowBodyEffect->bAutoActivate = true;
DestroyEffect = CreateDefaultSubobject<UParticleSystemComponent>(TEXT("DestroyEffect"));
DestroyEffect->SetupAttachment(Body);
DestroyEffect->bAutoActivate = false;
ProjectileMovement = CreateDefaultSubobject<UProjectileMovementComponent>(TEXT("ProjectileMovement"));
ProjectileMovement->SetUpdatedComponent(Body);
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
Body->SetGenerateOverlapEvents(true);
Body->OnComponentBeginOverlap.AddDynamic(this, &APMSniping::OnArrowHitAnyWhere);
InitialLifeSpan = 5.f;
}
스나이핑 스킬 사용
스킬에 대한 사용을 InputMapping을 이용해 R 버튼과 연결해주고 R 버튼을 눌렀을 때는 다음과 같이 작동하도록 PMPlayer.cpp에서 작성해준다.
case EAttackTypes::SkillR:
if (Sniping)
{
FVector SpawnLocation = Center->GetComponentLocation();
FRotator SpawnRotation = Center->GetComponentRotation();
FActorSpawnParameters SpawnParams;
SpawnParams.Owner = this;
SpawnParams.Instigator = GetInstigator();
APMProjectile* Arrow = Cast<APMProjectile>(GetWorld()->SpawnActor<APMSniping>(Sniping, SpawnLocation, SpawnRotation, SpawnParams));
if (Arrow)
{
Arrow->FireArrow(this, SpawnLocation, SpawnRotation, 0);
}
}
break;
이제 FireArrow 부분을 작성한다.
void APMSniping::OnArrowHitAnyWhere(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
if (OtherActor && Owner && OtherActor != this && OtherActor != Owner && OtherComp)
{
// 이펙트 주기
FVector Center = Body->GetComponentLocation();
const float SphereRadius = 300.f;
FCollisionShape CollisionSphere = FCollisionShape::MakeSphere(SphereRadius);
FCollisionQueryParams Params(FName(TEXT("Attack")), false, this);
Params.MobilityType = EQueryMobilityType::Any;
TArray<FOverlapResult> Overlaps;
DrawDebugSphere(GetWorld(), Center, SphereRadius, 12, FColor::Green, false, 1.0f, 0, 2.0f);
bool bIsHit = GetWorld()->OverlapMultiByChannel(
Overlaps, // 결과를 저장할 배열
Center, // 중심점
FQuat::Identity, // 회전
ECC_PMAttack, // 채널
CollisionSphere, // 콜리전 형태
Params // 추가 파라미터
);
if (bIsHit)
{
for (auto& Overlap : Overlaps)
{
AActor* OverlapActor = Overlap.GetActor();
if (OverlapActor == Owner)
{
continue;
}
if (OverlapActor)
{
// 데미지를 받는 인터페이스를 생성해서 인터페이스가 구현됐는지 확인
UE_LOG(LogTemp, Log, TEXT("%s"), *OverlapActor->GetName())
}
}
}
DestroyArrow();
}
}
void APMSniping::DestroyArrow()
{
ProjectileMovement->StopMovementImmediately();
ProjectileMovement->SetComponentTickEnabled(false);
Body->SetCollisionEnabled(ECollisionEnabled::NoCollision);
ArrowBodyEffect->SetVisibility(false);
if (DestroyEffect && DestroyEffect->Template)
{
DestroyEffect->SetWorldLocation(Body->GetComponentLocation());
DestroyEffect->SetVisibility(true);
DestroyEffect->ActivateSystem();
float ParticleDuration = PMUtils::GetParticleSystemDuration(DestroyEffect->Template);
if (ParticleDuration >= TNumericLimits<float>::Max())
{
DestroyEffect->DeactivateSystem();
Destroy();
}
else
{
GetWorld()->GetTimerManager().SetTimer(DestroyEffectHandle,
FTimerDelegate::CreateLambda(
[this]()
{
Destroy();
}
), ParticleDuration, false
);
}
}
else
{
Destroy();
}
}
void APMSniping::FireArrow(AActor* InOwner, FVector InShootLocation, FRotator InShootRotation, int32 InDamage /*= 0*/)
{
Super::FireArrow(InOwner, InShootLocation, InShootRotation, InDamage);
if (ProjectileMovement)
{
SetOwner(InOwner);
ProjectileMovement->Velocity = InShootRotation.Vector() * ProjectileMovement->InitialSpeed;
}
}
화살이 발사되고 어느 물체에 닿으면 멈추고 주변에게 스플레시 데미지를 주게 만들었다.
또한 Destroy할 때 이펙트를 재생한 후 삭제되게 만들었다.
이제 데미지를 주는 작업은 Damage 인터페이스를 구현한 객체에게만 줄 계획이다.
결과
Skill 4 - Golden Hawk
골든 호크같은 경우 소환수로 스킬을 쓰면 2분간 플레이어 주변을 따라다니며 5초마다 적들을 감지해 가장 가까운 적에게 돌진해 데미지와 스턴을 주는 역할을 한다.
이를 구현하기 위해서는 일단 플레이어 주변을 계속 따라 다녀야한다는 점 과 자연스러운 움직임을 위해 플레이어 주변을 서성거리는 코드를 Tick에서 구현해야한다.
또한 공격에 대한 처리를 만들어 주어야한다.
Golden Hawk Actor
이 역시 하나의 C++로 만들고 이를 상속받는 BP로 제작할 것이다.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "PMGoldenHawk.generated.h"
UCLASS()
class PROJECTM_API APMGoldenHawk : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
APMGoldenHawk();
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Character, meta = (AllowPrivateAccess = "true"))
TObjectPtr<class UBoxComponent> BoxComponent;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Character, meta = (AllowPrivateAccess = "true"))
TObjectPtr<class USkeletalMeshComponent> SkeletalMeshComponent;
// Move Section
protected:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Movement, meta = (AllowPrivateAccess = "true"))
float MoveSpeed;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Movement, meta = (AllowPrivateAccess = "true"))
TObjectPtr<class APMPlayer> OwnerPlayer;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Movement, meta = (AllowPrivateAccess = "true"))
float WanderRange;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Movement, meta = (AllowPrivateAccess = "true"))
float WanderRandom;
// GoldenHawk.h
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Movement)
float WanderTime = 0.0f;
FVector TargetLocation;
FVector RandomWanderOffset;
float TimeSinceLastWander;
// Attack Section
protected:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Attack, meta = (AllowPrivateAccess = "true"))
float CurrentCheckInterval;
FTimerHandle EnemyCheckTimerHandle;
void SetEnemyCheckTimer();
UFUNCTION()
void CheckForNearByEnemy();
AActor* FindClosestEnemy(const TArray<FOverlapResult>& Overlaps);
void AttackEnemy(AActor* EnemyActor);
public:
// Called every frame
virtual void Tick(float DeltaTime) override;
void SettingSkill(class APMPlayer* InOwnerPlayer);
};
Construct
간단하게 생성자는 구현할 수 있다. 일단 RootComponent가 될 BoxComponent를 만든다. 이는 아무런 충돌이 없을 계획이다. 나중에 혹여나 추가하게될 가능성을 염두에 제작한 것이다.
그 아래로 SkeletalMeshComponent와 그것에 맞는 애니메이션을 넣어준다.
APMGoldenHawk::APMGoldenHawk()
{
// Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = true;
BoxComponent = CreateDefaultSubobject<UBoxComponent>("BoxComponent");
RootComponent = BoxComponent;
SkeletalMeshComponent = CreateDefaultSubobject<USkeletalMeshComponent>("SkeletalMeshComponent");
SkeletalMeshComponent->SetupAttachment(BoxComponent);
static ConstructorHelpers::FObjectFinder<USkeletalMesh> SkeletalMeshRef(TEXT("/Script/Engine.SkeletalMesh'/Game/QuadrapedCreatures/Griffon/Meshes/SK_Griffon.SK_Griffon'"));
if (SkeletalMeshRef.Object)
{
SkeletalMeshComponent->SetSkeletalMesh(SkeletalMeshRef.Object);
}
static ConstructorHelpers::FClassFinder<UAnimInstance> AnimInstanceClassRef(TEXT("/Script/Engine.AnimBlueprint'/Game/ProjectM/Animation/GoldenHawk/BP_PMGoldenHawk.BP_PMGoldenHawkAnimation_C'"));
if (AnimInstanceClassRef.Class)
{
SkeletalMeshComponent->SetAnimInstanceClass(AnimInstanceClassRef.Class);
}
MoveSpeed = 200.f;
WanderRange = 100.f;
WanderRandom = 10.f;
TimeSinceLastWander = 0.f;
InitialLifeSpan = 120.f;
}
기본 지속시간은 120초로 설정한다.
자연스러운 움직임
플레이어를 따라다니며 무한대의 모양을 그리며 자유로운 비행을 하기 위해 다음과 같은 수학 공식을 이용해 코드를 작성한다. 플레이어 스켈레톤에는 Pet이 위치할 장소를 소켓으로 지정해둔 상태이다.
void APMGoldenHawk::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
if (!OwnerPlayer)
return;
USkeletalMeshComponent* SkeletalMesh = OwnerPlayer->GetMesh();
if (!SkeletalMesh)
return;
FVector PetSocketLocation = SkeletalMesh->GetSocketLocation(TEXT("pet_location"));
WanderTime += DeltaTime;
if (WanderTime > 2 * PI)
{
WanderTime = 0.0f;
}
// 무한대 공식 (로컬 좌표계에서의 X, Z 계산)
float A = WanderRange;
float B = WanderRange * 0.75f;
float LocalY = (A * FMath::Cos(WanderTime)) / (1 + FMath::Pow(FMath::Sin(WanderTime), 2)); // 좌우 이동 (Y축)
float LocalZ = (B * FMath::Sin(WanderTime) * FMath::Cos(WanderTime)) / (1 + FMath::Pow(FMath::Sin(WanderTime), 2)); // 위아래 이동 (Z축)
FVector LocalOffset(0.0f, LocalY, LocalZ);
FVector RandomOffset(0.f,
FMath::RandRange(-WanderRandom, WanderRandom),
FMath::RandRange(-WanderRandom, WanderRandom));
LocalOffset += RandomOffset;
// 로컬 오프셋을 월드 좌표계로 변환
FVector WorldOffset = OwnerPlayer->GetActorRotation().RotateVector(LocalOffset);
// 목표 위치 계산
TargetLocation = PetSocketLocation + WorldOffset;
FVector NewLocation = FMath::VInterpTo(
GetActorLocation(),
TargetLocation,
DeltaTime,
MoveSpeed * 0.01f
);
SetActorLocation(NewLocation);
FRotator PlayerRotation = OwnerPlayer->GetActorRotation();
FRotator PetRotation = FRotator(0.0f, PlayerRotation.Yaw - 90.f, 0.0f);
SetActorRotation(FMath::RInterpTo(GetActorRotation(), PetRotation, DeltaTime, 5.0f));
}
방향은 항상 플레이어가 바라보는 방향을 보게끔 한다.
공격하기
공격같은 경우는 5초마다 타이머를 만들고 주변에 큰 원을 만들어 그 안에서 가장 가까운 적을 찾아 공격하는 것이다.
void APMGoldenHawk::BeginPlay()
{
Super::BeginPlay();
CurrentCheckInterval = 5.f;
SetEnemyCheckTimer();
}
void APMGoldenHawk::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
Super::EndPlay(EndPlayReason);
GetWorld()->GetTimerManager().ClearTimer(EnemyCheckTimerHandle);
}
void APMGoldenHawk::SetEnemyCheckTimer()
{
GetWorld()->GetTimerManager().SetTimer(
EnemyCheckTimerHandle,
this,
&APMGoldenHawk::CheckForNearByEnemy,
CurrentCheckInterval,
false
);
}
void APMGoldenHawk::CheckForNearByEnemy()
{
if (!OwnerPlayer)
{
Destroy();
return;
}
FVector HawkLocation = GetActorLocation();
float DetectionRadius = 1500.f;
FCollisionShape CollisionSphere = FCollisionShape::MakeSphere(DetectionRadius);
FCollisionQueryParams Params(FName(TEXT("Attack")), false, this);
Params.MobilityType = EQueryMobilityType::Any;
#if WITH_EDITOR
DrawDebugSphere(GetWorld(), HawkLocation, DetectionRadius, 12, FColor::Green, false, 3.0f);
#endif
TArray<FOverlapResult> Overlaps;
bool bIsHit = GetWorld()->OverlapMultiByChannel(
Overlaps, // 결과를 저장할 배열
HawkLocation, // 중심점
FQuat::Identity, // 회전
ECC_PMAttack, // 채널
CollisionSphere, // 콜리전 형태
Params // 추가 파라미터
);
if (bIsHit)
{
AActor* ClosestEnemy = FindClosestEnemy(Overlaps);
if (ClosestEnemy)
{
AttackEnemy(ClosestEnemy);
}
}
SetEnemyCheckTimer();
}
AActor* APMGoldenHawk::FindClosestEnemy(const TArray<FOverlapResult>& Overlaps)
{
AActor* ClosestEnemy = nullptr;
float MinDistance = FLT_MAX;
FVector HawkLocation = GetActorLocation();
for (auto& Overlap : Overlaps)
{
AActor* Actor = Overlap.GetActor();
if (Actor == nullptr) continue;
if (Actor == OwnerPlayer) continue;
float Distance = FVector::Dist(HawkLocation, Actor->GetActorLocation());
if (Distance < MinDistance)
{
MinDistance = Distance;
ClosestEnemy = Actor;
}
}
return ClosestEnemy;
}
void APMGoldenHawk::AttackEnemy(AActor* EnemyActor)
{
if (EnemyActor == nullptr) return;
// 데미지 주기 애니메이션 재생
UE_LOG(LogTemp, Log, TEXT("GoldenHawk Attack to % s"), *EnemyActor->GetName());
}
조금 코드가 복잡하지만 그래도 만족스러운 결과이다.
결과
'Unreal Engine 5 > ProjectM' 카테고리의 다른 글
[UE5] ProjectM - 스킬 디자인 구성 (0) | 2025.01.08 |
---|---|
[UE5] ProjectM - 공격 애니메이션과 화살 발사 (0) | 2025.01.03 |
[UE5] ProjectM - 점프와 타이머, 카메라 범위 제한 (1) | 2024.12.24 |
[UE5] ProjectM - 캐릭터 움직임과 애니메이션 (1) | 2024.12.23 |