내일배움캠프-언리얼

TIL - 85

이시보 2026. 6. 5. 21:36

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])

  1. GE_Cold 부여 전 검사 → 대상 Burn 없음 → 반응 X → Cold 적용
  2. GE_Burn 부여 전 검사 → 대상 Cold 보유반응 발동(이속-40% + Cold 제거) → Burn 적용
  3. 결과: 화상 ON, 이속 -40% ✓

'내일배움캠프-언리얼' 카테고리의 다른 글

TIL - 87  (0) 2026.06.09
TIL - 86  (0) 2026.06.08
TIL - 84  (0) 2026.06.04
TIL - 83  (0) 2026.06.02
TIL - 82  (0) 2026.06.01