0. 개요

기존 몬스터의 AI를 수정하여 보스 형태의 몬스터를 생성한다.

 

 

1. 기존의 AI 공격 로직 파악

(1) 엘리멘탈리스트 클래스의 비헤이비어 트리

워리어, 아쳐, 엘리멘탈리스트 3개의 직업군 중, 마법을 활용하는 클래스의 비헤이비어 트리이다.

EQS 쿼리를 사용하여 발사 위치를 계산하고, 공격을 시행하게 되어 있다.

 

 

(2) 엘리멘탈리스트 클래스의 공격 태스크

공격 태스크를 살펴보면, 기본적으로 소환한 미니언의 갯수를 체크하여 Minion Spawn Threshold의 갯수보다 미니언의 갯수가 적으면 Summon Tag를 이용하여 GA_Summon 어빌리티를 실행한다.

미니언의 갯수가 적절하면 Ability Tag를 Attack 태그(Abilities.Attack)으로 설정하여, 파이어볼트 공격을 하는 데, 이는 엘리멘탈리스트 직업군 캐릭터인 샤먼 몬스터의 몽타주에서 해당 태그를 전달하고 있기 때문이다.

 

 

(3) 엘리멘탈리스트(샤먼)의 공격 몽타주

애님 노티파이가 Montage.Attack.1로 지정되어 있음을 볼 수 있다.

 

 

2. 보스 캐릭터 생성

(1) 보스 캐릭터 블루프린트

메시 크기를 크게 설정하고 이에 맞춰 캡슐 크기를 맞춘다.

공격 몽타주를 추가하고 올바른 몽타주 태그를 연결한다.

 

 

(2) 몽타주 복제 후 애님 노티파이 변경

사용하게 하려는 어빌리티의 태그와 애님 노티파이의 태그를 일치시킨다.

 

 

(3) BTTasks 수정

소환, 아케인 파편 스펠, 파이어 볼트 스펠을 각각의 BT 태스크로 나누고, 비헤이비어 트리를 재구성할 것이다.

 

(3-1) BTT_Boss_Summon

 

 

(3-2) BTT_Boss_ArcaneShards

 

 

(3-3) BTT_Boss_FireBolt

 

 

 

(4) 보스 비헤이비어 트리 재구성

하수인 소환, 아케인 파편 스펠, 파이어 볼트 스펠에 대하여 시퀀스 노드를 배치한다.

하수인 소환 어빌리티는 내부적으로 소환수 갯수가 Threshold 이하이면 발동될 것이고, 아케인 파편 스펠은 쿨다운이 존재하므로 쿨다운이 있는 상태에서는 어빌리티가 자동으로 취소될 것이다.

따라서, 일반적인 상황(아케인 파편 스펠 사용 직후, 소환수 일정 마리 이상)에서는 플레이어를 향하여 파이어볼트 공격을 실행하게 된다.

 

 

3. 디버그

(1) 소환수와 보스가 아무 동작도 하지 않는 상황

이는 소환수와 보스가 EQS 쿼리를 실행하는 과정에서 획득한 MoveToLocation으로 이동하려고 할 때 서로가 서로의 진로를 방해하기 때문에 발생하는 버그이다.

모든 몬스터 캐릭터 대상으로 RVO 회피 사용을 체크하고, 반경을 적절히 지정한다.

소환수 몬스터의 경우 일정 가중치를 추가 해주면, 보스 몬스터의 움직임을 더 고려하게 된다.

 

 

(2) ExecCalc에서 보스가 아케인 파편 스펠로 데미지를 입힐 때 크래시가 발생하는 버그

