Dev8 Interpolation, 적 타겟팅, 마법 스킬 등
보간법..? 이라고 칭하는 게 맞나..?
아무튼 플레이어가 적을 공격할 때 다른 곳을 보고 공격하지 않고 적을 보고 공격하도록 하는 것
Main.h
// When player attck enemy, player look at enemy
float InterpSpeed;
bool bInterpToEnemy;
void SetInterpToEnemy(bool Interp);
FRotator GetLookAtRotationYaw(FVector Target);
Main.cpp
in 생성자, 변수들 초기화
InterpSpeed = 15.f;
bInterpToEnemy = false;
SetInterpToEnemy메서드(함수는 독자적으로 하나의 기능을 하고 메서드는 클래스나 객체에 종속되어 기능을 하는 아무튼 그렇다 카더라)
void AMain::SetInterpToEnemy(bool Interp)
{
bInterpToEnemy = Interp;
}
in Tick, 타겟이 있어야함
if (bInterpToEnemy && CombatTarget)
{
FRotator LookAtYaw = GetLookAtRotationYaw(CombatTarget->GetActorLocation());
FRotator InterpRotation = FMath::RInterpTo(GetActorRotation(), LookAtYaw, DeltaTime, InterpSpeed); //smooth transition
SetActorRotation(InterpRotation);
}
FRotator AMain::GetLookAtRotationYaw(FVector Target)
{
FRotator LookAtRotation = UKismetMathLibrary::FindLookAtRotation(GetActorLocation(), Target);
FRotator LookAtRotationYaw(0.f, LookAtRotation.Yaw, 0.f);
return LookAtRotationYaw;
}
매개변수로 받은 타겟 위치와 플레이어 위치 이용해서 플레이어가 적을 바라보게 하는 회전값을 찾고..?
거기서 Yaw값을 이용해서 변수를 선언하는데..
FRotator LookAtRotationYaw(0.f, LookAtRotation.Yaw, 0.f);
이렇게도 변수 선언을 할 수 있나요..? 신기…..
아무튼 이것을 반환함
그리고 플레이어의 회전값과 목표회전값(?), 시간, 회전속도를 RInterpTo함수에 넣어서 회전값을 구한 뒤
플레이어의 로테이션 위치를 설정함. SetActorRotation함수는 액터클래스의 함수임.
적 타겟팅
in Main.h
//About Combat System
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Combat")
class USphereComponent* CombatSphere;
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Combat")
bool bOverlappingCombatSphere;
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Combat")
class AEnemy* CombatTarget;
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Combat")
FVector CombatTargetLocation;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Combat")
TArray<AEnemy*> Targets;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Combat")
bool bHasCombatTarget;
int targetIndex;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Controller")
class AMainPlayerController* MainPlayerController;
UFUNCTION()
virtual void CombatSphereOnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
UFUNCTION()
virtual void CombatSphereOnOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex);
void Targeting();
보시다시피 전투 범위 설정하고 타겟 변수 등등 두고 했는데
리스트 혼자 처음 써봄.. 리스트를 쓴 이유는 전투 범위에 적이 여러마리일 경우에 필요하기 때문.
탭 키를 사용해서 타겟팅하게 할 건데, 탭키를 눌러서 처음 타겟을 설정한 뒤, 탭키를 또 누르면 전투범위에 들어와있는 몬스터 중 (처음 타겟 말고) 다른 몬스터가 타겟이 됨. 즉, 전투범위에 몹이 여럿일 때 타겟을 바꾸게 하려고 한 것.
그래서 인덱스도 필요했음ㅇㅇ
그리고 타겟팅 화살표 표시해주는 걸 컨트롤러에서 제어하기 때문에 컨트롤러 변수도 선언
in Main.cpp
in 생성자
루트 컴포넌트 아래에 전투범위 붙여주고 600으로 설정, 인덱스 0으로 초기화
CombatSphere = CreateDefaultSubobject<USphereComponent>(TEXT("CombatSphere"));
CombatSphere->SetupAttachment(GetRootComponent());
CombatSphere->InitSphereRadius(600.f);
bOverlappingCombatSphere = false;
bHasCombatTarget = false;
targetIndex = 0
in BeginPlay()
CombatSphere->OnComponentBeginOverlap.AddDynamic(this, &AMain::CombatSphereOnOverlapBegin);
CombatSphere->OnComponentEndOverlap.AddDynamic(this, &AMain::CombatSphereOnOverlapEnd);
MainPlayerController = Cast<AMainPlayerController>(GetController());
OnComponent Begin/End Overlap함수 대신 실행할 함수를 정해준다..? 암튼 그런 거임
그리고 컨트롤러 할당해줌
in Tick
타겟이 있으면 타겟위치 변수에 현재 타겟의 위치 할당해주고
컨트롤러 있으면 컨트롤러의 적 위치 변수에다 타겟위치 할당해줌
if (CombatTarget)
{
CombatTargetLocation = CombatTarget->GetActorLocation();
if (MainPlayerController)
{
MainPlayerController->EnemyLocation = CombatTargetLocation;
}
}
void AMain::CombatSphereOnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
if (OtherActor)
{
AEnemy* Enemy = Cast<AEnemy>(OtherActor);
if (Enemy)
{
bOverlappingCombatSphere = true;
for (int i = 0; i < Targets.Num(); i++)
{
if (Enemy == Targets[i]) //already exist
{
return;
}
}
Targets.Add(Enemy);
}
}
}
전투 범위에 몹 들어오면 타겟 (가능)리스트에 들어온 순서대로 넣어줌. 참고로 여기서 중복 검사도 함
리스트에는 중복이 없도록. 이미 해당 몹이 리스트에 존재하면 메서드 나가기~
void AMain::CombatSphereOnOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
if (OtherActor)
{
AEnemy* Enemy = Cast<AEnemy>(OtherActor);
if (Enemy)
{
for (int i = 0; i < Targets.Num(); i++)
{
if (Enemy == Targets[i]) //already exist
{
Targets.Remove(Enemy); //타겟팅 가능 몹 배열에서 제거
}
}
if (Targets.Num() == 0)
{
bOverlappingCombatSphere = false;
}
if (CombatTarget == Enemy)
{
MainPlayerController->bTargetArrowVisible = false;
MainPlayerController->RemoveTargetArrow();
CombatTarget = nullptr;
bHasCombatTarget = false;
}
}
}
}
전투범위에서 몹이 나가면 리스트에서도 제거함
범위 안에 없으면 타겟팅 불가이기 때문.
또한 범위를 나간 애가 타겟팅되었던 애라면 컨트롤러 통해서 타겟팅 화살표 없애고,
타겟에 null로 지정
여기서 리스트 안에 들어간 요소의 수가 0일 때만 불 변수 false
요소가 0이 아니란 얘기는 아직 전투 범위 안에 몹이 있다는 뜻임. 근데 그냥 false 해버리면
아예 타겟팅이 불가능하게 됨. 불 변수를 true로 하려면 전투 범위 바깥에서 다시 전투범위로 들어오는(오버랩) 수 밖에 없음.
범위 안에 그대로 있는다고 오버랩되는 게 아님 ㅇㅇ
플젝 세팅에서 탭키 액션 매핑해주고
SetupPlayerInputComponent메서드에서 액션 바인딩
PlayerInputComponent->BindAction("Targeting", IE_Pressed, this, &AMain::Targeting);
타겟팅 메서드
void AMain::Targeting() //Targeting using Tap key
{
if (bOverlappingCombatSphere) //There is a enemy in combatsphere
{
if (targetIndex >= Targets.Num()) //타겟인덱스가 총 타겟 가능 몹 수 이상이면 다시 0으로 초기화
{
targetIndex = 0;
}
//There is already exist targeted enemy, then targetArrow remove
if (MainPlayerController->bTargetArrowVisible)
{
MainPlayerController->RemoveTargetArrow();
}
bHasCombatTarget = true;
CombatTarget = Targets[targetIndex];
//UE_LOG(LogTemp, Log, TEXT("%s"), *(CombatTarget->GetName()));
targetIndex++;
MainPlayerController->DisplayTargetArrow();
}
}
이 함수가 실행되었다는 건 탭키를 눌렀다는 거기에 기존에 타겟팅을 이미 해서 화살표가 보이는 상태면 없애고
현재 인덱스의 타겟 새롭게 지정해준 뒤 인덱스 증가시키고(다음에 탭키 누르면 다른 애가 타겟이 되어야하니까)
컨트롤러를 통해 타겟팅 화살표 보이도록 함.
MainPlayerController.h
위젯 클래스와 위젯 변수 선언 그리고 타겟팅 화살표(위젯) 보이게할지 말지하는 함수와 불변수
UCLASS()
class YARO_API AMainPlayerController : public APlayerController
{
GENERATED_BODY()
public:
//Player can Targeting, then TargetArrow appear on Targeted enemy
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Widgets")
TSubclassOf<UUserWidget> WTargetArrow;
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Widgets")
UUserWidget* TargetArrow;
bool bTargetArrowVisible;
void DisplayTargetArrow();
void RemoveTargetArrow();
FVector EnemyLocation;
protected:
virtual void BeginPlay() override;
virtual void Tick(float DeltaTime) override;
};
MainPlayerController.cpp
void AMainPlayerController::BeginPlay()
{
Super::BeginPlay();
this->PlayerCameraManager->ViewPitchMin = -50.f; // 세로회전 위
this->PlayerCameraManager->ViewPitchMax = 10.f; //아래
if (WTargetArrow)
{
TargetArrow = CreateWidget<UUserWidget>(this, WTargetArrow);
if (TargetArrow)
{
TargetArrow->AddToViewport();
TargetArrow->SetVisibility(ESlateVisibility::Hidden);
}
FVector2D Alignment(0.f, 0.f);
TargetArrow->SetAlignmentInViewport(Alignment);
}
}
타겟화살표 변수에 위젯 넣어줌. (에디터에서 WTargetArrow에다가 위젯 블루프린트 클래스 넣어줘야함)
그리고 타겟화살표 변수에 위젯이 잘 들어갔으면 뷰포트에 추가 후 안 보이게 함. (타겟팅할 때만 나와야함)
Alignment가 정렬, 뭐 그런 거 같은데 그냥 초기 위치 지정해준듯..?
void AMainPlayerController::DisplayTargetArrow()
{
if (TargetArrow)
{
bTargetArrowVisible = true;
TargetArrow->SetVisibility(ESlateVisibility::Visible);
}
}
void AMainPlayerController::RemoveTargetArrow()
{
if (TargetArrow)
{
bTargetArrowVisible = false;
TargetArrow->SetVisibility(ESlateVisibility::Hidden);
}
}
보이게 하거나 안 보이게 하기
void AMainPlayerController::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
if (TargetArrow)
{
FVector2D PositionInViewport;
ProjectWorldLocationToScreen(EnemyLocation, PositionInViewport);
PositionInViewport.Y -= 250.f;
PositionInViewport.X -= 50.f;
//FVector2D SizeInViewport = FVector2D(10.f, 10.f);
TargetArrow->SetPositionInViewport(PositionInViewport);
//TargetArrow->SetDesiredSizeInViewport(SizeInViewport);
}
}
월드에서의 (타겟)적 위치와 뷰포트에서의 위치를 받아서 (뷰포트에서 보이는)타겟팅화살표의 위치를 정해줌.
몹 머리 위쯤에 떠야하기 때문에 Y값을 빼서 좀 더 위로 올림. 그리고 오른쪽으로 조금 치우쳐 있길래 왼쪽으로 좀 당김.
뷰포트에서 왼쪽 상단 모서리가 X : 0, Y : 0 임
그리고 사이즈 정하는 함수는 저걸 쓰든 안 쓰든 변화가 없어서 주석 처리함
타겟화살표 위젯
음 사실 이건 유데미쌤이 체력바 조절에 바인딩한 건데 나는 컬러랑 투명도에 함.
참, 타겟 해제는 왼쪽 마우스 클릭으로 가능, 혹은 적이 플레이어의 전투범위 바깥으로 나가면 자동 해제
void AMain::LMBDown() //Left Mouse Button
{
bLMBDown = true;
if (ActiveOverlappingItem && !EquippedWeapon)
{
AWeapon* Weapon = Cast<AWeapon>(ActiveOverlappingItem);
if (Weapon)
{
Weapon->Equip(this);
SetActiveOverlappingItem(nullptr);
}
}
// Targeting Off
if (CombatTarget)
{
if (MainPlayerController->bTargetArrowVisible)
{
MainPlayerController->RemoveTargetArrow();
}
CombatTarget = nullptr;
bHasCombatTarget = false;
}
}
드디어 대망의 마법..!!
마법 스킬 블루프린트
원래는 플레이어 블루프린트 클래스에 유튜브 따라서 이런 식으로 먼저 시험해봄.
성공적으로 잘 됐고 이제 c++로 바꾸는 작업..
먼저 새로운 c++ 클래스 만들기
MagicSkill.h
UCLASS()
class YARO_API AMagicSkill : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
AMagicSkill();
UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
class USphereComponent* Sphere;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
class UParticleSystemComponent* Particle;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "ExplosionFX")
class UParticleSystem* ParticleFX;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
class UProjectileMovementComponent* MovementComponent;
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
public:
// Called every frame
//virtual void Tick(float DeltaTime) override;
UFUNCTION()
virtual void OnComponentBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
};
콜리전을 가지고 있을 구(스피어) 컴포넌트, 마법 파티클, 몬스터에게 충돌했을 때 나올 폭발 파티클과 프로젝타일무브먼트(속도와 중력 조절).. 그리고 오버랩 시작 메서드
MagicSkill.cpp
in 생성자
AMagicSkill::AMagicSkill()
{
// 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;
Sphere = CreateDefaultSubobject<USphereComponent>(TEXT("Sphere"));
Sphere->SetupAttachment(GetRootComponent());
Particle = CreateDefaultSubobject<UParticleSystemComponent>(TEXT("Particle"));
Particle->SetupAttachment(Sphere);
MovementComponent = CreateDefaultSubobject<UProjectileMovementComponent (TEXT("MovementComponent"));
MovementComponent->InitialSpeed = 400.f;
MovementComponent->MaxSpeed = 1000.f;
MovementComponent->ProjectileGravityScale = 0.03f;
}
루트 컴포넌트 자식으로 구 추가, 파티클은 스피어 자식으로 추가, 그리고 무브먼트도 추가
(발사체의) 기본 속도, 최대 속도, 중력 설정
in BeginPlay()
void AMagicSkill::BeginPlay()
{
Super::BeginPlay();
Sphere->OnComponentBeginOverlap.AddDynamic(this, &AMagicSkill::OnComponentBeginOverlap);
}
구 콜리전이 오버랩될 때 실행될 메서드를 지정해줌
오버랩 시작 메서드
void AMagicSkill::OnComponentBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
if (OtherActor)
{
AEnemy* Enemy = Cast<AEnemy>(OtherActor);
if (Enemy)
{
UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), ParticleFX, GetActorLocation());
Destroy(true);
}
}
}
오버랩된 게 몬스터면 폭발 파티클을 현재 위치에서 스폰하기?!
그리고 파괴~ 파티클이 계속 남아있으면 안 되니까.
구 콜리전은 WorldStatic타입 오브젝트는 무시하도록 함. 앞으로도 이 부분 반복적으로 나올 예정인데 생각날 때만 쓰겠음.
몬스터의 인식 범위, 전투범위 콜리전을 WorldStatic타입으로 뒀음.
마법 구 콜리전이 WorldStatic타입 오브젝트를 무시하지 않으면 몬스터의 인식 범위에 닿아도 오버랩으로 인식.
따라서 진짜로 몬스터의 캡슐에 닿을 때 오버랩되기 위해서 특정 오브젝트 타입을 무시하도록 함.
몬스터의 인식범위 콜리전 카테고리 안에서 오브젝트 타입이 WorldStatic인 것을 볼 수 있음
마법 구의 콜리전 카테고리 안에서 오브젝트 반응->WorldStatic을 무시하도록 설정한 것을 볼 수 있음
마법 공격 블루프린트에서
폭발 파티클 설정해주고
파티클에서 마법 파티클 설정해주고
속도와 중력은 스크립트에서 설정함
Main.cpp
마법 공격
void AMain::Attack()
{
if (EquippedWeapon)
{
bAttacking = true;
SetInterpToEnemy(true);
UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
if (AnimInstance && CombatMontage)
{
AnimInstance->Montage_Play(CombatMontage);
AnimInstance->Montage_JumpToSection(FName("Attack"), CombatMontage);
Spawn();
}
}
}
void AMain::AttackEnd()
{
bAttacking = false;
SetInterpToEnemy(false);
}
타겟도 바라보도록 하고 (마법)스폰 메서드 실행
void AMain::Spawn()
{
if (ToSpawn)
{
FTimerHandle WaitHandle;
GetWorld()->GetTimerManager().SetTimer(WaitHandle, FTimerDelegate::CreateLambda([&]()
{
UWorld* world = GetWorld();
if (world)
{
FActorSpawnParameters spawnParams;
spawnParams.Owner = this;
FRotator rotator = this->GetActorRotation();
FVector spawnLocation = AttackArrow->GetComponentTransform().GetLocation();
world->SpawnActor<AMagicSkill>(ToSpawn, spawnLocation, rotator, spawnParams);
}
}), 0.6f, false); // 0.6초 뒤 실행, 반복X
}
}
지팡이를 휘두르는 모션 후 나와야하므로 딜레이를 줌.
ToSpawn이 마법인데 이건 직접 블루프린트 클래스에서 넣어줌. (이걸 스크립트에서 제어할 수 있어야할 듯)
마법 공격이 스폰되는 위치인 AttackArrow 컴포넌트
마지막으로… 플레이어의 전투범위를 설정한 뒤 무기를 집으려는데 본인 무기가 아니라 npc의 무기를 집는 현상이 나타남.
이유는 전투범위에 npc의 무기까지 오버랩되어서 그게 오버랩아이템으로 인식되어서 집을 수 있던 것…
해결한 방법으로는
npc의 무기(+플레이어의 무기)의 콜리전 설정에서 WorldStatic타입을 무시하게 함.
그리고 플레이어의 전투 범위 콜리전에서 오브젝트 타입을 WorldStatic으로 설정.
플레이어의 무기도 전투범위를 오버랩을 무시해야함. 그렇지 않으면 플레이어 자체는 무기에 가까이 있지 않은데
무기가 전투범위에만 들어오면 집을 수 있기 때문에.
또 플레이어의 전투 범위와 몬스터의 인식/전투 범위도 WorldStatic타입을 무시함.
이걸 무시하지 않으면 범위들끼리 오버랩됨.
하지만 npc에게 아주 가까이 다가가서 npc의 무기에 오버랩되기만 하면 npc의 무기를 뺏을 수 있다…
(단, 빈 손이어야 함)
다음에 할 것 : 마법 여러개 추가(스폰될 마법을 스크립트에서 설정할 수 있도록..), 마법 사운드 추가, 마나 소모, 적 체력바 + 적 체력 감소