PostGameplayEffectExecute(...)에서 체력을 변경했을 때, 0보다 작거나 같은 경우 서버에서만 사망 델리게이트를 Broadcast하게 했다.// AO_PlayerCharacter_AttributeSet.h
DECLARE_MULTICAST_DELEGATE(FOnPlayerDeath);
UCLASS()
class AO_API UAO_PlayerCharacter_AttributeSet : public UAttributeSet
{
...
public:
FOnPlayerDeath OnPlayerDeath;
};
// AO_PlayerCharacter_AttributeSet.cpp
void UAO_PlayerCharacter_AttributeSet::PostGameplayEffectExecute(const struct FGameplayEffectModCallbackData& Data)
{
Super::PostGameplayEffectExecute(Data);
if (Data.EvaluatedData.Attribute == GetHealthAttribute())
{
const float NewHealth = GetHealth();
if (NewHealth <= 0.f)
{
if (AActor* Owner = GetOwningActor())
{
if (Owner->HasAuthority())
{
OnPlayerDeath.Broadcast();
}
}
}
SetHealth(FMath::Clamp(NewHealth, 0.f, GetMaxHealth()));
}
...
}
BeginPlay() 과정에서 BindAttributeDelegates() 함수를 통해 위에서 선언한 델리게이트와 바인딩을 해주었다.SpringArm의 TargetArmLength도 증가시켰다.// AO_PlayerCharacter.cpp
void AAO_PlayerCharacter::BindAttributeDelegates()
{
if (!AttributeSet)
{
return;
}
AttributeSet->OnPlayerDeath.AddUObject(this, &AAO_PlayerCharacter::HandlePlayerDeath);
}
void AAO_PlayerCharacter::HandlePlayerDeath()
{
if (!HasAuthority())
{
return;
}
// PlayerState에서 생존 상태 변경
AAO_PlayerState* PS = GetPlayerState<AAO_PlayerState>();
checkf(PS, TEXT("PlayerState is null"));
if (!PS->GetIsAlive())
{
return;
}
PS->SetIsAlive(false);
// Death Ability 실행
FGameplayTagContainer DeathTag(FGameplayTag::RequestGameplayTag(FName("Ability.State.Death")));
AbilitySystemComponent->TryActivateAbilitiesByTag(DeathTag);
// Death UI 출력
if (Cast<APlayerController>(GetController()))
{
ClientRPC_HandleDeathView();
}
}
void AAO_PlayerCharacter::ClientRPC_HandleDeathView_Implementation()
{
if (SpringArm)
{
SpringArm->TargetArmLength += DeathCameraArmOffset;
}
if (TObjectPtr<AAO_PlayerController_Stage> PC = Cast<AAO_PlayerController_Stage>(GetController()))
{
PC->ShowDeathUI();
}
}
bIsAlive 변수를 수정하고, 이를 받아오는 Getter, Setter 함수를 추가했다.protected로 해서 캡슐화를 신경쓰고자 했으나, 다른 분이 이미 사용하고 있는 코드여서 접근 지정자를 변경하지는 않았다.// AO_PlayerState.h
UCLASS()
class AO_API AAO_PlayerState : public APlayerState
{
...
public:
UPROPERTY(ReplicatedUsing=OnRep_IsAlive)
bool bIsAlive = true;
FORCEINLINE bool GetIsAlive() const { return bIsAlive; };
void SetIsAlive(bool bInIsAlive);
};
// AO_PlayerController_Stage.h
UCLASS()
class AO_API AAO_PlayerController_Stage : public AAO_PlayerController_InGameBase
{
...
protected:
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "AO|UI")
TSubclassOf<UUserWidget> DeathWidgetClass;
UPROPERTY()
TObjectPtr<UUserWidget> DeathWidget;
public:
UFUNCTION(BlueprintCallable, Category = "AO|UI")
void ShowDeathUI();
}
// AO_PlayerController_Stage.cpp
void AAO_PlayerController_Stage::ShowDeathUI()
{
if (!IsLocalController())
{
return;
}
if (!DeathWidget && DeathWidgetClass)
{
DeathWidget = CreateWidget<UUserWidget>(this, DeathWidgetClass);
}
if (DeathWidget)
{
DeathWidget->AddToViewport();
}
FInputModeUIOnly InputMode;
InputMode.SetWidgetToFocus(DeathWidget->TakeWidget());
SetInputMode(InputMode);
bShowMouseCursor = true;
}
GE_BlockAbilities를 재활용하고자 했다.// AO_GameplayAbility_Death.h
UCLASS()
class AO_API UAO_GameplayAbility_Death : public UGameplayAbility
{
GENERATED_BODY()
public:
UAO_GameplayAbility_Death();
protected:
virtual void ActivateAbility(
const FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayAbilityActivationInfo ActivationInfo,
const FGameplayEventData* TriggerEventData) override;
protected:
UPROPERTY(EditDefaultsOnly, Category = "Death")
TObjectPtr<UAnimMontage> DeathMontage;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "HitReact")
TSubclassOf<UGameplayEffect> BlockAbilitiesEffectClass;
UFUNCTION()
void OnMontageCompleted();
UFUNCTION()
void OnMontageCancelled();
};
NetExecutionPolicy를 ServerOnly로 해서 트래픽을 줄일 수 있지만, 몽타주를 재생하게 되면 클라에서 리플리케이션 안 되기 때문에 ServerInitiated로 지정을 했다. "Ability.State.Death"로 지정했고, 해당 Ability가 실행되면 액터가 죽은 상태이기 때문에 "Status.Death" 태그를 붙여서 사망 상태를 확인하고자 했다.UAO_GameplayAbility_Death::UAO_GameplayAbility_Death()
{
InstancingPolicy = EGameplayAbilityInstancingPolicy::InstancedPerActor;
NetExecutionPolicy = EGameplayAbilityNetExecutionPolicy::ServerInitiated;
AbilityTags.AddTag(FGameplayTag::RequestGameplayTag(FName("Ability.State.Death")));
ActivationOwnedTags.AddTag(FGameplayTag::RequestGameplayTag(FName("Status.Death")));
}
GE_BlockAbilities도 실행시켰다.void UAO_GameplayAbility_Death::ActivateAbility(const FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo,
const FGameplayEventData* TriggerEventData)
{
if (!CommitAbility(Handle, ActorInfo, ActivationInfo))
{
EndAbility(Handle, ActorInfo, ActivationInfo, true, true);
return;
}
if (HasAuthority(&ActivationInfo))
{
// 피격 리액션 Ability 취소
if (TObjectPtr<UAbilitySystemComponent> ASC = ActorInfo->AbilitySystemComponent.Get())
{
FGameplayTagContainer HitReactTags;
HitReactTags.AddTag(FGameplayTag::RequestGameplayTag(FName("Ability.State.HitReact")));
ASC->CancelAbilities(&HitReactTags);
}
TObjectPtr<ACharacter> Character = Cast<ACharacter>(ActorInfo->AvatarActor.Get());
checkf(Character, TEXT("Failed to cast AvatarActor to ACharacter"));
Character->GetCharacterMovement()->DisableMovement();
Character->GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
if (BlockAbilitiesEffectClass)
{
FGameplayEffectSpecHandle SpecHandle = MakeOutgoingGameplayEffectSpec(Handle, ActorInfo, ActivationInfo, BlockAbilitiesEffectClass, 1.f);
FActiveGameplayEffectHandle BlockAbilitiesEffectHandle
= ApplyGameplayEffectSpecToOwner(Handle, ActorInfo, ActivationInfo, SpecHandle);
}
}
checkf(DeathMontage, TEXT("DeathMontage is null"));
TObjectPtr<UAbilityTask_PlayMontageAndWait> MontageTask
= UAbilityTask_PlayMontageAndWait::CreatePlayMontageAndWaitProxy(this, NAME_None, DeathMontage);
checkf(MontageTask, TEXT("Failed to create MontageTask"));
MontageTask->OnCompleted.AddDynamic(this, &UAO_GameplayAbility_Death::OnMontageCompleted);
MontageTask->OnBlendOut.AddDynamic(this, &UAO_GameplayAbility_Death::OnMontageCompleted);
MontageTask->OnCancelled.AddDynamic(this, &UAO_GameplayAbility_Death::OnMontageCancelled);
MontageTask->OnInterrupted.AddDynamic(this, &UAO_GameplayAbility_Death::OnMontageCancelled);
MontageTask->ReadyForActivation();
}


