PART 7 - 강의 2/4

캐릭터 스왑 시스템

다중 캐릭터 전환과 GAS 상태 관리

01

스왑 시스템 아키텍처

PlayerState 기반 설계

캐릭터 스왑 시스템에서는 PlayerState에 ASC를 배치하여 캐릭터가 바뀌어도 플레이어 레벨의 Effect가 유지되도록 합니다. 각 캐릭터는 개별 AttributeSet을 가집니다.

시스템 구조 // PlayerState: 공유 ASC 소유 // - 플레이어 레벨 버프/디버프 유지 // - 파티 전체 효과 관리 // 각 캐릭터: 개별 AttributeSet // - 캐릭터별 HP, 스탯 독립 // - 스왑 시 AttributeSet 교체 UCLASS() class AMyPlayerState : public APlayerState, public IAbilitySystemInterface { GENERATED_BODY() public: AMyPlayerState(); virtual UAbilitySystemComponent* GetAbilitySystemComponent() const override { return AbilitySystemComponent; } // 현재 활성 캐릭터 UPROPERTY(ReplicatedUsing = OnRep_ActiveCharacter) ASwappableCharacter* ActiveCharacter; // 파티 슬롯 (최대 4명) UPROPERTY(Replicated) TArray<FCharacterData> PartySlots; UFUNCTION(Server, Reliable) void Server_SwapCharacter(int32 SlotIndex); protected: UPROPERTY() UAbilitySystemComponent* AbilitySystemComponent; // 플레이어 레벨 AttributeSet (공유) UPROPERTY() UPlayerAttributeSet* PlayerAttributes; UFUNCTION() void OnRep_ActiveCharacter(); };
02

캐릭터 데이터 구조

스왑용 데이터 저장

