PART 7 - 강의 4/4

GAS 성능 최적화

대규모 게임을 위한 최적화 전략

01

ASC 최적화

AbilitySystemComponent 경량화

ASC 설정 최적화 // 프로젝트 요구에 맞는 ASC 서브클래스 UCLASS() class UOptimizedAbilitySystemComponent : public UAbilitySystemComponent { GENERATED_BODY() public: UOptimizedAbilitySystemComponent() { // Replication Mode 설정 // AI는 Minimal, 플레이어는 Mixed SetReplicationMode(EGameplayEffectReplicationMode::Minimal); // 불필요한 델리게이트 비활성화 bSuppressGrantAbility = false; bSuppressGameplayCues = false; } // 어빌리티 개수 제한 virtual void GiveAbility(const FGameplayAbilitySpec& Spec) override { if (ActivatableAbilities.Abilities.Num() >= MaxAbilityCount) { UE_LOG(LogGAS, Warning, TEXT("Max ability count reached!")); return; } Super::GiveAbility(Spec); } // Effect 개수 제한 virtual FActiveGameplayEffectHandle ApplyGameplayEffectSpecToSelf( const FGameplayEffectSpec& Spec, FPredictionKey PredictionKey = FPredictionKey()) override { // 최대 활성 Effect 수 체크 if (GetNumActiveGameplayEffects() >= MaxActiveEffects) { // 가장 오래된 것 제거 또는 경고 CleanupOldestEffect(); } return Super::ApplyGameplayEffectSpecToSelf(Spec, PredictionKey); } protected: UPROPERTY(EditDefaultsOnly) int32 MaxAbilityCount = 20; UPROPERTY(EditDefaultsOnly) int32 MaxActiveEffects = 50; };
02

Effect 최적화

GameplayEffect 성능 개선

Effect 최적화 전략 // 1. Periodic Effect 최소화 // 나쁜 예: 매 0.1초마다 데미지 UCLASS() class UGE_BadDoT : public UGameplayEffect { UGE_BadDoT() { Period = 0.1f; // 초당 10번 = 많은 오버헤드 } }; // 좋은 예: 총 데미지를 한 번에 또는 긴 주기로 UCLASS() class UGE_GoodDoT : public UGameplayEffect { UGE_GoodDoT() { Period = 1.0f; // 초당 1번 // 또는 Duration Modifier로 초당 데미지 적용 } }; // 2. Modifier 대신 Execution 사용 (복잡한 계산) // Modifier는 매번 재계산, Execution은 한 번만 UCLASS() class UGE_OptimizedDamage : public UGameplayEffect { UGE_OptimizedDamage() { // Modifier 대신 Execution 사용 Executions.Add(FGameplayEffectExecutionDefinition()); Executions[0].CalculationClass = UEC_OptimizedDamage::StaticClass(); } }; // 3. SetByCaller로 런타임 값 전달 void ApplyOptimizedDamage(float DamageAmount) { FGameplayEffectSpecHandle SpecHandle = MakeOutgoingGameplayEffectSpec( DamageEffectClass, 1); // SetByCaller로 값 전달 (Effect 재생성 불필요) SpecHandle.Data->SetSetByCallerMagnitude( FGameplayTag::RequestGameplayTag(FName("Data.Damage")), DamageAmount); ApplyGameplayEffectSpecToTarget(..., SpecHandle, ...); } // 4. Effect Pooling (커스텀) TMap<TSubclassOf<UGameplayEffect>, TArray<FGameplayEffectSpec>> EffectSpecPool; FGameplayEffectSpec& GetPooledSpec(TSubclassOf<UGameplayEffect> EffectClass) { TArray<FGameplayEffectSpec>& Pool = EffectSpecPool.FindOrAdd(EffectClass); if (Pool.Num() > 0) { return Pool.Pop(); } return CreateNewSpec(EffectClass); }
Periodic Effect 주의

