AI Perception은 다양한 자극(Stimulus)을 받아 작동하는데, 자극의 종류에 따라 시스템에 보고되는 '출처(Source Actor)'가 다르다는 점을 이해하는 것이 매우 중요하다.
피해 인지는 두 단계로 이루어진다.
1. 감지 설정 (Controller): AIPerceptionComponent에 UAISenseConfig_Damage를 추가하여 "나는 이제부터 피해 자극을 받겠다"고 선언해야 한다.
2. 자극 보고 (Pawn): 데미지를 '받는' 쪽(몬스터)의 TakeDamage 함수 내에서 UAISense_Damage::ReportDamageEvent를 명시적으로 호출하여, "나 방금 여기에서 맞았어!"라고 Perception System에 직접 보고해야 한다. ApplyDamage만 호출한다고 해서 자동으로 보고되지 않는다.
캐릭터의 Collision은 여러 컴포넌트가 역할을 분담한다. 특히 Trace(레이저)에 어떻게 반응할지 설정하는 것이 중요하다.
CapsuleComponent (몸통): Pawn 타입. 게임플레이의 핵심. 이동, 다른 액터와의 충돌, 피격 판정을 담당한다. 따라서 총알이나 AI 시야 같은 Visibility Trace를 Block 해야 한다.SkeletalMeshComponent (외형): CharacterMesh 타입. 시각적 표현이 주 목적. 피격 판정은 캡슐이 담당하므로, Visibility Trace를 Ignore하여 중복 계산을 피하고 성능을 확보하는 것이 표준적인 방식이다.WidgetComponent (UI): 눈에 보이는 UI지만, 보이지 않는 충돌 판정을 가질 수 있다. 다른 충돌(Overlap, Trace)을 방해하지 않도록 Collision을 NoCollision으로 설정하는 것이 안전하다.// OnTargetPerceptionUpdated 함수 내부
void AAIC_Monster::OnTargetPerceptionUpdated(AActor* Actor, FAIStimulus Stimulus)
{
// ... 유효성 검사 ...
if (Stimulus.WasSuccessfullySensed())
{
AActor* DetectedEnemy = nullptr;
// 1. 자극의 출처(Actor)가 직접 플레이어인지 확인 (주로 Sight)
APawn* DetectedPawn = Cast<APawn>(Actor);
if (DetectedPawn && DetectedPawn->GetController() && DetectedPawn->GetController()->IsA<APlayerController>())
{
DetectedEnemy = DetectedPawn;
}
// 2. 아니라면, 출처의 '주인(Instigator)'이 플레이어인지 확인 (주로 Damage)
else if (Actor->GetInstigator())
{
APawn* InstigatorPawn = Cast<APawn>(Actor->GetInstigator());
if (InstigatorPawn && InstigatorPawn->GetController() && InstigatorPawn->GetController()->IsA<APlayerController>())
{
DetectedEnemy = InstigatorPawn;
}
}
// 3. 유효한 적을 찾았을 경우에만 블랙보드 업데이트
if (DetectedEnemy)
{
MyBlackboard->SetValueAsObject(TEXT("TargetActor"), DetectedEnemy);
}
}
// ...
}
// TakeDamage 함수 내부
float AAIMonsterBase::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
// 방어력을 적용한 최종 데미지 계산
float ActualDamage = FMath::Max(1.0f, DamageAmount - Defense);
if (ActualDamage > 0.0f)
{
// ... 체력 감소 ...
// 피해 발생 사실을 Perception System에 직접 보고
if (DamageCauser)
{
UAISense_Damage::ReportDamageEvent(GetWorld(), this, DamageCauser, ActualDamage, DamageCauser->GetActorLocation(), GetActorLocation());
}
// ... 죽음 또는 피격 반응 처리 ...
// 체력바 UI 업데이트 및 데미지 숫자 팝업 요청
if (HealthBarWidget)
{
// ... 체력바 보이기 및 업데이트 ...
HealthBarWidget->PlayDamageText(ActualDamage); // 최종 데미지를 UI로 전달
// ... 타이머 설정 ...
}
}
return ActualDamage;
}
ApplyDamage 함수를 호출하면 AI Perception이 자동으로 피해를 감지할 것이라고 착각했다. 실제로는 피해를 '받는' 쪽의 TakeDamage 함수에서 UAISense_Damage::ReportDamageEvent를 통해 명시적으로 시스템에 보고해야만 했다.OnTargetPerceptionUpdated가 받는 Actor가 당연히 '플레이어'일 것이라고 가정했다. 실제로는 '총알'과 같은 DamageCauser였고, 이로 인해 플레이어 확인 로직(Cast<APawn>)이 실패했다. Actor의 GetInstigator()를 확인하는 로직을 추가하여 해결했다.AggroSphere 기능이 Zombie에게서는 작동하지 않았다. C++ 코드는 부모 클래스에 동일하게 구현되어 있었지만, 문제는 Zombie 블루프린트 자체의 AggroSphere 컴포넌트 설정(Generate Overlap Events가 꺼져 있었음)에 있었다. 상속을 받았더라도 개별 블루프린트의 설정을 반드시 확인해야 한다.HealthBarWidgetComponent)가 보이지 않는 충돌 영역을 가져 AggroSphere의 Overlap을 방해할 수 있다는 가능성을 간과했다. 위젯 컴포넌트의 Collision을 NoCollision으로 설정하여 해결했다.| 개념 | 설명 | 비고 |
|---|---|---|
| Damage Perception | AIPerceptionComponent에 Damage 센서를 설정하는 것과, TakeDamage에서 ReportDamageEvent를 호출하는 두 단계가 모두 필요하다. | ApplyDamage만으로는 불충분 |
| Stimulus Instigator | Sight 자극은 '대상'을 직접 반환하지만, Damage 자극은 '피해 유발자'(총알 등)를 반환한다. 실제 공격자를 찾으려면 GetInstigator()를 사용해야 한다. | OnTargetPerceptionUpdated의 핵심 |
| Component Collision | Capsule은 피격 판정(Pawn, Visibility Block), Mesh는 외형(CharacterMesh, Visibility Ignore), Widget은 UI(NoCollision)로 역할을 명확히 분리해야 버그가 없다. | 슈터 게임의 표준적인 충돌 설정 |
| 블루프린트 설정 확인 | C++ 상속이 완벽하더라도, 각 블루프린트 애셋의 컴포넌트별 세부 설정(Details 패널)이 다를 수 있다. 기능이 특정 자식에게서만 작동하지 않는다면 블루프린트 설정을 의심해야 한다. | Rampage vs Zombie 버그의 원인 |