[UE5 C++] 마법사 - 원거리 공격, 특수 동작

LeeTaes·2024년 5월 10일
0

[UE_Project] MysticMaze

목록 보기
11/17

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

  • 마법사의 원거리 공격(기본 공격) 구현
  • 우클릭으로 에너지를 모으는 동작 추가(추후 스킬에 사용 예정)

직업 - 마법사 구현하기

  • 기존 포스팅에서 구현했던 궁수와 유사합니다.
  • 애니메이션을 추가하고, InputAction을 추가하는 부분까지는 생략하도록 하겠습니다.
  • 스태프 무기를 장착하고, 기본 공격을 위한 소켓 3가지를 추가해주도록 합니다.
    - 마법사는 기본 공격으로 에너지볼을 스폰하여 발사합니다.


무기(스태프) 구현하기

  • 스태프는 기존 검과 활을 절반씩 섞은 느낌입니다.
  • 에너지 볼을 발사하기 위한 클래스 정보를 가지고 있으며 공격 시 에너지 볼을 스폰하여 발사합니다.

MMStaffWeapon Class

  • 기존 화살을 소환하고 발사하는 로직과 유사합니다.
  • 발사 동작에 시간이 걸리기에 스폰과 동시에 발사할 방향을 저장합니다.
// MMStaffWeapon Header
#pragma once

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

/**
 * 
 */
UCLASS()
class MYSTICMAZE_API AMMStaffWeapon : public AMMWeapon
{
	GENERATED_BODY()
	
public:
	AMMStaffWeapon();
	
protected:
	virtual void BeginPlay() override;
	virtual void Tick(float DeltaSeconds) override;

public:
	// 생성 및 발사 함수
	void SpawnEnergyBall();
	void ShootEnergyBall();

protected:
	// 타겟 위치를 구하기 위한 함수
	void SetFireLocation();

	// 에너지볼 클래스
	UPROPERTY(VisibleAnywhere, Category = "Quiver", meta = (AllowPrivateAccess = "true"))
	TSubclassOf<AActor> EnergyBallClass;

	// 현재 스폰된 에너지볼 객체
	UPROPERTY()
	TObjectPtr<class AMMEnergyBall> TempEnergyBall;

	// 에너지 볼을 스폰하기 위한 소켓
	FName EnergyBallSocket;

private:
	FVector FireLocation;
};
// MMStaffWeapon Cpp

#include "Item/MMStaffWeapon.h"
#include "Item/MMEnergyBall.h"
#include "Interface/MMPlayerVisualInterface.h"
#include "Collision/MMCollision.h"

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

AMMStaffWeapon::AMMStaffWeapon()
{
	//PrimaryActorTick.bCanEverTick = true;

	static ConstructorHelpers::FClassFinder<AActor>EnergyBallRef(TEXT("/Script/Engine.Blueprint'/Game/MysticMaze/Items/Weapons/BP_EnergyBall.BP_EnergyBall_C'"));
	if (EnergyBallRef.Succeeded())
	{
		EnergyBallClass = EnergyBallRef.Class;
	}

	WeaponType = EWeaponType::WT_Staff;
	BaseSocketName = TEXT("StaffPosition");
	DrawSocketName = TEXT("StaffSocket");
	EnergyBallSocket = TEXT("EnergyBallSocket");
}

void AMMStaffWeapon::BeginPlay()
{
	Super::BeginPlay();
}

void AMMStaffWeapon::Tick(float DeltaSeconds)
{
	Super::Tick(DeltaSeconds);
}

void AMMStaffWeapon::SpawnEnergyBall()
{
	TempEnergyBall = Cast<AMMEnergyBall>(GetWorld()->SpawnActor(EnergyBallClass));

	if (TempEnergyBall)
	{
		// 에너지 볼을 소켓에 부착합니다.
		ACharacter* PlayerCharacter = Cast<ACharacter>(GetOwner());
		if (PlayerCharacter)
		{
			TempEnergyBall->SetOwner(Owner);
			TempEnergyBall->AttachToComponent(PlayerCharacter->GetMesh(), FAttachmentTransformRules::KeepRelativeTransform, EnergyBallSocket);
		}

		// 발사할 위치를 선정합니다.
		SetFireLocation();
	}
}

void AMMStaffWeapon::ShootEnergyBall()
{
	if (TempEnergyBall)
	{
		// 부모 액터로부터 부착 해제
		TempEnergyBall->DetachFromActor(FDetachmentTransformRules::KeepWorldTransform);

		TempEnergyBall->Fire(FireLocation);
		TempEnergyBall = nullptr;
	}
}

void AMMStaffWeapon::SetFireLocation()
{
	// 플레이어의 카메라에서 화면의 중앙으로 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
		);

		if (bHasHit)
		{
			FireLocation = HitResult.ImpactPoint;
		}
		else
		{
			FireLocation = End;
		}

		// 플레이어를 회전시켜줍니다.
		ACharacter* Character = Cast<ACharacter>(GetOwner());
		if (Character)
		{
			FRotator PlayerRotator = Character->GetActorRotation();
			Character->SetActorRotation(FRotator(PlayerRotator.Pitch, Character->GetControlRotation().Yaw, PlayerRotator.Roll));
		}
	}
}

에너지 볼 구현하기

MMEnergyBall Class

  • 마법사의 평타 공격에 사용될 에너지 볼 클래스를 제작합니다.
  • 스폰 시 파티클 시스템 컴포넌트를 활성화 시켜줍니다.
  • 히트 시 Hit 파티클을 재생하고 비활성화 시켜줍니다.
