[2025_12-29] 유닛 떨림 현상 해결 - 유닛 회피 시스템

2025. 12. 29. 20:53TIL

문제 상황

증상

  • 유닛들이 FlowField를 따라 이동하는 중 좌우로 지그재그로 떨림
  • 특히 유닛 무리가 함께 이동할 때 심화
  • 사용자 경험 저하 (움직임이 부자연스럽고 어지러움)

    초기 가설
  • "FlowField 방향 벡터가 문제일 것이다"

원인 분석

 FlowField 의심

csharp
// UnitFlowDirectionDebugger로 방향 추적
[Warrior_01] #89 방향: (0.028, 1.000), 안정
[Warrior_01] #91 방향: (-0.174, 0.985), 변화 3.2도
[Warrior_01] #92 방향: (0.000, 1.000), 변화 10.0도

결과: FlowField 방향은 10도 내외의 미세한 변화 → 심각한 떨림의 원인이 아님

Local Avoidance 발견

csharp
// MoveState.cs
private float avoidanceCheckInterval = 0.08f;  // <- 문제점!

public void PhysicsExecute(UnitFSM unitFSM)
{
    lastAvoidanceCheck += Time.fixedDeltaTime;
    if (lastAvoidanceCheck >= avoidanceCheckInterval)
    {
        lastAvoidanceCheck = 0f;
        cachedSeparation = CalculateSeparation();
        cachedLateralSpread = CalculateLateralSpread();
    }
    
    Vector3 finalDirection = (moveDirection + cachedLateralSpread + cachedSeparation).normalized;
    // ...
}

 

  • 0.08초마다 회피력 재계산 (초당 12.5회)
  • 주변 유닛이 조금만 움직여도 즉시 반응
  • 새로 계산된 회피력을 즉시 100% 적용
  • 급격한 방향 전환 -> 떨림 발생

해결 과정

시도 1: 체크 주기 증가

csharp
private float avoidanceCheckInterval = 0.2f;  // 0.08 -> 0.2초

결과:

  • 떨림 감소
  • 반응이 느려져서 유닛이 서성거리며 멍청해 보임

시도 2: Smoothing 도입

csharp
// 부드러운 값 저장용 변수 추가
private Vector3 smoothedSeparation = Vector3.zero;
private Vector3 smoothedLateralSpread = Vector3.zero;
private float avoidanceSmoothFactor = 0.3f;  // 30%만 적용

public void PhysicsExecute(UnitFSM unitFSM)
{
    lastAvoidanceCheck += Time.fixedDeltaTime;
    if (lastAvoidanceCheck >= avoidanceCheckInterval)
    {
        lastAvoidanceCheck = 0f;

        cachedSeparation = CalculateSeparation();
        cachedLateralSpread = CalculateLateralSpread();

        // 이전 값과 새 값을 부드럽게 보간 (Lerp)
        smoothedSeparation = Vector3.Lerp(
            smoothedSeparation,      // 이전 값 (70%)
            cachedSeparation,        // 새 값 (30%)
            avoidanceSmoothFactor
        );
        
        smoothedLateralSpread = Vector3.Lerp(
            smoothedLateralSpread,
            cachedLateralSpread,
            avoidanceSmoothFactor
        );
    }

    // smoothed 값 사용
    Vector3 finalDirection = (moveDirection + smoothedLateralSpread + smoothedSeparation).normalized;
}


원리
:

  • 회피력이 급격히 바뀌지 않고 점진적으로 전환
  • 이전 프레임과의 연속성 유지

Forward Bias 추가

csharp
private float forwardBias = 1.5f;  // 전진 방향에 1.5배 가중치

public void PhysicsExecute(UnitFSM unitFSM)
{
    // ... Smoothing 적용 후 ...
    
    // 전진 방향에 가중치 추가
    Vector3 biasedMoveDirection = moveDirection * forwardBias;
    
    // 최종 방향 = 전진(가중치) + 횡분산 + 밀어내기
    Vector3 finalDirection = (biasedMoveDirection + smoothedLateralSpread + smoothedSeparation).normalized;
}

 

효과:

  • 회피보다 목표를 향해 가는 것을 우선시
  • 서성거림 감소
  • 더 직진성 있는 움직임

Dead Zone 적용

csharp
private float minAvoidanceForce = 0.05f;

public void PhysicsExecute(UnitFSM unitFSM)
{
    // ... Smoothing 후 ...
    
    // 너무 작은 힘은 무시
    if (smoothedSeparation.magnitude < minAvoidanceForce)
    {
        smoothedSeparation = Vector3.zero;
    }

    if (smoothedLateralSpread.magnitude < minAvoidanceForce)
    {
        smoothedLateralSpread = Vector3.zero;
    }
}

효과:

  • 0.05 이하의 미세한 힘 무시
  • 불필요한 미세 조정 제거

문제 발생 -> 유닛이 너무 뭉침

현상: 떨림은 해결됐지만 유닛들이 한 덩어리로 몰려다님

원인: Smoothing + Forward Bias로 회피 반응이 약해짐


시도 5: 분산력 증가 

csharp
// 회피 강도 조정
private float lateralSpreadStrength = 2.0f;  // 1.5 → 2.0 (33% 증가)
private float separationStrength = 1.5f;     // 1.2 → 1.5 (25% 증가)

// 체크 주기도 약간 조정
private float avoidanceCheckInterval = 0.12f;  // 0.2 → 0.12초

효과:

  • 떨림 없음 (Smoothing + Dead Zone)
  • 직진성 유지 (Forward Bias)
  • 적절한 간격 유지 (강화된 분산력)
  • 자연스러운 대형

핵심 개념 정리

Lerp (Linear Interpolation)

csharp
Vector3.Lerp(A, B, t)
// A와 B 사이를 t 비율로 보간
// t = 0.3 → A의 70% + B의 30%

Dead Zone

  • 입력/힘이 임계값 이하면 무시
  • 조이스틱, 센서 노이즈 제거에도 사용

Forward Bias

  • 주 목표에 가중치를 주어 우선순위 부여
  • RTS 게임의 자연스러운 움직임에 필수

결론: FlowField가 아닌 Local Avoidance의 과도한 업데이트 빈도가 문제였으며, Smoothing + Forward Bias + Dead Zone + 분산력 조정으로 해결했다.