네트웍 멀티플레이를 위한 캐릭터의 공격 구현과 체력 동기화

  • 네트웍 멀티플레이 게임 제작을 위한 기본 구현 원칙의 학습
  • 액터 컴포넌트를 리플리케이션하는 방법의 학습

 

 

인프런 이득우님의 '언리얼 프로그래밍 Part3 - 네트웍 멀티플레이 프레임웍의 이해' 강의를 참고하였습니다. 
😎 [이득우의 언리얼 프로그래밍 Part3 - 네트웍 멀티플레이 프레임웍의 이해] 강의 들으러 가기!

 

 

 

목차

     

     


     

     

    캐릭터 공격 구현


     

     

    학습 목표

     

    네트웍 멀티플레이를 위한 캐릭터의 공격 구현과 체력 동기화

    RPC의 신뢰성을 집중적으로 학


     

     

    캐릭터의 공격 플로우 (Standalone)

     

    액터와 애님 인스턴스간에 수행되는 캐릭터의 공격 플로우

     


     

     

    네트웍 멀티플레이의 구현을 위한 4원칙

     

    1.  클라이언트의 명령은 Server RPC를 사용한다.

    • Server RPC는 클라이언트에서 서버로 패킷을 보내는 유일한 방법이다.
    • 서버에 보낼 명령의 중요도에 따라 Reliable, Unreliable을 결정하여 RPC 함수를 선언한다.

     

    2.  중요한 게임 플레이 판정은 서버에서 처리한다.

    • 게임을 구현할 때 가끔씩은 서버의 부하를 줄이고 게임의 쾌적한 반응성을 위해서 클라이언트에서 중요한 기능을 처리하는 경우가 종종 있다.
    • 하지만 최종 판정은 항상 서버에서 진행되도록 원칙을 지켜줘야 한다.

     

    3.  게임 플레이에 영향을 주는 중요한 정보는 프로퍼티 리플리케이션(Property Replication)을 사용한다.

    • 어떤 데이터가 중요한 데이터인가?
      • 이를 위해서는 클라이언트가 여러가지 이유로 접속이 끊어진 후에 재접속한 상황을 가정해보자.
      • ex. 사용자가 모바일 게임 중인데 엘리베이터를 타서 접속이 잠시 끊어진 상황. >> 엘리베이터에서 내리면 재접속한다.
      • 재접속한 상황에서 마치 점속이 끊어진 적이 없었던 것처럼 게임이 진행되어야 한다. 이 때 필요한 데이터가 게임 플레이에 영향을 주는 중요한 데이터다. 이러한 데이터는 프로퍼티 리플리케이션으로 관리해주는 것이 좋다.

     

    4.  클라이언트의 시각적인 효과(Cosmetic)는 Client RPC와 Multicast RPC를 사용한다.

    • 둘 중에서 가급적이면 Client RPC를 사용하는게 좋다.
    • 플레이와 무관한 단순 효과라면 이 둘 중 가급적 Client RPC를 사용하자.

     

     

    네트웍 멀티플레이를 위한 공격 기능 구현 기획

     

    입력 명령을 전달한 이후에는 모두 서버에서 처리하도록 설계

     

    1. 클라이언트 입력에서 공격이 시작.
    2. 서버가 이것을 처리하도록 Server RPC를 호출.
    3. Server RPC를 호출을 받은 서버는 _Implementation 함수를 실행한다. 이 함수에서는 NetMulticast RPC를 호출해서 서버와 모든 클라이언트에게 명령을 보낸다.
    4. NetMulticast RPC의 구현부에는 서버에서 동작하는 특별한 로직을 넣어 주었다. 현재 Pawn의 공격 상태를 관리할 수 있도록 서버에서 공격이 시작되었음을 체크하는 프로퍼티를 설정하다. 공격이 끝날 때 발동하는 타이머를 걸어주었다.
    5. 타이머가 발동되면 서버는 공격이 끝났다고 판정해서 해당 프로퍼티를 다시 갱신시킨다. 서버와 모든 클라이언트가 모션을 재생하도록 몽타주를 재생하는 로직을 구현하였다.

     

    정리하면 NetMulticast RPC 신호를 받은 모든 컴퓨터들은 자신의 화면에서 공격 명령을 내린 Pawn의 공격 애니메이션을 재생하게 된다. 공격 애니메이션을 재생하면 몽타주에 설정된 AnimNotify 기능에 의해서 특정 시점에서 이벤트가 발생한다. 클라이언트에서 발생한 AnimNotify는 모두 무시하고, 서버에서 발생한 AnimNotify만 처리한다.

     

    서버에서 발생한 이벤트 타이밍에 다른 유저가 공격에 맞았는지를 체크하는 RayCasting 함수를 호출해 공격 판정을 처리하고 맞은 유저에게 데미지를 전달한다.

     

    서버에서는 전달된 데미지를 통해 유저의 현재 HP값이 깍인다. 나머지 클라이언트에서도 이 깎인 데이터를 동기화하는 기능은 이후에 구현하겠다.

     

    그 다음, 설정된 타이머에 의해서 공격이 종료되면 캐릭터의 공격 상태가 업데이트 된다. 이것을 클라이언트에 보내 클라이언트의 공격 프로퍼티를 최종적으로 업데이트하면 기본적인 공격 기능을 구현할 수 있다.


     

     

    예제 실습


     

     

    코드

     

    ABCharacterPlayer.h

    더보기
    #pragma once
    #include "CoreMinimal.h"
    #include "Character/ABCharacterBase.h"
    #include "InputActionValue.h"
    #include "Interface/ABCharacterHUDInterface.h"
    #include "ABCharacterPlayer.generated.h"
    
    UCLASS()
    class ARENABATTLE_API AABCharacterPlayer : public AABCharacterBase, public IABCharacterHUDInterface
    {
    	GENERATED_BODY()
    	
    public:
    	AABCharacterPlayer();
    
    protected:
    	virtual void BeginPlay() override;
    	virtual void SetDead() override;
    	virtual void PossessedBy(AController* NewController) override;
    	virtual void OnRep_Owner() override;
    	virtual void PostNetInit() override;
    
    public:
    	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
    
    // Character Control Section
    protected:
    	void ChangeCharacterControl();
    	void SetCharacterControl(ECharacterControlType NewCharacterControlType);
    	virtual void SetCharacterControlData(const class UABCharacterControlData* CharacterControlData) override;
    
    // Camera Section
    protected:
    	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, Meta = (AllowPrivateAccess = "true"))
    	TObjectPtr<class USpringArmComponent> CameraBoom;
    
    	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, Meta = (AllowPrivateAccess = "true"))
    	TObjectPtr<class UCameraComponent> FollowCamera;
    
    // Input Section
    protected:
    	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, Meta = (AllowPrivateAccess = "true"))
    	TObjectPtr<class UInputAction> JumpAction;
    
    	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, Meta = (AllowPrivateAccess = "true"))
    	TObjectPtr<class UInputAction> ChangeControlAction;
    
    	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, Meta = (AllowPrivateAccess = "true"))
    	TObjectPtr<class UInputAction> ShoulderMoveAction;
    
    	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, Meta = (AllowPrivateAccess = "true"))
    	TObjectPtr<class UInputAction> ShoulderLookAction;
    
    	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, Meta = (AllowPrivateAccess = "true"))
    	TObjectPtr<class UInputAction> QuaterMoveAction;
    
    	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, Meta = (AllowPrivateAccess = "true"))
    	TObjectPtr<class UInputAction> AttackAction;
    
    	void ShoulderMove(const FInputActionValue& Value);
    	void ShoulderLook(const FInputActionValue& Value);
    
    	void QuaterMove(const FInputActionValue& Value);
    
    	ECharacterControlType CurrentCharacterControlType;
    
    protected:
    	virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
    	void Attack();
    	virtual void AttackHitCheck() override;
    
    	UFUNCTION(Server, Reliable, WithValidation)
    	void ServerRPCAttack();
    
    	UFUNCTION(NetMulticast, Reliable)
    	void MulticastRPCAttack();
    
    	UPROPERTY(ReplicatedUsing = OnRep_CanAttack)
    	uint8 bCanAttack : 1;
    
    	UFUNCTION()
    	void OnRep_CanAttack();
    
    	float AttackTime = 1.4667f;
    
    // UI Section
    protected:
    	virtual void SetupHUDWidget(class UABHUDWidget* InHUDWidget) override;
    };

    함수 & 변수 추가

    • virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
    • void AttackHitCheck()
    • UFUNCTION(Server, Reliable, WithValidation)
      void ServerRPCAttack();
    • UFUNCTION(NetMulticast, Reliable)
      void MulticastRPCAttack();
    • UPROPERTY(ReplicatedUsing = OnRep_CanAttack)
      uint8 bCanAttack : 1;
    • UFUNCTION()
      void OnRep_CanAttack();

     

     

    ABCharacterPlayer.cpp

    더보기
    #include "Character/ABCharacterPlayer.h"
    #include "Camera/CameraComponent.h"
    #include "GameFramework/SpringArmComponent.h"
    #include "InputMappingContext.h"
    #include "EnhancedInputComponent.h"
    #include "EnhancedInputSubsystems.h"
    #include "ABCharacterControlData.h"
    #include "UI/ABHUDWidget.h"
    #include "CharacterStat/ABCharacterStatComponent.h"
    #include "Interface/ABGameInterface.h"
    #include "ArenaBattle.h"
    #include "Components/CapsuleComponent.h"
    #include "Physics/ABCollision.h"
    #include "GameFramework/CharacterMovementComponent.h"
    #include "Engine/DamageEvents.h"
    #include "Net/UnrealNetwork.h"
    
    AABCharacterPlayer::AABCharacterPlayer()
    {
    	// Camera
    	CameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom"));
    	CameraBoom->SetupAttachment(RootComponent);
    	CameraBoom->TargetArmLength = 400.0f;
    	CameraBoom->bUsePawnControlRotation = true;
    
    	FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera"));
    	FollowCamera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName);
    	FollowCamera->bUsePawnControlRotation = false;
    
    	// Input
    	static ConstructorHelpers::FObjectFinder<UInputAction> InputActionJumpRef(TEXT("/Script/EnhancedInput.InputAction'/Game/ArenaBattle/Input/Actions/IA_Jump.IA_Jump'"));
    	if (nullptr != InputActionJumpRef.Object)
    	{
    		JumpAction = InputActionJumpRef.Object;
    	}
    
    	static ConstructorHelpers::FObjectFinder<UInputAction> InputChangeActionControlRef(TEXT("/Script/EnhancedInput.InputAction'/Game/ArenaBattle/Input/Actions/IA_ChangeControl.IA_ChangeControl'"));
    	if (nullptr != InputChangeActionControlRef.Object)
    	{
    		ChangeControlAction = InputChangeActionControlRef.Object;
    	}
    
    	static ConstructorHelpers::FObjectFinder<UInputAction> InputActionShoulderMoveRef(TEXT("/Script/EnhancedInput.InputAction'/Game/ArenaBattle/Input/Actions/IA_ShoulderMove.IA_ShoulderMove'"));
    	if (nullptr != InputActionShoulderMoveRef.Object)
    	{
    		ShoulderMoveAction = InputActionShoulderMoveRef.Object;
    	}
    
    	static ConstructorHelpers::FObjectFinder<UInputAction> InputActionShoulderLookRef(TEXT("/Script/EnhancedInput.InputAction'/Game/ArenaBattle/Input/Actions/IA_ShoulderLook.IA_ShoulderLook'"));
    	if (nullptr != InputActionShoulderLookRef.Object)
    	{
    		ShoulderLookAction = InputActionShoulderLookRef.Object;
    	}
    
    	static ConstructorHelpers::FObjectFinder<UInputAction> InputActionQuaterMoveRef(TEXT("/Script/EnhancedInput.InputAction'/Game/ArenaBattle/Input/Actions/IA_QuaterMove.IA_QuaterMove'"));
    	if (nullptr != InputActionQuaterMoveRef.Object)
    	{
    		QuaterMoveAction = InputActionQuaterMoveRef.Object;
    	}
    
    	static ConstructorHelpers::FObjectFinder<UInputAction> InputActionAttackRef(TEXT("/Script/EnhancedInput.InputAction'/Game/ArenaBattle/Input/Actions/IA_Attack.IA_Attack'"));
    	if (nullptr != InputActionAttackRef.Object)
    	{
    		AttackAction = InputActionAttackRef.Object;
    	}
    
    	CurrentCharacterControlType = ECharacterControlType::Quater;
    	bCanAttack = true;
    }
    
    void AABCharacterPlayer::BeginPlay()
    {
    	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));
    	Super::BeginPlay();
    
    	APlayerController* PlayerController = Cast<APlayerController>(GetController());
    	if (PlayerController)
    	{
    		EnableInput(PlayerController);
    	}
    
    	SetCharacterControl(CurrentCharacterControlType);
    }
    
    void AABCharacterPlayer::SetDead()
    {
    	Super::SetDead();
    
    	APlayerController* PlayerController = Cast<APlayerController>(GetController());
    	if (PlayerController)
    	{
    		DisableInput(PlayerController);
    	}
    }
    
    void AABCharacterPlayer::PossessedBy(AController* NewController)
    {
    	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));
    	AActor* OwnerActor = GetOwner();
    	if (OwnerActor)
    	{
    		AB_LOG(LogABNetwork, Log, TEXT("Owner : %s"), *OwnerActor->GetName());
    	}
    	else
    	{
    		AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("No Owner"));
    	}
    
    	Super::PossessedBy(NewController);
    
    	OwnerActor = GetOwner();
    	if (OwnerActor)
    	{
    		AB_LOG(LogABNetwork, Log, TEXT("Owner : %s"), *OwnerActor->GetName());
    	}
    	else
    	{
    		AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("No Owner"));
    	}
    
    	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("End"));
    }
    
    void AABCharacterPlayer::OnRep_Owner()
    {
    	AB_LOG(LogABNetwork, Log, TEXT("%s %s"), *GetName(), TEXT("Begin"));
    
    	Super::OnRep_Owner();
    
    	AActor* OwnerActor = GetOwner();
    	if (OwnerActor)
    	{
    		AB_LOG(LogABNetwork, Log, TEXT("Owner : %s"), *OwnerActor->GetName());
    	}
    	else
    	{
    		AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("No Owner"));
    	}
    
    	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("End"));
    }
    
    void AABCharacterPlayer::PostNetInit()
    {
    	AB_LOG(LogABNetwork, Log, TEXT("%s %s"), TEXT("Begin"), *GetName());
    
    	Super::PostNetInit();
    
    	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("End"));
    }
    
    void AABCharacterPlayer::SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent)
    {
    	Super::SetupPlayerInputComponent(PlayerInputComponent);
    
    	UEnhancedInputComponent* EnhancedInputComponent = CastChecked<UEnhancedInputComponent>(PlayerInputComponent);
    
    	EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Triggered, this, &ACharacter::Jump);
    	EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Completed, this, &ACharacter::StopJumping);
    	EnhancedInputComponent->BindAction(ChangeControlAction, ETriggerEvent::Triggered, this, &AABCharacterPlayer::ChangeCharacterControl);
    	EnhancedInputComponent->BindAction(ShoulderMoveAction, ETriggerEvent::Triggered, this, &AABCharacterPlayer::ShoulderMove);
    	EnhancedInputComponent->BindAction(ShoulderLookAction, ETriggerEvent::Triggered, this, &AABCharacterPlayer::ShoulderLook);
    	EnhancedInputComponent->BindAction(QuaterMoveAction, ETriggerEvent::Triggered, this, &AABCharacterPlayer::QuaterMove);
    	EnhancedInputComponent->BindAction(AttackAction, ETriggerEvent::Triggered, this, &AABCharacterPlayer::Attack);
    }
    
    void AABCharacterPlayer::ChangeCharacterControl()
    {
    	if (CurrentCharacterControlType == ECharacterControlType::Quater)
    	{
    		SetCharacterControl(ECharacterControlType::Shoulder);
    	}
    	else if (CurrentCharacterControlType == ECharacterControlType::Shoulder)
    	{
    		SetCharacterControl(ECharacterControlType::Quater);
    	}
    }
    
    void AABCharacterPlayer::SetCharacterControl(ECharacterControlType NewCharacterControlType)
    {
    	if (!IsLocallyControlled())
    	{
    		return;
    	}
    
    	UABCharacterControlData* NewCharacterControl = CharacterControlManager[NewCharacterControlType];
    	check(NewCharacterControl);
    
    	SetCharacterControlData(NewCharacterControl);
    
    	APlayerController* PlayerController = CastChecked<APlayerController>(GetController());
    	if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
    	{
    		Subsystem->ClearAllMappings();
    		UInputMappingContext* NewMappingContext = NewCharacterControl->InputMappingContext;
    		if (NewMappingContext)
    		{
    			Subsystem->AddMappingContext(NewMappingContext, 0);
    		}
    	}
    
    	CurrentCharacterControlType = NewCharacterControlType;
    }
    
    void AABCharacterPlayer::SetCharacterControlData(const UABCharacterControlData* CharacterControlData)
    {
    	Super::SetCharacterControlData(CharacterControlData);
    
    	CameraBoom->TargetArmLength = CharacterControlData->TargetArmLength;
    	CameraBoom->SetRelativeRotation(CharacterControlData->RelativeRotation);
    	CameraBoom->bUsePawnControlRotation = CharacterControlData->bUsePawnControlRotation;
    	CameraBoom->bInheritPitch = CharacterControlData->bInheritPitch;
    	CameraBoom->bInheritYaw = CharacterControlData->bInheritYaw;
    	CameraBoom->bInheritRoll = CharacterControlData->bInheritRoll;
    	CameraBoom->bDoCollisionTest = CharacterControlData->bDoCollisionTest;
    }
    
    void AABCharacterPlayer::ShoulderMove(const FInputActionValue& Value)
    {
    	FVector2D MovementVector = Value.Get<FVector2D>();
    
    	const FRotator Rotation = Controller->GetControlRotation();
    	const FRotator YawRotation(0, Rotation.Yaw, 0);
    
    	const FVector ForwardDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
    	const FVector RightDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
    
    	AddMovementInput(ForwardDirection, MovementVector.X);
    	AddMovementInput(RightDirection, MovementVector.Y);
    }
    
    void AABCharacterPlayer::ShoulderLook(const FInputActionValue& Value)
    {
    	FVector2D LookAxisVector = Value.Get<FVector2D>();
    
    	AddControllerYawInput(LookAxisVector.X);
    	AddControllerPitchInput(LookAxisVector.Y);
    }
    
    void AABCharacterPlayer::QuaterMove(const FInputActionValue& Value)
    {
    	FVector2D MovementVector = Value.Get<FVector2D>();
    
    	float InputSizeSquared = MovementVector.SquaredLength();
    	float MovementVectorSize = 1.0f;
    	float MovementVectorSizeSquared = MovementVector.SquaredLength();
    	if (MovementVectorSizeSquared > 1.0f)
    	{
    		MovementVector.Normalize();
    		MovementVectorSizeSquared = 1.0f;
    	}
    	else
    	{
    		MovementVectorSize = FMath::Sqrt(MovementVectorSizeSquared);
    	}
    
    	FVector MoveDirection = FVector(MovementVector.X, MovementVector.Y, 0.0f);
    	GetController()->SetControlRotation(FRotationMatrix::MakeFromX(MoveDirection).Rotator());
    	AddMovementInput(MoveDirection, MovementVectorSize);
    }
    
    void AABCharacterPlayer::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
    {
    	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
    
    	DOREPLIFETIME(AABCharacterPlayer, bCanAttack);
    }
    
    void AABCharacterPlayer::Attack()
    {
    	//ProcessComboCommand();
    
    	if (bCanAttack)
    	{
    		ServerRPCAttack();
    	}
    }
    
    void AABCharacterPlayer::AttackHitCheck()
    {
    	if (HasAuthority())
    	{
    		AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));
    
    		FHitResult OutHitResult;
    		FCollisionQueryParams Params(SCENE_QUERY_STAT(Attack), false, this);
    
    		const float AttackRange = Stat->GetTotalStat().AttackRange;
    		const float AttackRadius = Stat->GetAttackRadius();
    		const float AttackDamage = Stat->GetTotalStat().Attack;
    		const FVector Start = GetActorLocation() + GetActorForwardVector() * GetCapsuleComponent()->GetScaledCapsuleRadius();
    		const FVector End = Start + GetActorForwardVector() * AttackRange;
    
    		bool HitDetected = GetWorld()->SweepSingleByChannel(OutHitResult, Start, End, FQuat::Identity, CCHANNEL_ABACTION, FCollisionShape::MakeSphere(AttackRadius), Params);
    		if (HitDetected)
    		{
    			FDamageEvent DamageEvent;
    			OutHitResult.GetActor()->TakeDamage(AttackDamage, DamageEvent, GetController(), this);
    		}
    
    #if ENABLE_DRAW_DEBUG
    
    		FVector CapsuleOrigin = Start + (End - Start) * 0.5f;
    		float CapsuleHalfHeight = AttackRange * 0.5f;
    		FColor DrawColor = HitDetected ? FColor::Green : FColor::Red;
    
    		DrawDebugCapsule(GetWorld(), CapsuleOrigin, CapsuleHalfHeight, AttackRadius, FRotationMatrix::MakeFromZ(GetActorForwardVector()).ToQuat(), DrawColor, false, 5.0f);
    
    #endif
    	}
    }
    
    bool AABCharacterPlayer::ServerRPCAttack_Validate()
    {
    	return true;
    }
    
    void AABCharacterPlayer::ServerRPCAttack_Implementation()
    {
    	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));
    	MulticastRPCAttack();
    }
    
    void AABCharacterPlayer::MulticastRPCAttack_Implementation()
    {
    	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));
    
    	if (HasAuthority())
    	{
    		bCanAttack = false;
    		OnRep_CanAttack(); // OnRep_함수는 항상 클라이언트에서만 호출된다. 서버에서는 자동으로 호출되지 않기 때문에 서버 로직의 경우, 이 함수가 필요한 경우 명시적으로 호출해야 한다.
    
    		FTimerHandle Handle;
    		GetWorld()->GetTimerManager().SetTimer(Handle, FTimerDelegate::CreateLambda([&]
    			{
    				bCanAttack = true;				
    				OnRep_CanAttack();
    			}
    		), AttackTime, false, -1.0f);
    
    	}
    
    	// 서버와, 클라이언트 모두 몽타주 재생
    	UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
    	AnimInstance->Montage_Play(ComboActionMontage);
    }
    
    void AABCharacterPlayer::OnRep_CanAttack()
    {
    	if (!bCanAttack) // 공격할 수 없다면 움직이지 못하게 설정
    	{
    		GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_None);
    	}
    	else // 공격할 수 있다면 움직일 수 있게 설정
    	{
    		GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_Walking);
    	}
    }
    
    void AABCharacterPlayer::SetupHUDWidget(UABHUDWidget* InHUDWidget)
    {
    	if (InHUDWidget)
    	{
    		InHUDWidget->UpdateStat(Stat->GetBaseStat(), Stat->GetModifierStat());
    		InHUDWidget->UpdateHpBar(Stat->GetCurrentHp());
    
    		Stat->OnStatChanged.AddUObject(InHUDWidget, &UABHUDWidget::UpdateStat);
    		Stat->OnHpChanged.AddUObject(InHUDWidget, &UABHUDWidget::UpdateHpBar);
    	}
    }

    함수 정의

    • void AABCharacterPlayer::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
    • void AABCharacterPlayer::AttackHitCheck()
      • 서버에서 실행된다.
    • bool AABCharacterPlayer::ServerRPCAttack_Validate()
    • void AABCharacterPlayer::ServerRPCAttack_Implementation()
    • void AABCharacterPlayer::MulticastRPCAttack_Implementation()

     

     

    실행화면

     

     

    클라이언트 접속 후


     

     

     

    액터 컴포넌트 리플리케이션  Actor Component Replication

     

    HP 값이 모든 클라이언트에서 동기화되도록 현재 HP 프퍼티를 관리하고 있는 액터 컴포넌트를 리플리케이션 하자.


     

     

    액터 컴포넌트 리플리케이션

     

    • 언리얼에서 리플리케이션의 주체는 액터임.
    • 액터가 소유하는 언리얼 오브젝트에 대해 리플리케이션 진행이 가능
      • 이를 통틀어 서브오브젝트(Subobject)라고도 함.
      • 액터가 아닌 액터에 종속된 오브젝트도 리플리케이션을 사용할 수 있다.
    • 서브 오브젝트 중에서  (스탯을 관리하는) 액터 컴포넌트를 리플리케이션 설정.
      • 리플리케이션 지정:  SetIsReplicated(true)
        • 액터 컴포넌트의 생성자에서 리플리케이션 기능을 활성화한다. 
      • 리플리케이션이 준비되면 호출되는 이벤트 함수:  ReadyForReplication() 
        • ReadyForReplication() 함수는 액터 컴포넌트의 기본 설계 기능이 완료되는 타이밍에 호출되는 InitializeComponent() 함수보다 늦게 호출이 되고 게임이 참여할 때 실행되는 BeginPlay() 함수보다 먼저 호출된다.
        • 액터가 리플리케이션을 활성화할 때 PostInitializeComponent() 함수와 BeginPlay() 사이에서 호출되는 PostNetInit() 함수와 유사하다고 볼 수 있다.

     


     

     

    예시 코드

     

    ABCharacterStatComponent.h

    더보기
    #pragma once
    #include "CoreMinimal.h"
    #include "Components/ActorComponent.h"
    #include "GameData/ABCharacterStat.h"
    #include "ABCharacterStatComponent.generated.h"
    
    DECLARE_MULTICAST_DELEGATE(FOnHpZeroDelegate);
    DECLARE_MULTICAST_DELEGATE_OneParam(FOnHpChangedDelegate, float /*CurrentHp*/);
    DECLARE_MULTICAST_DELEGATE_TwoParams(FOnStatChangedDelegate, const FABCharacterStat& /*BaseStat*/, const FABCharacterStat& /*ModifierStat*/);
    
    UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
    class ARENABATTLE_API UABCharacterStatComponent : public UActorComponent
    {
    	GENERATED_BODY()
    
    public:	
    	// Sets default values for this component's properties
    	UABCharacterStatComponent();
    
    protected:
    	virtual void InitializeComponent() override;
    
    public:
    	FOnHpZeroDelegate OnHpZero;
    	FOnHpChangedDelegate OnHpChanged;
    	FOnStatChangedDelegate OnStatChanged;
    
    	void SetLevelStat(int32 InNewLevel);
    	FORCEINLINE float GetCurrentLevel() const { return CurrentLevel; }
    	FORCEINLINE void AddBaseStat(const FABCharacterStat& InAddBaseStat) { BaseStat = BaseStat + InAddBaseStat; OnStatChanged.Broadcast(GetBaseStat(), GetModifierStat()); }
    	FORCEINLINE void SetBaseStat(const FABCharacterStat& InBaseStat) { BaseStat = InBaseStat; OnStatChanged.Broadcast(GetBaseStat(), GetModifierStat()); }
    	FORCEINLINE void SetModifierStat(const FABCharacterStat& InModifierStat) { ModifierStat = InModifierStat; OnStatChanged.Broadcast(GetBaseStat(), GetModifierStat()); }
    
    	FORCEINLINE const FABCharacterStat& GetBaseStat() const { return BaseStat; }
    	FORCEINLINE const FABCharacterStat& GetModifierStat() const { return ModifierStat; }
    	FORCEINLINE FABCharacterStat GetTotalStat() const { return BaseStat + ModifierStat; }
    	FORCEINLINE float GetCurrentHp() const { return CurrentHp; }
    	FORCEINLINE void HealHp(float InHealAmount) { CurrentHp = FMath::Clamp(CurrentHp + InHealAmount, 0, GetTotalStat().MaxHp); OnHpChanged.Broadcast(CurrentHp); }
    	FORCEINLINE float GetAttackRadius() const { return AttackRadius; }
    	float ApplyDamage(float InDamage);
    
    protected:
    	void SetHp(float NewHp);
    
    	UPROPERTY(ReplicatedUsing = OnRep_CurrentHp, Transient, VisibleInstanceOnly, Category = Stat)
    	float CurrentHp;
    
    	UPROPERTY(Transient, VisibleInstanceOnly, Category = Stat)
    	float CurrentLevel;
    
    	UPROPERTY(VisibleInstanceOnly, Category = Stat, Meta = (AllowPrivateAccess = "true"))
    	float AttackRadius;
    
    	UPROPERTY(Transient, VisibleInstanceOnly, Category = Stat, Meta = (AllowPrivateAccess = "true"))
    	FABCharacterStat BaseStat;
    
    	UPROPERTY(Transient, VisibleInstanceOnly, Category = Stat, Meta = (AllowPrivateAccess = "true"))
    	FABCharacterStat ModifierStat;
    
    protected:
    	virtual void BeginPlay() override;
    	virtual void ReadyForReplication() override;
    	virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
    
    	UFUNCTION()
    	void OnRep_CurrentHp();
    };

    변수 변경

    • UPROPERTY(ReplicatedUsing = OnRep_CurrentHp, Transient, VisibleInstanceOnly, Category = Stat)
      float CurrentHp;

     

    함수 추가

    • UFUNCTION()
      void OnRep_CurrentHp();
    • virtual void ReadyForReplication() override;
    • virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;

     

     

    ABCharacterStatComponent.cpp

    더보기
    #include "CharacterStat/ABCharacterStatComponent.h"
    #include "GameData/ABGameSingleton.h"
    #include "Net/UnrealNetwork.h"
    #include "ArenaBattle.h"
    
    UABCharacterStatComponent::UABCharacterStatComponent()
    {
    	CurrentLevel = 1;
    	AttackRadius = 50.0f;
    
    	bWantsInitializeComponent = true;
    
    	SetIsReplicated(true);
    }
    
    void UABCharacterStatComponent::InitializeComponent()
    {
    	Super::InitializeComponent();
    
    	SetLevelStat(CurrentLevel);
    	SetHp(BaseStat.MaxHp);
    }
    
    void UABCharacterStatComponent::SetLevelStat(int32 InNewLevel)
    {
    	CurrentLevel = FMath::Clamp(InNewLevel, 1, UABGameSingleton::Get().CharacterMaxLevel);
    	SetBaseStat(UABGameSingleton::Get().GetCharacterStat(CurrentLevel));
    	check(BaseStat.MaxHp > 0.0f);
    }
    
    float UABCharacterStatComponent::ApplyDamage(float InDamage)
    {
    	const float PrevHp = CurrentHp;
    	const float ActualDamage = FMath::Clamp<float>(InDamage, 0, InDamage);
    
    	SetHp(PrevHp - ActualDamage);
    	if (CurrentHp <= KINDA_SMALL_NUMBER)
    	{
    		OnHpZero.Broadcast();
    	}
    
    	return ActualDamage;
    }
    
    void UABCharacterStatComponent::SetHp(float NewHp)
    {
    	CurrentHp = FMath::Clamp<float>(NewHp, 0.0f, BaseStat.MaxHp);
    	
    	OnHpChanged.Broadcast(CurrentHp);
    }
    
    void UABCharacterStatComponent::BeginPlay()
    {
    	AB_SUBLOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));
    	Super::BeginPlay();
    }
    
    void UABCharacterStatComponent::ReadyForReplication()
    {
    	AB_SUBLOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));
    	Super::ReadyForReplication();
    }
    
    void UABCharacterStatComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
    {
    	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
    
    	DOREPLIFETIME(UABCharacterStatComponent, CurrentHp);
    }
    
    void UABCharacterStatComponent::OnRep_CurrentHp()
    {
    	AB_SUBLOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));
    	OnHpChanged.Broadcast(CurrentHp);
    	if (CurrentHp <= KINDA_SMALL_NUMBER)
    	{
    		OnHpZero.Broadcast();
    	}
    }

    UABCharacterStatComponent::UABCharacterStatComponent() 

    • 생성자에 SetIsReplicated(true) 추가

     

     

    실행화면

     

     

    클라이언트 접속 후

     

     


     

     

    캐릭터 공격 구현 정리

     

    • 네트웍 멀티플레이어 구현의 기본 원칙의 이해
    • 네트웍 멀티플레이어에서 동작하는 캐릭터 공격 구현
    • 액터 컴포넌트 리플리케이션의 설정과 관련 이벤트 함수의 학습
    • 캐릭터의 체력 프로퍼티 동기화 구현