캐릭터 데이터 USTRUCT(BlueprintType) struct FCharacterData { GENERATED_BODY() // 캐릭터 클래스 UPROPERTY(EditAnywhere) TSubclassOf<ASwappableCharacter> CharacterClass; // 저장된 어트리뷰트 값 UPROPERTY() TMap<FGameplayAttribute, float> SavedAttributes; // 저장된 쿨다운 UPROPERTY() TMap<TSubclassOf<UGameplayAbility>, float> SavedCooldowns; // 부여된 어빌리티 핸들 UPROPERTY() TArray<FGameplayAbilitySpecHandle> GrantedAbilities; // 활성 Effect 핸들 UPROPERTY() TArray<FActiveGameplayEffectHandle> ActiveEffects; // 대기 중 (필드에 없음) bool bIsOnStandby = true; }; // 캐릭터별 AttributeSet UCLASS() class UCharacterAttributeSet : public UAttributeSet { GENERATED_BODY() public: UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_Health) FGameplayAttributeData Health; UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_MaxHealth) FGameplayAttributeData MaxHealth; UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_Attack) FGameplayAttributeData Attack; UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_Defense) FGameplayAttributeData Defense; UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_ElementalMastery) FGameplayAttributeData ElementalMastery; };
03

스왑 로직 구현

AttributeSet 교체

캐릭터 스왑 구현 void AMyPlayerState::Server_SwapCharacter_Implementation(int32 SlotIndex) { if (!PartySlots.IsValidIndex(SlotIndex)) return; if (SlotIndex == GetActiveSlotIndex()) return; // 1. 현재 캐릭터 상태 저장 SaveCurrentCharacterState(); // 2. 현재 캐릭터 숨기기 if (ActiveCharacter) { ActiveCharacter->SetActorHiddenInGame(true); ActiveCharacter->SetActorEnableCollision(false); } // 3. 새 캐릭터 생성 또는 활성화 FCharacterData& NewCharData = PartySlots[SlotIndex]; if (NewCharData.bIsOnStandby) { // 스폰 위치 FVector SpawnLocation = ActiveCharacter ? ActiveCharacter->GetActorLocation() : FVector::ZeroVector; // 새 캐릭터 스폰 ASwappableCharacter* NewChar = GetWorld()->SpawnActor<ASwappableCharacter>( NewCharData.CharacterClass, SpawnLocation, FRotator::ZeroRotator); // ASC 연결 NewChar->SetAbilitySystemComponent(AbilitySystemComponent); // 저장된 상태 복원 RestoreCharacterState(NewChar, NewCharData); ActiveCharacter = NewChar; NewCharData.bIsOnStandby = false; } else { // 기존 캐릭터 활성화 ActiveCharacter->SetActorHiddenInGame(false); ActiveCharacter->SetActorEnableCollision(true); } // 4. AttributeSet 교체 SwapAttributeSet(SlotIndex); // 5. 어빌리티 교체 SwapAbilities(SlotIndex); // 6. 컨트롤러 연결 if (APlayerController* PC = GetOwningController()) { PC->Possess(ActiveCharacter); } } void AMyPlayerState::SaveCurrentCharacterState() { int32 CurrentSlot = GetActiveSlotIndex(); if (!PartySlots.IsValidIndex(CurrentSlot)) return; FCharacterData& CharData = PartySlots[CurrentSlot]; // 어트리뷰트 저장 if (UCharacterAttributeSet* CharAttribs = GetActiveCharacterAttributes()) { CharData.SavedAttributes.Empty(); CharData.SavedAttributes.Add( UCharacterAttributeSet::GetHealthAttribute(), CharAttribs->GetHealth()); // ... 다른 어트리뷰트 } // 쿨다운 저장 SaveCooldownsToData(CharData); }
04

어빌리티 관리

캐릭터별 어빌리티

어빌리티 스왑 void AMyPlayerState::SwapAbilities(int32 NewSlotIndex) { int32 OldSlot = GetActiveSlotIndex(); // 이전 캐릭터 어빌리티 제거 (비활성화만) if (PartySlots.IsValidIndex(OldSlot)) { for (FGameplayAbilitySpecHandle& Handle : PartySlots[OldSlot].GrantedAbilities) { // 취소하지 않고 비활성화만 AbilitySystemComponent->SetRemoveAbilityOnEnd(Handle); } } // 새 캐릭터 어빌리티 부여 FCharacterData& NewCharData = PartySlots[NewSlotIndex]; // 캐릭터 고유 어빌리티 for (TSubclassOf<UGameplayAbility> AbilityClass : GetCharacterAbilities(NewCharData.CharacterClass)) { FGameplayAbilitySpecHandle Handle = AbilitySystemComponent->GiveAbility( FGameplayAbilitySpec(AbilityClass, 1, INDEX_NONE, this)); NewCharData.GrantedAbilities.Add(Handle); } // 저장된 쿨다운 복원 for (auto& CooldownPair : NewCharData.SavedCooldowns) { if (CooldownPair.Value > 0.f) { ApplyCooldownEffect(CooldownPair.Key, CooldownPair.Value); } } } // 스왑 쿨다운 적용 void AMyPlayerState::ApplySwapCooldown() { // 스왑 자체에 쿨다운 적용 FGameplayEffectSpecHandle SwapCooldownSpec = AbilitySystemComponent->MakeOutgoingSpec( SwapCooldownEffectClass, 1, FGameplayEffectContextHandle()); AbilitySystemComponent->ApplyGameplayEffectSpecToSelf( *SwapCooldownSpec.Data.Get()); } // 스왑 가능 여부 확인 bool AMyPlayerState::CanSwapCharacter() const { // 스왑 쿨다운 태그 확인 return !AbilitySystemComponent->HasMatchingGameplayTag( FGameplayTag::RequestGameplayTag(FName("Cooldown.Swap"))); }
스왑 쿨다운

캐릭터 스왑에 쿨다운을 적용하여 남용을 방지합니다. 쿨다운 동안은 스왑 버튼을 비활성화하거나 시각적 피드백을 제공하세요.

05

파티 공유 Effect

전체 캐릭터에 적용

파티 버프 시스템 // 파티 전체에 적용되는 버프 (음식 등) UCLASS() class UGE_PartyBuff : public UGameplayEffect { public: UGE_PartyBuff() { DurationPolicy = EGameplayEffectDurationType::HasDuration; DurationMagnitude = FScalableFloat(1800.0f); // 30분 // 파티 버프 태그 InheritableOwnedTagsContainer.AddTag( FGameplayTag::RequestGameplayTag(FName("Buff.Party"))); } }; // 파티 버프 적용 void AMyPlayerState::ApplyPartyBuff(TSubclassOf<UGameplayEffect> BuffClass) { // PlayerState의 ASC에 적용 (캐릭터 스왑해도 유지) AbilitySystemComponent->ApplyGameplayEffectToSelf( BuffClass->GetDefaultObject<UGameplayEffect>(), 1.0f, AbilitySystemComponent->MakeEffectContext()); } // 대기 캐릭터 HP 회복 시스템 void AMyPlayerState::TickStandbyRecovery(float DeltaTime) { for (int32 i = 0; i < PartySlots.Num(); i++) { if (i == GetActiveSlotIndex()) continue; FCharacterData& CharData = PartySlots[i]; if (!CharData.bIsOnStandby) continue; // 대기 중인 캐릭터 HP 회복 float MaxHealth = GetCharacterMaxHealth(CharData.CharacterClass); float CurrentHealth = CharData.SavedAttributes.FindRef( UCharacterAttributeSet::GetHealthAttribute()); float RecoveryRate = 0.01f; // 초당 1% 회복 CurrentHealth = FMath::Min(CurrentHealth + MaxHealth * RecoveryRate * DeltaTime, MaxHealth); CharData.SavedAttributes.Add( UCharacterAttributeSet::GetHealthAttribute(), CurrentHealth); } }
SUMMARY

핵심 요약

  • PlayerState에 ASC: 캐릭터 스왑 시에도 플레이어 Effect 유지
  • FCharacterData: 대기 캐릭터 상태 저장
  • AttributeSet 교체: 스왑 시 캐릭터별 스탯 전환
  • 쿨다운 저장/복원: 스왑해도 쿨다운 유지
  • 파티 버프: PlayerState ASC에 적용하여 전체 공유
PRACTICE

도전 과제

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

실습 1: ASC 아바타 전환

ASC를 PlayerState에 배치하고, InitAbilityActorInfo()의 아바타 파라미터를 새 캐릭터로 변경하는 스왑 함수를 구현하세요. 기존 GE가 새 캐릭터에 유지되는지 확인하세요.

실습 2: 캐릭터별 어빌리티 관리

GiveAbility/ClearAbility로 캐릭터 스왑 시 이전 캐릭터의 고유 어빌리티를 제거하고 새 캐릭터의 어빌리티를 부여하세요. 공유 어빌리티는 유지되도록 태그로 구분하세요.

심화 과제

undefined