내일배움캠프_Unreal/TIL

내일배움캠프_Unreal/TIL - Day 152 (260708)

하임 2026. 7. 8. 21:39

강제 리스폰과 낙하 정리 - 자기 피격이 '적중'으로 집계되던 버그


1. 설계 결정 — 텔레포트를 새로 짜지 말고 "죽음"을 재사용

플레이어를 마지막 체크포인트로 되돌리는 가장 쉬운 방법은 텔레포트지만, 즉시 텔레포트는 월드 파티션 스트리밍/로딩 커버를 안 거쳐 도착 지점이 로드되지 않은 채 스폰될 위험이 있음.

그런데 이 게임엔 이미 사망 → Result → Retry → 체크포인트 리스폰 흐름이 있고, 여기엔 로딩 커버·스트리밍 보장이 전부 들어 있음. → 언스턱을 정규 사망 처리로 라우팅해서 그 흐름을 통째로 재사용.

[UI Btn_Respawn]
  └→ PC::RequestUnstuck → Server_RequestUnstuck      (클라 → 서버 RPC)
     └→ GameMode::HandleUnstuck    (IsRequestorHost 권한 가드)
        └→ HealthComponent::KillOwner()   (Health → 0)
           └→ 정규 사망 파이프라인:
              GA_Die → Channel.Player.Died → HandlePlayerDied
              → Result 화면 → 자동 Retry → 로딩 커버 + 마지막 체크포인트 리스폰

교훈: "A와 결과가 같은 견고한 경로 B가 이미 있으면, A를 새로 만들지 말고 B로 진입시킨다." 실제 사망과 100% 동일 경로라 언스턱만의 엣지케이스가 아예 안 생긴다.


2. KillOwner — Health를 직접 0으로 세팅해 사망을 "단일 소스"로 구동

사망 파이프라인의 트리거는 결국 Health 0. 그래서 데미지 GE를 만들지 않고 어트리뷰트 베이스를 직접 0으로 세팅 → HandleHealthChanged가 나머지를 알아서 굴림.

void URetrieveHealthComponent::KillOwner()
{
    if (!AbilitySystemComponent || !AttributeSet) return;
    if (bDeathStarted) return;   // 이미 사망 처리 중이면 재진입 방지

    // GE 파이프라인 우회 — Health 0 → GA_Die → ... → Result.
    AbilitySystemComponent->SetNumericAttributeBase(UCombatAttributeSet::GetHealthAttribute(), 0.f);
}
  • 재진입 가드: bDeathStarted로 이미 죽는 중이면 no-op.
  • SetNumericAttributeBase vs GE: 환경적 강제 사망은 데미지 계산·감쇠·면역이 필요 없으니 베이스 직접 세팅이 더 정확. (실데미지가 아니라 "상태를 죽음으로 만든다".)

3. 권한 라우팅 — 클라 UI에서 시작해 서버에서 실행

리스폰은 서버 권위 동작이라, 클라 UI 입력을 RPC로 서버에 넘기고 서버에서만 실행.

// PlayerController (클라 진입점 → 서버 RPC)
void ARetrievePlayerController::RequestUnstuck() { Server_RequestUnstuck(); }

void ARetrievePlayerController::Server_RequestUnstuck_Implementation()
{
    if (ARetrieveGameMode* GM = GetWorld()->GetAuthGameMode<ARetrieveGameMode>())
        GM->HandleUnstuck(this);
}

// GameMode (서버, 호스트만)
void ARetrieveGameMode::HandleUnstuck(APlayerController* Requestor)
{
    if (!IsRequestorHost(Requestor)) return;   // 요청자 권한 확인
    APawn* Pawn = Requestor ? Requestor->GetPawn() : nullptr;
    if (auto* CombatPawn = Cast<ARetrieveAlsCombatCharacter>(Pawn))
        CombatPawn->GetHealthComponent()->KillOwner();
}
  • GetAuthGameMode<T>()서버에만 유효(클라에선 null) → 자연스러운 서버 가드. 여기에 IsRequestorHost로 한 겹 더.
  • 곁들여 RequestQuitGame(포즈 팝업 게임 종료, 로컬 QuitGame)도 같은 자리에 추가해 "빠져나가는" 동작을 한 곳에 모음.

