플레이어 스킬 제작

JUNWOO KIM·2023년 10월 12일

Unreal

목록 보기
2/3

본 프로젝트는 언리얼5.1.1버전을 사용하여 제작하였습니다.
현재 진행하고 있는 언리얼 슈팅 게임 프로젝트의 제작 상황을 작성하였습니다.
게임 내 무료 에셋을 사용하고 있습니다.

제작할 내용

이번에는 코스트를 소모하여 사용 가능한 플레이어의 공격 스킬을 CascadeParticleSystemAnimationMontage를 사용하여 제작하겠습니다.

공격 애니메이션 설정

Animation Montage에서 자신이 제작한 Notifies기능을 사용하기 위해서는 UAnimNotify를 상속받은 C++파일을 사용해야 합니다.
Tools -> New C++ Class 를 클릭하여 UAnimNotify를 상속받은 C++파일을 만들어줍니다.

이후 제작된 C++파일에 코드를 작성해줍니다.
Interface를 이용하여 플레이어에게 스킬을 사용하라고 알려줍니다.

AnimNotify_SpecialAttackFire.h

virtual void Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, const FAnimNotifyEventReference& EventReference) override;
AnimNotify_SpecialAttackFire.cpp

void UAnimNotify_SpecialAttackFire::Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, const FAnimNotifyEventReference& EventReference)
{
	Super::Notify(MeshComp, Animation, EventReference);
	if (MeshComp)
	{
		IABAnimationAttackInterface* AttackPawn = Cast<IABAnimationAttackInterface>(MeshComp->GetOwner());
		if (AttackPawn)
		{
			AttackPawn->SpecialAttackFire();
		}
	}
}

작성한 코드를 저장 후 새로운 AnimationMontage를 생성해줍니다.

스킬 애니메이션이 상체에 잘 작동될 수 있도록 Group을 UpperBodySlot에 등록시켜 줍니다.
이후에 등에서 무기가 나와서 발사하는 애니메이션을 순서대로 연결시켰습니다.
애니메이션을 확인하며 스킬을 발사하는 모션 위치를 찾아 전에 만든 Notify를 설정해줍니다.

이제 플레이어가 발사할 스킬을 만들어줍니다.
에너지 구를 발사한 후 적에게 적중할 시 커다란 토네이도를 생성시켜 주변의 적도 같이 정리할 수 있도록 제작할 예정입니다.
제작에 필요한 두 CascadeParticleSystem을 준비합니다. 저는 보라색의 파티클들을 준비하였습니다.

Actor를 상속받은 C++파일을 만들어서 프로그래밍합니다.
충돌했을 때 보여줄 파티클을 추가로 넣었으며 적과 충돌을 알아내기 위하여 Collision을 추가하였습니다.

ABSpecialBullet.cpp

