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