Period가 짧은 Effect는 서버와 클라이언트 모두에서 큰 부하를 발생시킵니다. 0.5초 이상의 주기를 권장합니다.

03

Attribute 최적화

어트리뷰트 계산 효율화

Attribute 최적화 // 1. 불필요한 어트리뷰트 제거 // 계산에만 사용되는 값은 어트리뷰트가 아닌 일반 변수로 UCLASS() class UOptimizedAttributeSet : public UAttributeSet { // 복제가 필요한 것만 어트리뷰트로 UPROPERTY(ReplicatedUsing = OnRep_Health) FGameplayAttributeData Health; // 계산용 임시 값은 일반 변수로 float CachedDamageMultiplier; // 어트리뷰트 아님 }; // 2. Aggregator 캐싱 활용 // CurrentValue는 캐시됨, BaseValue 변경 시만 재계산 void UMyAttributeSet::PreAttributeChange( const FGameplayAttribute& Attribute, float& NewValue) { // 여기서 무거운 계산 피하기 // 단순 클램핑만 수행 if (Attribute == GetHealthAttribute()) { NewValue = FMath::Clamp(NewValue, 0.f, GetMaxHealth()); } } // 3. Gameplay Effect Aggregator 최적화 // 많은 Modifier가 있는 어트리뷰트는 성능 저하 // 가능하면 Modifier 수를 줄이고 Execution 사용 // 4. FAggregatorEvaluateParameters 재사용 static FAggregatorEvaluateParameters CachedEvalParams; float EvaluateAttributeOptimized(const FGameplayAttribute& Attribute) { // 매번 새로 생성하지 않고 재사용 return ASC->GetNumericAttribute(Attribute); }
04

GameplayCue 최적화

시각 효과 성능 개선

Cue 최적화 전략 // 1. Static Cue 우선 사용 (Actor Cue는 무거움) UCLASS() class UGC_LightweightHit : public UGameplayCueNotify_Static { // CDO에서 직접 실행, 인스턴스 없음 }; // 2. Actor Cue 풀링 UCLASS() class UOptimizedGameplayCueManager : public UGameplayCueManager { public: // Cue Actor 재사용 TMap<UClass*, TArray<AGameplayCueNotify_Actor*>> CueActorPool; virtual AGameplayCueNotify_Actor* GetInstancedCueActor( AActor* TargetActor, UClass* CueClass, const FGameplayCueParameters& Parameters) override { // 풀에서 가져오기 TArray<AGameplayCueNotify_Actor*>* Pool = CueActorPool.Find(CueClass); if (Pool && Pool->Num() > 0) { AGameplayCueNotify_Actor* PooledCue = Pool->Pop(); PooledCue->SetActorHiddenInGame(false); return PooledCue; } return Super::GetInstancedCueActor(TargetActor, CueClass, Parameters); } void ReturnCueToPool(AGameplayCueNotify_Actor* CueActor) { CueActor->SetActorHiddenInGame(true); CueActorPool.FindOrAdd(CueActor->GetClass()).Add(CueActor); } }; // 3. Local Only Cue 활용 void ExecuteLocalCue(UAbilitySystemComponent* ASC) { // 복제 필요 없는 이펙트는 로컬에서만 if (ASC->IsOwnerActorAuthoritative() || ASC->IsNetSimulating()) { ASC->ExecuteGameplayCueLocal(CueTag, CueParams); } } // 4. Cue 비활성화 거리 설정 UCLASS() class AGC_DistanceCulled : public AGameplayCueNotify_Actor { public: AGC_DistanceCulled() { // 50m 이상 거리에서 비활성화 SetCullDistance(5000.f); } };
05

프로파일링과 디버깅

성능 병목 찾기