AABSpecialBullet::AABSpecialBullet()
{
	PrimaryActorTick.bCanEverTick = true;

	BulletParticle = CreateDefaultSubobject<UParticleSystemComponent>(TEXT("BulletParticle"));
	BulletHitParticle = CreateDefaultSubobject<UParticleSystemComponent>(TEXT("BulletHitParticle"));
	TornadoParticle = CreateDefaultSubobject<UParticleSystemComponent>(TEXT("TornadoParticle"));
	BulletMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
	BulletCollision = CreateDefaultSubobject<UCapsuleComponent>(TEXT("CapsuleCollision"));

	RootComponent = BulletMesh;	//루트 오브젝트 설정
	BulletParticle->SetupAttachment(BulletMesh);
	BulletParticle->SetRelativeScale3D(FVector(0.5f, 0.5f, 0.5f));
	BulletHitParticle->SetupAttachment(BulletMesh);
	BulletHitParticle->SetRelativeScale3D(FVector(0.5f, 0.5f, 0.5f));
	TornadoParticle->SetupAttachment(BulletMesh);
	TornadoParticle->SetRelativeScale3D(FVector(0.9f, 0.9f, 0.9f));

	static ConstructorHelpers::FObjectFinder<UParticleSystem> BulletParticleRef(TEXT("/Script/Engine.ParticleSystem'/Game/FXVarietyPack/Particles/P_ky_thunderBall.P_ky_thunderBall'"));
	if (BulletParticleRef.Object)
	{
		BulletParticle->SetTemplate(BulletParticleRef.Object);
	}
	static ConstructorHelpers::FObjectFinder<UParticleSystem> BulletHitParticleRef(TEXT("/Script/Engine.ParticleSystem'/Game/FXVarietyPack/Particles/P_ky_ThunderBallHit.P_ky_ThunderBallHit'"));
	if (BulletHitParticleRef.Object)
	{
		BulletHitParticle->SetTemplate(BulletHitParticleRef.Object);
		BulletHitParticle->bAutoActivate = false;
	}

	static ConstructorHelpers::FObjectFinder<UParticleSystem> TornadoParticleRef(TEXT("/Script/Engine.ParticleSystem'/Game/FXVarietyPack/Particles/P_ky_darkStorm.P_ky_darkStorm'"));
	if (TornadoParticleRef.Object)
	{
		TornadoParticle->SetTemplate(TornadoParticleRef.Object);
		TornadoParticle->bAutoActivate = false;
	}

	BulletHitParticle->DeactivateSystem();
	TornadoParticle->DeactivateSystem();
	BulletCollision->InitCapsuleSize(25.0f, 25.0f);
	BulletMesh->SetGenerateOverlapEvents(true);
	BulletMesh->SetCollisionProfileName(TEXT("SpecialBullet"));

	Speed = 15;
	Angle = 0;
	LifeTime = 0;
	TornadoLifeTime = 0;
	bIsHit = false;
}

NotifyActorBeginOverlap과 Tag를 사용하여 적과의 충돌을 확인해줍니다. 만약 충돌하면 충돌 이펙트와 토네이도 이펙트를 실행시켜줍니다.
이후 TickInterval의 시간을 조절하여 토네이도가 정해진 Tick마다 주변의 적에게 데미지를 입힐 수 있도록 제작하였습니다.

void AABSpecialBullet::NotifyActorBeginOverlap(AActor* OtherActor)
{
	if (OtherActor->Tags.Num() != 0 && Tags.Num() != 0)
	{
		if (!OtherActor->ActorHasTag(Tags[0]) && OtherActor->Tags.Num() == 1 && OtherActor->ActorHasTag(FName("Enemy")))
		{
			if (!bIsHit)
			{
				SetActorLocation(FVector(OtherActor->GetActorLocation().X, OtherActor->GetActorLocation().Y, 78.0f));
				FDamageEvent DamageEvent;
				OtherActor->TakeDamage(Damage, DamageEvent, Controller, Controller->GetPawn());
				BulletParticle->DeactivateSystem();
				BulletHitParticle->ActivateSystem(true);
				TornadoParticle->ActivateSystem(true);
				PrimaryActorTick.TickInterval = 0.25f;
				bIsHit = true;
			}
		}
	}
}

토네이도 내 주변의 적 탐지는 Collision을 사용했었지만 근처의 적들을 찾는 것이 어려웠습니다.
대신 SweepMultiByChannel을 사용하여 Tick마다 감지된 적들에게 데미지를 주었습니다.
또한 ENABLE_DRAW_DEBUG를 사용하여 개발 시 토네이도의 피해 범위를 시각적으로 확인할 수 있었습니다.