4. (버그 수정) 낙하 데미지가 원소 게이지를 채우던 문제

강제 리스폰을 검증하려면 일부러 절벽에서 떨어져 죽어봐야 했는데, 낙사를 반복하다 보니 죽을 때마다 원소 게이지가 조금씩 차는 게 보였다. 원인은 공격자 측 "적중" 이벤트(원소 게이지 충전 등 "남을 때렸다" 피드백)가, 낙사/환경 데미지(instigator == target인 self-damage)에도 발행되던 것.

❌ Before — 무조건 공격자 적중 이벤트 발행

EventData.EventTag = AttackerEventTag;
UAbilitySystemBlueprintLibrary::SendGameplayEventToActor(AttackerActor, AttackerEventTag, EventData);

✅ After — 자기 자신에게 입힌 데미지는 "적중" 아님 → 제외

if (TargetActor != AttackerActor)   // self-damage(낙하 등) 제외
{
    EventData.EventTag = AttackerEventTag;
    UAbilitySystemBlueprintLibrary::SendGameplayEventToActor(AttackerActor, AttackerEventTag, EventData);
}

강제 리스폰을 붙이니 드러난 버그. "적중" 피드백은 남을 때렸을 때만 — instigator와 target이 같으면 전투 적중이 아니다.


5. (튜닝) 낙법 실패 착지 몽타주 배속

착지 경직이 길게 느껴져, 낙법 실패 몽타주에 재생 배속 파라미터를 추가(값만, 로직 동일).

// 헤더: EditDefaultsOnly, ClampMin=0.1
float LandingFailMontagePlayRate = 1.f;   // >1.0 = 짧게(빠르게)
// 재생:
PlayAnimMontage(LandingFailMontage, LandingFailMontagePlayRate);

로직은 그대로 두고 파라미터 하나만 늘린 것이라, 애니 에셋을 건드리지 않고 착지 경직 길이를 튜닝할 수 있게 됨.


6. 올리기 전 죽은 에셋 정리

스태프 테스트용으로 만들어두고 안 쓰던 OLD_AM 몽타주 7개(Blink/Burst/HeavyAttack 테스트본)를 삭제하고, 버스트 조합 데이터(DT_SkillCombination)를 최신 값으로 갱신. 데이터 주도로 몽타주를 정리한 뒤 죽어 있던 에셋이라 이번에 같이 제거.


비교 정리

구분 ❌ 안 했다면 ✅ 이번 구현
언스턱 방식 즉시 텔레포트(스트리밍 미보장) 정규 사망 흐름 라우팅(로딩 커버·체크포인트 재사용)
사망 트리거 데미지 GE 조작 SetNumericAttributeBase(Health, 0) + 재진입 가드
실행 권한 클라에서 직접 UI→PC RPC→GM(IsRequestorHost)→HealthComp
낙하 데미지 원소 게이지 충전됨(버그) instigator==target이면 적중 이벤트 제외
착지 경직 고정 길이 LandingFailMontagePlayRate로 배속 튜닝

핵심 요약

  • 강제 리스폰은 새 텔레포트 로직 대신 이미 견고한 "사망→Result→Retry→체크포인트" 흐름에 라우팅 — 로딩 커버/스트리밍/엣지케이스를 공짜로 얻는다.
  • 사망 트리거는 Health를 직접 0으로(SetNumericAttributeBase) 세팅해 파이프라인을 단일 소스로 구동, bDeathStarted로 재진입 방지.
  • 서버 권위 동작은 UI(클라) → PC RPC → GM 권한 가드 → 실행으로 라우팅하고, GetAuthGameMode<T>() null 특성이 자연스러운 서버 가드가 된다.
  • "적중" 피드백은 instigator ≠ target일 때만 — self-damage(낙사 등)로 원소 게이지가 차는 버그를 이 한 줄로 막는다.