GAS 프로파일링 // 콘솔 명령어 // AbilitySystem.Debug.NextCategory - 디버그 카테고리 전환 // AbilitySystem.Debug.NextTarget - 디버그 타겟 전환 // showdebug AbilitySystem - 화면에 GAS 정보 표시 // 커스텀 통계 DECLARE_CYCLE_STAT(TEXT("GAS Effect Apply"), STAT_GASEffectApply, STATGROUP_GAS); DECLARE_CYCLE_STAT(TEXT("GAS Attribute Calculate"), STAT_GASAttributeCalc, STATGROUP_GAS); DECLARE_CYCLE_STAT(TEXT("GAS Cue Execute"), STAT_GASCueExecute, STATGROUP_GAS); void UMyASC::ApplyGameplayEffectSpecToSelf(...) { SCOPE_CYCLE_COUNTER(STAT_GASEffectApply); Super::ApplyGameplayEffectSpecToSelf(...); } // 메모리 사용량 체크 void DebugGASMemory(UAbilitySystemComponent* ASC) { int32 AbilityCount = ASC->GetActivatableAbilities().Num(); int32 EffectCount = ASC->GetNumActiveGameplayEffects(); UE_LOG(LogGAS, Log, TEXT("ASC: %s - Abilities: %d, Effects: %d"), *ASC->GetOwner()->GetName(), AbilityCount, EffectCount); // 경고 임계값 if (EffectCount > 30) { UE_LOG(LogGAS, Warning, TEXT("High effect count on %s!"), *ASC->GetOwner()->GetName()); } } // Insights에서 GAS 이벤트 추적 #if WITH_EDITOR UE_TRACE_EVENT_BEGIN(GASChannel, EffectApplied) UE_TRACE_EVENT_FIELD(UE::Trace::WideString, EffectName) UE_TRACE_EVENT_FIELD(float, Duration) UE_TRACE_EVENT_END() void TraceEffectApplied(const FGameplayEffectSpec& Spec) { UE_TRACE_LOG(GASChannel, EffectApplied) << EffectApplied.EffectName(*Spec.Def->GetName()) << EffectApplied.Duration(Spec.GetDuration()); } #endif
최적화 우선순위

1. Periodic Effect 주기 늘리기, 2. Minimal Replication Mode, 3. Static Cue 사용, 4. Effect/Ability 개수 제한 순으로 최적화하세요.

SUMMARY

핵심 요약

  • ASC 경량화: Ability/Effect 개수 제한, Replication Mode 최적화
  • Effect 최적화: Periodic 주기 늘리기, SetByCaller 활용
  • Attribute 최적화: 불필요한 어트리뷰트 제거, 캐싱 활용
  • Cue 최적화: Static Cue 우선, 풀링, Local Only
  • 프로파일링: SCOPE_CYCLE_COUNTER, showdebug AbilitySystem
COMPLETE

강좌 완료

축하합니다! UE5 GAS Deep Dive 전체 강좌를 완료했습니다.

학습한 내용
  • Part 1: GAS 기초 - ASC, 어빌리티 활성화, 네트워크 예측
  • Part 2: AttributeSet 심화 - 구조, 콜백, Meta Attributes
  • Part 3: GameplayEffect 심화 - Duration, MMC, Execution, Stacking
  • Part 4: GameplayAbility 심화 - Instancing, AbilityTask, 콤보, 차징
  • Part 5: GameplayCue - 타입별 활용, 네트워크 최적화
  • Part 6: 네트워크 복제 - Replication Mode, Prediction Key
  • Part 7: 실전 패턴 - 엘리먼트 반응, 캐릭터 스왑, 동적 쿨다운, 성능 최적화
PRACTICE

도전 과제

배운 내용을 직접 실습해보세요

실습 1: GAS 프로파일링

stat abilitysystem 명령어로 GAS의 CPU 사용량을 확인하세요. 어떤 어빌리티/GE가 가장 많은 시간을 소모하는지 식별하고 NonInstanced로 전환하세요.

실습 2: GE 풀링과 재사용

FGameplayEffectSpec을 캐싱하여 동일 GE의 반복 생성을 줄이세요. MakeOutgoingGameplayEffectSpec 호출을 최소화하고, 미리 생성된 SpecHandle을 재사용하세요.

심화 과제

undefined