[GAS] 함정 시스템 개선

2025. 12. 24. 20:59·Dev./UE 언리얼 엔진

함정 시스템은 겉으로는 잘 돌아가고 있었다.

플레이어가 밟으면 데미지를 주고, 이펙트도 나오고, 사운드도 나온다. 문제될 게 없어 보였다.

 

함정은 게임에서 가장 빈번하게 호출되는 오브젝트 중 하나였다.

하나가 터지면 열 개가 같이 터지고,

그때마다 월드 전체를 순회하며 매니저를 찾고,

모든 컴포넌트를 뒤지며 충돌체를 제어하고, 비슷한 RPC가 세 개씩 전달되었다.

그리고 다중 히트가 발생하면 혈흔 VFX가 제한 없이 쏟아졌다.

 

각각은 사소해 보이지만 함정 시스템의 본질은 동시 다발성이다.

이 구조 그대로라면 스케일이 커질수록 성능은 기하급수적으로 무너질 게 뻔했다.


Smell Code 네 가지

LLM을 활용하여 코드 리뷰를 하면서 발견한 문제점들을 정리해봤다.

 

첫째, 매번 월드를 뒤지는 매니저 검색.

GetTrapManager()가 호출될 때마다 TActorIterator가 월드 전체 액터를 순회하고 있었다.

[함정 수 X 호출 빈도]만큼의 반복문이 매 프레임 돌아가고 있었던 셈이다.

 

항상 같은 매니저를 찾는 구조인데, 왜 매번 새로 검색해야 할까?

 

둘째, 컴포넌트 전수조사.

특정 충돌체만 제어하려고 매번 GetComponents()를 호출하고,

모든 컴포넌트에 대해 ComponentHasTag(FName)을 실행하고 있었다.

이 로직이 활성화, 비활성화, 이벤트 처리 시마다 반복됐다.

문자열 기반 태그 비교와 반복 루프의 조합은 CPU 입장에서 꽤 비싼 조합이다.

 

셋째, 중복된 Multicast RPC.

사운드 종류만 다른 Multicast RPC가 세 개 존재했다.

로직은 동일한데 관리 포인트만 늘어나는 구조였고,

사운드가 추가될 때마다 RPC도 늘어나야 하는 확장에 취약한 설계였다.

 

넷째, 제한 없는 VFX 스폰.

함정에서 다중 히트가 발생할 때, 히트마다 Multicast로 VFX를 호출했다.

쿨다운이 없었기에 네트워크 트래픽이 늘고, 클라이언트 화면은 이펙트가 마구 나올 수 밖에 없다.


해결의 키워드: 캐싱, 사전 분류, 통합, 제한

복잡한 알고리즘이나 화려한 기법이 필요한 게 아니었다.

핵심은 단순했다. 불필요한 일을 안 하게 만드는 것.


1. 캐싱으로 검색 비용 제거

TrapManager는 한 번만 찾고 재사용하도록 변경했다.

// 헤더
mutable TWeakObjectPtr<AGS_TrapManager> CachedTrapManager;

// 소스
AGS_TrapManager* AGS_TrapBase::GetTrapManager() const
{
    if (CachedTrapManager.IsValid())
        return CachedTrapManager.Get();
    
    for (TActorIterator<AGS_TrapManager> It(GetWorld()); It; ++It)
    {
        CachedTrapManager = *It;
        return *It;
    }
    return nullptr;
}

여기서 TWeakObjectPtr을 사용한 이유가 있다.

TrapManager는 UObject이기 때문에 언제든 가비지 컬렉션에 의해 파괴될 수 있다.

일반 포인터로 캐싱하면 객체가 파괴된 후에도 포인터가 유효한 것처럼 보여 크래시로 이어질 수 있다.

TWeakObjectPtr은 참조하는 객체가 파괴되면 자동으로 무효화되어 IsValid() 체크로 안전하게 접근할 수 있다.

 

George Prosser의 TWeakObjectPtr 최적화 가이드에 따르면,

IsValid()와 Get()은 내부적으로 거의 동일한 구현을 가지고 있어 한 번만 체크하면 된다.

