* 본 게시물은 3D 유니티 프로젝트를 다룬다.
지난 게시물에서 다뤘던 Project Audtior를 활요하여 최적화를 몇 가지 더 진행하였다.
Object.FindObjectsOfType()
PipeInteractionRig.cs/ConveyorBeltDiagonalRig.cs - FindPlayerCC
//수정 전
static CharacterController FindPlayerCC(string tagName)
{
GameObject go = null;
if (!string.IsNullOrEmpty(tagName))
go = GameObject.FindGameObjectWithTag(tagName);
CharacterController cc = null;
if (go)
{
cc = go.GetComponent<CharacterController>()
?? go.GetComponentInChildren<CharacterController>(true)
?? go.GetComponentInParent<CharacterController>();
if (cc) return cc;
}
var all = Object.FindObjectsOfType<CharacterController>(true);
foreach (var c in all)
{
if (!string.IsNullOrEmpty(tagName) &&
(c.CompareTag(tagName) || c.transform.root.CompareTag(tagName)))
return c;
if (c.name.ToLower().Contains("player")) return c;
}
//수정 후
static CharacterController FindPlayerCC(string tagName)
{
GameObject go = null;
// 1) 태그로 플레이어 오브젝트 찾기
if (!string.IsNullOrEmpty(tagName))
go = GameObject.FindGameObjectWithTag(tagName);
if (!go)
return null;
// 2) 그 오브젝트(혹은 자식/부모)에서 CC 찾기
CharacterController cc = go.GetComponent<CharacterController>();
return cc;
}
기존 로직은 Player 태그가 없다면, ‘씬에 있는 모든 오브젝트 중 CharacterController가 붙어 있는 걸 먼저 찾는 비효율적인 코드였다.
하지만 FindObjectsOfType는 다음과 같은 문제가 존재한다.
- 씬에 존재하는 모든 활성/비활성 객체를 순회하며 CharacterController 컴포넌트를 찾는 CPU 부하가 매우 심한 작업이다.
- FindObjectsOfType는 결과를 반환할 때 내부적으로 새로운 배열을 생성하여 힙 메모리에 할당한다. 루프 안의 c.name.ToLower()도소문자로 변환된 새로운 문자열을 힙에 생성한다. 이 역시 GC 문제가 발생할 것이다.
우리는 이미 Player에 태그가 있고, 그 본체에 Player Controller가 할당된 사실을 알고 있다. 따라서 이러한 과정을 넣을 필요가 없다.
내가 알고 있는 정보를 최대한 활용하여 프로그래밍을 최적화할 필요가 있다는 사실을 다시금 깨달았다.
Physics.RaycastAll
Boss.cs
namespace Maggi.Character.Boss
{
// ... 기존 변수들 ...
private Vector3 _handCatch_InitPosition;
private Vector3 _handUnCatch_InitPosition;
+ // [최적화] RaycastNonAlloc 결과를 재사용할 버퍼 (메모리 할당 방지)
+ private readonly RaycastHit[] _raycastHits = new RaycastHit[8];
// ... (탐지 로직 내부) ...
Vector3 directionToTarget = (_target.position - origin).normalized;
float distanceToTarget = Vector3.Distance(origin, _target.position);
- Ray ray = new Ray(origin, directionToTarget);
- RaycastHit[] hits = Physics.RaycastAll(ray, distanceToTarget);
+ // [최적화] NonAlloc 사용 (새 배열 생성 안 함)
+ int hitCount = Physics.RaycastNonAlloc(
+ origin,
+ directionToTarget,
+ _raycastHits,
+ distanceToTarget
+ );
- // 충돌 결과를 가까운 순서대로 정렬 (O(N log N) + GC 발생 가능성)
- System.Array.Sort(hits, (hit1, hit2) => hit1.distance.CompareTo(hit2.distance));
+ if (hitCount <= 0)
+ {
+ Debug.DrawRay(origin, directionToTarget * distanceToTarget, Color.red, 3.0f);
+ return;
+ }
- Color rayColor = hits[0].transform.CompareTag(PLAYER_TAG) ? Color.green : Color.red;
- Debug.DrawRay(origin, directionToTarget * distanceToTarget, rayColor, 3.0f);
+ // [최적화] 가장 가까운 히트 직접 찾기 (O(N), 정렬 비용 제거)
+ int closestIndex = -1;
+ float closestDistance = float.MaxValue;
+
+ for (int i = 0; i < hitCount; i++)
+ {
+ var hit = _raycastHits[i];
+ if (hit.collider == null) continue;
+
+ if (hit.distance < closestDistance)
+ {
+ closestDistance = hit.distance;
+ closestIndex = i;
+ }
+ }
+
+ // 유효한 히트가 없으면 종료
+ if (closestIndex == -1)
+ {
+ Debug.DrawRay(origin, directionToTarget * distanceToTarget, Color.red, 3.0f);
+ return;
+ }
+
+ var closestHit = _raycastHits[closestIndex];
+ bool hitPlayerFirst = closestHit.transform.CompareTag(PLAYER_TAG);
+ Color rayColor = hitPlayerFirst ? Color.green : Color.red;
+ Debug.DrawRay(origin, directionToTarget * distanceToTarget, rayColor, 3.0f);
- if (hits[0].transform.CompareTag(PLAYER_TAG))
+ if (hitPlayerFirst)
{
if (_currentPlayable.Director != null)
{
RemoveBindingsFromTimeline(_currentPlayable.Director, gameObject);
}
SetMode(Mode.Detect, "PlayerDetected");
return;
}
- RaycastAll은 호출 될 때마다 매번 새로운 RaycastHit[] 배열을 힙 메모리에 생성한다. GC 문제가 발생한다. 따라서 미리 생성한 배열에 결과를 한 번만 할당하는 것으로 수정하기 위해, 버퍼를 생성하고, RaycastAll 대신 RaycastNonAlloc을 써준다.
- Array.sort은 전체 히트 결과를 거리 순으로 정렬하여 O(NlogN)의 비용이 든다. 하지만 가장 가까운 것 하나만 알고 싶은데 전체 줄을 세우는 것은 낭비다. 따라서 for문을 사용하여 closestDistance보다 작은 값이 나오면 갱신하는 방식으로 사용하면, O(N)의 비용이 든다. 미미하지만 CPU 부하를 줄여준다.
- hits[0]에 바로 접근하는 것보다는 hitCount ≤ 0 체크를 통해 충돌체가 없는 경우를 명확하게 방어하는 것이 훨씬 안전한 코드다.
* RaycastNonAlloc 인자 참고:
- origin (Vector3): Ray가 발사되는 시작 위치
- directionToTarget (Vector3): Ray가 뻗어 나갈 방향
- _raycastHits (RaycastHit[]): 충돌한 결과물들의 정보를 담을 결과 버퍼 배열
- distanceToTarget (float): Ray를 검사할 최대 거리
이렇게 Maggi 내의 Major한 문제를 모두 해결하였다.
문제는 이렇게 모두 최적화를 해주었는데도 계속 문제가 남아있다는 메세지가 남아 있는 경우가 있었는데, Project Auditor는 최적화에 문제가 될 여지가 있는 인자를 사용하기만 해도 해당 문제가 해결되지 않았다고 판단하기 때문이다.
당연히 Project Auditor는 신이 아니기 때문에, 개발자가 능력껏 최적화를 한 뒤 알아서 걸러서 볼 필요가 있다.
'공부 > Unity' 카테고리의 다른 글
| [최적화 스터디] 4. 드로우 콜, 배칭, SRP Batcher (0) | 2025.11.27 |
|---|---|
| [최적화 스터디] 2. Project Auditor (0) | 2025.11.21 |
| [최적화 스터디] 1. GC에 대해 (0) | 2025.11.18 |