SetViewTargetWithBlend()를 활용해서 다른 플레이어를 보여주는 카메라로 이동하는 기능을 구현하고자 한다.RequestSpectate()가 실행되고, ServerRPC_RequestSpectate()를 통해 관전할 대상을 지정하고, ClientRPC_SetSpectateTarget()를 통해 클라가 지정한 Pawn을 관전하게 하고자 한다.// AO_PlayerController_Stage.h
UCLASS()
class AO_API AAO_PlayerController_Stage : public AAO_PlayerController_InGameBase
{
...
public:
UFUNCTION(BlueprintCallable, Category = "AO|UI")
void RequestSpectate();
protected:
UFUNCTION(Server, Reliable)
void ServerRPC_RequestSpectate();
UFUNCTION(Client, Reliable)
void ClientRPC_SetSpectateTarget(APawn* NewTarget);
UPROPERTY()
TObjectPtr<APawn> CurrentSpectateTarget;
}
RequestSpectate()를 Death UI의 관전하기 버튼과 연결하여 정상적으로 동작하는 것을 알 수 있었다.// AO_PlayerController_Stage.cpp
void AAO_PlayerController_Stage::RequestSpectate()
{
if (IsLocalController())
{
ServerRPC_RequestSpectate();
}
}
void AAO_PlayerController_Stage::ServerRPC_RequestSpectate_Implementation()
{
TObjectPtr<APawn> NewTarget = nullptr;
TObjectPtr<UWorld> World = GetWorld();
checkf(World, TEXT("World is null"));
TObjectPtr<AGameStateBase> GS = GetWorld()->GetGameState<AGameStateBase>();
checkf(GS, TEXT("GameState is null"));
TObjectPtr<AAO_PlayerState> MyPS = GetPlayerState<AAO_PlayerState>();
checkf(MyPS, TEXT("PlayerState is null"));
// 현재 게임의 플레이어들을 전부 가져와서 검사하기
const TArray<TObjectPtr<APlayerState>>& Players = GS->PlayerArray;
const int32 NumPlayers = Players.Num();
for (int32 i = 0; i < NumPlayers; ++i)
{
TObjectPtr<AAO_PlayerState> PS = Cast<AAO_PlayerState>(Players[i]);
if (!PS || PS == MyPS) continue; // 자기 자신은 제외
if (!PS->GetIsAlive()) continue; // 플레이어가 죽은 경우 제외
TObjectPtr<APawn> OtherPawn = PS->GetPawn();
if (!OtherPawn) continue;
NewTarget = OtherPawn;
break;
}
if (NewTarget)
{
ClientRPC_SetSpectateTarget(NewTarget);
}
}
void AAO_PlayerController_Stage::ClientRPC_SetSpectateTarget_Implementation(APawn* NewTarget)
{
if (!IsLocalController() || !NewTarget)
{
return;
}
CurrentSpectateTarget = NewTarget;
const float BlendTime = 0.4f;
SetViewTargetWithBlend(NewTarget, BlendTime);
}