위 코드처럼 IsValid() 후 Get()을 호출하면 불필요한 중복 검증을 피할 수 있다.

 

결과: 호출 비용이 O(n)에서 O(1)로 바뀌었다.


2. BeginPlay에서 데이터 미리 분류

런타임 중 GetComponents() 호출을 제거하기 위해,

BeginPlay 시점에 필요한 충돌체만 선별해 배열로 저장했다.

// 헤더
TArray<TWeakObjectPtr<UPrimitiveComponent>> OptimizedCollisionComponents;

// 소스
void AGS_TrapBase::BeginPlay()
{
    TArray<UActorComponent*> PrimComponents;
    GetComponents(UPrimitiveComponent::StaticClass(), PrimComponents);
    
    for (UActorComponent* Comp : PrimComponents)
    {
        if (UPrimitiveComponent* Prim = Cast<UPrimitiveComponent>(Comp))
        {
            if (Prim->ComponentHasTag("OptimizedCollision"))
            {
                OptimizedCollisionComponents.Add(Prim);
            }
        }
    }
}

태그 비교는 문자열 비교를 수반하기 때문에 생각보다 비용이 크다.

초기화 단계에서 한 번만 수행하고 결과를 저장해두면, 이후에는 미리 분류된 배열만 순회하면 된다.

 

결과: 실행 중 루프가 최소화되고, 빈번한 이벤트에서도 안정적인 성능을 유지하게 됐다.


3. Enum 기반 RPC 통합

세 개의 Multicast RPC를 하나로 통합했다.

사운드 타입을 Enum으로 정의하고, 하나의 RPC가 모든 사운드를 처리하도록 구조를 변경했다.

UENUM(BlueprintType)
enum class ETrapSoundType : uint8
{
    Activation,
    Deactivation,
    Hit
};

UFUNCTION(NetMulticast, Reliable)
void Multicast_PlayTrapSound(ETrapSoundType SoundType);

WizardCell의 멀티플레이어 가이드에서 강조하듯이, Multicast RPC는 공짜가 아니다.

서버에서 호출하면 모든 연결된 클라이언트에게 전송된다.

RPC 개수를 줄이는 것만으로도 네트워크 대역폭을 절약할 수 있고, 코드 관리도 쉬워진다.

사운드가 추가되면? Enum에 값 하나 추가하고, 데이터 테이블에서 해당 사운드를 매핑하면 끝이다.

 

결과: RPC 개수 감소, 코드 가독성 향상, 확장성 확보.


4. VFX 쿨다운

VFX는 게임플레이에 영향을 주지 않는 순수한 시각 요소다.

서버에서 Multicast를 날릴 때마다 네트워크 비용이 발생하지만,

결국 VFX 재생 여부는 클라이언트가 결정해도 된다.

 

그래서 서버는 Multicast를 그대로 보내되, 클라이언트에서 시간 기반 필터링을 적용했다.

void AGS_TrapBase::Multicast_PlayTrapHitBloodEffect_Implementation(FVector HitLocation)
{
    const double CurrentTime = GetWorld()->GetTimeSeconds();
    
    if (CurrentTime - LastBloodVFXTime < BloodVFXCooldown)
        return; // 0.3초 이내면 무시
    
    LastBloodVFXTime = CurrentTime;
    // VFX 재생 로직
}

왜 서버가 아닌 클라이언트에서 제한했을까?

 

VFX는 각 클라이언트의 화면에서만 의미가 있다.

서버에서 필터링하면 어떤 클라이언트에게 VFX를 보내야 할지 결정하는 추가 로직이 필요해진다.

반면 클라이언트에서 필터링하면 각자의 상황에 맞게 알아서 처리하면서도 네트워크 부하를 줄일 수 있다.

 

결과: 네트워크 트래픽 감소, 화면 과부하 제거, 체감 프레임 안정화.


🌟인사이트

이번 작업을 하면서 정리한 원칙들이다.

 

검색은 한 번, 사용은 여러 번.

FindActor, GetComponent, FindRow 같은 검색 함수들은 BeginPlay에서 캐싱하는 게 기본이다.

