GAS 원소 반응(Elemental Reaction) 구현
버스트 스킬의 상태 효과(화상/추위)에 원소 반응을 데이터 주도로 구현
반응 명세
| 조건 | 결과 |
|---|---|
| 화상 상태 + 추위 부여 | 추가 데미지 + 화상 제거 (추위는 남김) |
| 추위 상태 + 화상 부여 | 이속 -40% + 추위 제거 (화상은 남김) |
설계 결정
- 감지 위치:
PlayerBurstComponent::ApplyHitToTarget— 상태 GE 부여 직전에 검사. (명세상 상태는 전부 버스트 경유라 충분) - 반응 정의:
DT_ElementReaction데이터 테이블로 분리 (코드 하드코딩 대신 확장성 확보). - 핵심 발견:
ApplyHitToTarget이 그동안 DamageEffect만 적용하고 StatusEffects는 아예 안 붙이고 있었음 → 이번에 상태 부여 + 반응 둘 다 추가.
1. 데이터 구조 (FElementReactionRow)
USTRUCT(BlueprintType)
struct RETRIEVE_API FElementReactionRow : public FTableRowBase
{
GENERATED_BODY()
// 이 상태 GE가 부여될 때 반응 검사
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Reaction")
TSubclassOf<UGameplayEffect> IncomingStatusEffect;
// 대상이 이 태그를 이미 보유 중이면 반응 발동
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Reaction", meta = (Categories = "State.Status"))
FGameplayTag RequiredExistingTag;
// 반응 시 적용할 효과 (추가 데미지 / 이속 디버프)
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Reaction")
TSubclassOf<UGameplayEffect> ReactionEffect;
// 반응 시 제거할 상태 태그
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Reaction", meta = (Categories = "State.Status"))
FGameplayTag RemoveStatusTag;
};
2. 적용 흐름 (ApplyHitToTarget)
// DamageEffect 적용 후
for (const TSubclassOf<UGameplayEffect>& StatusGE : Hit.StatusEffects)
{
if (!IsValid(StatusGE)) continue;
// 상태 부여 "전"에 반응 검사 (기존 상태 + 새 상태)
TryElementReaction(SourceASC, TargetASC, StatusGE);
// 들어온 상태는 반응 여부와 무관하게 적용 (새 상태는 남김)
FGameplayEffectSpecHandle StatusSpec = SourceASC->MakeOutgoingSpec(StatusGE, 1.f, Context);
if (StatusSpec.IsValid() && StatusSpec.Data.IsValid())
SourceASC->ApplyGameplayEffectSpecToTarget(*StatusSpec.Data.Get(), TargetASC);
}
3. 반응 판정 (TryElementReaction)
void UPlayerBurstComponent::TryElementReaction(UAbilitySystemComponent* SourceASC,
UAbilitySystemComponent* TargetASC, const TSubclassOf<UGameplayEffect>& IncomingStatusGE)
{
if (!ReactionTable || !IsValid(SourceASC) || !IsValid(TargetASC) || !IsValid(IncomingStatusGE)) return;
TArray<FElementReactionRow*> Rows;
ReactionTable->GetAllRows<FElementReactionRow>(TEXT("..."), Rows);
for (const FElementReactionRow* Row : Rows)
{
if (!Row || Row->IncomingStatusEffect != IncomingStatusGE) continue;
if (!TargetASC->HasMatchingGameplayTag(Row->RequiredExistingTag)) continue;
// 반응 효과 적용
if (IsValid(Row->ReactionEffect)) { /* MakeOutgoingSpec → ApplyGameplayEffectSpecToTarget */ }
// 기존 상태 제거 (그 태그를 부여하는 액티브 GE를 제거)
if (Row->RemoveStatusTag.IsValid())
TargetASC->RemoveActiveEffectsWithGrantedTags(FGameplayTagContainer(Row->RemoveStatusTag));
break; // 한 상태당 한 반응
}
}
동작 검증 (WWF: StatusEffects = [GE_Cold, GE_Burn])
GE_Cold부여 전 검사 → 대상 Burn 없음 → 반응 X → Cold 적용GE_Burn부여 전 검사 → 대상 Cold 보유 → 반응 발동(이속-40% + Cold 제거) → Burn 적용- 결과: 화상 ON, 이속 -40% ✓