내일배움캠프_Unreal/TIL

내일배움캠프_Unreal/TIL - Day 149 (260703)

하임 2026. 7. 3. 23:44

Staff 전투 완성 - 얼음창 다중 투사체 · 지속 장판 · 부분 슈퍼아머 · 워프 옵션화


1. Water Heavy 얼음창 - 다중 투사체 "동시 소환 → 개별 발사"

3발이 머리 주변에 동시에 맺힌 뒤 각자 다른 타이밍에 날아가는 연출. 세 가지 장치가 필요했음.

(a) 지연 발사 - 스폰 후 제자리에 떠 있다 발사

ProjectileMovement의 속도를 0으로 두면 (중력 0 가정) 제자리에 뜬 채 대기 → 타이머로 저장한 방향에 발사.

void AStaffProjectile::ArmDelayedLaunch(const FVector& Direction, float Speed, float Delay)
{
    PendingLaunchDirection = Direction;  PendingLaunchSpeed = Speed;
    ProjectileMovement->Velocity = FVector::ZeroVector;
    ProjectileMovement->InitialSpeed = 0.f;   // PMC가 안 움직임 = 스폰 위치에 정지
    World->GetTimerManager().SetTimer(LaunchDelayTimerHandle, this, &AStaffProjectile::HandleDelayedLaunch, Delay, false);
}

(b) 소켓 앵커 + 오프셋 가산 - 좌/상/우 배치

기존엔 "소켓이 있으면 SpawnOffset 무시"였음. 다중 스폰을 위해 소켓을 앵커로 두고 그 위에 오프셋을 가산하는 모드(bAddOffsetToSocketBase)를 추가.

if (bUsedSocket && Params.bAddOffsetToSocketBase)
    SpawnLocation += AvatarActor->GetActorRotation().RotateVector(Params.SpawnOffset);

(c) 볼리 상호 충돌 무시 - 형제 창에 막혀 소멸 방지 🐛

먼저 발사된 창이, 아직 떠 있는 형제 창에 부딪혀 즉시 파괴되는 문제. → 같은 볼리끼리 이동 스윕에서 서로 건너뛰게 등록.

// AStaffProjectile
void IgnoreOtherProjectile(AStaffProjectile* Other)
{ CollisionSphere->IgnoreActorWhenMoving(Other, true); }   // 블로킹/오버랩 모두 무시

// GA_HeavyAttack::ScheduleProjectiles — 오프셋 개수만큼 동시 스폰 후 O(n²) 상호 등록
for (int32 i = 0; i < Offsets.Num(); ++i)
    Volley.Add(SpawnOneProjectile(Offsets[i], /*bAddOffsetToSocketBase=*/true, FireDelays[i]));
for (a: Volley) for (b: Volley) if (a != b) Volley[a]->IgnoreOtherProjectile(Volley[b]);

리팩토링: SpawnProjectile()(void, 단일) → SpawnOneProjectile(offset, bAddToSocket, delay)(스폰된 투사체 반환)로 바꿔 레거시 단일 경로와 다중 볼리 경로가 같은 스폰 함수를 공유.


2. Water Burst 지속 장판(소용돌이) - 매 프레임 노티 + 적별 간격 데미지

신규 실행 타입 EAttackExecutionType::AreaContinuous. 노티 구간 동안 매 프레임 호출되며, 데미지는 적별 간격으로, 넉백은 매 프레임 방사.

void UPlayerBurstComponent::DoAreaContinuousHit(const FBurstHitInstance& Hit, int32 HitIndex)
{
    // 중심 반경 오버랩 (AoeRadius 재사용) ...
    const double Now = World->GetTimeSeconds();
    TMap<TWeakObjectPtr<AActor>, double>* TimeMap = &PerHitLastHitTime[HitIndex];  // 적별 마지막 타격 시각

    // 이중 넉백 방지: 데미지 틱에서 GE 방향성 넉백은 끄고, 방사 넉백을 단일 소스로.
    FBurstHitInstance DamageHit = Hit;
    DamageHit.KnockbackStrength = 0.f;
    DamageHit.KnockbackUpwardStrength = 0.f;

    for (Target : Overlaps)
    {
        bool bApplyDamage = true;   // 첫 진입 즉시 1틱
        if (const double* Last = TimeMap->Find(Target)) bApplyDamage = (Now - *Last) >= Interval;
        if (bApplyDamage) { ApplyHitToTarget(Target, DamageHit, ...); TimeMap->Add(Target, Now); }
        // + 매 프레임 중심→바깥 방사 넉백 수집(보스 제외 옵션)
    }
}
  • 핵심 자료구조: PerHitLastHitTime = per-HitIndex의 TMap<약참조 액터, 시각> → 장판 위에 서 있는 적마다 재적용 간격(ContinuousDamageInterval)을 독립 관리.
  • 이중 넉백 방지 🐛: 데미지 GE에도 방향성 넉백이 붙어 있으면 방사 넉백과 두 소스가 충돌 → 데미지 틱에서는 GE 넉백을 끄고 방사만 밈.