매 프레임, 매 이벤트에서 검색하는 순간 이미 늦다.

 

TWeakObjectPtr을 적극 활용하라.

UObject는 언제든 파괴될 수 있다. 캐싱할 때는 메모리 안정성을 확보하기 위해 약참조를 쓰자.

다만 IsValid()와 Get()을 따로 호출하면 내부적으로 동일한 검증을 두 번 수행하게 되니, 가능하면 Get() 한 번으로 처리하자.

 

Multicast는 모든 클라이언트에게 뿌려진다.

이걸 항상 염두에 두자.

Enum으로 매개변수를 단순화하면 RPC 개수를 줄일 수 있고,

게임플레이에 영향 없는 요소는 클라이언트 단에서 필터링하는 게 효율적이다.

 

태그 비교는 생각보다 무겁다.

런타임 문자열 비교는 최후의 수단이어야 한다.

초기화 시점에 분류해두고 인덱스로 접근하는 게 베스트 프랙티스다.

 

BeginPlay는 최고의 최적화 타이밍이다.

여기서 할 수 있는 건 전부 여기서 끝내자.


마무리하며

Trap 다중 활성화 상황에서 프레임 드랍이 눈에 띄게 줄었다.

Multicast 호출 횟수도 크게 감소했다.

무엇보다 코드 구조가 단순해지면서 이후 기능 추가가 쉬워졌다.

 

"최적화했다"기보다는 “불필요한 일을 안 하게 만들었다”는 표현이 더 정확할 것 같다.

 

그리고 이 최적화 패턴은 함정 시스템에만 국한되지 않는다.

스킬 시스템, 투사체, 상호작용 오브젝트, AI 매니저…

"빈번한 호출 + 월드 탐색 + 네트워크"가 결합된 모든 시스템에 동일하게 적용할 수 있는 핵심 지침이다.

'Dev. > UE 언리얼 엔진' 카테고리의 다른 글

[GAS] 렌더링 최적화 : 30 FPS에서 60 FPS까지  (2) 2025.12.30
[GAS] UE5 네트워크 설정에 대한 조사... 그리고 프로젝트 적용  (0) 2025.12.29
[GAS] RTS 미니맵 구현 & 성능 최적화  (0) 2025.12.05
언리얼 엔진 5.4 + Visual Studio 2026 호환성 문제 해결  (0) 2025.12.02
Git 크래시 관련 트러블슈팅 (GitKraken Blame 기능)  (0) 2025.05.28
'Dev./UE 언리얼 엔진' 카테고리의 다른 글
  • [GAS] 렌더링 최적화 : 30 FPS에서 60 FPS까지
  • [GAS] UE5 네트워크 설정에 대한 조사... 그리고 프로젝트 적용
  • [GAS] RTS 미니맵 구현 & 성능 최적화
  • 언리얼 엔진 5.4 + Visual Studio 2026 호환성 문제 해결
raindrovvv
raindrovvv
raindrovvv 님의 블로그 입니다.
  • raindrovvv
    raindrovvv 님의 블로그
    raindrovvv
  • 전체
    오늘
    어제
    • 분류 전체보기 (170) N
      • Dev. (163) N
        • AI 인공지능 (27)
        • UE 언리얼 엔진 (81) N
        • Unity 유니티 (0)
        • Wwise 와이즈 (7)
        • 게임 네트워크 (8)
        • 그래픽스 Graphics (22)
        • 프로젝트 (8)
        • 기타 개발 관련 (10)
      • Computer Science (0)
        • 하드웨어 HW (0)
        • 소프트웨어 SW (0)
        • 통신 (0)
        • 데이터 (0)
      • 블로그 (3)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    생산성
    dev
    devlog
    게임개발
    바이브코딩
    트러블슈팅
    Git
    언리얼
    UE
    언리얼엔진
    그래픽스
    네트워크
    깃
    인디게임
    AI
    Unreal
    에이전트
    TA
    unrealengine
    Wwise
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
raindrovvv
[GAS] 함정 시스템 개선
상단으로

티스토리툴바