[2025_01_23] 반사 스킬 무한 루프 방지 & 관통 투사체 최적화

2026. 1. 23. 21:22TIL

문제 1: 반사 데미지 무한 루프

상황

성기사 A가 반사 실드를 가진 상태에서 성기사 B에게 공격당하면?

 
B가 A 공격 → A가 데미지 반사 → B가 데미지 반사 → A가 데미지 반사 → ...

무한 루프 발생 → 스택 오버플로우 

처음 시도한 방법들

1. bool 플래그 추가

csharp
public void TakeDamage(int damage, UnitBase attacker, bool canReflect = true)
{
    if (canReflect && reflectShield != null)
    {
        attacker.TakeDamage(reflectedDamage, this, false); // 반사 비활성화
    }
}
  • 문제: 기존 TakeDamage 호출부를 전부 수정해야 함
  • 다른 시스템(DOT, 환경 데미지 등)과의 호환성 문제

2. 별도 ReflectController 클래스

csharp
public class ReflectController
{
    private HashSet<(UnitBase, UnitBase)> reflectingPairs;
    // 현재 반사 중인 쌍을 추적...
}
  • 문제: 과도한 복잡성, 전역 상태 관리 필요

최종 해결: attacker를 null로 전달

csharp
public void TakeDamage(int damage, UnitBase attacker)
{
    // 실제 데미지 처리
    currentHp -= damage;
    
    // 반사 처리: attacker가 null이면 반사하지 않음
    if (attacker != null && reflectShield != null && reflectShield.charges > 0)
    {
        int reflectedDamage = Mathf.RoundToInt(damage * reflectShield.reflectRatio);
        
        // 핵심: attacker를 null로 전달하여 재반사 방지
        attacker.TakeDamage(reflectedDamage, null);
        
        reflectShield.charges--;
    }
}

왜 이게 좋은가?

기준결과
기존 코드 수정 없음 (null은 이미 유효한 값)
추가 파라미터 없음
의미적 명확성 "공격자 없음 = 반사 불가"
확장성 DOT, 환경 데미지도 자연스럽게 처리

교훈: 복잡한 문제에 복잡한 해결책이 필요한 건 아니다. 복작하게 해결할려고 하다가 오히려 오래 걸렷다...


문제 2: 관통 투사체 중복 타격

상황

스나이퍼의 관통 화살이 직선으로 날아가며 모든 적을 타격해야 하는데:

  • 같은 적을 프레임마다 중복 타격
  • 많은 적과 충돌 체크 시 성능 저하

해결: HashSet + Interval 기반 체크

csharp
public class PiercingProjectile : MonoBehaviour
{
    private HashSet<UnitBase> hitTargets = new HashSet<UnitBase>();
    
    [SerializeField] private float checkInterval = 0.05f;
    private float lastCheckTime;

    private void Update()
    {
        // 매 프레임이 아닌 일정 간격으로 체크
        if (Time.time - lastCheckTime < checkInterval) return;
        lastCheckTime = Time.time;
        
        CheckCollisions();
    }

    private void CheckCollisions()
    {
        Collider2D[] hits = Physics2D.OverlapCircleAll(
            transform.position, 
            hitRadius, 
            targetLayer
        );

        foreach (var hit in hits)
        {
            if (hit.TryGetComponent<UnitBase>(out var unit))
            {
                // HashSet으로 O(1) 중복 체크
                if (hitTargets.Add(unit))
                {
                    unit.TakeDamage(damage, owner);
                    
                    Debug.Log($"[Piercing] Hit: {unit.name}, " +
                              $"Total hits: {hitTargets.Count}");
                }
            }
        }
    }
}

성능 비교

방식중복 체크시간 복잡도프레임당 체크
List + Contains 순회 검색 O(n) 매 프레임
HashSet + Interval 해시 조회 O(1) 0.05초 간격

50개 적 기준, HashSet + Interval이 약 5배 효율적

디버그 로깅

관통 투사체처럼 검증이 어려운 시스템은 로깅이 필수:

 
csharp
Debug.Log($"[Piercing] Spawned at {transform.position}, " +
          $"Direction: {direction}, Owner: {owner.name}");

Debug.Log($"[Piercing] Hit: {unit.name}, " +
          $"Position: {unit.transform.position}, " +
          $"Total hits: {hitTargets.Count}");

Debug.Log($"[Piercing] Destroyed - Final hit count: {hitTargets.Count}");

정리

반사 스킬

  • 문제: 양측 반사 시 무한 루프
  • 해결: 반사 데미지의 attacker를 null로 전달
  • 원칙: 기존 시스템을 수정하지 않는 가장 단순한 방법 선택

관통 투사체

  • 문제: 중복 타격 + 성능
  • 해결: HashSet(O(1) 조회) + Interval 체크(프레임 부하 분산)
  • 원칙: 자료구조 선택이 성능을 결정한다

공통 교훈

"가장 단순한 해결책이 종종 최선이다"

복잡한 컨트롤러나 플래그 시스템 대신, 기존 구조 안에서 자연스럽게 동작하는 방법을 먼저 찾자.