개요
기존 싱글플레이 기준으로 제작된 플레이어 HP Bar와
어트리뷰트셋을 리플리케이션을 이용해 동기화하여 개선하였다
어트리뷰트 동기화는 기존 변수의 리플리케이션과 다르게
ReplicationUsing과 별도 매크로를 사용해야 GAS가 제공하는
기능을 정상적으로 사용할 수 있다
또한 플레이어 HPBar를 NativeTick 대신 Timer를 사용하여
보간을 사용해 부드럽게 Bar를 변경 시켰다
Tick을 사용하지 않은 이유는 UI의 NativeTick은 Actor의 Tick과 다르게
필요할 때 활성화, 비활성화 하는 함수를 발견하지 못하여
Timer를 대신 사용하여 필요할 때 타이머를 사용하는 것으로 해당 기능을
대체하였다
어트리뷰트셋 동기화
헤더파일
class PROJECT_P_API UPPCharacterAttributeSet : public UAttributeSet
{
GENERATED_BODY()
public:
mutable FIsDeadDelegate ActorIsDead;
ATTRIBUTE_ACCESSORS(UPPCharacterAttributeSet, Health);
virtual void GetLifetimeReplicatedProps(TArray< class FLifetimeProperty >& OutLifetimeProps) const override;
UPROPERTY(BlueprintReadOnly, Category = "Health", ReplicatedUsing = OnRep_Health, Meta = (AllowPrivateAccess = true))
FGameplayAttributeData Health;
UPROPERTY(ReplicatedUsing = OnRep_IsDead)
bool bIsDead = false;
UFUNCTION()
void OnRep_Health(const FGameplayAttributeData& OldValue);
UFUNCTION()
void OnRep_IsDead();
};
어트리뷰트는 대표로 Health만 남기고 생략하였다
- FIsDeadDelegate ActorIsDead
- 액터 죽음 이벤트 델리게이트
- ATTRIBUTE_ACCESSORS
- 어트리뷰트 Get, Set, Init 함수 생성 매크로
- GetLifetimeReplicatedProps
- 리플리케이션 변수 등록 함수
- UPROPERTY(ReplicatedUsing = OnRep_Health, 생략)
FGameplayAttributeData Health;
- 어트리뷰트 생성 및 리플리케이션 설정
- UPROPERTY(ReplicatedUsing = OnRep_IsDead)
bool bIsDead = false;- 액터 죽음 플래그 리플리케이션 설정
- UFUNCTION()
void OnRep_Health(const FGameplayAttributeData& OldValue);
UFUNCTION()
void OnRep_IsDead();- Health 어트리뷰트와 액터 죽음 플래그가 변경되면 실행될 함수
특이점으로 UPROPERTY(ReplicatedUsing = OnRep_Health) 으로 어트리뷰트도
리플리케이트 콜백함수를 설정한다
GetLifetimeReplicatedProps
void UPPCharacterAttributeSet::GetLifetimeReplicatedProps(TArray<class FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
//생략
DOREPLIFETIME_CONDITION_NOTIFY(UPPCharacterAttributeSet, Health, COND_None, REPNOTIFY_Always);
DOREPLIFETIME(UPPCharacterAttributeSet, bIsDead);
}
GetLifetimeReplicatedProps 함수에서 리플리케이션 변수를 매크로를 통해 등록한다
어트리뷰트의 경우 DOREPLIFETIME_CONDITION_NOTIFY 로
REPNOTIFY_Always 설정하여 등록한다
OnRep_Health
void UPPCharacterAttributeSet::OnRep_Health(const FGameplayAttributeData& OldValue)
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UPPCharacterAttributeSet, Health, OldValue);
}
OnRep 함수에서 GAMEPLAYATTRIBUTE_REPNOTIFY 매크로를 실행시켜 준다
해당 매크로와 DOREPLIFETIME_CONDITION_NOTIFY 를 사용하여
REPNOTIFY_Always 로 등록을 해야 GAS에서 제공하는 예측, 보정 로직을 정상적으로 사용할 수 있다
OnRep_IsDead
void UPPCharacterAttributeSet::OnRep_IsDead()
{
PPNET_ATTLOG(LogGAS, Log, TEXT("Begin"));
if (bIsDead)
{
ActorIsDead.Broadcast();
}
UAbilitySystemComponent* ASC = GetOwningAbilitySystemComponent();
if (ASC)
{
if (bIsDead)
{
ASC->AddLooseGameplayTag(PPTAG_CHARACTER_ISDEAD);
}
else
{
// 만약 부활 기능이 있다면 태그 제거도 필요
ASC->RemoveLooseGameplayTag(PPTAG_CHARACTER_ISDEAD);
}
APPGASCharacterPlayer* Player = Cast<APPGASCharacterPlayer>(ASC->GetAvatarActor());
if (Player)
{
if (bIsDead)
{
Player->SetDead();
}
else
{
Player->SetAlive();
}
}
}
}
캐릭터 죽음 플래그의 리플리케이션 함수를 사용하여
클라이언트에서 액터의 죽음을 알려준다
또한 추후 작성할 캐릭터 부활기능을 위한 로직들도 추가한다
해당 로직은 살거나 죽엇을 때 GameplayTag 를 제거 또는 부착하고
리플리케이션을 통해 시뮬레이티드 프록시 액터들도 필요한 로직을 실행한다
PostGameplayEffectExecute
void UPPCharacterAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
Super::PostGameplayEffectExecute(Data);
float MinHealth = 0.0f;
if (Data.EvaluatedData.Attribute == GetHealthAttribute())
{
SetHealth(FMath::Clamp(GetHealth(), MinHealth, GetMaxHealth()));
if (AActor* Target = Data.Target.GetAvatarActor())
{
Target->ForceNetUpdate();
}
}
else if (Data.EvaluatedData.Attribute == GetDamageAttribute())
{
SetHealth(FMath::Clamp(GetHealth() - GetDamage(), MinHealth, GetMaxHealth()));
SetDamage(0.0f);
if (AActor* Target = Data.Target.GetAvatarActor())
{
Target->ForceNetUpdate();
}
}
if (GetHealth() <= 0.0f && !bIsDead)
{
Data.Target.AddLooseGameplayTag(PPTAG_CHARACTER_ISDEAD);
ActorIsDead.Broadcast();
}
bIsDead = GetHealth() <= 0.0f;
}
서버에서만 실행되는 함수 (GameplayEffect 적용을 서버에서만 진행)
해당 함수를 간단하게 보면 어트리뷰트가 갱신되면
Target->ForceNetUpdate(); 함수를 실행하여
업데이트 주기를 무시하고 동기화를 실행한다
GetHealth() <= 0.0f가 되면 ActorIsDead.Broadcast();와
GameplayTag를 부착하여 서버에서 액터 죽음을 반영한다
클라이언트는 bIsDead = GetHealth() <= 0.0f; 로 값이 바뀌면
OnRep 함수가 실행되어 클라이언트에서의 로직을 실행한다
유저 HPBar 초기화 시점 변경
기존에는 NativeConstruct를 사용해 AttributeValueChangeDelegate 에 바인드 했는데
클라이언트에선 해당 시점에 ASC가 초기화가 마무리가 안되어 정상적으로 동작하지
않는 문제가 발생했다

