2025. 12. 17. 21:06ㆍTIL
Unity 유닛 FSM 시스템 성능 최적화 (GC 압박 및 연산 최적화)
문제 상황 분석
프로파일링 결과 발견된 성능 문제:
- GetNearByAllies에서 Transform 접근이 6548번 발생
- 회피 계산이 매 FixedUpdate마다 실행
- 매 프레임 List 생성/파괴로 인한 GC 압박
- Transform 접근 최적화
문제점: public void Execute(UnitFSM unitFSM) { unit.transform.position // 매번 네이티브 호출 other.transform.position // 매번 네이티브 호출 }
Unity의 transform 프로퍼티는 C# -> C++ 네이티브 호출이므로 반복 접근 시 오버헤드가 크다.
해결 방법: // Transform 캐싱 private Transform unitTransform;
public void Execute(UnitFSM unitFSM) { Vector3 unitPos = unitTransform.position; // 한 번만 접근 // 이후 unitPos 변수 사용 }
효과: 네이티브 호출 횟수 대폭 감소
- 회피 계산 빈도 감소
문제점: public void PhysicsExecute(UnitFSM unitFSM) { Vector3 avoidanceDirection = CalculateAvoidance(); // 매 FixedUpdate }
회피 계산은 상대적으로 무거운 연산인데 매 물리 프레임마다 실행할 필요는 없다.
해결 방법: private float avoidanceCheckInterval = 0.1f; // 0.1초 간격 private float lastAvoidanceCheck = 0f; private Vector3 cachedAvoidance = Vector3.zero;
public void PhysicsExecute(UnitFSM unitFSM) { lastAvoidanceCheck += Time.fixedDeltaTime;
if (lastAvoidanceCheck >= avoidanceCheckInterval)
{
lastAvoidanceCheck = 0f;
cachedAvoidance = CalculateAvoidance(); // 0.1초마다만 계산
}
finalDirection = (moveDirection + cachedAvoidance).normalized;
}
효과: 회피 계산 빈도 50fps 기준 약 80% 감소 (50회 → 10회)
- SqrMagnitude 사용 (Sqrt 연산 제거)
문제점: float distance = Vector3.Distance(pos1, pos2); // 내부적으로 Sqrt 연산 if (distance < 3f)
Vector3.Distance는 내부적으로 제곱근(Sqrt) 연산을 수행하는데, 이는 비용이 높은 연산이다.
해결 방법: float sqrDistance = (pos1 - pos2).sqrMagnitude; // Sqrt 없음 if (sqrDistance < 9f) // 3f * 3f
원리: 거리 비교만 필요한 경우 양변을 제곱하면 Sqrt 연산 생략 가능
- distance < 3f → distance² < 9f
효과: 매 거리 비교마다 Sqrt 연산 제거
- UnitCollisionManager 최적화
4-1. List Pool 도입
문제점: private void UpdateSpatialGrid() { spatialGrid.Clear(); // 모든 List 파괴
foreach (var unit in allUnits)
{
if (!spatialGrid.ContainsKey(cell))
{
spatialGrid[cell] = new List<UnitBase>(); // 매번 새로 생성 → GC 압박
}
}
}
매 프레임 수십~수백 개의 List가 생성/파괴되면서 GC 압박 발생.
해결 방법: private Queue<List<UnitBase>> listPool = new Queue<List<UnitBase>>();
private void UpdateSpatialGrid() { // 기존 List들을 Pool에 반환 foreach (var list in spatialGrid.Values) { list.Clear(); listPool.Enqueue(list); }
spatialGrid.Clear();
foreach (var unit in allUnits)
{
if (!spatialGrid.ContainsKey(cell))
{
// Pool에서 재사용 (없으면 새로 생성)
List<UnitBase> list = listPool.Count > 0
? listPool.Dequeue()
: new List<UnitBase>();
spatialGrid[cell] = list;
}
}
}
효과: List 생성/파괴 최소화 → GC 발생 빈도 감소
4-2. 업데이트 간격 조절
문제점: private void LateUpdate() { UpdateSpatialGrid(); // 매 프레임 (60fps = 60회/초) }
Spatial Grid는 약간의 지연이 있어도 게임플레이에 큰 영향이 없다.
해결 방법: private float gridUpdateInterval = 0.1f; // 0.1초 간격 private float lastGridUpdate = 0f;
private void LateUpdate() { lastGridUpdate += Time.deltaTime;
if (lastGridUpdate >= gridUpdateInterval)
{
lastGridUpdate = 0f;
UpdateSpatialGrid(); // 0.1초마다만 실행 (10회/초)
}
}
효과: Grid 업데이트 빈도 83% 감소
4-3. GetNearbyAllies 최적화
문제점: public List<UnitBase> GetNearbyAllies(UnitBase unit, float searchRadius) { foreach (var otherUnit in spatialGrid[cell]) { float distance = Vector3.Distance( unit.transform.position, // 매번 네이티브 호출 otherUnit.transform.position // 매번 네이티브 호출 ); } }
Transform 반복 접근 + Sqrt 연산의 조합.
해결 방법: public List<UnitBase> GetNearbyAllies(UnitBase unit, float searchRadius) { Vector3 unitPos = unit.transform.position; // 한 번만 접근 float searchRadiusSqr = searchRadius * searchRadius; // 미리 계산
foreach (var otherUnit in spatialGrid[cell])
{
Vector3 otherPos = otherUnit.transform.position; // 한 번만 접근
float distanceSqr = (unitPos - otherPos).sqrMagnitude; // Sqrt 없음
if (distanceSqr <= searchRadiusSqr)
{
nearby.Add(otherUnit);
}
}
}
효과: Transform 접근 및 Sqrt 연산 대폭 감소
- State 객체 재사용 (Object Pool 패턴)
문제점: if (unit.IsTargetInAttackRange(currentTarget)) { unitFSM.ChangeState(new AttackState()); // 매번 new → GC 압박 }
State 전환 시마다 새로운 객체 생성.
해결 방법: public class UnitFSM : MonoBehaviour { // State 미리 생성 private MoveState moveState; private AttackState attackState; private DieState dieState;
public void Init(UnitBase unit)
{
moveState = new MoveState(); // 한 번만 생성
attackState = new AttackState();
dieState = new DieState();
ChangeState(moveState);
}
// 재사용
public void ChangeToAttackState() => ChangeState(attackState);
public void ChangeToMoveState() => ChangeState(moveState);
}
효과: State 객체 생성 제로화 → GC 압박 제거
최적화 원칙 정리
- 캐싱: 반복적으로 접근하는 데이터는 변수에 저장
- 빈도 조절: 매 프레임 실행이 필요 없는 작업은 간격 조절
- 연산 최적화: 비싼 연산(Sqrt, 네이티브 호출) 최소화
- Object Pool: 반복 생성/파괴되는 객체는 재사용
- 적절한 자료구조: 용도에 맞는 자료구조 선택
성능 측정 팁
Unity Profiler 마커 활용: using Unity.Profiling;
static readonly ProfilerMarker marker = new ProfilerMarker("MyFunction");
void MyFunction() { marker.Begin(); // 코드 marker.End(); }
Profiler에서 정확한 성능 측정 가능.
참고 자료
- Unity Manual: Performance Optimization
- Unity Best Practices: Transform Access
- C# Object Pooling Pattern
'TIL' 카테고리의 다른 글
| [2025_12_22] 모의면접 피드백 (1) | 2025.12.22 |
|---|---|
| [2025_12_18] Factory패턴의 실적용 여부 (0) | 2025.12.18 |
| [2025_12_16] 오토배틀 게임 유닛 AI 구현하며 마주친 문제들 (1) | 2025.12.16 |
| [2025_12_15] SpreadSheet에서 배열 파싱 오류 (0) | 2025.12.15 |
| [2025_12_12]FSM에서 StatePattern사용 (0) | 2025.12.12 |