3. 공격 중 "부분 슈퍼아머" - 전역 억제 vs 비보스 억제 2단 구성

콤보/강공이 잡몹 피격엔 안 끊기고 보스 공격엔 끊기게. 기존 전역 억제(버스트 슈퍼아머)에 조건부 억제를 한 겹 더.

❌ Before — 억제 태그 1종(전역)만: 있으면 무조건 리액션 스킵
✅ After — 2단 억제

// (1) 전역: 모든 공격자에 대해 스킵 (버스트 슈퍼아머 = State.Player.Bursting)
if (ASC->HasAnyMatchingGameplayTags(ReactionSuppressionTags)) return;

// (2) 부분: 공격 중이면 '비보스' 공격자만 스킵 (보스는 통과)
if (ASC->HasAnyMatchingGameplayTags(AttackStateSuppressionTags))   // State.Player.Attacking / UsingHeavyAttack
    if (!(bBossBypassesAttackSuppression && IsInstigatorBoss(Payload)))
        return;
  • 보스 판정: 피격 이벤트 Payload->Instigator의 ASC에 Monster.Type.Boss 태그가 있는지(IsInstigatorBoss).
  • 공용 안전장치: AttackStateSuppressionTags 기본값이 플레이어 공격 태그라, 이 태그가 없는 적에게는 영향 없음(플레이어/적 공용 컴포넌트).
  • 데미지는 두 경우 모두 정상 적용 — 억제되는 건 리액션(몽타주/어빌리티 캔슬)뿐.

4. AttackWarp - 무타겟 시 입력방향 회전을 옵션화

돌진 워프가 타겟이 없으면 항상 이동 입력 방향으로 회전했음 → 특정 콤보에선 원치 않는 재정렬. 플래그로 끌 수 있게.

// 무타겟일 때만 해당. 타겟이 있으면 여전히 워프.
- if (bPlayerControlled && !InputDir.IsNearlyZero())
+ if (bRotateToInputWhenNoTarget && bPlayerControlled && !InputDir.IsNearlyZero())

원칙: "무입력에도 정면 강제회전" 버그를 잡을 때 raw 이동 입력만 쓰도록 한 데 더해, 회전 자체를 데이터로 on/off → 콤보별로 재정렬을 허용/금지.


5. (짧게) 무기 히트볼륨 BoundsRadiusScale 기본값 3.0

Bounds 트레이스 반경 스케일 기본값을 3.0으로 올리고 데이터에 반영. 판정이 얇게 느껴지던 걸 완화(값만 변경, 로직 동일).


비교 정리

구분 ❌ Before ✅ After
투사체 스폰 단일 SpawnProjectile()(void) SpawnOneProjectile(...)(반환) — 단일/볼리 공용
다중 발사 순차 스폰 동시 스폰 + 투사체별 FireDelays[i] 발사 지연
소켓+오프셋 소켓 있으면 오프셋 무시 bAddOffsetToSocketBase로 앵커 위 가산
볼리 충돌 형제 창에 막혀 소멸 IgnoreActorWhenMoving로 상호 무시
장판 데미지 (없음) AreaContinuous — 적별 TMap<Actor,시각> 간격 재적용
장판 넉백 매 프레임 방사(데미지 틱 GE 넉백 끔=이중 방지)
피격 억제 전역 1종 전역(모든 공격자) + 부분(비보스만, 보스 예외)
워프 회전 무타겟이면 항상 입력방향 bRotateToInputWhenNoTarget로 on/off

핵심 요약

  • "동시 소환 → 개별 발사" 볼리는 ①속도 0 대기(지연 발사) ②소켓 앵커+오프셋 가산 ③형제끼리 IgnoreActorWhenMoving 세 장치가 함께 필요. 특히 형제 충돌 무시를 빼면 먼저 나간 발사체가 소멸함.
  • 지속 장판은 매 프레임 노티 + per-target TMap<Actor,시각> 로 적별 간격 데미지, 넉백은 매 프레임 방사. 넉백 소스는 하나로(데미지 GE 넉백 끄기).
  • 슈퍼아머는 전역/부분 2단 — 전역은 모든 공격자, 부분은 비보스만(보스는 Instigator ASC의 Monster.Type.Boss로 예외). 데미지는 항상 적용, 리액션만 억제.
  • "항상 하는 동작"이 콤보마다 거슬리면 데이터 플래그로 on/off(예: 워프 무타겟 회전) — 코드 분기보다 데이터로 끄고 켠다.