Dev12 마법 공격 정보 추가, 적이 npc 인식 가능, 적 히트 애니메이션, 공격 받은 뒤 공격한 사람 인식, 적 본인 위치로 돌아가기, 스탯 수치 텍스트, 카메라 관련
마법 공격에 정보 추가
공격받은 적이 공격한 이가 누군지 판별하기 위해 필요
MagicSkill.h
UPROPERTY(EditDefaultsOnly)
int index;
UPROPERTY(EditAnywhere,BlueprintReadWrite, Category = "Combat")
class ACharacter* Caster; // who cast this spell(magic)
인덱스와 캐스터 정보를 추가.
인덱스는 플레이어가 0, 모모 1, 루코 2, 보보 3, 비비 4, 지지 5
캐스터는 NPC만 해당
공격한 NPC 정보가 있어야 공격받은 몹이 해당 NPC에게로 이동하고 공격함.
인덱스는 블루프린트 클래스의 클래스 디폴트에서 넣어줌.
AquaAttack은 보보의 기술이므로 인덱스는 3
캐스터의 경우, 스크립트에서 넣어줌
in Spawn() of YaroCharacter.cpp
MagicAttack = world->SpawnActor<AMagicSkill>(ToSpawn, spawnLocation, rotator, spawnParams);
if (MagicAttack && CombatTarget)
{
MagicAttack->Target = CombatTarget;
MagicAttack->Caster = this;
}
마법 스폰 뒤에 타겟도 지정하고 캐스터도 넣어줌.
캐스터를 넣는 방법은 여러가지를 시도해봤지만, 그냥 이게 제일 나은 방법이라고 판단.
월드(맵)에 존재하는 NPC의 정보를 넣어야하는데 클래스디폴트에서 넣기도 불가능하고
TSubclassOf로 넣어도 월드에 있는 NPC로 인식하지 않음..
이 값을 클래스디폴트에서 넣어주고 싶었던 이유는 위의 코드로 썼을 때
이동하지 않는, 즉 몹 위치에서 바로 스폰되는 마법의 경우 몹의 TakeDamage()함수가 위 코드의 MagicAttack->Caster = this; 보다 더 빠르게 실행됨. 따라서 몹이 데미지를 입었을 때는 Caster가 Null인 상태인 것. 그래서 미리 디폴트에서 넣어주고 싶었던 건데..
그래서 결국 가장 쉽고 간단한 이 방법을 사용한 뒤, Enemy쪽에서 해결법을 찾음.
이건 조금 뒤에 설명하겠음.
적의 타겟 인식(NPC 인식 가능)
플레이어와 npc 인식 부분 때문에 며칠간 고생 하다가 다시 이어서 쓴다..
in Enemy.h
class AMagicSkill* MagicAttack;
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Combat")
class AMain* Main;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "AI")
TArray<ACharacter*> AgroTargets;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "AI")
TArray<ACharacter*> CombatTargets;
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "AI")
ACharacter* AgroTarget;
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "AI")
ACharacter* CombatTarget;
공격받은 마법, 플레이어, 어그로/전투 타겟 배열 선언, 전투 타겟 타입 변경, 어그로 타겟 추가. 내용은 후술.
in Enemy.cpp
일단 어그로 범위 오버랩
void AEnemy::AgroSphereOnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
if (auto actor = Cast<AEnemy>(OtherActor)) return; // 오버랩된 게 Enemy라면 코드 실행X
if (OtherActor && Alive())
{
if (AgroSound && AgroTargets.Num() == 0) UGameplayStatics::PlaySound2D(this, AgroSound);
ACharacter* target = Cast<ACharacter>(OtherActor);
if (target == Main)
{
MoveToTarget(Main);
}
else
{
if (target)
{
if (!CombatTarget && AgroTarget != Main)
{
MoveToTarget(target);
}
}
}
for (int i = 0; i < AgroTargets.Num(); i++)
{
if (target == AgroTargets[i]) return;
}
AgroTargets.Add(target); // Add to target list
}
}
ACharacter* target = Cast
를 보면 알 수 있듯이 타겟의 타입을 캐릭터로 만들었다. 하지만 이것만 바꾸니 문제가 생긴 게 Enemy 본인이 인식된다는 것… Enemy도 캐릭터 타입 클래스이기 때문에..
따라서 오버랩된 것이 Enemy클래스면 return하게 했고
플레이어를 아예 그냥 Main이라는 변수를 만들어서 넣어줬다.
in BeginPlay()
Main = Cast<AMain>(UGameplayStatics::GetPlayerCharacter(this, 0));
플레이어 가져오기는 그래도 쉬우니까..
그리고 NPC의 경우는 전투타겟이 없고 누굴 쫓아가고 있지 않을 때만 인식(추적)하도록 한다.
플레이어 인식은 전투타겟이 있어도 가능하다.
즉, 플레이어를 인식한 뒤에는 NPC를 인식하지 않는다.
플레이어는 언제든 인식 가능하다.
어그로 범위 오버랩, 전투 범위 오버랩 둘 다 타겟 인식을 저런 식으로 변경하였다.
그리고 어그로 타겟 배열에 없으면 추가해준다.
어그로 범위에서 나갔을 때
void AEnemy::AgroSphereOnOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
if (auto actor = Cast<AEnemy>(OtherActor)) return; // 오버랩된 게 Enemy라면 코드 실행X
if (OtherActor && Alive())
{
ACharacter* target = Cast<ACharacter>(OtherActor);
if (target)
{
if (CombatTarget == target)
{
CombatTarget = nullptr;
bHasValidTarget = false;
}
for (int i = 0; i < AgroTargets.Num(); i++) // Remove target in the target list
{
if (target == AgroTargets[i])
{
AgroTargets.Remove(target);
}
}
if (AgroTarget != Main)
{
AgroTarget = nullptr;
if (CombatTargets.Num() != 0)
{
bOverlappingCombatSphere = true;
bHasValidTarget = true;
CombatTarget = CombatTargets[0];
if (bAttacking) AttackEnd();
}
else
{
if (AgroTargets.Num() != 0)
{
MoveToTarget(AgroTargets[0]);
}
}
}
else
{
if (target == Main)
{
AgroTarget = nullptr;
if (!CombatTarget) bOverlappingCombatSphere = true;
}
}
if (AgroTargets.Num() == 0) // no one's in agrosphere
{
MoveToLocation();
}
}
}
}
어그로 타겟 배열에서 빼주고, 현재 추적 중인 타겟이 플레이어일 때와 아닐 때로 나눠서 처리하는데..
설명하기 복잡하다. 아무튼 그리고 어그로 타겟 배열에 아무도 없으면 즉 어그로 범위 내에 아무도 없으면 초기 위치로 돌아간다. (함수 설명은 조금 이따가)
전투 범위 오버랩
void AEnemy::CombatSphereOnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
if (auto actor = Cast<AEnemy>(OtherActor)) return; // 오버랩된 게 Enemy라면 코드 실행X
if (OtherActor && Alive())
{
ACharacter* target = Cast<ACharacter>(OtherActor);
if (target)
{
for (int i = 0; i < CombatTargets.Num(); i++)
{
if (target == CombatTargets[i]) return;
}
CombatTargets.Add(target); // Add to target list
if (CombatTarget && target != Main) return;
if ((AgroTarget == Main && target == Main) || AgroTarget != Main)
{
AgroTarget = nullptr;
CombatTarget = target;
bOverlappingCombatSphere = true;
bHasValidTarget = true;
Attack();
}
}
}
}
전투 타겟 배열에 추가하고, 전투 타겟이 존재하는데 지금 전투 범위에 들어온 게 npc면 리턴한다.
추적 타겟, 현재 전투 범위에 들어온 타겟 모두 플레이어면
추적 타겟 null로 만들고 전투타겟 설정 후 공격한다.
플레이어를 추적하고 있지 않다면 (=npc 추적 중이었다면) 전투 범위에 누가 들어오든 바로 전투 타겟으로 설정 후 공격한다.
전투 범위에서 나갔을 때
void AEnemy::CombatSphereOnOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
if (auto actor = Cast<AEnemy>(OtherActor)) return; // 오버랩된 게 Enemy라면 코드 실행X
if (OtherActor && Alive())
{
ACharacter* target = Cast<ACharacter>(OtherActor);
UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
AnimInstance->Montage_Stop(0.1f, CombatMontage);
if (target)
{
for (int i = 0; i < CombatTargets.Num(); i++) // Remove target in the target list
{
if (target == CombatTargets[i])
{
CombatTargets.Remove(target);
}
}
if(target == Main) MoveToTarget(target);
if (!CombatTarget && CombatTargets.Num() == 0) // no one's in Combatsphere
{
bOverlappingCombatSphere = false;
if (AgroTarget != Main)
{
CombatTarget = nullptr;
bHasValidTarget = false;
if (AgroTargets.Num() != 0)
{
MoveToTarget(AgroTargets[0]);
}
}
}
else
{
if (AgroTarget != Main && CombatTargets.Num() != 0)
{
CombatTarget = CombatTargets[0];
bHasValidTarget = true;
bOverlappingCombatSphere = true;
}
else if (target == Main)
{
CombatTarget = Main;
bOverlappingCombatSphere = false;
bHasValidTarget = true;
MoveToTarget(Main);
}
}
if(bAttacking) AttackEnd();
}
}
}
몽타주 멈추고, 전투 타겟 배열에서 삭제하고, 지금 범위에서 나간 게 플레이어면 플레이어를 추적.
그 뒤는 복잡함… 나중에 주석이나 추가해야지..
공격 타이머 설정 조건도 조금 바꿨음. 추적 타겟이 없을 때만 다시 공격하도록.
추적 타겟이 있으면 공격하지 말고 쫓아가야 하니까.
void AEnemy::AttackEnd()
{
bAttacking = false;
SetInterpToTarget(true);
//UE_LOG(LogTemp, Log, TEXT("attack end"));
if (bOverlappingCombatSphere && !AgroTarget)
{
GetWorldTimerManager().SetTimer(AttackTimer, this, &AEnemy::Attack, AttackDelay);
}
}
적 Hit 애니메이션
TakeDamage메서드에서 죽는 게 아니면 전투 몽타주에서 Hit 애니메이션 재생하도록 함.
float AEnemy::TakeDamage(float DamageAmount, struct FDamageEvent const& DamageEvent, class AController* EventInstigator, AActor* DamageCauser)
{
if (Health - DamageAmount <= 0.f) // Decrease Health
{
Health = 0.f;
Die();
}
else
{
UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
if (AnimInstance)
{
AnimInstance->Montage_Play(CombatMontage);
AnimInstance->Montage_JumpToSection(FName("Hit"), CombatMontage);
}
Health -= DamageAmount;
}
MagicAttack = Cast<AMagicSkill>(DamageCauser);
return DamageAmount;
}
그리고 DamageCauser를 마법 공격에 할당. (공격자 판단 위함)
HitEnd노티파이를 추가해서 (애니메이션 블루프린트에서) HitEnd()가 실행되도록 연결함.
void AEnemy::HitEnd()
{
if (!Main) Main = Cast<AMain>(UGameplayStatics::GetPlayerCharacter(this, 0));
if (bOverlappingCombatSphere) AttackEnd();
if (AgroTarget) MoveToTarget(AgroTarget);
int index = MagicAttack->index;
if (!CombatTarget && AgroSound) UGameplayStatics::PlaySound2D(this, AgroSound);
// When enemy doesn't have any combat target, Ai attacks enemy
if (!CombatTarget && AgroTarget != Main && index != 0)
{
ACharacter* npc = MagicAttack->Caster;
if (CombatTargets.Contains(npc))
{
CombatTarget = npc;
bOverlappingCombatSphere = true;
bHasValidTarget = true;
}
else
{
MoveToTarget(npc);
}
}
/* When enemy doesn't have combat target, player attacks enemy
or when enemy's combat target is not player and player attacks enemy.
At this time, enemy must sets player as a combat target.
*/
if ((!CombatTarget || CombatTarget != Main) && index == 0)
{
if (CombatTarget)
{
if (EnemyMovementStatus == EEnemyMovementStatus::EMS_Attacking)
{
UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
AnimInstance->Montage_Stop(0.1f, CombatMontage);
if(bAttacking) AttackEnd();
SetInterpToTarget(false);
GetWorldTimerManager().ClearTimer(AttackTimer);
}
}
MoveToTarget(Main);
}
}
플레이어 안 들어가있으면 넣어주고, 추적 타겟있으면 따라가도록 하고,
마법 공격의 인덱스를 가져와서 플레이어의 마법인지, npc의 마법인지 구분
전투타겟 없고 추적 타겟이 플레이어가 아닐 때 npc가 공격한 거면
전투 타겟 배열에 방금 공격한 npc가 있는지 없는지 판단하여 처리.
있으면 전투타겟 바로 변경. 없으면 해당 npc 추적.
전투타겟이 없거나 전투타겟이 플레이어가 아니고 플레이어가 공격했을 때는
전투 타겟이 있고 몹이 공격 상태일 때, 전투 몽타주 멈추는 등의 처리를 함.
그리고 플레이어 추적.
적 초기 위치로 복귀
// 이동 후 범위 내에 아무도 없을 때 다시 초기 위치로 이동
void AEnemy::MoveToLocation()
{
if (AIController && Alive())
{
GetWorldTimerManager().SetTimer(CheckHandle, this, &AEnemy::CheckLocation, 0.5f);
SetEnemyMovementStatus(EEnemyMovementStatus::EMS_MoveToTarget);
AIController->MoveToLocation(InitialLocation);
}
}
CheckLocation()을 타이머를 설정해서 호출한다. 이동상태를 MoveToTarget으로 변경하고 InitialLocation, 즉 초기 위치로 이동하게 한다.
BeginPlay()에서 초기위치와 초기 회전값을 받아두었다.
InitialLocation = GetActorLocation(); // Set initial enemy location
InitialRotation = GetActorRotation();
MoveToLocation() 호출 후 0.5초 뒤에 호출되는 CheckLocation()이다.
말 그대로 위치를 확인하는 메서드이다. 본인 위치에 도달하면 멈춰야 하므로.
void AEnemy::CheckLocation()
{
if (Alive())
{
if (AgroTarget)
{
AIController->StopMovement();
GetWorldTimerManager().ClearTimer(CheckHandle);
}
Count += 1;
if (Count >= 20) SetActorLocation(InitialLocation);
float distance = (GetActorLocation() - InitialLocation).Size();
//UE_LOG(LogTemp, Log, TEXT("%f"), distance);
if (distance <= 70.f)
{
if (AIController)
{
AIController->StopMovement();
SetEnemyMovementStatus(EEnemyMovementStatus::EMS_Idle);
Count = 0;
SetActorRotation(InitialRotation);
GetWorldTimerManager().ClearTimer(CheckHandle);
}
}
else
{
GetWorldTimerManager().SetTimer(CheckHandle, this, &AEnemy::CheckLocation, 0.5f);
}
}
else
{
GetWorldTimerManager().ClearTimer(CheckHandle);
}
}
참고로 초기 위치로 이동 중간에 추적 타겟이 생긴다면 일단 이동을 멈추고, 타이머를 제거한다.
이 메서드가 실행될 때마다 Count를 1씩 증가시키는데, 이는 몹이 이동하다가 중간에 끼어서 초기 위치로 못 가게 되는 경우 때문이다. Count가 20이상, 즉 10초 이상 본인 위치로 못 갔을 땐 그냥 초기 위치로 순간이동시킨다.
현재 몹 위치랑 초기위치 거리 계산해서 70 이하면 대충 도착했다고 판단.
이동 멈추고 유휴상태로 변경하며 Count 0으로 초기화하고 초기 회전값으로 돌려놓고 타이머를 제거한다.
거리가 70 초과면 아직 이동해야 하므로 0.5초 뒤에 다시 메서드를 호출한다.
참고로 초기 위치로 돌아가는 건 당연히 살아있을 때 해야하므로 초기 위치로 돌아가다가 죽은 거면 타이머를 제거한다. (이동 멈추는 건 Die()에서 해준다.)
스탯 텍스트 표현
위젯에다가 텍스트를 추가해줬다. 빨간색 위에 올려야하므로 오버레이에 전부 다 넣어줬다.
그리고 디테일 패널 보면 Content 키테고리의 텍스트에서 함수 바인딩.
함수 내용은 별 거 없다. 플레이어의 HP(float) 가져와서 Text형식으로 변환 후 리턴.
다른 스탯, 최대 스탯도 모두 이런 식으로 해줬다.
결과물은 영상에서 확인하시라. 나중에 더 간지나게 바꿔야지.
카메라 콜리전 관련
포기하고 있던 건데, 유데미 선생님 덕분에 해결했다!
카메라가 주변 환경 콜리전뿐만 아니라 npc, 적들도 똑같이 취급해서 콜리전 충돌된 걸로 판단해서 카메라를 쭈우욱 땡기는 문제가 있었는데
BeginPlay()에 아래 코드를 추가하여 해결됐다.
GetMesh()->SetCollisionResponseToChannel(ECollisionChannel::ECC_Camera, ECollisionResponse::ECR_Ignore);
GetCapsuleComponent()->SetCollisionResponseToChannel(ECollisionChannel::ECC_Camera, ECollisionResponse::ECR_Ignore);
메시와 캡슐 콜라이더 콜리전 설정에서 카메라는 오버랩 안 되도록, 즉 무시하도록 설정하는 것이다.
BeginPlay()에서 설정한 것이므로 블루프린트 클래스 디폴트에서는 적용된 모습을 볼 수 없고
게임 시작하고 확인하면 위 사진처럼 적용된 것을 볼 수 있다.
참고로 적의 경우엔, 무기(손, 방망이 등)가 콜리전이 없어야하고, npc의 경우엔 지팡이 콜리전을 없애야한다.
근데 생성자에서 설정하면 디폴트로 들어갈 텐데, 유데미 선생님께서는 왜 BeginPlay()에서 설정한 걸까?