// 방사형 피해 계산
		if (UAuraAbilitySystemLibrary::IsRadialDamage(EffectContextHandle))
		{
			if (ICombatInterface* CombatInterface = Cast<ICombatInterface>(TargetAvatar))
			{
				CombatInterface->GetOnDamageDelegate().AddLambda([&](float DamageAmount)
					{
						DamageTypeValue = DamageAmount;
						if (CombatInterface)
							CombatInterface->GetOnDamageDelegate().Clear();
					});
			}

			FVector DamageOrigin = UAuraAbilitySystemLibrary::GetRadialDamageOrigin(EffectContextHandle);
			DamageOrigin.Z += 100.f;
			
			UGameplayStatics::ApplyRadialDamageWithFalloff(
				TargetAvatar,
				DamageTypeValue,
				0.f, // 최소 대미지
				DamageOrigin,
				UAuraAbilitySystemLibrary::GetRadialDamageInnerRadius(EffectContextHandle),
				UAuraAbilitySystemLibrary::GetRadialDamageOuterRadius(EffectContextHandle),
				1.f, // 감쇠 계수
				UDamageType::StaticClass(), // 
				TArray<AActor*>(), // IgnoreActor
				SourceAvatar, // DamageCauser
				nullptr); // Controller Instigator
		}

		Damage += DamageTypeValue;
	}

이 코드에서 데미지를 계산하는 ExecCalc 클래스의 계산 함수의 전투 인터페이스의 델리게이트를 바인딩하고 클리어하는 과정에서 댕글링 포인터가 유발되어 크래시가 발생하였다.

이는 여러개의 아케인 파편이 생성되는 과정에서 이전 파편에 플레이어 캐릭터가 사망한 직후, 다음 파편이 방사형 데미지를 계산할 때 널 포인터가 되기 때문이다.

 

한편으로 위의 코드를 사용한 이유는, 데미지 감쇠를 ApplyRadialDamageWithFalloff 로 계산하고, 해당 데미지를 TakeDamage에서 델리게이트 브로드캐스트로 데미지 양을 전달받아 할당하려 하기 위함이었으나, ExecCalc는 데미지를 계산하는 역할만 해야하고 이외의 어떠한 역할도 하면 안된다(델리게이트 바인딩, 호출 등등).

 

따라서 코드를 안전하게 리팩토링한다.

 

 

(2-1) ExecCalc_Damage - 방사형 데미지 계산

	float CalculateRadialDamage(const AActor* TargetActor, float BaseDamage, const FVector& Origin, float InnerRadius, float OuterRadius, float DamageFalloff) const;
float UExecCalc_Damage::CalculateRadialDamage(const AActor* TargetActor, float BaseDamage, const FVector& Origin, float InnerRadius, float OuterRadius,
	float DamageFalloff) const
{
	FVector TargetLocation = TargetActor->GetActorLocation();

	// 타겟 - 데미지 중심 = 중심에서 타겟까지의 벡터
	FVector DamageVector = TargetLocation - Origin;
	float DistanceToTarget = DamageVector.Length();

	const TRange<float> DistanceRange(InnerRadius, OuterRadius);
	const TRange<float> DamageScaleRange(DamageFalloff, 0.f);

	const float DamageScale = FMath::GetMappedRangeValueClamped(DistanceRange, DamageScaleRange, DistanceToTarget);
	const float Damage = BaseDamage * DamageScale;
	return Damage;
}

방사형 데미지의 변수를 받아 TRange로 만들고, GetMappedRangeValueClamped 함수를 이용하여 퍼센트를 꺼내올 수 있다.

이 함수는 선형 감쇠에 적합하도록 설정되었으므로, 지수형 감쇠를 위하면 리팩토링이 필요하다.

 

 

(2-2) 데미지 계산식 수정

		// 속성 저항이 데미지를 퍼센트로 무시함
		DamageTypeValue *= (100.f - Resistance) / 100.f;
		Damage += DamageTypeValue;
		
		// 방사형 피해 계산
		if (UAuraAbilitySystemLibrary::IsRadialDamage(EffectContextHandle))
		{
			Damage = CalculateRadialDamage(
				TargetAvatar,
				DamageTypeValue,
				UAuraAbilitySystemLibrary::GetRadialDamageOrigin(EffectContextHandle),
				UAuraAbilitySystemLibrary::GetRadialDamageInnerRadius(EffectContextHandle),
				UAuraAbilitySystemLibrary::GetRadialDamageOuterRadius(EffectContextHandle),
				1.0f);
		}
	}

방사형 피해를 속성 저항 계산 이후로 보내고, 새로 만든 커스텀 함수를 사용하도록 한다.