GAS – Gameplay Effect(GE)로 값 변화 다뤄보기
“Attribute 값을 직접 만지지 말고, ‘변화의 규칙’을 Gameplay Effect로 정의하자.”
GAS에서 Gameplay Effect는
👉 값을 바꾸는 ‘결과’가 아니라, ‘방법’을 정의하는 도구였다.
처음 GAS를 배울 때는 이런 코드가 자연스럽다.
float CurrentHealth = GetHealth();
SetHealth(CurrentHealth - 10);
학습용으로는 충분하지만, 실제 게임 로직에서는 문제가 너무 많다.
❌ 직접 수정 방식의 한계
| 문제 | 설명 |
|---|---|
| 네트워크 동기화 | 수동으로 복제 처리 필요 |
| 데미지 계산 | 공격력/방어력/크리티컬 등 직접 계산 필요 |
| 재사용성 | 같은 코드를 매번 다시 작성하게 됨 |
| 추적 | 어떤 변화a 수치 변화가 언제/왜 일어났는지 확인 어려움 |
| 버프/디버프 | 타이머로 수동 관리해야 해서 복잡해짐 |
| 디버깅 | 변화 원인을 찾기 힘들고 로그/추적이 번거로움 |
Gameplay Effect = Attribute를 어떻게 바꿀지 정의한 규칙
값을 직접 바꾸지 않고,
“이런 변화가 일어난다”는 레시피를 만들어서 적용한다.
예시로 이해하기
Health - 10 → 즉시 데미지 Effect
5초간, 1초마다 Health -5 → 독 Effect
무한히 1초마다 Health +2 → 자동 회복 Effect
👉 같은 Effect를 여러 캐릭터, 여러 상황에서 재사용 가능
직접 수정 vs Gameplay Effect
| 항목 | 직접 수정 (Set) | Gameplay Effect |
|---|---|---|
| 네트워크 복제 | 수동 처리 😓 | 자동 ✅ |
| 데미지 계산 | 직접 코딩 😓 | Modifier로 정의 ✅ |
| 재사용 | 매번 코딩 😓 | 한 번 만들면 재사용 ✅ |
| 추적 | 어려움 😓 | Active Effects 확인 ✅ |
| 버프/디버프 | 타이머/수동 관리 😓 | Duration으로 자동 ✅ |
| 조건 처리 | if문 지옥 😓 | Tag Query로 깔끔 ✅ |
| 디버깅 | 힘듦 😓 | showdebug로 확인 ✅ |
💡 요리 비유
재료 = Attribute
조리법 = Modifier
레시피 = Gameplay Effect
같은 레시피로 계속 요리 가능 → 같은 Effect로 계속 적용 가능
| 타입 | Duration | 자동 제거 | Period | Active Effects | 사용 예시 |
|---|---|---|---|---|---|
| Instant | 0초 (즉시) | ✅ 즉시 | ❌ 불가 | ❌ 남지 않음 | 데미지, 힐, 자원 소모 |
| Duration | N초 | ✅ N초 후 | ✅ 가능 | ✅ 남음 | 독, 버프/디버프, 쿨다운 |
| Infinite | 무한 | ❌ 수동 제거 | ✅ 가능 | ✅ 남음 | 패시브, 장비효과, 자동회복 |
⚠️ Infinite Effect는 Handle 관리가 핵심
| Operation | 설명 | 예시 | 자주 쓰는 케이스 |
|---|---|---|---|
| Add | 더하기 | Health + (-10) | 데미지/힐/자원 소비 |
| Multiply | 곱하기 | AttackPower × 1.5 | % 버프/디버프 |
| Divide | 나누기 | Speed ÷ 2 | 속도 감소 디버프 |
| Override | 덮어쓰기 | Health = 100 | 완전 회복/리셋 |
변화량 계산 방식 (Magnitude)
| 방식 | 특징 | 예시 | 사용처 |
|---|---|---|---|
| Scalable Float | 고정값 + 레벨 스케일링 가능(커브) | -10 / +30 | 대부분의 기본 Effect |
| Attribute Based | 다른 Attribute를 참조해서 계산 | MaxHealth × 0.1 | 비율 데미지/회복 |
| Custom Calculation Class | C++로 복잡 계산 | (공격-방어)×크리×속성 | 실전 데미지 공식 |
1️⃣ Scalable Float (가장 많이 씀)
고정 값
CurveTable로 레벨 스케일링 가능
-10 → 항상 10 데미지
2️⃣ Attribute Based
다른 Attribute 기반 계산
MaxHealth × 0.1
3️⃣ Custom Calculation (고급)
C++ 계산 클래스
실전 데미지 공식
Period (주기)
Duration / Infinite에서만 사용
Period > 0 → 반복 적용
5초 / 1초 = 5번 적용
Stacking (중첩)
같은 Effect 여러 번 적용 시 처리 방식
| 정책 | 의미 | 예시 |
|---|---|---|
| None | 중첩 가능(각각 독립) | 독 2개면 따로 2개가 돌음 |
| Aggregate by Source | 소스별 1개 유지 | 적 A 독 + 적 B 독은 별개 |
| Aggregate by Target | 타겟당 1개 유지 | 독 새로 걸면 갱신/덮어쓰기 |
✔ Instant Damage
Duration Policy: Instant
Modifier: Health Add -10
✔ Duration Poison
Duration: 5초
Period: 1초
Modifier: Health Add -5
✔ Infinite Regen
Duration: Infinite
Period: 1초
Modifier: Health Add +2
Handle 저장 & Remove 필수
Apply Gameplay Effect to Target
Infinite → Handle 저장
Duration → EndOverlap 필요 없음 (자동 제거)
💡 이 지점에서
“직접 수치 수정 로직이 완전히 사라진다”는 게 체감됨
| 항목 | Blueprint | C++ |
|---|---|---|
| 생성 속도 | 빠름 (클릭) | 느림 (코드 작성) |
| 수정 편의성 | 빠름 | 재컴파일 필요 |
| 버전 관리(diff) | 어려움(바이너리) | 쉬움(텍스트) |
| 코드 리뷰 | 어려움 | 쉬움 |
| 재사용/확장 | 복사 위주 | 상속/조합 가능 |
| 비프로그래머 접근 | 가능 | 어려움 |
| 대량 생산(템플릿화) | 느림 | 빠름 |
Effect 정의는 C++,
적용은 Blueprint
→ 실무에서 가장 많이 쓰는 패턴
제거 (Infinite)
ASC->RemoveActiveGameplayEffect(Handle);
| 노드 | 파라미터 | 의미 |
|---|---|---|
| Apply Gameplay Effect to Target | Target | Effect를 받을 ASC |
| Apply Gameplay Effect to Target | Gameplay Effect Class | 적용할 GE |
| Apply Gameplay Effect to Target | Level | Effect 레벨(기본 1.0) |
| Apply Gameplay Effect to Target | Return Value | Active Effect Handle (Instant는 보통 저장 X) |
| Remove Active Gameplay Effect | Target | ASC |
| Remove Active Gameplay Effect | Handle | 저장해둔 ActiveEffectHandle |
| Remove Active Gameplay Effect | Stack Count | 제거할 스택 수 (-1 = 전부) |
⚠️ Handle 없으면 제거 못 함
Instant → 안 보임
Duration / Infinite → Active 상태 확인 가능
👉 “지금 어떤 Effect가 캐릭터에 걸려 있는지”
눈으로 확인 가능해지는 순간 GAS가 이해되기 시작함
🎯 오늘 목표
레벨 → 웨이브 구조의 게임 루프 완성
웨이브별 목표 코인 수 / 제한 시간 관리
HUD 실시간 업데이트
캐릭터 사망 시 게임오버 처리
메인 메뉴 / 게임 오버 메뉴 UI 통합
오버헤드 HP UI 개선 (프로그레스바 + 부드러운 감소)
1️⃣ 게임 루프 구조 정리 (Level → Wave)
전체 흐름
Game Start
→ Level Start
→ Wave 1
→ Wave 2
→ Wave 3
→ Next Level
→ ...
→ Game Over
핵심 상태 변수
int32 CurrentLevelIndex;
int32 CurrentWaveIndex;
int32 WavesPerLevel;
int32 SpawnedCoinCount;
int32 CollectedCoinCount;
int32 RequiredCoinCount;
웨이브 시작 로직 핵심
웨이브 시작 시
코인 카운트 초기화
목표 코인 수 설정
웨이브 제한 시간 타이머 설정
아이템 스폰
HUD 즉시 갱신
void AGLGamceState::StartWave()
{
CollectedCoinCount = 0;
RequiredCoinCount = DefaultRequiredCoins;
if (RequiredCoinsPerWave.IsValidIndex(CurrentWaveIndex))
{
RequiredCoinCount = RequiredCoinsPerWave[CurrentWaveIndex];
}
float WaveTime = DefaultWaveDuration;
if (WaveDurations.IsValidIndex(CurrentWaveIndex))
{
WaveTime = WaveDurations[CurrentWaveIndex];
}
GetWorldTimerManager().SetTimer(
WaveTimerHandle,
this,
&AGLGamceState::OnWaveTimeUp,
WaveTime,
false
);
UpdateHUD();
}
2️⃣ 웨이브 클리어 조건 개선
❌ 기존
모든 코인을 먹어야 웨이브 종료
✅ 개선
웨이브마다 “필요한 개수만” 먹으면 클리어
void AGLGamceState::OnCoinCollected()
{
CollectedCoinCount++;
UpdateHUD();
if (CollectedCoinCount >= RequiredCoinCount)
{
EndWave();
}
}
3️⃣ HUD 업데이트 구조 정리
HUD에 표시하는 정보
Level
Wave (현재 / 전체)
Goal (웨이브 목표 코인 수)
Collected (현재 획득 수)
Time (웨이브 남은 시간)
HUD는 GameState에서 단일 책임으로 갱신
void AGLGamceState::UpdateHUD()
{
float RemainingTime = GetWorldTimerManager().GetTimerRemaining(WaveTimerHandle);
if (RemainingTime < 0.f) RemainingTime = 0.f;
// TextBlock 이름:
// Level / Wave / Goal / Collected / Time
}
배운 점
HUD는 Tick이 아니라 Timer 기반 주기 업데이트가 훨씬 안정적
웨이브 전환 직후, 코인 획득 직후에는 즉시 UpdateHUD() 호출 필요
4️⃣ GameInstance를 “점수의 단일 소스”로 통일
문제
게임 중 점수는 오르는데
게임 오버 화면에서 TotalScore가 항상 0
원인
GameState의 Score만 증가
GameInstance의 TotalScore는 갱신 안 됨
해결
void AGLGamceState::AddScore(int32 Amount)
{
Score += Amount;
if (UGLGameInstance* GI = GetGameInstance<UGLGameInstance>())
{
GI->AddToScore(Amount);
}
}
5️⃣ 메인 메뉴 / 게임 오버 메뉴 통합
구조
WBP_MainMenu 하나로 관리
내부에 패널 2개:
Panel_Main (Start / Exit)
Panel_GameOver (Restart / BackToMainMenu)
장점
위젯 중복 생성 X
UI 관리 단순
입력 문제 최소화
6️⃣ 게임 오버 시 버튼 클릭 안 되던 문제
증상
버튼 Hover / Click 안 됨
커서는 보이지만 반응 없음
원인
HUD 위젯이 화면에 남아 입력을 가로챔
InputMode / 포커스 미설정
해결
HUDWidgetInstance->RemoveFromParent();
bShowMouseCursor = true;
FInputModeUIOnly Mode;
Mode.SetWidgetToFocus(MainMenuWidgetInstance->TakeWidget());
SetInputMode(Mode);
배운 점
UI 입력 문제의 90%는
“보이지 않는 위젯이 히트테스트를 먹고 있는 것”
7️⃣ 캐릭터 낙사(Kill Z) & 사망 처리
방법 1 – 엔진 기본 훅 사용
void AGLCharacter::FellOutOfWorld(const UDamageType& dmgType)
{
Health = 0.f;
UpdateOverheadHP(false);
OnDeath();
}
사망 처리에서 GameOver 호출
void AGLCharacter::OnDeath()
{
if (AGLGamceState* GS = GetWorld()->GetGameState<AGLGamceState>())
{
GS->OnGameOver();
}
}
8️⃣ 오버헤드 HP UI 개선
요구사항
ProgressBar 기반 HP 표시
데미지 시 깜빡임
HP 감소 부드럽게 보간
구조
오버헤드 위젯을 C++ 기반 위젯 클래스로 분리
표시용 HPPercent를 따로 두고 Interp
DisplayPercent = FMath::FInterpTo(
DisplayPercent,
TargetPercent,
DeltaTime,
InterpSpeed
);