// MMEnergyBall Header
#pragma once

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

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

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 = "Particle", meta = (AllowPrivateAccess = "true"))
	TObjectPtr<class UParticleSystemComponent> EnergyBallParticleSystemComponent;

	UPROPERTY(VisibleAnywhere, Category = "Particle", meta = (AllowPrivateAccess = "true"))
	TObjectPtr<class UParticleSystemComponent> HitParticleSystemComponent;

	UPROPERTY(VisibleAnywhere, Category = "Collision", meta = (AllowPrivateAccess = "true"))
	TObjectPtr<class USphereComponent> EnergyBallCollision;

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

private:
	float Speed = 3000.0f;
	uint8 bIsHit : 1;
};
// MMEnergyBall Cpp

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

#include "Components/SphereComponent.h"
#include "GameFramework/ProjectileMovementComponent.h"
#include "Particles/ParticleSystemComponent.h"

// Sets default values
AMMEnergyBall::AMMEnergyBall()
{
	EnergyBallCollision = CreateDefaultSubobject<USphereComponent>(TEXT("EnergyBallCollision"));
	EnergyBallCollision->SetSphereRadius(8.0f);
	EnergyBallCollision->SetCollisionProfileName(MMWEAPON);
	RootComponent = EnergyBallCollision;

	EnergyBallParticleSystemComponent = CreateDefaultSubobject<UParticleSystemComponent>(TEXT("EnergyBallParticleSystemComponent"));
	EnergyBallParticleSystemComponent->SetupAttachment(EnergyBallCollision);

	HitParticleSystemComponent = CreateDefaultSubobject<UParticleSystemComponent>(TEXT("HitParticleSystemComponent"));
	HitParticleSystemComponent->SetupAttachment(EnergyBallCollision);

	MovementComponent = CreateDefaultSubobject<UProjectileMovementComponent>(TEXT("MovementComponent"));
	MovementComponent->bRotationFollowsVelocity = true;
	MovementComponent->InitialSpeed = Speed;
	bIsHit = false;
}

// Called when the game starts or when spawned
void AMMEnergyBall::BeginPlay()
{
	Super::BeginPlay();
	
	// Projectile Movement Component 비활성화
	MovementComponent->SetActive(false);

	HitParticleSystemComponent->SetActive(false);
}

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

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

void AMMEnergyBall::Fire(FVector TargetLocation)
{
	FVector LaunchDirection;

	// 방향 구하기
	if ((TargetLocation - Owner->GetActorLocation()).Length() < 300.0f)
	{
		LaunchDirection = (TargetLocation - GetActorLocation()).GetSafeNormal();
		LaunchDirection.Z = 0.0f;
	}
	else
	{
		LaunchDirection = (TargetLocation - GetActorLocation()).GetSafeNormal();
	}
	
	// 방향 지정 및 Projectile Movement Component 활성화
	MovementComponent->Velocity = LaunchDirection * MovementComponent->InitialSpeed;
	MovementComponent->Activate();

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

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

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

	EnergyBallParticleSystemComponent->SetActive(false);
	HitParticleSystemComponent->SetActive(true);
	bIsHit = true;
}

원거리 공격 추가하기

  • 좌클릭으로 플레이어의 기본 콤보 공격을 사용 가능했습니다.
  • Maze 콤보 공격 몽타주와 데이터를 등록해주도록 합니다.
    - 기존과 같이 좌클릭으로 에너지 볼을 발사할 수 있습니다.

애님 노피파이 2종

  • MMSpawnEnergyBall
  • MMShootEnergyBall

특수 동작 구현하기

  • 지금까지 전사는 가드 동작, 궁수는 화살 발사의 동작을 추가하였습니다.
  • 마법사의 경우 기를 모으는 동작을 추가해보도록 하겠습니다.
    - 추후 스킬 데미지 증가에 사용
  1. 입력 액션을 추가하고 함수를 매핑해주도록 합니다.

  2. 기모으기 행동 시 스폰할 파티클 시스템 컴포넌트를 추가합니다.

// Header
// Particle Section
protected:
	UPROPERTY(VisibleAnywhere, Category = "Particle", meta = (AllowPrivateAccess = "true"))
	TObjectPtr<class UParticleSystemComponent> ChargeParticleSystemComponent;
    
// CPP
AMMPlayerCharacter::AMMPlayerCharacter()
{
	...
    
	// Effect
	{
		ChargeParticleSystemComponent = CreateDefaultSubobject<UParticleSystemComponent>(TEXT("UParticleSystemComponent"));
		ChargeParticleSystemComponent->SetupAttachment(RootComponent);
		ChargeParticleSystemComponent->SetRelativeLocation(FVector(0.0f, 0.0f, -90.0f));
		static ConstructorHelpers::FObjectFinder<UParticleSystem>ChargeParticleRef(TEXT("/Script/Engine.ParticleSystem'/Game/FXVarietyPack/Particles/P_ky_healAura.P_ky_healAura'"));
		if (ChargeParticleRef.Object)
		{
			ChargeParticleSystemComponent->Template = ChargeParticleRef.Object;
		}
	}
}
  1. 입력 액션과 매핑한 함수들을 구현합니다.

  2. 차지하고 있는 동안 Tick 함수에서 값을 증가시켜줍니다.


결과

  • 구현 후 테스트용 무기 코드를 제거하고, Merge하여.. 영상을 남기지 못하였습니다.
  • 마법사는 우클릭 시 아래 이펙트를 방출하며 ChargeNum을 증가시키도록 구현이 완료되었습니다.

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

0개의 댓글