void AABSpecialBullet::CheckEnemy()
{
	TArray<FHitResult> OutHitResult;
	FCollisionQueryParams Params(SCENE_QUERY_STAT(Attack), false, this);

	const float AttackRange = 180.0f;
	const float AttackRadius = 180.0f;
	const FVector Start = GetActorLocation();
	const FVector End = Start;

	bool HitDetected = GetWorld()->SweepMultiByChannel(OutHitResult, Start, End, FQuat::Identity, ECC_GameTraceChannel1, 
														FCollisionShape::MakeSphere(AttackRadius), Params);
	if (HitDetected)
	{
		for (FHitResult Enemy : OutHitResult)
		{
			if (Enemy.GetActor()->Tags.Num() != 0)
			{
				if (!Enemy.GetActor()->ActorHasTag(Tags[0]) && Enemy.GetActor()->Tags.Num() == 1 && 
					Enemy.GetActor()->ActorHasTag(FName("Enemy")))
				{
					FDamageEvent DamageEvent;
					Enemy.GetActor()->TakeDamage(Damage / 2, DamageEvent, Controller, Controller->GetPawn());
				}
			}
		}	
	}

#if ENABLE_DRAW_DEBUG

	FVector CapsuleOrigin = Start + (End - Start) * 0.5f;
	float CapsuleHalfHeight = AttackRange * 0.5f;
	FColor DrawColor = HitDetected ? FColor::Green : FColor::Red;

	DrawDebugCapsule(GetWorld(), CapsuleOrigin, CapsuleHalfHeight, AttackRadius, FRotationMatrix::MakeFromZ(GetActorForwardVector()).ToQuat(), DrawColor, false, 0.25f);

#endif
}

스킬이 완성되면 C++파일로 돌아와서 플레이어의 SpecialAttackFire함수를 완성시켜 스킬이 사용될 수 있도록 제작합니다.

GetSocketLocation을 사용해 스킬의 생성 위치를 Mesh에서 찾아줍니다.
이후 SapwnActorDeferred를 사용해 스킬의 필요한 정보들을 넣어준 후 FinishSpawning을 적어 완료합니다.

AABCharacterBase.cpp

void AABCharacterBase::SpecialAttackFire()
{
	const FTransform SpawnTransform(GetMesh()->GetSocketLocation("Muzzle_03"));
	AABSpecialBullet* ABSpecialBullet = GetWorld()->SpawnActorDeferred<AABSpecialBullet>(SpecialBulletClass, SpawnTransform);

	if (ABSpecialBullet)
	{
		ABSpecialBullet->Tags.Add(Tags[0]);
		ABSpecialBullet->Tags.Add(FName("Bullet"));
		ABSpecialBullet->Controller = GetController();
		ABSpecialBullet->Damage = Stat->GetTotalStat().Attack * 2;
		ABSpecialBullet->Angle = BulletAngle;
		ABSpecialBullet->FinishSpawning(SpawnTransform);
	}
}

그리고 스킬이 일정량의 코스트를 지불하여 사용할 수 있도록 제작해줍니다.
일정량의 코스트가 있다면 정해진 AnimationMontage를 실행해줍니다.

AMyABCharacterPlayer.cpp

void AMyABCharacterPlayer::SpecialAttack()
{
	if (Stat->GetCurrentEnerge() - Stat->SkillCost >= 0)
	{
		Stat->SetEnerge(Stat->GetCurrentEnerge() - Stat->SkillCost);

		CurrentCombo = 1;

		const float AttackSpeedRate = Stat->GetTotalStat().AttackSpeed;
		UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
		AnimInstance->Montage_Play(SpecialActionMontage, AttackSpeedRate);

		FOnMontageEnded EndDelegate;
		EndDelegate.BindUObject(this, &AABCharacterBase::ComboActionEnd);
		AnimInstance->Montage_SetEndDelegate(EndDelegate, SpecialActionMontage);

		bIsSpecialAttack = true;
		ComboTimerHandle.Invalidate();
		SetComboCheckTimer();
	}

}

제작한 스킬이 잘 작동하는지 확인해줍니다.

오늘은 여기까지 하겠습니다. 다들 편안한 하루 되세요.

profile
게임 프로그래머 준비생

0개의 댓글