- ASC 초기화가 마무리 되는 PossessedBy, OnRep_PlayerState에서 인터페이스 함수 실행
- InitHUD 함수에서 HUD위젯, ASC가 생성이 되어있으면 BindAbilitySystem 함수 실행
아니면 ASC_Cache로 저장 - 플레이어 컨트롤러의 BeginPlay 실행 시점에 ASC_Cache가 있으면 BindAbilitySystem 함수 실행
해당 순서로 ASC의 델리게이트에 바인드를 해주었다
NativeTick 대신 Timer 사용
HP를 보간을 위해 Tick을 사용해야 되지만
UI의 경우 필요할때 틱을 키고 끄는 기능이 없어 Timer를 대신 사용했다
CheckShouldTick
void UPPPlayerStatBarUserWidget::CheckShouldTick()
{
bool bHealthMatched = FMath::IsNearlyEqual(CurrentHealth, TargetHealth, 0.1f) &&
FMath::IsNearlyEqual(CurrentMaxHealth, TargetMaxHealth, 0.1f);
bool bManaMatched = FMath::IsNearlyEqual(CurrentMana, TargetMana, 0.1f) &&
FMath::IsNearlyEqual(CurrentMaxMana, TargetMaxMana, 0.1f);
if (bHealthMatched && bManaMatched)
{
if (InterpTimerHandle.IsValid())
{
GetWorld()->GetTimerManager().ClearTimer(InterpTimerHandle);
InterpTimerHandle.Invalidate();
}
}
else
{
if (!InterpTimerHandle.IsValid())
{
GetWorld()->GetTimerManager().SetTimer(InterpTimerHandle, this, &UPPPlayerStatBarUserWidget::UpdateStatBar, TimerFrequency, true);
}
}
}
CheckShouldTick 함수는 IsNearlyEqual 함수로 타이머를 실행할지 종료할지 결정한다
- bHealthMatched && bManaMatched
- 체력과 마나가 다찼을경우 타이머를 클리어, 핸들을 초기화
- if (!InterpTimerHandle.IsValid())
- 타이머를 TimerFrequency(1/60초)로 실행한다
UpdateStatBar
void UPPPlayerStatBarUserWidget::UpdateStatBar()
{
float DeltaTime = GetWorld()->GetDeltaSeconds();
if (!FMath::IsNearlyEqual(CurrentHealth, TargetHealth, 0.1f) ||
!FMath::IsNearlyEqual(CurrentMaxHealth, TargetMaxHealth, 0.1f))
{
CurrentHealth = FMath::FInterpTo(CurrentHealth, TargetHealth, DeltaTime, BarInterpSpeed);
CurrentMaxHealth = FMath::FInterpTo(CurrentMaxHealth, TargetMaxHealth, DeltaTime, BarInterpSpeed);
UpdateHpBar();
}
if (!FMath::IsNearlyEqual(CurrentMana, TargetMana, 0.1f) ||
!FMath::IsNearlyEqual(CurrentMaxMana, TargetMaxMana, 0.1f))
{
CurrentMana = FMath::FInterpTo(CurrentMana, TargetMana, DeltaTime, BarInterpSpeed);
CurrentMaxMana = FMath::FInterpTo(CurrentMaxMana, TargetMaxMana, DeltaTime, BarInterpSpeed);
UpdateMpBar();
}
CheckShouldTick();
}
타이머 콜백 함수이다 1/60초마다 실행된다
Tick의 DeletaTime 변수는 GetWorld()->GetDeltaSeconds();로 대채할 수 있다
체력과 마나를 IsNearlyEqual로 확인하여 Update 함수로 UI를 변경한다
함수 마지막에 CheckShouldTick 함수를 실행시켜 타이머를 종료 시킬지 체크한다
UpdateHpBar
void UPPPlayerStatBarUserWidget::UpdateHpBar()
{
if (PbHpBar)
{
PbHpBar->SetPercent(CurrentHealth / CurrentMaxHealth);
}
if (TxtHpStat)
{
TxtHpStat->SetText(FText::FromString(FString::Printf(TEXT("%.0f/%0.f"), CurrentHealth, CurrentMaxHealth)));
}
}
업데이트 함수는 변경된 어트리뷰트를 UI에 반영한다
'포트폴리오 제작 > Project_P' 카테고리의 다른 글
| Project_P GameOver(재시작) UI 재설계 (0) | 2025.12.26 |
|---|---|
| Project_P 몬스터 HP Bar 네트워크 동기화 (0) | 2025.12.22 |
| Project_P 공격판정 GA 개선 (네트워크 지원) (2) | 2025.12.12 |
| Project_P Attack GA 로직 변경 (네트워크 지원) (0) | 2025.11.21 |
| Project_P 몬스터 AI 개선(행동트리 테스크를 활용하여 몬스터 회전) (0) | 2025.06.04 |