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